Skip to content

Commit 1a3109d

Browse files
authored
Ask user tool! (#382)
1 parent 65c8b72 commit 1a3109d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3855
-40
lines changed

.agents/base2/base2.ts

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function createBase2(
5252
!isFast && 'write_todos',
5353
'str_replace',
5454
'write_file',
55+
'ask_user',
5556
),
5657
spawnableAgents: buildArray(
5758
'file-picker',
@@ -392,15 +393,23 @@ function buildImplementationStepPrompt({
392393
function buildPlanOnlyInstructionsPrompt({}: {}) {
393394
return `Orchestrate the completion of the user's request using your specialized sub-agents.
394395
395-
You are in plan mode, so you should default to creating a spec/plan based on the user's request. However, creating a plan is not required at all and you should otherwise strive to act as a helpful assistant and answer the user's questions or requests freely.
396+
You are in plan mode, so you should default to asking the user a few clarifying questions and then creating a spec/plan based on the user's request. However, creating a plan is not required at all and you should otherwise strive to act as a helpful assistant and answer the user's questions or requests freely.
396397
397398
## Example response
398399
399400
The user asks you to implement a new feature. You respond in multiple steps:
400401
401402
${buildArray(
402403
EXPLORE_PROMPT,
403-
`- After exploring the codebase, translate the user request into a clear and concise spec. If the user is just asking a question, you can answer it instead of writing a spec.
404+
`- After exploring the codebase, your goal is to translate the user request into a clear and concise spec. If the user is just asking a question, you can answer it instead of writing a spec.
405+
406+
## Asking questions
407+
408+
To clarify the user's intent, or get them to weigh in on key decisions, you should use the ask_user tool.
409+
410+
It's good to use this tool before generating a spec, so you can make the best possible spec for the user's request.
411+
412+
If you don't have any important questions to ask, you can skip this step.
404413
405414
## Creating a spec
406415
@@ -420,28 +429,7 @@ It should not include:
420429
- A summary of the spec.
421430
422431
This is more like an extremely short PRD which describes the end result of what the user wants. Think of it like fleshing out the user's prompt to make it more precise, although it should be as short as possible.
423-
424-
## Follow-up questions
425-
426-
After closing the <PLAN> tags, the last optional section is Follow-up questions, which has a numbered list of questions and alternate choices demarcated by letters to clarify and improve upon the spec. These questions are optional for to complete for the user.
427-
428-
For example, here is a nice short follow-up question, where the options are helpfully written out for the user, with the answers a) and b) indented with two spaces for readability:
429-
430-
<example>
431-
## Optional follow-up questions:
432-
433-
1. Do you want to:
434-
a) (CURRENT) Keep Express and integrate Bun WebSockets
435-
b) Migrate the entire HTTP server to Bun.serve()
436-
</example>
437-
438-
Try to have as few questions as possible (even none), and focus on the most important decisions or assumptions that it would be helpful to clarify with the user.
439-
440-
You should also let them know what the plan currently does by default by labeling that option with "(CURRENT)", and let them know that they can choose a different option if they want to.
441-
442-
The questions section should be last and there should be no summary or further elaboration. Just end your turn.
443-
444-
On subsequent turns with the user, you should rewrite the spec to reflect the user's choices.`,
432+
`,
445433
).join('\n')}`
446434
}
447435

.agents/types/tools.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44
export type ToolName =
55
| 'add_message'
6+
| 'ask_user'
67
| 'code_search'
78
| 'end_turn'
89
| 'find_files'
@@ -29,6 +30,7 @@ export type ToolName =
2930
*/
3031
export interface ToolParamsMap {
3132
add_message: AddMessageParams
33+
ask_user: AskUserParams
3234
code_search: CodeSearchParams
3335
end_turn: EndTurnParams
3436
find_files: FindFilesParams
@@ -59,6 +61,39 @@ export interface AddMessageParams {
5961
content: string
6062
}
6163

64+
/**
65+
* Ask the user multiple choice questions and pause execution until they respond.
66+
*/
67+
export interface AskUserParams {
68+
/** List of multiple choice questions to ask the user */
69+
questions: {
70+
/** The question to ask the user */
71+
question: string
72+
/** Short label (max 12 chars) displayed as a chip/tag */
73+
header?: string
74+
/** Array of answer options with label and optional description (minimum 2) */
75+
options: {
76+
/** The display text for this option */
77+
label: string
78+
/** Explanation shown when option is focused */
79+
description?: string
80+
}[]
81+
/** If true, allows selecting multiple options (checkbox). If false, single selection only (radio). */
82+
multiSelect?: boolean
83+
/** Validation rules for "Other" text input */
84+
validation?: {
85+
/** Maximum length for "Other" text input */
86+
maxLength?: number
87+
/** Minimum length for "Other" text input */
88+
minLength?: number
89+
/** Regex pattern for "Other" text input */
90+
pattern?: string
91+
/** Custom error message when pattern fails */
92+
patternError?: string
93+
}
94+
}[]
95+
}
96+
6297
/**
6398
* Search for string patterns in the project's files. This tool uses ripgrep (rg), a fast line-oriented search tool. Use this tool only when read_files is not sufficient to find the files you need.
6499
*/
@@ -248,10 +283,10 @@ export interface WriteFileParams {
248283
}
249284

250285
/**
251-
* Write a todo list to track tasks. Use this frequently to maintain a step-by-step plan.
286+
* Write a todo list to track tasks for multi-step implementations. Use this frequently to maintain an updated step-by-step plan.
252287
*/
253288
export interface WriteTodosParams {
254-
/** List of todos with their completion status. Try to order the todos the same way you will complete them. Do not mark todos as completed if you have not completed them yet! */
289+
/** List of todos with their completion status. Add ALL of the applicable tasks to the list, so you don't forget to do anything. Try to order the todos the same way you will complete them. Do not mark todos as completed if you have not completed them yet! */
255290
todos: {
256291
/** Description of the task */
257292
task: string

cli/src/chat.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { StatusBar } from './components/status-bar'
1919
import { SLASH_COMMANDS } from './data/slash-commands'
2020
import { useAgentValidation } from './hooks/use-agent-validation'
2121
import { authQueryKeys } from './hooks/use-auth-query'
22+
import { useAskUserBridge } from './hooks/use-ask-user-bridge'
2223
import { useChatInput } from './hooks/use-chat-input'
2324
import { useClipboard } from './hooks/use-clipboard'
2425
import { useConnectionStatus } from './hooks/use-connection-status'
@@ -114,6 +115,9 @@ export const Chat = ({
114115

115116
const { validate: validateAgents } = useAgentValidation(validationErrors)
116117

118+
// Subscribe to ask_user bridge to trigger form display
119+
useAskUserBridge()
120+
117121
const {
118122
inputValue,
119123
cursorPosition,
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Unit tests for focus-helpers.ts
3+
* Testing focus utility functions
4+
*/
5+
6+
import { describe, it, expect } from 'bun:test'
7+
import {
8+
isFocusEqual,
9+
isFocusOnQuestion,
10+
isFocusOnSpecificOption,
11+
isFocusOnSpecificTextInput,
12+
focusToString,
13+
} from '../utils/focus-helpers'
14+
import {
15+
createOptionFocus,
16+
createTextInputFocus,
17+
createConfirmSubmitFocus,
18+
} from '../types'
19+
20+
describe('isFocusEqual', () => {
21+
it('returns true for identical confirm submit focus', () => {
22+
const a = createConfirmSubmitFocus()
23+
const b = createConfirmSubmitFocus()
24+
expect(isFocusEqual(a, b)).toBe(true)
25+
})
26+
27+
it('returns true for identical text input focus', () => {
28+
const a = createTextInputFocus(2)
29+
const b = createTextInputFocus(2)
30+
expect(isFocusEqual(a, b)).toBe(true)
31+
})
32+
33+
it('returns false for text input with different question index', () => {
34+
const a = createTextInputFocus(0)
35+
const b = createTextInputFocus(1)
36+
expect(isFocusEqual(a, b)).toBe(false)
37+
})
38+
39+
it('returns true for identical option focus', () => {
40+
const a = createOptionFocus(1, 2)
41+
const b = createOptionFocus(1, 2)
42+
expect(isFocusEqual(a, b)).toBe(true)
43+
})
44+
45+
it('returns false for option with different question index', () => {
46+
const a = createOptionFocus(0, 1)
47+
const b = createOptionFocus(1, 1)
48+
expect(isFocusEqual(a, b)).toBe(false)
49+
})
50+
51+
it('returns false for option with different option index', () => {
52+
const a = createOptionFocus(1, 0)
53+
const b = createOptionFocus(1, 1)
54+
expect(isFocusEqual(a, b)).toBe(false)
55+
})
56+
57+
it('returns false for different focus types', () => {
58+
const option = createOptionFocus(0, 0)
59+
const textInput = createTextInputFocus(0)
60+
const confirmSubmit = createConfirmSubmitFocus()
61+
62+
expect(isFocusEqual(option, textInput)).toBe(false)
63+
expect(isFocusEqual(option, confirmSubmit)).toBe(false)
64+
expect(isFocusEqual(textInput, confirmSubmit)).toBe(false)
65+
})
66+
})
67+
68+
describe('isFocusOnQuestion', () => {
69+
it('returns true for option focus on specified question', () => {
70+
const focus = createOptionFocus(2, 1)
71+
expect(isFocusOnQuestion(focus, 2)).toBe(true)
72+
})
73+
74+
it('returns false for option focus on different question', () => {
75+
const focus = createOptionFocus(2, 1)
76+
expect(isFocusOnQuestion(focus, 1)).toBe(false)
77+
})
78+
79+
it('returns true for text input focus on specified question', () => {
80+
const focus = createTextInputFocus(3)
81+
expect(isFocusOnQuestion(focus, 3)).toBe(true)
82+
})
83+
84+
it('returns false for text input focus on different question', () => {
85+
const focus = createTextInputFocus(3)
86+
expect(isFocusOnQuestion(focus, 2)).toBe(false)
87+
})
88+
89+
it('returns false for confirm submit focus (not on any question)', () => {
90+
const focus = createConfirmSubmitFocus()
91+
expect(isFocusOnQuestion(focus, 0)).toBe(false)
92+
expect(isFocusOnQuestion(focus, 5)).toBe(false)
93+
})
94+
})
95+
96+
describe('isFocusOnSpecificOption', () => {
97+
it('returns true for exact match', () => {
98+
const focus = createOptionFocus(1, 2)
99+
expect(isFocusOnSpecificOption(focus, 1, 2)).toBe(true)
100+
})
101+
102+
it('returns false for different question index', () => {
103+
const focus = createOptionFocus(1, 2)
104+
expect(isFocusOnSpecificOption(focus, 0, 2)).toBe(false)
105+
})
106+
107+
it('returns false for different option index', () => {
108+
const focus = createOptionFocus(1, 2)
109+
expect(isFocusOnSpecificOption(focus, 1, 1)).toBe(false)
110+
})
111+
112+
it('returns false for text input focus', () => {
113+
const focus = createTextInputFocus(1)
114+
expect(isFocusOnSpecificOption(focus, 1, 0)).toBe(false)
115+
})
116+
117+
it('returns false for confirm submit focus', () => {
118+
const focus = createConfirmSubmitFocus()
119+
expect(isFocusOnSpecificOption(focus, 0, 0)).toBe(false)
120+
})
121+
})
122+
123+
describe('isFocusOnSpecificTextInput', () => {
124+
it('returns true for exact match', () => {
125+
const focus = createTextInputFocus(3)
126+
expect(isFocusOnSpecificTextInput(focus, 3)).toBe(true)
127+
})
128+
129+
it('returns false for different question index', () => {
130+
const focus = createTextInputFocus(3)
131+
expect(isFocusOnSpecificTextInput(focus, 2)).toBe(false)
132+
})
133+
134+
it('returns false for option focus', () => {
135+
const focus = createOptionFocus(3, 0)
136+
expect(isFocusOnSpecificTextInput(focus, 3)).toBe(false)
137+
})
138+
139+
it('returns false for confirm submit focus', () => {
140+
const focus = createConfirmSubmitFocus()
141+
expect(isFocusOnSpecificTextInput(focus, 0)).toBe(false)
142+
})
143+
})
144+
145+
describe('focusToString', () => {
146+
it('formats confirm submit focus', () => {
147+
const focus = createConfirmSubmitFocus()
148+
expect(focusToString(focus)).toBe('confirmSubmit')
149+
})
150+
151+
it('formats text input focus', () => {
152+
const focus = createTextInputFocus(2)
153+
expect(focusToString(focus)).toBe('textInput:Q2')
154+
})
155+
156+
it('formats option focus', () => {
157+
const focus = createOptionFocus(1, 3)
158+
expect(focusToString(focus)).toBe('option:Q1:O3')
159+
})
160+
161+
it('formats various question and option indices', () => {
162+
expect(focusToString(createOptionFocus(0, 0))).toBe('option:Q0:O0')
163+
expect(focusToString(createOptionFocus(5, 2))).toBe('option:Q5:O2')
164+
expect(focusToString(createTextInputFocus(0))).toBe('textInput:Q0')
165+
expect(focusToString(createTextInputFocus(10))).toBe('textInput:Q10')
166+
})
167+
})

0 commit comments

Comments
 (0)