Skip to content

Commit 79c3020

Browse files
committed
initial impl
1 parent 36ee12b commit 79c3020

File tree

22 files changed

+963
-7
lines changed

22 files changed

+963
-7
lines changed

.agents/base2/base2.ts

Lines changed: 1 addition & 0 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',

.agents/types/tools.ts

Lines changed: 17 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,19 @@ 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+
/** Array of answer options for the question (minimum 2) */
73+
options: string[]
74+
}[]
75+
}
76+
6277
/**
6378
* 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.
6479
*/
@@ -248,10 +263,10 @@ export interface WriteFileParams {
248263
}
249264

250265
/**
251-
* Write a todo list to track tasks. Use this frequently to maintain a step-by-step plan.
266+
* Write a todo list to track tasks for multi-step implementations. Use this frequently to maintain an updated step-by-step plan.
252267
*/
253268
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! */
269+
/** 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! */
255270
todos: {
256271
/** Description of the task */
257272
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'
@@ -113,6 +114,9 @@ export const Chat = ({
113114

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

117+
// Subscribe to ask_user bridge to trigger form display
118+
useAskUserBridge()
119+
116120
const {
117121
inputValue,
118122
cursorPosition,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react'
2+
import { TextAttributes } from '@opentui/core'
3+
import { useTheme } from '../../hooks/use-theme'
4+
import { BORDER_CHARS } from '../../utils/ui-constants'
5+
import type { AskUserContentBlock } from '../../types/chat'
6+
7+
interface AskUserBranchProps {
8+
block: AskUserContentBlock
9+
availableWidth: number
10+
}
11+
12+
export const AskUserBranch = ({ block, availableWidth }: AskUserBranchProps) => {
13+
const theme = useTheme()
14+
15+
return (
16+
<box
17+
style={{
18+
flexDirection: 'column',
19+
gap: 0,
20+
width: availableWidth,
21+
borderStyle: 'single',
22+
borderColor: theme.secondary,
23+
customBorderChars: BORDER_CHARS,
24+
padding: 1,
25+
marginTop: 1,
26+
marginBottom: 1,
27+
}}
28+
>
29+
{block.skipped ? (
30+
<text style={{ fg: theme.muted, attributes: TextAttributes.ITALIC }}>
31+
User skipped the questions.
32+
</text>
33+
) : (
34+
<box style={{ flexDirection: 'column', gap: 1 }}>
35+
<text style={{ fg: theme.secondary, attributes: TextAttributes.BOLD }}>
36+
User Answers:
37+
</text>
38+
{block.questions.map((q, idx) => {
39+
const answer = block.answers?.find((a) => a.questionIndex === idx)
40+
const displayAnswer = answer?.otherText
41+
? `"${answer.otherText}"`
42+
: answer?.selectedOption || 'No answer'
43+
const isCustomAnswer = !!answer?.otherText
44+
return (
45+
<box key={idx} style={{ flexDirection: 'column', gap: 0 }}>
46+
<text style={{ fg: theme.foreground }}>
47+
{idx + 1}. {q.question}
48+
</text>
49+
<text style={{
50+
fg: theme.primary,
51+
marginLeft: 2,
52+
attributes: isCustomAnswer ? TextAttributes.ITALIC : undefined,
53+
}}>
54+
{displayAnswer}
55+
</text>
56+
</box>
57+
)
58+
})}
59+
</box>
60+
)}
61+
</box>
62+
)
63+
}

cli/src/components/blocks/tool-branch.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export const ToolBranch = memo(
3636
if (toolBlock.toolName === 'end_turn') {
3737
return null
3838
}
39+
if (toolBlock.toolName === 'ask_user') {
40+
return null
41+
}
3942
if ('includeToolCall' in toolBlock && toolBlock.includeToolCall === false) {
4043
return null
4144
}

cli/src/components/chat-input-bar.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import React from 'react'
22
import { AgentModeToggle } from './agent-mode-toggle'
33
import { FeedbackContainer } from './feedback-container'
4+
import { MultipleChoiceForm } from './multiple-choice-form'
45
import { MultilineInput, type MultilineInputHandle } from './multiline-input'
56
import { SuggestionMenu, type SuggestionItem } from './suggestion-menu'
67
import { UsageBanner } from './usage-banner'
78
import { BORDER_CHARS } from '../utils/ui-constants'
89
import { useTheme } from '../hooks/use-theme'
910
import { useChatStore } from '../state/chat-store'
11+
import { useAskUserBridge } from '../hooks/use-ask-user-bridge'
1012
import type { AgentMode } from '../utils/constants'
1113
import type { InputValue } from '../state/chat-store'
1214

@@ -84,6 +86,11 @@ export const ChatInputBar = ({
8486
}: ChatInputBarProps) => {
8587
const isBashMode = useChatStore((state) => state.isBashMode)
8688
const setBashMode = useChatStore((state) => state.setBashMode)
89+
const askUserState = useChatStore((state) => state.askUserState)
90+
const updateAskUserAnswer = useChatStore((state) => state.updateAskUserAnswer)
91+
const updateAskUserOtherText = useChatStore((state) => state.updateAskUserOtherText)
92+
const { submitAnswers, skip } = useAskUserBridge()
93+
8794
if (feedbackMode) {
8895
return (
8996
<FeedbackContainer
@@ -115,12 +122,66 @@ export const ChatInputBar = ({
115122
setInputValue(value)
116123
}
117124

125+
const handleFormSubmit = (finalAnswers?: number[], finalOtherTexts?: string[]) => {
126+
console.log('[ChatInputBar] handleFormSubmit called', { askUserState, finalAnswers, finalOtherTexts })
127+
if (!askUserState) return
128+
129+
// Use final values if provided (for immediate submission), otherwise use current state
130+
const answersToUse = finalAnswers || askUserState.selectedAnswers
131+
const otherTextsToUse = finalOtherTexts || askUserState.otherTexts
132+
133+
const answers = askUserState.questions.map((q, idx) => {
134+
const otherText = otherTextsToUse[idx]?.trim()
135+
if (otherText) {
136+
// User provided custom text
137+
return {
138+
questionIndex: idx,
139+
otherText,
140+
}
141+
} else {
142+
// User selected an option
143+
return {
144+
questionIndex: idx,
145+
selectedOption: q.options[answersToUse[idx]],
146+
}
147+
}
148+
})
149+
console.log('[ChatInputBar] Submitting answers', { answers })
150+
submitAnswers(answers)
151+
}
152+
118153
// Adjust input width for bash mode (subtract 2 for '!' column)
119154
const adjustedInputWidth = isBashMode ? inputWidth - 2 : inputWidth
120155
const effectivePlaceholder = isBashMode
121156
? 'Enter bash command...'
122157
: inputPlaceholder
123158

159+
if (askUserState) {
160+
return (
161+
<box
162+
title=" Action Required "
163+
titleAlignment="center"
164+
style={{
165+
width: '100%',
166+
borderStyle: 'single',
167+
borderColor: theme.primary,
168+
customBorderChars: BORDER_CHARS,
169+
}}
170+
>
171+
<MultipleChoiceForm
172+
questions={askUserState.questions}
173+
selectedAnswers={askUserState.selectedAnswers}
174+
otherTexts={askUserState.otherTexts}
175+
onSelectAnswer={updateAskUserAnswer}
176+
onOtherTextChange={updateAskUserOtherText}
177+
onSubmit={handleFormSubmit}
178+
onSkip={skip}
179+
width={inputWidth}
180+
/>
181+
</box>
182+
)
183+
}
184+
124185
return (
125186
<>
126187
<box

cli/src/components/message-block.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
selectHasSubmittedFeedback,
1717
selectMessageFeedbackCategory,
1818
} from '../state/feedback-store'
19-
import { isTextBlock, isToolBlock } from '../types/chat'
19+
import { isTextBlock } from '../types/chat'
2020
import { shouldRenderAsSimpleText } from '../utils/constants'
2121
import {
2222
isImplementorAgent,
@@ -28,6 +28,7 @@ import { AgentListBranch } from './blocks/agent-list-branch'
2828
import { ContentWithMarkdown } from './blocks/content-with-markdown'
2929
import { ThinkingBlock } from './blocks/thinking-block'
3030
import { ToolBranch } from './blocks/tool-branch'
31+
import { AskUserBranch } from './blocks/ask-user-branch'
3132
import { PlanBox } from './renderers/plan-box'
3233

3334
import type {
@@ -36,6 +37,7 @@ import type {
3637
HtmlContentBlock,
3738
AgentContentBlock,
3839
} from '../types/chat'
40+
import { isAskUserBlock, isToolBlock } from '../types/chat'
3941
import type { ThemeColor } from '../types/theme-system'
4042

4143
interface MessageBlockProps {
@@ -434,6 +436,7 @@ const isRenderableTimelineBlock = (
434436
case 'agent-list':
435437
case 'plan':
436438
case 'mode-divider':
439+
case 'ask-user':
437440
return true
438441
default:
439442
return false
@@ -1050,6 +1053,16 @@ const SingleBlock = memo(
10501053
return null
10511054
}
10521055

1056+
case 'ask-user': {
1057+
return (
1058+
<AskUserBranch
1059+
key={`${messageId}-ask-user-${idx}`}
1060+
block={block}
1061+
availableWidth={availableWidth}
1062+
/>
1063+
)
1064+
}
1065+
10531066
case 'agent': {
10541067
return (
10551068
<AgentBranchWrapper

0 commit comments

Comments
 (0)