From 857d0244af8498046c9c796e0a82bbc2fef79368 Mon Sep 17 00:00:00 2001 From: Gille <4317663+helix4u@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:05:58 -0600 Subject: [PATCH] fix(tui): handle dispatch payloads from slash exec (#49337) --- .../src/__tests__/createSlashHandler.test.ts | 36 ++++++++ ui-tui/src/app/createSlashHandler.ts | 85 ++++++++++--------- 2 files changed, 82 insertions(+), 39 deletions(-) diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 8f49dd9a513..1057578093f 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -694,6 +694,42 @@ describe('createSlashHandler', () => { expect(ctx.transcript.send).toHaveBeenCalledWith(skillMessage) }) + it('handles command.dispatch payloads returned directly by slash.exec', async () => { + patchUiState({ sid: 'sid-abc' }) + + const ctx = buildCtx({ + gateway: { + gw: { + getLogTail: vi.fn(() => ''), + request: vi.fn((method: string) => { + if (method === 'slash.exec') { + return Promise.resolve({ + message: 'complete all the steps and provide a final report', + notice: '⊙ Goal set (20-turn budget): complete all the steps and provide a final report', + type: 'send' + }) + } + + return Promise.resolve({}) + }) + }, + rpc: vi.fn(() => Promise.resolve({})) + } + }) + + const h = createSlashHandler(ctx) + expect(h('/goal complete all the steps and provide a final report')).toBe(true) + + await vi.waitFor(() => { + expect(ctx.transcript.sys).toHaveBeenCalledWith( + '⊙ Goal set (20-turn budget): complete all the steps and provide a final report' + ) + }) + expect(ctx.transcript.send).toHaveBeenCalledWith('complete all the steps and provide a final report') + expect(ctx.transcript.sys).not.toHaveBeenCalledWith('/goal: no output') + expect(ctx.gateway.gw.request).not.toHaveBeenCalledWith('command.dispatch', expect.anything()) + }) + it('/history pages the current TUI transcript (user + assistant)', () => { const ctx = buildCtx({ local: { diff --git a/ui-tui/src/app/createSlashHandler.ts b/ui-tui/src/app/createSlashHandler.ts index 9148b5bebbf..044200d6b90 100644 --- a/ui-tui/src/app/createSlashHandler.ts +++ b/ui-tui/src/app/createSlashHandler.ts @@ -74,12 +74,57 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b } } + const handleDispatch = (raw: unknown): void => { + const d = asCommandDispatch(raw) + + if (!d) { + return sys('error: invalid response: command.dispatch') + } + + if (d.type === 'exec' || d.type === 'plugin') { + return sys(d.output || '(no output)') + } + + if (d.type === 'alias') { + return void handler(`/${d.target}${argTail}`) + } + + if (d.type === 'skill') { + sys(`⚡ loading skill: ${d.name}`) + + return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: skill payload missing message`) + } + + if (d.type === 'send') { + if (d.notice?.trim()) { + sys(d.notice) + } + return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: empty message`) + } + + if (d.type === 'prefill') { + // /undo returns prefill: drop the backed-up message 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) + } + } + } + gw.request('slash.exec', { command: cmd.slice(1), session_id: sid }) .then(r => { if (stale()) { return } + if (asCommandDispatch(r)) { + return handleDispatch(r) + } + const body = r?.output || `/${parsed.name}: no output` const text = r?.warning ? `warning: ${r.warning}\n${body}` : body const long = text.length > 180 || text.split('\n').filter(Boolean).length > 2 @@ -93,45 +138,7 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b return } - const d = asCommandDispatch(raw) - - if (!d) { - return sys('error: invalid response: command.dispatch') - } - - if (d.type === 'exec' || d.type === 'plugin') { - return sys(d.output || '(no output)') - } - - if (d.type === 'alias') { - return handler(`/${d.target}${argTail}`) - } - - if (d.type === 'skill') { - sys(`⚡ loading skill: ${d.name}`) - - return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: skill payload missing message`) - } - - if (d.type === 'send') { - if (d.notice?.trim()) { - sys(d.notice) - } - return d.message?.trim() ? send(d.message) : sys(`/${parsed.name}: empty message`) - } - - if (d.type === 'prefill') { - // /undo returns prefill: drop the backed-up message 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 - } + handleDispatch(raw) }) .catch(guardedErr) })