mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
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.
38 lines
1.5 KiB
TypeScript
38 lines
1.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { asCommandDispatch } from '../lib/rpc.js'
|
|
|
|
describe('asCommandDispatch', () => {
|
|
it('parses exec, alias, skill, and send', () => {
|
|
expect(asCommandDispatch({ type: 'exec', output: 'hi' })).toEqual({ type: 'exec', output: 'hi' })
|
|
expect(asCommandDispatch({ type: 'alias', target: 'help' })).toEqual({ type: 'alias', target: 'help' })
|
|
expect(asCommandDispatch({ type: 'skill', name: 'x', message: 'do' })).toEqual({
|
|
type: 'skill',
|
|
name: 'x',
|
|
message: 'do'
|
|
})
|
|
expect(asCommandDispatch({ type: 'send', message: 'hello world' })).toEqual({
|
|
type: 'send',
|
|
message: 'hello world'
|
|
})
|
|
expect(asCommandDispatch({ type: 'prefill', message: 'edit me' })).toEqual({
|
|
type: 'prefill',
|
|
message: 'edit me'
|
|
})
|
|
expect(asCommandDispatch({ type: 'prefill', message: 'edit me', notice: '↶ rewound' })).toEqual({
|
|
type: 'prefill',
|
|
message: 'edit me',
|
|
notice: '↶ rewound'
|
|
})
|
|
})
|
|
|
|
it('rejects malformed payloads', () => {
|
|
expect(asCommandDispatch(null)).toBeNull()
|
|
expect(asCommandDispatch({ type: 'alias' })).toBeNull()
|
|
expect(asCommandDispatch({ type: 'skill', name: 1 })).toBeNull()
|
|
expect(asCommandDispatch({ type: 'send' })).toBeNull()
|
|
expect(asCommandDispatch({ type: 'send', message: 42 })).toBeNull()
|
|
expect(asCommandDispatch({ type: 'prefill' })).toBeNull()
|
|
expect(asCommandDispatch({ type: 'prefill', message: 42 })).toBeNull()
|
|
})
|
|
})
|