mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
feat(onboarding): port first-touch hints to the TUI (#16054)
PR #16046 added /busy and /verbose hints to the classic CLI and the gateway runner but skipped the Ink TUI (and therefore the dashboard /chat page, which embeds the TUI via PTY). This extends the same latch to the TUI with TUI-native wording. The TUI's busy-input model is not the /busy knob from the CLI — single Enter while busy auto-queues, double Enter on an empty line interrupts. The new busy-input hint teaches THAT gesture instead of telling the user to flip a config that does not apply. Changes: - agent/onboarding.py — add busy_input_hint_tui() + tool_progress_hint_tui() - tui_gateway/server.py — onboarding.claim JSON-RPC (Ink triggers busy hint on enqueue) + _maybe_emit_onboarding_hint helper hooked into _on_tool_complete for the 30s/tool_progress=all path. Same config.yaml latch so each hint fires at most once per install across CLI, gateway, and TUI combined. - ui-tui/src/gatewayTypes.ts — OnboardingClaimResponse + onboarding.hint event - ui-tui/src/app/createGatewayEventHandler.ts — render the hint event as sys() - ui-tui/src/app/useSubmission.ts — claim busy_input_prompt on first busy enqueue - tests/agent/test_onboarding.py — +3 cases for TUI hint shape - tests/tui_gateway/test_protocol.py — +4 cases for onboarding.claim - website/docs/user-guide/tui.md — new 'Interrupting and queueing' section explaining the TUI's double-Enter model and the hints Validation: scripts/run_tests.sh tests/agent/test_onboarding.py \ tests/tui_gateway/test_protocol.py \ tests/gateway/test_busy_session_ack.py -> 66 passed npm --prefix ui-tui run type-check -> clean npm --prefix ui-tui run lint -> clean npm --prefix ui-tui run build -> clean
This commit is contained in:
@@ -80,6 +80,26 @@ def tool_progress_hint_cli() -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def busy_input_hint_tui() -> str:
|
||||||
|
"""Hint shown the first time a user sends a message while the TUI is busy.
|
||||||
|
|
||||||
|
The TUI auto-queues messages sent mid-turn and uses double-Enter on empty
|
||||||
|
input as the interrupt gesture. There is no ``/busy`` knob to flip — this
|
||||||
|
hint teaches the keybind instead of a command.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
"queued for after the current turn — press Enter twice on an empty "
|
||||||
|
"line to interrupt the current turn instead. This tip only shows once."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tool_progress_hint_tui() -> str:
|
||||||
|
return (
|
||||||
|
"that tool ran for a while — use /verbose to cycle tool-progress "
|
||||||
|
"display modes (all → new → off → verbose). This tip only shows once."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# State read / write
|
# State read / write
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@@ -137,8 +157,10 @@ __all__ = [
|
|||||||
"TOOL_PROGRESS_FLAG",
|
"TOOL_PROGRESS_FLAG",
|
||||||
"busy_input_hint_gateway",
|
"busy_input_hint_gateway",
|
||||||
"busy_input_hint_cli",
|
"busy_input_hint_cli",
|
||||||
|
"busy_input_hint_tui",
|
||||||
"tool_progress_hint_gateway",
|
"tool_progress_hint_gateway",
|
||||||
"tool_progress_hint_cli",
|
"tool_progress_hint_cli",
|
||||||
|
"tool_progress_hint_tui",
|
||||||
"is_seen",
|
"is_seen",
|
||||||
"mark_seen",
|
"mark_seen",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ from agent.onboarding import (
|
|||||||
TOOL_PROGRESS_FLAG,
|
TOOL_PROGRESS_FLAG,
|
||||||
busy_input_hint_cli,
|
busy_input_hint_cli,
|
||||||
busy_input_hint_gateway,
|
busy_input_hint_gateway,
|
||||||
|
busy_input_hint_tui,
|
||||||
is_seen,
|
is_seen,
|
||||||
mark_seen,
|
mark_seen,
|
||||||
tool_progress_hint_cli,
|
tool_progress_hint_cli,
|
||||||
tool_progress_hint_gateway,
|
tool_progress_hint_gateway,
|
||||||
|
tool_progress_hint_tui,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -128,6 +130,14 @@ class TestHintMessages:
|
|||||||
def test_tool_progress_hints_mention_verbose(self):
|
def test_tool_progress_hints_mention_verbose(self):
|
||||||
assert "/verbose" in tool_progress_hint_gateway()
|
assert "/verbose" in tool_progress_hint_gateway()
|
||||||
assert "/verbose" in tool_progress_hint_cli()
|
assert "/verbose" in tool_progress_hint_cli()
|
||||||
|
assert "/verbose" in tool_progress_hint_tui()
|
||||||
|
|
||||||
|
def test_busy_input_hint_tui_teaches_double_enter(self):
|
||||||
|
msg = busy_input_hint_tui()
|
||||||
|
# TUI uses double-Enter as the interrupt gesture, not /busy.
|
||||||
|
assert "Enter" in msg
|
||||||
|
assert "queued" in msg.lower()
|
||||||
|
assert "/busy" not in msg
|
||||||
|
|
||||||
def test_hints_are_not_empty(self):
|
def test_hints_are_not_empty(self):
|
||||||
for hint in (
|
for hint in (
|
||||||
@@ -135,8 +145,10 @@ class TestHintMessages:
|
|||||||
busy_input_hint_gateway("interrupt"),
|
busy_input_hint_gateway("interrupt"),
|
||||||
busy_input_hint_cli("queue"),
|
busy_input_hint_cli("queue"),
|
||||||
busy_input_hint_cli("interrupt"),
|
busy_input_hint_cli("interrupt"),
|
||||||
|
busy_input_hint_tui(),
|
||||||
tool_progress_hint_gateway(),
|
tool_progress_hint_gateway(),
|
||||||
tool_progress_hint_cli(),
|
tool_progress_hint_cli(),
|
||||||
|
tool_progress_hint_tui(),
|
||||||
):
|
):
|
||||||
assert hint.strip()
|
assert hint.strip()
|
||||||
|
|
||||||
|
|||||||
@@ -542,3 +542,94 @@ def test_dispatch_unknown_long_method_still_goes_inline(server):
|
|||||||
resp = server.dispatch({"id": "r4", "method": "some.method", "params": {}})
|
resp = server.dispatch({"id": "r4", "method": "some.method", "params": {}})
|
||||||
|
|
||||||
assert resp["result"] == {"ok": True}
|
assert resp["result"] == {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
# ── onboarding.claim ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def test_onboarding_claim_rejects_unknown_flag(server):
|
||||||
|
resp = server.handle_request({
|
||||||
|
"id": "o1",
|
||||||
|
"method": "onboarding.claim",
|
||||||
|
"params": {"flag": "bogus_flag"},
|
||||||
|
})
|
||||||
|
assert "error" in resp
|
||||||
|
assert resp["error"]["code"] == 4002
|
||||||
|
assert "unknown onboarding flag" in resp["error"]["message"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_onboarding_claim_busy_input_returns_tui_hint(server, tmp_path, monkeypatch):
|
||||||
|
"""First claim returns the TUI hint text and marks the config.yaml flag."""
|
||||||
|
monkeypatch.setattr(server, "_hermes_home", tmp_path)
|
||||||
|
# Bust cached cfg so the new _hermes_home is re-read.
|
||||||
|
server._cfg_cache = None
|
||||||
|
server._cfg_mtime = None
|
||||||
|
|
||||||
|
resp = server.handle_request({
|
||||||
|
"id": "o2",
|
||||||
|
"method": "onboarding.claim",
|
||||||
|
"params": {"flag": "busy_input_prompt"},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert "result" in resp
|
||||||
|
result = resp["result"]
|
||||||
|
assert result["claimed"] is True
|
||||||
|
assert isinstance(result["hint"], str) and result["hint"].strip()
|
||||||
|
# The TUI hint must teach the double-Enter gesture, not the /busy knob.
|
||||||
|
assert "Enter" in result["hint"]
|
||||||
|
assert "/busy" not in result["hint"]
|
||||||
|
|
||||||
|
# config.yaml should now be written with the flag set.
|
||||||
|
cfg_path = tmp_path / "config.yaml"
|
||||||
|
assert cfg_path.exists()
|
||||||
|
import yaml
|
||||||
|
loaded = yaml.safe_load(cfg_path.read_text())
|
||||||
|
assert loaded["onboarding"]["seen"]["busy_input_prompt"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_onboarding_claim_second_call_returns_null_hint(server, tmp_path, monkeypatch):
|
||||||
|
"""Second claim on the same flag reads config.yaml and returns hint=null."""
|
||||||
|
import yaml
|
||||||
|
(tmp_path / "config.yaml").write_text(
|
||||||
|
yaml.safe_dump({"onboarding": {"seen": {"tool_progress_prompt": True}}})
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(server, "_hermes_home", tmp_path)
|
||||||
|
server._cfg_cache = None
|
||||||
|
server._cfg_mtime = None
|
||||||
|
|
||||||
|
resp = server.handle_request({
|
||||||
|
"id": "o3",
|
||||||
|
"method": "onboarding.claim",
|
||||||
|
"params": {"flag": "tool_progress_prompt"},
|
||||||
|
})
|
||||||
|
|
||||||
|
assert "result" in resp
|
||||||
|
assert resp["result"]["claimed"] is False
|
||||||
|
assert resp["result"]["hint"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_onboarding_claim_flags_are_independent(server, tmp_path, monkeypatch):
|
||||||
|
"""Claiming one flag does not affect the other."""
|
||||||
|
monkeypatch.setattr(server, "_hermes_home", tmp_path)
|
||||||
|
server._cfg_cache = None
|
||||||
|
server._cfg_mtime = None
|
||||||
|
|
||||||
|
# Claim busy_input_prompt first
|
||||||
|
resp1 = server.handle_request({
|
||||||
|
"id": "o4a",
|
||||||
|
"method": "onboarding.claim",
|
||||||
|
"params": {"flag": "busy_input_prompt"},
|
||||||
|
})
|
||||||
|
assert resp1["result"]["claimed"] is True
|
||||||
|
|
||||||
|
# tool_progress_prompt must still be claimable. Cache bust because the
|
||||||
|
# first claim wrote to disk mid-test.
|
||||||
|
server._cfg_cache = None
|
||||||
|
server._cfg_mtime = None
|
||||||
|
resp2 = server.handle_request({
|
||||||
|
"id": "o4b",
|
||||||
|
"method": "onboarding.claim",
|
||||||
|
"params": {"flag": "tool_progress_prompt"},
|
||||||
|
})
|
||||||
|
assert resp2["result"]["claimed"] is True
|
||||||
|
assert "/verbose" in resp2["result"]["hint"]
|
||||||
|
|||||||
@@ -1016,6 +1016,64 @@ def _tool_summary(name: str, result: str, duration_s: float | None) -> str | Non
|
|||||||
return f"{text or 'Completed'}{suffix}" if (text or dur) else None
|
return f"{text or 'Completed'}{suffix}" if (text or dur) else None
|
||||||
|
|
||||||
|
|
||||||
|
# ── Onboarding hint emission ─────────────────────────────────────────
|
||||||
|
# First-touch hints are latched to config.yaml (onboarding.seen.<flag>)
|
||||||
|
# and shared with CLI + gateway so each hint fires at most once per
|
||||||
|
# install across all surfaces. Best-effort — never raises.
|
||||||
|
|
||||||
|
_ONBOARDING_HINTS_EMITTED: set[str] = set()
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_emit_onboarding_hint(sid: str, flag: str) -> bool:
|
||||||
|
"""Atomically claim an onboarding flag and emit its hint to Ink.
|
||||||
|
|
||||||
|
Returns True if a hint was emitted this call, False if the flag was
|
||||||
|
already seen (or if anything went wrong — onboarding must never
|
||||||
|
interrupt the normal event flow). Also deduplicates within a single
|
||||||
|
process run via ``_ONBOARDING_HINTS_EMITTED`` so concurrent callers
|
||||||
|
can't double-emit before the config.yaml write lands.
|
||||||
|
"""
|
||||||
|
if flag in _ONBOARDING_HINTS_EMITTED:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
from agent.onboarding import (
|
||||||
|
BUSY_INPUT_FLAG,
|
||||||
|
TOOL_PROGRESS_FLAG,
|
||||||
|
busy_input_hint_tui,
|
||||||
|
is_seen,
|
||||||
|
mark_seen,
|
||||||
|
tool_progress_hint_tui,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfg = _load_cfg()
|
||||||
|
except Exception:
|
||||||
|
cfg = {}
|
||||||
|
if is_seen(cfg, flag):
|
||||||
|
_ONBOARDING_HINTS_EMITTED.add(flag)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if flag == BUSY_INPUT_FLAG:
|
||||||
|
hint_text = busy_input_hint_tui()
|
||||||
|
elif flag == TOOL_PROGRESS_FLAG:
|
||||||
|
hint_text = tool_progress_hint_tui()
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
_ONBOARDING_HINTS_EMITTED.add(flag)
|
||||||
|
try:
|
||||||
|
mark_seen(_hermes_home / "config.yaml", flag)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
_emit("onboarding.hint", sid, {"flag": flag, "text": hint_text})
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _on_tool_start(sid: str, tool_call_id: str, name: str, args: dict):
|
def _on_tool_start(sid: str, tool_call_id: str, name: str, args: dict):
|
||||||
session = _sessions.get(sid)
|
session = _sessions.get(sid)
|
||||||
if session is not None:
|
if session is not None:
|
||||||
@@ -1067,6 +1125,20 @@ def _on_tool_complete(sid: str, tool_call_id: str, name: str, args: dict, result
|
|||||||
if _tool_progress_enabled(sid) or payload.get("inline_diff"):
|
if _tool_progress_enabled(sid) or payload.get("inline_diff"):
|
||||||
_emit("tool.complete", sid, payload)
|
_emit("tool.complete", sid, payload)
|
||||||
|
|
||||||
|
# First-touch onboarding: the first time a tool runs >= 30s in the
|
||||||
|
# noisiest progress mode ("all"), emit a one-time hint suggesting
|
||||||
|
# /verbose. Claim is atomic via config.yaml so the hint fires at
|
||||||
|
# most once per install across CLI + gateway + TUI.
|
||||||
|
try:
|
||||||
|
if (
|
||||||
|
duration_s is not None
|
||||||
|
and duration_s >= 30.0
|
||||||
|
and _session_tool_progress_mode(sid) == "all"
|
||||||
|
):
|
||||||
|
_maybe_emit_onboarding_hint(sid, "tool_progress_prompt")
|
||||||
|
except Exception as _hint_err: # pragma: no cover — onboarding is best-effort
|
||||||
|
logger.debug("tui onboarding tool-progress hint failed: %s", _hint_err)
|
||||||
|
|
||||||
|
|
||||||
def _on_tool_progress(
|
def _on_tool_progress(
|
||||||
sid: str,
|
sid: str,
|
||||||
@@ -1934,6 +2006,53 @@ def _(rid, params: dict) -> dict:
|
|||||||
return _ok(rid, {"status": "interrupted"})
|
return _ok(rid, {"status": "interrupted"})
|
||||||
|
|
||||||
|
|
||||||
|
# ── Methods: onboarding ──────────────────────────────────────────────
|
||||||
|
# First-touch hint latch, shared with CLI + gateway via config.yaml
|
||||||
|
# (``onboarding.seen.<flag>``). Ink calls ``onboarding.claim`` the first
|
||||||
|
# time it hits a behavior fork (busy enqueue, long tool completion); the
|
||||||
|
# method atomically returns the hint text AND marks the flag seen, so a
|
||||||
|
# second fast trigger in the same session never double-renders.
|
||||||
|
|
||||||
|
_VALID_ONBOARDING_FLAGS = {"busy_input_prompt", "tool_progress_prompt"}
|
||||||
|
|
||||||
|
|
||||||
|
@method("onboarding.claim")
|
||||||
|
def _(rid, params: dict) -> dict:
|
||||||
|
flag = str(params.get("flag", "") or "").strip()
|
||||||
|
if flag not in _VALID_ONBOARDING_FLAGS:
|
||||||
|
return _err(rid, 4002, f"unknown onboarding flag: {flag}")
|
||||||
|
try:
|
||||||
|
from agent.onboarding import (
|
||||||
|
BUSY_INPUT_FLAG,
|
||||||
|
TOOL_PROGRESS_FLAG,
|
||||||
|
busy_input_hint_tui,
|
||||||
|
is_seen,
|
||||||
|
mark_seen,
|
||||||
|
tool_progress_hint_tui,
|
||||||
|
)
|
||||||
|
except Exception as e: # pragma: no cover — onboarding is best-effort
|
||||||
|
return _ok(rid, {"hint": None, "claimed": False, "error": str(e)})
|
||||||
|
|
||||||
|
cfg = _load_cfg()
|
||||||
|
if is_seen(cfg, flag):
|
||||||
|
return _ok(rid, {"hint": None, "claimed": False})
|
||||||
|
|
||||||
|
if flag == BUSY_INPUT_FLAG:
|
||||||
|
hint = busy_input_hint_tui()
|
||||||
|
elif flag == TOOL_PROGRESS_FLAG:
|
||||||
|
hint = tool_progress_hint_tui()
|
||||||
|
else: # defensive — validated above
|
||||||
|
return _err(rid, 4002, f"unknown onboarding flag: {flag}")
|
||||||
|
|
||||||
|
# Mark seen atomically before returning. If persistence fails, still
|
||||||
|
# return the hint so the user sees it at least once this session.
|
||||||
|
try:
|
||||||
|
mark_seen(_hermes_home / "config.yaml", flag)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return _ok(rid, {"hint": hint, "claimed": True})
|
||||||
|
|
||||||
|
|
||||||
# ── Delegation: subagent tree observability + controls ───────────────
|
# ── Delegation: subagent tree observability + controls ───────────────
|
||||||
# Powers the TUI's /agents overlay (see ui-tui/src/components/agentsOverlay).
|
# Powers the TUI's /agents overlay (see ui-tui/src/components/agentsOverlay).
|
||||||
# The registry lives in tools/delegate_tool — these handlers are thin
|
# The registry lives in tools/delegate_tool — these handlers are thin
|
||||||
|
|||||||
@@ -570,6 +570,17 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
|||||||
sys(`error: ${message}`)
|
sys(`error: ${message}`)
|
||||||
setStatus('ready')
|
setStatus('ready')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
case 'onboarding.hint': {
|
||||||
|
const text = String(ev.payload?.text || '').trim()
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
sys(`(tip) ${text}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { type MutableRefObject, useCallback, useRef } from 'react'
|
|||||||
import { attachedImageNotice } from '../domain/messages.js'
|
import { attachedImageNotice } from '../domain/messages.js'
|
||||||
import { looksLikeSlashCommand } from '../domain/slash.js'
|
import { looksLikeSlashCommand } from '../domain/slash.js'
|
||||||
import type { GatewayClient } from '../gatewayClient.js'
|
import type { GatewayClient } from '../gatewayClient.js'
|
||||||
import type { InputDetectDropResponse, PromptSubmitResponse, ShellExecResponse } from '../gatewayTypes.js'
|
import type { InputDetectDropResponse, OnboardingClaimResponse, PromptSubmitResponse, ShellExecResponse } from '../gatewayTypes.js'
|
||||||
import { asRpcResult } from '../lib/rpc.js'
|
import { asRpcResult } from '../lib/rpc.js'
|
||||||
import { hasInterpolation, INTERPOLATION_RE } from '../protocol/interpolation.js'
|
import { hasInterpolation, INTERPOLATION_RE } from '../protocol/interpolation.js'
|
||||||
import { PASTE_SNIPPET_RE } from '../protocol/paste.js'
|
import { PASTE_SNIPPET_RE } from '../protocol/paste.js'
|
||||||
@@ -218,6 +218,22 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
|||||||
composerActions.pushHistory(full)
|
composerActions.pushHistory(full)
|
||||||
|
|
||||||
if (getUiState().busy) {
|
if (getUiState().busy) {
|
||||||
|
// First-touch onboarding: teach the TUI's auto-queue + double-Enter
|
||||||
|
// interrupt pattern the first time the user hits it. Claim is
|
||||||
|
// atomic server-side (config.yaml latch), shared with CLI + gateway.
|
||||||
|
gw.request<OnboardingClaimResponse>('onboarding.claim', { flag: 'busy_input_prompt' })
|
||||||
|
.then(raw => {
|
||||||
|
const r = asRpcResult<OnboardingClaimResponse>(raw)
|
||||||
|
const text = r?.hint
|
||||||
|
|
||||||
|
if (typeof text === 'string' && text.trim()) {
|
||||||
|
sys(`(tip) ${text.trim()}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Onboarding is best-effort — never block the enqueue path.
|
||||||
|
})
|
||||||
|
|
||||||
return composerActions.enqueue(full)
|
return composerActions.enqueue(full)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +245,7 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
|||||||
|
|
||||||
send(full)
|
send(full)
|
||||||
},
|
},
|
||||||
[appendMessage, composerActions, composerRefs, interpolate, send, sendQueued, shellExec, slashRef]
|
[appendMessage, composerActions, composerRefs, gw, interpolate, send, sendQueued, shellExec, slashRef, sys]
|
||||||
)
|
)
|
||||||
|
|
||||||
const submit = useCallback(
|
const submit = useCallback(
|
||||||
|
|||||||
@@ -174,6 +174,11 @@ export interface PromptSubmitResponse {
|
|||||||
ok?: boolean
|
ok?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OnboardingClaimResponse {
|
||||||
|
claimed?: boolean
|
||||||
|
hint?: null | string
|
||||||
|
}
|
||||||
|
|
||||||
export interface BackgroundStartResponse {
|
export interface BackgroundStartResponse {
|
||||||
task_id?: string
|
task_id?: string
|
||||||
}
|
}
|
||||||
@@ -417,3 +422,4 @@ export type GatewayEvent =
|
|||||||
type: 'message.complete'
|
type: 'message.complete'
|
||||||
}
|
}
|
||||||
| { payload?: { message?: string }; session_id?: string; type: 'error' }
|
| { payload?: { message?: string }; session_id?: string; type: 'error' }
|
||||||
|
| { payload: { flag: string; text: string }; session_id?: string; type: 'onboarding.hint' }
|
||||||
|
|||||||
@@ -106,6 +106,18 @@ The TUI's status line tracks agent state in real time:
|
|||||||
|
|
||||||
The per-skin status-bar colors and thresholds are shared with the classic CLI — see [Skins](features/skins.md) for customization.
|
The per-skin status-bar colors and thresholds are shared with the classic CLI — see [Skins](features/skins.md) for customization.
|
||||||
|
|
||||||
|
## Interrupting and queueing
|
||||||
|
|
||||||
|
The TUI's busy-input model is different from the classic CLI's `display.busy_input_mode` knob. There is no mode to configure — both behaviors are always available:
|
||||||
|
|
||||||
|
- **Single Enter while busy** — message is **queued** and sent as the next turn after the agent finishes.
|
||||||
|
- **Double Enter on an empty line while busy** — **interrupts** the current turn.
|
||||||
|
- **Double Enter on an empty line with queued messages and no running turn** — drains the next queued message.
|
||||||
|
|
||||||
|
The first time you send a message while the agent is working, the TUI prints a one-time `(tip)` line explaining the double-Enter gesture. It fires once per install — the same `onboarding.seen.busy_input_prompt` latch used by the classic CLI and the gateway. Delete that key from `~/.hermes/config.yaml` to see the tip again.
|
||||||
|
|
||||||
|
Similarly, the first time a tool runs for 30 seconds or longer while you're in the noisiest `tool_progress: all` mode, the TUI prints a one-time `(tip)` about `/verbose` for cycling display modes. Latched under `onboarding.seen.tool_progress_prompt`.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The TUI respects all standard Hermes config: `~/.hermes/config.yaml`, profiles, personalities, skins, quick commands, credential pools, memory providers, tool/skill enablement. No TUI-specific config file exists.
|
The TUI respects all standard Hermes config: `~/.hermes/config.yaml`, profiles, personalities, skins, quick commands, credential pools, memory providers, tool/skill enablement. No TUI-specific config file exists.
|
||||||
|
|||||||
Reference in New Issue
Block a user