From 1b7b4d138a67dd9a9aa92625cbfeaba4778e68d3 Mon Sep 17 00:00:00 2001 From: Harish Kukreja Date: Fri, 19 Jun 2026 22:11:16 -0400 Subject: [PATCH] fix(desktop): handle slash exec dispatch payloads (#49358) --- .../session/hooks/use-prompt-actions.test.tsx | 61 ++++++++++++++++++ .../app/session/hooks/use-prompt-actions.ts | 63 +++++++++++-------- 2 files changed, 99 insertions(+), 25 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx index f9d9e58d09d..5a3c3241752 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.test.tsx @@ -205,6 +205,67 @@ describe('usePromptActions /title', () => { }) }) +describe('usePromptActions slash.exec dispatch payloads', () => { + afterEach(() => { + cleanup() + $busy.set(false) + vi.restoreAllMocks() + }) + + it('submits /goal send directives returned directly by slash.exec instead of rendering no output', async () => { + const calls: { method: string; params?: Record }[] = [] + const states: Record[] = [] + const requestGateway = vi.fn(async (method: string, params?: Record) => { + calls.push({ method, params }) + + if (method === 'slash.exec') { + return { + type: 'send', + notice: '⊙ Goal set. Starting now.', + message: 'write the implementation plan' + } as never + } + + return {} as never + }) + + let handle: HarnessHandle | null = null + render( + (handle = h)} + onSeedState={s => states.push(s)} + refreshSessions={async () => undefined} + requestGateway={requestGateway} + /> + ) + + await handle!.submitText('/goal write the implementation plan') + + expect(calls.map(c => c.method)).toEqual(['slash.exec', 'prompt.submit']) + expect(calls[0]?.params).toEqual({ + command: 'goal write the implementation plan', + session_id: RUNTIME_SESSION_ID + }) + expect(calls[1]?.params).toEqual({ + session_id: RUNTIME_SESSION_ID, + text: 'write the implementation plan' + }) + + const renderedText = states + .flatMap(state => { + const messages = Array.isArray(state.messages) + ? (state.messages as Array<{ parts?: Array<{ text?: string }> }>) + : [] + + return messages.flatMap(message => (message.parts ?? []).map(part => part.text ?? '')) + }) + .join('\n') + + expect(renderedText).toContain('⊙ Goal set. Starting now.') + expect(renderedText).not.toContain('/goal: no output') + }) +}) + describe('usePromptActions desktop slash pickers', () => { beforeEach(() => { setSessions(() => [sessionInfo({ id: '20260610_120000_abcdef', title: 'Loaded session' })]) diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts index ed3f6498cd1..f594d410c77 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts @@ -915,31 +915,7 @@ export function usePromptActions({ return } - try { - const result = await requestGateway('slash.exec', { - session_id: sessionId, - command: command.replace(/^\/+/, '') - }) - - const body = result?.output || `/${name}: no output` - renderSlashOutput(result?.warning ? `warning: ${result.warning}\n${body}` : body) - - return - } catch { - // Fall back to command.dispatch for skill/send/alias directives. - } - - try { - const dispatch = parseCommandDispatch( - await requestGateway('command.dispatch', { session_id: sessionId, name, arg }) - ) - - if (!dispatch) { - renderSlashOutput('error: invalid response: command.dispatch') - - return - } - + const handleDispatch = async (dispatch: NonNullable>): Promise => { if (dispatch.type === 'exec' || dispatch.type === 'plugin') { renderSlashOutput(dispatch.output ?? '(no output)') @@ -991,6 +967,43 @@ export function usePromptActions({ } await submitPromptText(message) + } + + try { + const result = await requestGateway('slash.exec', { + session_id: sessionId, + command: command.replace(/^\/+/, '') + }) + + const dispatch = parseCommandDispatch(result) + + if (dispatch) { + await handleDispatch(dispatch) + + return + } + + const output = result && typeof result === 'object' ? (result as SlashExecResponse) : null + const body = output?.output || `/${name}: no output` + renderSlashOutput(output?.warning ? `warning: ${output.warning}\n${body}` : body) + + return + } catch { + // Fall back to command.dispatch for skill/send/alias directives. + } + + try { + const dispatch = parseCommandDispatch( + await requestGateway('command.dispatch', { session_id: sessionId, name, arg }) + ) + + if (!dispatch) { + renderSlashOutput('error: invalid response: command.dispatch') + + return + } + + await handleDispatch(dispatch) } catch (err) { renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`) }