From b936f92b25b4dab55855aba76741a2e4f0d717e1 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 19 Jun 2026 07:28:50 -0700 Subject: [PATCH] fix(desktop): render send/prefill directive notices (/goal, /undo) (#49073) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop slash dispatcher dropped the `notice` field on `send` and never handled `prefill` directives at all. `/goal ` returns {type: send, notice: "⊙ Goal set …", message} from command.dispatch — the desktop submitted the goal text as a plain prompt with no feedback, so the goal looked like it did nothing. `/undo` returns a prefill directive that fell through to "invalid response". - types: add `notice?` to SendCommandDispatchResponse; add PrefillCommandDispatchResponse to the union. - parseCommandDispatch: keep `notice` on send, parse prefill. - runExec dispatcher: render the notice as a system line before acting, and handle prefill by dropping the message into the composer for editing (mirrors the TUI's createSlashHandler). Tests: parseCommandDispatch send-notice / prefill cases. --- .../app/session/hooks/use-prompt-actions.ts | 19 ++++++++++++ apps/desktop/src/app/types.ts | 8 +++++ apps/desktop/src/lib/chat-runtime.test.ts | 30 ++++++++++++++++++- apps/desktop/src/lib/chat-runtime.ts | 7 ++++- 4 files changed, 62 insertions(+), 2 deletions(-) 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 829119f65b4..ed3f6498cd1 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions.ts @@ -32,6 +32,7 @@ import { clearComposerAttachments, type ComposerAttachment, setComposerAttachmentUploadState, + setComposerDraft, terminalContextBlocksFromDraft, updateComposerAttachment } from '@/store/composer' @@ -951,8 +952,26 @@ export function usePromptActions({ return } + // send / prefill carry an optional `notice` (e.g. "⊙ Goal set …") + // that the backend wants shown as a system line before the message + // is acted on. Mirrors the TUI's createSlashHandler — without it a + // `/goal ` looked like it did nothing. + if ((dispatch.type === 'send' || dispatch.type === 'prefill') && dispatch.notice?.trim()) { + renderSlashOutput(dispatch.notice.trim()) + } + const message = ('message' in dispatch ? dispatch.message : '')?.trim() ?? '' + // /undo returns a prefill directive: drop the backed-up message into + // the composer for editing instead of submitting it immediately. + if (dispatch.type === 'prefill') { + if (message) { + setComposerDraft(message) + } + + return + } + if (!message) { renderSlashOutput( `/${name}: ${dispatch.type === 'skill' ? 'skill payload missing message' : 'empty message'}` diff --git a/apps/desktop/src/app/types.ts b/apps/desktop/src/app/types.ts index 9500468482c..1adc2bdec4e 100644 --- a/apps/desktop/src/app/types.ts +++ b/apps/desktop/src/app/types.ts @@ -106,6 +106,13 @@ export interface SkillCommandDispatchResponse { export interface SendCommandDispatchResponse { type: 'send' message: string + notice?: string +} + +export interface PrefillCommandDispatchResponse { + type: 'prefill' + message: string + notice?: string } export type CommandDispatchResponse = @@ -113,6 +120,7 @@ export type CommandDispatchResponse = | AliasCommandDispatchResponse | SkillCommandDispatchResponse | SendCommandDispatchResponse + | PrefillCommandDispatchResponse export type SidebarNavId = 'artifacts' | 'command-center' | 'messaging' | 'new-session' | 'settings' | 'skills' diff --git a/apps/desktop/src/lib/chat-runtime.test.ts b/apps/desktop/src/lib/chat-runtime.test.ts index c2a9099a1a8..1b4efb33ad5 100644 --- a/apps/desktop/src/lib/chat-runtime.test.ts +++ b/apps/desktop/src/lib/chat-runtime.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest' import type { ComposerAttachment } from '@/store/composer' -import { coerceThinkingText, optimisticAttachmentRef } from './chat-runtime' +import { coerceThinkingText, optimisticAttachmentRef, parseCommandDispatch } from './chat-runtime' const DATA_URL = 'data:image/png;base64,iVBORw0KGgoAAAANS' @@ -52,3 +52,31 @@ describe('coerceThinkingText', () => { ).toBe('') }) }) + +describe('parseCommandDispatch', () => { + it('keeps the notice on a send directive (e.g. /goal set)', () => { + // The backend's /goal set returns {type:send, notice:"⊙ Goal set …", message}. + // Dropping the notice made /goal look like it did nothing in the desktop app. + const parsed = parseCommandDispatch({ type: 'send', notice: '⊙ Goal set', message: 'do the thing' }) + + expect(parsed).toEqual({ type: 'send', message: 'do the thing', notice: '⊙ Goal set' }) + }) + + it('keeps message-only send directives working (no notice)', () => { + expect(parseCommandDispatch({ type: 'send', message: 'hi' })).toEqual({ + type: 'send', + message: 'hi', + notice: undefined + }) + }) + + it('parses a prefill directive with its notice (e.g. /undo)', () => { + const parsed = parseCommandDispatch({ type: 'prefill', notice: 'backed up 1 turn', message: 'edit me' }) + + expect(parsed).toEqual({ type: 'prefill', message: 'edit me', notice: 'backed up 1 turn' }) + }) + + it('rejects a prefill directive missing its message', () => { + expect(parseCommandDispatch({ type: 'prefill', notice: 'x' })).toBeNull() + }) +}) diff --git a/apps/desktop/src/lib/chat-runtime.ts b/apps/desktop/src/lib/chat-runtime.ts index ac5273a2236..c573a1e5899 100644 --- a/apps/desktop/src/lib/chat-runtime.ts +++ b/apps/desktop/src/lib/chat-runtime.ts @@ -238,7 +238,12 @@ export function parseCommandDispatch(raw: unknown): CommandDispatchResponse | nu return typeof row.name === 'string' ? { type: 'skill', name: row.name, message: str(row.message) } : null case 'send': - return typeof row.message === 'string' ? { type: 'send', message: row.message } : null + return typeof row.message === 'string' ? { type: 'send', message: row.message, notice: str(row.notice) } : null + + case 'prefill': + return typeof row.message === 'string' + ? { type: 'prefill', message: row.message, notice: str(row.notice) } + : null default: return null