From 9a7026049088ef6545053313d71856703ff933f6 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 26 Apr 2026 06:31:37 -0700 Subject: [PATCH] Revert "feat(onboarding): port first-touch hints to the TUI (#16054)" (#16062) This reverts commit ffd2621039259ee8419549fedc8739bf1a350436. --- agent/onboarding.py | 22 ---- tests/agent/test_onboarding.py | 12 -- tests/tui_gateway/test_protocol.py | 91 --------------- tui_gateway/server.py | 119 -------------------- ui-tui/src/app/createGatewayEventHandler.ts | 11 -- ui-tui/src/app/useSubmission.ts | 20 +--- ui-tui/src/gatewayTypes.ts | 6 - website/docs/user-guide/tui.md | 12 -- 8 files changed, 2 insertions(+), 291 deletions(-) diff --git a/agent/onboarding.py b/agent/onboarding.py index 7b755ef47e..eed832ab90 100644 --- a/agent/onboarding.py +++ b/agent/onboarding.py @@ -80,26 +80,6 @@ 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 # ------------------------------------------------------------------------- @@ -157,10 +137,8 @@ __all__ = [ "TOOL_PROGRESS_FLAG", "busy_input_hint_gateway", "busy_input_hint_cli", - "busy_input_hint_tui", "tool_progress_hint_gateway", "tool_progress_hint_cli", - "tool_progress_hint_tui", "is_seen", "mark_seen", ] diff --git a/tests/agent/test_onboarding.py b/tests/agent/test_onboarding.py index ec88c1cc30..a14c7d1797 100644 --- a/tests/agent/test_onboarding.py +++ b/tests/agent/test_onboarding.py @@ -10,12 +10,10 @@ from agent.onboarding import ( TOOL_PROGRESS_FLAG, busy_input_hint_cli, busy_input_hint_gateway, - busy_input_hint_tui, is_seen, mark_seen, tool_progress_hint_cli, tool_progress_hint_gateway, - tool_progress_hint_tui, ) @@ -130,14 +128,6 @@ class TestHintMessages: def test_tool_progress_hints_mention_verbose(self): assert "/verbose" in tool_progress_hint_gateway() 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): for hint in ( @@ -145,10 +135,8 @@ class TestHintMessages: busy_input_hint_gateway("interrupt"), busy_input_hint_cli("queue"), busy_input_hint_cli("interrupt"), - busy_input_hint_tui(), tool_progress_hint_gateway(), tool_progress_hint_cli(), - tool_progress_hint_tui(), ): assert hint.strip() diff --git a/tests/tui_gateway/test_protocol.py b/tests/tui_gateway/test_protocol.py index 196e1ee517..42caaacc58 100644 --- a/tests/tui_gateway/test_protocol.py +++ b/tests/tui_gateway/test_protocol.py @@ -542,94 +542,3 @@ def test_dispatch_unknown_long_method_still_goes_inline(server): resp = server.dispatch({"id": "r4", "method": "some.method", "params": {}}) 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"] diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 419a911e76..03631bf174 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1016,64 +1016,6 @@ 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 -# ── Onboarding hint emission ───────────────────────────────────────── -# First-touch hints are latched to config.yaml (onboarding.seen.) -# 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): session = _sessions.get(sid) if session is not None: @@ -1125,20 +1067,6 @@ 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"): _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( sid: str, @@ -2006,53 +1934,6 @@ def _(rid, params: dict) -> dict: return _ok(rid, {"status": "interrupted"}) -# ── Methods: onboarding ────────────────────────────────────────────── -# First-touch hint latch, shared with CLI + gateway via config.yaml -# (``onboarding.seen.``). 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 ─────────────── # Powers the TUI's /agents overlay (see ui-tui/src/components/agentsOverlay). # The registry lives in tools/delegate_tool — these handlers are thin diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 0bd2faecf4..15cf00a5a9 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -570,17 +570,6 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: sys(`error: ${message}`) setStatus('ready') } - - return - case 'onboarding.hint': { - const text = String(ev.payload?.text || '').trim() - - if (text) { - sys(`(tip) ${text}`) - } - - return - } } } } diff --git a/ui-tui/src/app/useSubmission.ts b/ui-tui/src/app/useSubmission.ts index 8414126c32..f09dc36340 100644 --- a/ui-tui/src/app/useSubmission.ts +++ b/ui-tui/src/app/useSubmission.ts @@ -3,7 +3,7 @@ import { type MutableRefObject, useCallback, useRef } from 'react' import { attachedImageNotice } from '../domain/messages.js' import { looksLikeSlashCommand } from '../domain/slash.js' import type { GatewayClient } from '../gatewayClient.js' -import type { InputDetectDropResponse, OnboardingClaimResponse, PromptSubmitResponse, ShellExecResponse } from '../gatewayTypes.js' +import type { InputDetectDropResponse, PromptSubmitResponse, ShellExecResponse } from '../gatewayTypes.js' import { asRpcResult } from '../lib/rpc.js' import { hasInterpolation, INTERPOLATION_RE } from '../protocol/interpolation.js' import { PASTE_SNIPPET_RE } from '../protocol/paste.js' @@ -218,22 +218,6 @@ export function useSubmission(opts: UseSubmissionOptions) { composerActions.pushHistory(full) 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('onboarding.claim', { flag: 'busy_input_prompt' }) - .then(raw => { - const r = asRpcResult(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) } @@ -245,7 +229,7 @@ export function useSubmission(opts: UseSubmissionOptions) { send(full) }, - [appendMessage, composerActions, composerRefs, gw, interpolate, send, sendQueued, shellExec, slashRef, sys] + [appendMessage, composerActions, composerRefs, interpolate, send, sendQueued, shellExec, slashRef] ) const submit = useCallback( diff --git a/ui-tui/src/gatewayTypes.ts b/ui-tui/src/gatewayTypes.ts index ebaa24f2bd..e64d113c22 100644 --- a/ui-tui/src/gatewayTypes.ts +++ b/ui-tui/src/gatewayTypes.ts @@ -174,11 +174,6 @@ export interface PromptSubmitResponse { ok?: boolean } -export interface OnboardingClaimResponse { - claimed?: boolean - hint?: null | string -} - export interface BackgroundStartResponse { task_id?: string } @@ -422,4 +417,3 @@ export type GatewayEvent = type: 'message.complete' } | { payload?: { message?: string }; session_id?: string; type: 'error' } - | { payload: { flag: string; text: string }; session_id?: string; type: 'onboarding.hint' } diff --git a/website/docs/user-guide/tui.md b/website/docs/user-guide/tui.md index 2b936e34e3..8c1b179b67 100644 --- a/website/docs/user-guide/tui.md +++ b/website/docs/user-guide/tui.md @@ -106,18 +106,6 @@ 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. -## 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 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.