Skip to content

Commit 3d7ff96

Browse files
feat(cli): add a non-interrupting btw command
1 parent 366311e commit 3d7ff96

5 files changed

Lines changed: 155 additions & 4 deletions

File tree

cli/src/commands/__tests__/command-args.test.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { describe, test, expect, mock } from 'bun:test'
1+
import { afterEach, describe, test, expect, mock } from 'bun:test'
22

33
import { useFeedbackStore } from '../../state/feedback-store'
4+
import { useChatStore } from '../../state/chat-store'
45
import {
56
registerActiveRun,
67
stopActiveRun,
@@ -178,6 +179,7 @@ describe('command factory pattern', () => {
178179
// mode:* commands also accept args now
179180
const expectedWithArgs = [
180181
'feedback',
182+
'btw',
181183
'bash',
182184
'image',
183185
'publish',
@@ -346,4 +348,85 @@ describe('command factory pattern', () => {
346348
expect(result).toEqual({ openFeedbackMode: true })
347349
})
348350
})
351+
352+
describe('/btw command', () => {
353+
afterEach(() => {
354+
useChatStore.getState().clearPendingAttachments()
355+
})
356+
357+
test('queues the note and pending attachments while a turn is active', () => {
358+
const btwCmd = COMMAND_REGISTRY.find((c) => c.name === 'btw')
359+
expect(btwCmd).toBeDefined()
360+
361+
const attachment = {
362+
kind: 'text' as const,
363+
id: 'note.txt',
364+
content: 'remember the edge case',
365+
preview: 'remember the edge case',
366+
charCount: 22,
367+
}
368+
useChatStore.getState().clearPendingAttachments()
369+
useChatStore.getState().addPendingAttachment(attachment)
370+
371+
const addToQueue = mock(() => {})
372+
const sendMessage = mock(async () => {})
373+
const setInputFocused = mock(() => {})
374+
const params = createMockParams({
375+
inputValue: '/btw remember the edge case',
376+
isStreaming: true,
377+
addToQueue,
378+
sendMessage,
379+
setInputFocused,
380+
})
381+
382+
btwCmd!.handler(params, 'remember the edge case')
383+
384+
expect(addToQueue).toHaveBeenCalledWith(
385+
expect.stringContaining('remember the edge case'),
386+
[attachment],
387+
)
388+
expect(sendMessage).not.toHaveBeenCalled()
389+
expect(setInputFocused).toHaveBeenCalledWith(true)
390+
expect(useChatStore.getState().pendingAttachments).toEqual([])
391+
})
392+
393+
test('sends the note immediately when the CLI is idle', () => {
394+
const btwCmd = COMMAND_REGISTRY.find((c) => c.name === 'btw')
395+
expect(btwCmd).toBeDefined()
396+
397+
const addToQueue = mock(() => {})
398+
const sendMessage = mock(async () => {})
399+
const params = createMockParams({
400+
inputValue: '/btw check the parser',
401+
addToQueue,
402+
sendMessage,
403+
})
404+
405+
btwCmd!.handler(params, 'check the parser')
406+
407+
expect(sendMessage).toHaveBeenCalledWith({
408+
content: expect.stringContaining('check the parser'),
409+
agentMode: 'DEFAULT',
410+
})
411+
expect(addToQueue).not.toHaveBeenCalled()
412+
})
413+
414+
test('shows usage instead of sending an empty note', () => {
415+
const btwCmd = COMMAND_REGISTRY.find((c) => c.name === 'btw')
416+
expect(btwCmd).toBeDefined()
417+
418+
const setMessages = mock(() => {})
419+
const sendMessage = mock(async () => {})
420+
const params = createMockParams({
421+
inputValue: '/btw',
422+
setMessages,
423+
sendMessage,
424+
})
425+
426+
btwCmd!.handler(params, '')
427+
428+
expect(setMessages).toHaveBeenCalled()
429+
expect(sendMessage).not.toHaveBeenCalled()
430+
})
431+
})
349432
})

cli/src/commands/__tests__/prompt-builders.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { describe, expect, test } from 'bun:test'
22

33
import {
4+
buildBtwPrompt,
45
buildPlanPrompt,
56
buildReviewPrompt,
67
buildReviewPromptFromArgs,
78
} from '../prompt-builders'
89

910
describe('prompt-builders base prompts', () => {
11+
test('/btw keeps the note and removes command whitespace', () => {
12+
expect(buildBtwPrompt(' remember to run tests ')).toBe(
13+
'The user has an additional thought for the current task. Consider it without abandoning the original request:\n\nremember to run tests',
14+
)
15+
})
16+
1017
// These used to branch on whether the user had connected a ChatGPT account,
1118
// delegating the deep-thinking step to @thinker-gpt if so. That integration
1219
// is gone, so there is one branch: the user's selected model does the work.

cli/src/commands/command-registry.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ import {
99
collectProcessDiagnostics,
1010
formatProcessDiagnostics,
1111
} from './process-diagnostics'
12-
import { buildInterviewPrompt, buildPlanPrompt, buildReviewPromptFromArgs } from './prompt-builders'
12+
import {
13+
buildBtwPrompt,
14+
buildInterviewPrompt,
15+
buildPlanPrompt,
16+
buildReviewPromptFromArgs,
17+
} from './prompt-builders'
1318
import { runBashCommand } from './router'
1419
import { handleUsageCommand } from './usage'
1520
import { returnToFreebuffLanding } from '../hooks/use-freebuff-session'
@@ -592,6 +597,46 @@ const ALL_COMMANDS: CommandDefinition[] = [
592597
return { openReviewScreen: true }
593598
},
594599
}),
600+
defineCommandWithArgs({
601+
name: 'btw',
602+
handler: (params, args) => {
603+
const trimmedArgs = args.trim()
604+
const rawInput = params.inputValue.trim()
605+
606+
params.saveToHistory(rawInput)
607+
clearInput(params)
608+
609+
if (!trimmedArgs) {
610+
params.setMessages((prev) => [
611+
...prev,
612+
getSystemMessage('Usage: /btw <additional thought>'),
613+
])
614+
return
615+
}
616+
617+
const btwPrompt = buildBtwPrompt(trimmedArgs)
618+
const isBusy =
619+
params.isStreaming ||
620+
params.streamMessageIdRef.current ||
621+
params.isChainInProgressRef.current
622+
623+
if (isBusy) {
624+
const pendingAttachments = capturePendingAttachments()
625+
params.addToQueue(btwPrompt, pendingAttachments)
626+
params.setInputFocused(true)
627+
params.inputRef.current?.focus()
628+
return
629+
}
630+
631+
params.sendMessage({
632+
content: btwPrompt,
633+
agentMode: params.agentMode,
634+
})
635+
setTimeout(() => {
636+
params.scrollToLatest()
637+
}, 0)
638+
},
639+
}),
595640
defineCommand({
596641
// No `/q` alias: that one already quits the CLI, and a queue editor is not
597642
// worth the chance of a mis-fired exit.

cli/src/commands/prompt-builders.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Centralized prompt builders for /plan and /review commands.
2+
* Centralized prompt builders for /plan, /review, and /btw commands.
33
* This ensures consistent behavior regardless of entry path. Both run on the
44
* user's currently selected model.
55
*/
@@ -10,6 +10,17 @@ const PLAN_BASE_PROMPT =
1010
const REVIEW_BASE_PROMPT =
1111
'Please gather all relevant context and then carefully review:'
1212

13+
const BTW_BASE_PROMPT =
14+
'The user has an additional thought for the current task. Consider it without abandoning the original request:'
15+
16+
/** Build the prompt sent by `/btw` without the command syntax. */
17+
export function buildBtwPrompt(input: string): string {
18+
const trimmedInput = input.trim()
19+
return trimmedInput
20+
? `${BTW_BASE_PROMPT}\n\n${trimmedInput}`
21+
: BTW_BASE_PROMPT
22+
}
23+
1324
/**
1425
* Build a plan prompt from user input.
1526
* @param input - The user's plan request (e.g., "add OAuth login")
@@ -97,4 +108,3 @@ export function buildReviewPromptFromArgs(input: string): string {
97108
// Use the same format as preset scopes for consistency
98109
return `${REVIEW_BASE_PROMPT} ${trimmedInput}`
99110
}
100-

cli/src/data/slash-commands.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ const ALL_SLASH_COMMANDS: SlashCommand[] = [
114114
label: 'review',
115115
description: 'Review code changes',
116116
},
117+
{
118+
id: 'btw',
119+
label: 'btw',
120+
description:
121+
'Queue an additional thought without interrupting the current task',
122+
},
117123
{
118124
id: 'queue',
119125
label: 'queue',

0 commit comments

Comments
 (0)