feat(tui): wire /rewind through command.dispatch + prefill payload (#21910)
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.
This commit is contained in:
@@ -15,6 +15,15 @@ describe('asCommandDispatch', () => {
|
||||
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', () => {
|
||||
@@ -23,5 +32,7 @@ describe('asCommandDispatch', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -119,6 +119,19 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
|
||||
}
|
||||
return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: empty message`)
|
||||
}
|
||||
|
||||
if (d.type === 'prefill') {
|
||||
// /rewind returns prefill: drop the chosen text into the
|
||||
// composer so the user can edit and resubmit, instead of
|
||||
// submitting it immediately like 'send'.
|
||||
if (d.notice?.trim()) {
|
||||
sys(d.notice)
|
||||
}
|
||||
if (d.message) {
|
||||
ctx.composer.setInput(d.message)
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
.catch(guardedErr)
|
||||
})
|
||||
|
||||
@@ -48,6 +48,7 @@ export type CommandDispatchResponse =
|
||||
| { target: string; type: 'alias' }
|
||||
| { message?: string; name: string; type: 'skill' }
|
||||
| { message: string; notice?: string; type: 'send' }
|
||||
| { message: string; notice?: string; type: 'prefill' }
|
||||
|
||||
// ── Config ───────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -34,6 +34,14 @@ export const asCommandDispatch = (value: unknown): CommandDispatchResponse | nul
|
||||
}
|
||||
}
|
||||
|
||||
if (t === 'prefill' && typeof o.message === 'string') {
|
||||
return {
|
||||
type: 'prefill',
|
||||
message: o.message,
|
||||
notice: typeof o.notice === 'string' ? o.notice : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user