From 723a9cfb1e82c9d4fc69893c002ec483eb780cfb Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Tue, 21 Apr 2026 18:33:27 -0500 Subject: [PATCH] fix(tui): /history shows the TUI's own transcript, scrollable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported during TUI v2 blitz retest: `/history` in the TUI only shows prompts from non-TUI Hermes runs and can't scroll the window. Root cause is the slash-worker subprocess: it's a detached HermesCLI that never sees the TUI's turns, so its `conversation_history` starts empty and `show_history` surfaces whatever was persisted from earlier CLI sessions — not what the user just did inside the TUI. Intercept `/history` as a local slash command so it dumps `ctx.local.getHistoryItems()` — the TUI's own transcript — routed through the pager (which scrolls after #13591). Accepts an optional preview-length argument (default 400 chars per message). Adds createSlashHandler coverage. --- .../src/__tests__/createSlashHandler.test.ts | 36 +++++++++++++++++++ ui-tui/src/app/slash/commands/core.ts | 28 +++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 1f2f938a93..901564f732 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -211,6 +211,42 @@ describe('createSlashHandler', () => { expect(ctx.transcript.send).toHaveBeenCalledWith(skillMessage) }) + it('/history pages the current TUI transcript (user + assistant)', () => { + const ctx = buildCtx({ + local: { + ...buildLocal(), + getHistoryItems: vi.fn(() => [ + { role: 'user', text: 'hello' }, + { role: 'system', text: 'ignore me' }, + { role: 'assistant', text: 'hi there' }, + { role: 'user', text: 'test' } + ]) + } + }) + + createSlashHandler(ctx)('/history') + expect(ctx.transcript.page).toHaveBeenCalledTimes(1) + + const [body, title] = ctx.transcript.page.mock.calls[0]! + + expect(title).toBe('History') + expect(body).toContain('[You #1]') + expect(body).toContain('hello') + expect(body).toContain('[Hermes #2]') + expect(body).toContain('hi there') + expect(body).toContain('[You #3]') + expect(body).not.toContain('ignore me') + expect(ctx.gateway.gw.request).not.toHaveBeenCalled() + }) + + it('/history reports empty state without paging', () => { + const ctx = buildCtx() + + createSlashHandler(ctx)('/history') + expect(ctx.transcript.page).not.toHaveBeenCalled() + expect(ctx.transcript.sys).toHaveBeenCalledWith('no conversation yet') + }) + it('handles send-type dispatch for /plan command', async () => { const planMessage = 'Plan skill content loaded' diff --git a/ui-tui/src/app/slash/commands/core.ts b/ui-tui/src/app/slash/commands/core.ts index 3a254b2939..77eb20dec3 100644 --- a/ui-tui/src/app/slash/commands/core.ts +++ b/ui-tui/src/app/slash/commands/core.ts @@ -275,6 +275,34 @@ export const coreCommands: SlashCommand[] = [ } }, + { + help: 'view current transcript (user + assistant messages)', + name: 'history', + run: (arg, ctx) => { + // The CLI-side `/history` runs in a detached slash-worker subprocess + // that never sees the TUI's turns — it only surfaces whatever was + // persisted before this process started. Render the TUI's own + // transcript so `/history` actually reflects what the user just did. + const items = ctx.local.getHistoryItems().filter(m => m.role === 'user' || m.role === 'assistant') + + if (!items.length) { + return ctx.transcript.sys('no conversation yet') + } + + const preview = Math.max(80, parseInt(arg, 10) || 400) + + const lines = items.map((m, i) => { + const tag = m.role === 'user' ? `You #${i + 1}` : `Hermes #${i + 1}` + const body = m.text.trim() || (m.tools?.length ? `(${m.tools.length} tool calls)` : '(empty)') + const clipped = body.length > preview ? `${body.slice(0, preview).trimEnd()}…` : body + + return `[${tag}]\n${clipped}` + }) + + ctx.transcript.page(lines.join('\n\n'), 'History') + } + }, + { aliases: ['sb'], help: 'toggle status bar',