Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:
- Run prettier across both trees (printWidth/wrap drift; prettier is not
CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
import/export sorting.
- Manual fixes for non-auto-fixable rules:
- remove unused node:net import in electron/main.cjs (uses Electron net)
- replace inline `typeof import(...)` annotations with top-level
`import type * as EnvModule` in two ui-tui test files
- scoped eslint-disable no-control-regex on intentional sentinel/ANSI
regexes (mathUnicode.ts, text.ts)
- resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
deps, collapse redundant session.* members, and justified disables on
settings mount-only data-load effects to preserve run-once behavior
No behavior changes; test pass/fail counts are unchanged from the main
baseline.
Extends the existing /undo command from a single in-memory exchange
removal into a full rewind: back up N user turns (default 1), soft-delete
the truncated rows in SessionDB (active=0, kept for audit, hidden from
re-prompts and search), notify memory providers, and prefill the composer
with the backed-up message text for editing — CLI and TUI.
Reuses the SessionDB rewind primitives, the on_session_switch(rewound=True)
memory hook, and the TUI command.dispatch prefill payload from SaguaroDev's
#21910 work, wired to /undo [N] instead of a separate /rewind picker.
- cli.py: undo_last(n, prefill) — in-memory truncate + SQLite soft-delete
+ agent surgery (system-prompt invalidate, flush-index reset) + memory
notify + editable buffer prefill; /undo dispatch parses optional count;
checkpoint-rollback caller passes prefill=False
- tui_gateway/server.py: command.dispatch undo branch (was rewind) parses
count, picks Nth-from-last user turn, clamps to oldest
- commands.py: /undo gains [N] args_hint
- tests: rename + expand TUI suite (multi-turn, clamp, invalid-count)
- release.py: AUTHOR_MAP entry for SaguaroDev
Co-authored-by: SaguaroDev <74339271+SaguaroDev@users.noreply.github.com>
Adds the TUI half of the /rewind feature so the Ink terminal UI gets
the same affordance as the prompt_toolkit CLI.
Python side (tui_gateway/server.py):
- /rewind added to _PENDING_INPUT_COMMANDS so slash.exec rejects it
and the TUI falls through to command.dispatch (the only path with
access to live session state + memory hooks).
- New command.dispatch branch for name == "rewind":
v1 auto-picks the most recent user turn (Claude-Code-style single-
step undo), calls SessionDB.rewind_to_message, refreshes the
in-memory history, fires _memory_manager.on_session_switch with
rewound=True, and returns the new "prefill" payload.
- A dedicated picker overlay (multi-step rewind) is tracked as a
follow-up to #21910.
TS side (ui-tui/src/):
- New "prefill" variant on CommandDispatchResponse + asCommandDispatch
validator. Mirrors "send" but does NOT auto-submit; the client drops
the message into the composer for editing.
- createSlashHandler renders the optional notice via sys() and calls
ctx.composer.setInput(d.message), letting the user edit-and-resubmit
the rewound turn — the core UX promised by the issue.
Tests:
- 7 new tui_gateway tests covering prefill payload shape, in-memory
history truncation, DB soft-delete, memory-provider notification
(rewound=True), busy-session refusal, missing-session error, and
registry placement in _PENDING_INPUT_COMMANDS.
- Extended asCommandDispatch vitest covering the new prefill variant
(with + without notice, and rejection of malformed payloads).
Out of scope for v1 (tracked as #21910 follow-up):
- Dedicated picker overlay in Ink (the multi-step rewind UI). v1 auto-
picks the most recent user turn, matching the most common case.
- Gateway platforms (Telegram, Discord, etc.) — issue scopes v1 to
CLI + TUI only.
/goal was silently broken outside the classic CLI.
TUI: /goal was routed through the HermesCLI slash-worker subprocess,
which set the goal row in SessionDB but then called
_pending_input.put(state.goal) — the subprocess has no reader for that
queue, so the kickoff message was discarded. No post-turn judge was
wired into prompt.submit either, so even a manual kickoff would not
continue the goal loop. Intercept /goal in command.dispatch instead,
drive GoalManager directly, and return {type: send, notice, message}
so the TUI client renders the Goal-set notice and fires the kickoff.
Run the judge in _run_prompt_submit after message.complete, surface
the verdict via status.update {kind: goal}, and chain the continuation
turn after the running guard is released.
Gateway: _post_turn_goal_continuation was gated on
hasattr(adapter, 'send_message'), but adapters only expose send().
That branch was dead on every platform — users never saw
'✓ Goal achieved', 'Continuing toward goal', or budget-exhausted
messages. Replace the dead call with adapter.send(chat_id, content,
metadata) and drop a broken reference to self._loop.
Tests:
- tests/tui_gateway/test_goal_command.py — full /goal dispatch matrix
(set / status / pause / resume / clear / stop / done / whitespace)
plus regressions for slash.exec → 4018 and 'goal' staying in
_PENDING_INPUT_COMMANDS.
- tests/gateway/test_goal_verdict_send.py — locks in the adapter.send
path for done / continue / budget-exhausted and verifies the hook
no-ops when no goal is set or the adapter lacks send().
Additional TUI fixes discovered in the same audit:
1. /plan slash command was silently lost — process_command() queues the
plan skill invocation onto _pending_input which nobody reads in the
slash worker subprocess. Now intercepted in slash.exec and routed
through command.dispatch with a new 'send' dispatch type.
Same interception added for /retry, /queue, /steer as safety nets
(these already have correct TUI-local handlers in core.ts, but the
server-side guard prevents regressions if the local handler is
bypassed).
2. Tool results were stripping ANSI escape codes — the messageLine
component used stripAnsi() + plain <Text> for tool role messages,
losing all color/styling from terminal, search_files, etc. Now
uses <Ansi> component (already imported) when ANSI is detected.
3. Terminal tab title now shows model + busy status via useTerminalTitle
hook from @hermes/ink (was never used). Users can identify Hermes
tabs and see at a glance whether the agent is busy or ready.
4. Added 'send' variant to CommandDispatchResponse type + asCommandDispatch
parser + createSlashHandler handler for commands that need to inject
a message into the conversation (plan, queue fallback, steer fallback).
Python's slash worker already prints every echo/panel command through Rich.
TS was reformatting the same data client-side for 23 commands. Delete those
shadows; let the `slash.exec` fallback in `createSlashHandler` route the
worker's text (via `<Ansi>`) and page-wrap long output.
TS registry now contains 23 commands (down from 45) — only those that:
- mutate React-local state (composer, transcript, overlays, uiStore)
- touch the terminal (OSC52 copy, `$EDITOR`, clipboard)
- open pickers (`/model`, `/resume`)
- trigger history surgery (`/undo`, `/retry`, `/compress`, `/personality`)
- need TS-only composition (`/help` merges HOTKEYS + catalog)
Deleted shadows:
session: yolo, skin, verbose, reasoning, provider, stop, reload-mcp,
save, title, insights, debug, fast, platforms, snapshot,
usage, history, profile
ops: plugins, rollback, agents, tasks, cron, config, toolsets,
browser, skills (list/browse only; `/tools configure` kept
for its history-reset side effect)
Side effects:
- Drops `slash/shared.ts` + `SlashShared` + `shared`/`SLASH_OUTPUT_PAGE` —
generic slash.exec fallback handles titled paging via `createSlashHandler`.
- Prunes 17 now-unreferenced `*Response` interfaces from gatewayTypes.ts.
- `createSlashHandler` fallback now pages long output (len>180 || lines>2)
and uses the command name as title.
session.ts: 670 -> 199 (-70%)
ops.ts: 460 -> 52 (-88%)
gatewayTypes.ts: 450 -> 302 (-33%)
Hoist turn state from a 286-line hook into $turnState atom + turnController
singleton. createGatewayEventHandler becomes a typed dispatch over the
controller; its ctx shrinks from 30 fields to 5. Event-handler refs and 16
threaded actions are gone.
Fold three createSlash*Handler factories into a data-driven SlashCommand[]
registry under slash/commands/{core,session,ops}.ts. Aliases are data;
findSlashCommand does name+alias lookup. Shared guarded/guardedErr combinator
in slash/guarded.ts.
Split constants.ts + app/helpers.ts into config/ (timing/limits/env),
content/ (faces/placeholders/hotkeys/verbs/charms/fortunes), domain/ (roles/
details/messages/paths/slash/viewport/usage), protocol/ (interpolation/paste).
Type every RPC response in gatewayTypes.ts (26 new interfaces); drop all
`(r: any)` across slash + main app.
Shrink useMainApp from 1216 -> 646 lines by extracting useSessionLifecycle,
useSubmission, useConfigSync. Add <Fg> themed primitive and strip ~50
`as any` color casts.
Tests: 50 passing. Build + type-check clean.