From 5994b833b0d538357d034e04472a1817789cf7fc Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 31 Aug 2026 19:24:28 -0500 Subject: [PATCH] fix(desktop): route /btw through prompt.btw so the answer reaches the chat Desktop sent /btw through the slash worker, which printed the answer after process_command returned, so only the acknowledgement ever showed. Use the TUI's prompt.btw RPC and persist btw.complete on the originating session. Co-authored-by: SsSs Co-authored-by: kokhlo Co-authored-by: xxxigm --- .../gateway-event/status.ts | 29 ++++++++++++ .../session/hooks/use-prompt-actions/slash.ts | 46 +++++++++++++++++++ apps/desktop/src/lib/chat-messages/types.ts | 2 + .../desktop/src/lib/desktop-slash-commands.ts | 10 ++++ 4 files changed, 87 insertions(+) diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/status.ts b/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/status.ts index 3b5956f1ab..76b1b40a20 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/status.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream/gateway-event/status.ts @@ -41,6 +41,35 @@ export function handleStatusEvent(ctx: GatewayEventContext): boolean { return true } + if (event.type === 'btw.complete') { + // prompt.btw answers a side question and emits this on the originating + // session. Persistent transcript line, matching the TUI's `[btw "q"]` + // — without it Desktop only ever showed the acknowledgement (#99065). + const text = coerceGatewayText(payload?.text).trim() + + if (text && sessionId) { + const taskId = String(payload?.task_id ?? '').trim() + const question = coerceGatewayText(payload?.question).trim() + const header = `[btw${question ? ` "${question}"` : ''}${taskId ? ` (${taskId})` : ''}]` + + flushQueuedDeltas(sessionId) + updateSessionState(sessionId, state => ({ + ...state, + messages: [ + ...state.messages, + { + id: `btw-complete-${taskId || Date.now()}`, + role: 'system', + parts: [textPart(`${header}\n${text}`, occurredAt)], + timestamp: occurredAt + } + ] + })) + } + + return true + } + if (event.type === 'review.summary') { // Self-improvement background review saved something to memory/skills // and emitted a persistent summary (Python formats it as diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts index 096e482c64..1ee2bbe0a3 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts @@ -497,6 +497,52 @@ export function useSlashCommand(deps: SlashCommandDeps) { branch: async () => { await branchCurrentSession() }, + // /btw uses prompt.btw (the TUI's path). It must NOT go through + // runExec: the slash worker prints the answer after process_command + // returns, past the stdout capture window (#99065). The RPC replies + // with a task id; the answer arrives later as btw.complete. + btw: async ctx => { + const question = ctx.arg.trim() + + const resolved = await withSlashOutput(ctx) + + if (!resolved) { + return + } + + const { render: renderSlashOutput, sessionId } = resolved + + if (!question) { + renderSlashOutput( + 'Usage: /btw — answered from a snapshot of this conversation without interrupting it.' + ) + + return + } + + try { + const result = await requestGateway<{ task_id?: string }>('prompt.btw', { + session_id: sessionId, + text: question + }) + + renderSlashOutput( + result.task_id + ? `btw ${result.task_id} — answering from a conversation snapshot` + : 'btw — answering from a conversation snapshot' + ) + } catch (err) { + // Older gateways without the dedicated RPC still have the + // slash-worker route — same compatibility fallback as runRpc. + if (isMissingRpcMethod(err)) { + await runExec(ctx) + + return + } + + renderSlashOutput(`error: ${err instanceof Error ? err.message : String(err)}`) + } + }, // /compress (alias /compact) runs the gateway's dedicated // session.compress RPC — the TUI's path // (ui-tui/src/app/slash/commands/session.ts). It must NOT go through diff --git a/apps/desktop/src/lib/chat-messages/types.ts b/apps/desktop/src/lib/chat-messages/types.ts index 52d8ff589b..f7f0472736 100644 --- a/apps/desktop/src/lib/chat-messages/types.ts +++ b/apps/desktop/src/lib/chat-messages/types.ts @@ -94,6 +94,8 @@ export type GatewayEventPayload = { // clarify.request request_id?: string question?: string + // btw.complete / background.complete — id of the side/background task + task_id?: string choices?: string[] | null multi_select?: boolean // clarify.request batch form: questions replaces question/choices, and diff --git a/apps/desktop/src/lib/desktop-slash-commands.ts b/apps/desktop/src/lib/desktop-slash-commands.ts index bfdbaa5933..d06495c0dc 100644 --- a/apps/desktop/src/lib/desktop-slash-commands.ts +++ b/apps/desktop/src/lib/desktop-slash-commands.ts @@ -55,6 +55,7 @@ export interface DesktopThemeCommandOption { export type DesktopActionId = | 'branch' | 'browser' + | 'btw' | 'compress' | 'handoff' | 'hatch' @@ -240,6 +241,15 @@ const DESKTOP_COMMAND_SPECS: readonly DesktopCommandSpec[] = [ surface: action('compress'), argumentMode: 'text' }, + // /btw must be an action (prompt.btw RPC), not exec: the slash worker + // prints the answer after process_command returns, so Desktop only ever + // saw the acknowledgement (#99065). The answer arrives as btw.complete. + { + name: '/btw', + description: 'Ask a side question about this conversation without interrupting it', + surface: action('btw'), + argumentMode: 'text' + }, { name: '/pet', description: 'Toggle or adopt a petdex mascot (/pet, /pet list, /pet boba)',