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 <w-kwan@hotmail.com>
Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
This commit is contained in:
Brooklyn Nicholson
2026-08-31 19:24:28 -05:00
committed by brooklyn!
parent a2907a8bcd
commit 5994b833b0
4 changed files with 87 additions and 0 deletions
@@ -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
@@ -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 <question> — 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
@@ -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
@@ -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)',