diff --git a/packages/ui/src/hooks/useChat.ts b/packages/ui/src/hooks/useChat.ts
index 4fce528..a6f7c4a 100644
--- a/packages/ui/src/hooks/useChat.ts
+++ b/packages/ui/src/hooks/useChat.ts
@@ -13,6 +13,8 @@ import type {
ToolEndPayload,
MessagePart,
ToolMessagePart,
+ QuestionMessagePart,
+ Question,
AgentModeType,
SubagentStartPayload,
SubagentEndPayload,
@@ -240,7 +242,54 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
setState((prev) => {
if (!prev.streamingMessage) return prev;
- // 查找并更新对应的工具 part
+ // 查找完成的工具
+ const completedTool = prev.streamingMessage.parts.find(
+ (part) => part.type === 'tool' && part.id === payload.id
+ ) as ToolMessagePart | undefined;
+
+ // 检查是否为 ask_user_question 工具
+ const isAskUserQuestion = completedTool?.toolName === 'ask_user_question';
+
+ // 如果是 ask_user_question 且成功,转换为 QuestionMessagePart
+ if (isAskUserQuestion && payload.status === 'completed' && payload.result) {
+ const result = payload.result as {
+ metadata?: {
+ type?: string;
+ questions?: Array<{ index: number; header?: string; optionCount: number; multiSelect: boolean }>;
+ requiresUserInput?: boolean;
+ };
+ };
+
+ // 从工具参数中提取完整的问题数据
+ const questionsArg = completedTool?.arguments?.questions as Question[] | undefined;
+
+ if (result.metadata?.requiresUserInput && questionsArg) {
+ // 创建 QuestionMessagePart 替换原来的 tool part
+ const questionPart: QuestionMessagePart = {
+ type: 'question',
+ id: payload.id,
+ questions: questionsArg,
+ answered: false,
+ };
+
+ const parts = prev.streamingMessage.parts.map((part) => {
+ if (part.type === 'tool' && part.id === payload.id) {
+ return questionPart;
+ }
+ return part;
+ });
+
+ return {
+ ...prev,
+ streamingMessage: {
+ ...prev.streamingMessage,
+ parts,
+ },
+ };
+ }
+ }
+
+ // 查找并更新对应的工具 part(普通工具)
const parts = prev.streamingMessage.parts.map((part) => {
if (part.type === 'tool' && part.id === payload.id) {
return {
@@ -255,10 +304,7 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
});
// 查找完成的工具是否为 task,如果是则恢复主 agent
- const completedTool = prev.streamingMessage.parts.find(
- (part) => part.type === 'tool' && part.id === payload.id
- );
- const isTaskTool = completedTool?.type === 'tool' && completedTool.toolName === 'task';
+ const isTaskTool = completedTool?.toolName === 'task';
const newAgent = isTaskTool ? prev.agentMode : prev.currentAgent;
return {
@@ -606,6 +652,89 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
[sessionId]
);
+ // 回答问题(ask_user_question 工具)
+ const answerQuestion = useCallback(
+ (questionPartId: string, answers: string[]) => {
+ // 更新问题状态为已回答
+ setState((prev) => {
+ // 更新流式消息中的问题
+ if (prev.streamingMessage) {
+ const parts = prev.streamingMessage.parts.map((part) => {
+ if (part.type === 'question' && part.id === questionPartId) {
+ return {
+ ...part,
+ answered: true,
+ answers,
+ } as QuestionMessagePart;
+ }
+ return part;
+ });
+
+ // 发送回答作为用户消息
+ const answerText = answers.filter((a) => a).join('\n');
+ if (answerText && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
+ wsRef.current.send(
+ JSON.stringify({
+ type: 'message',
+ sessionId,
+ payload: {
+ content: answerText,
+ agentMode: state.agentMode,
+ autoApprove: state.autoApprove,
+ },
+ })
+ );
+ }
+
+ return {
+ ...prev,
+ streamingMessage: {
+ ...prev.streamingMessage,
+ parts,
+ },
+ };
+ }
+
+ // 也检查已完成的消息
+ const messages = prev.messages.map((msg) => {
+ if (msg.parts) {
+ const parts = msg.parts.map((part) => {
+ if (part.type === 'question' && part.id === questionPartId) {
+ return {
+ ...part,
+ answered: true,
+ answers,
+ } as QuestionMessagePart;
+ }
+ return part;
+ });
+ return { ...msg, parts };
+ }
+ return msg;
+ });
+
+ // 发送回答作为用户消息
+ const answerText = answers.filter((a) => a).join('\n');
+ if (answerText && wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
+ wsRef.current.send(
+ JSON.stringify({
+ type: 'message',
+ sessionId,
+ payload: {
+ content: answerText,
+ agentMode: state.agentMode,
+ autoApprove: state.autoApprove,
+ },
+ })
+ );
+ }
+
+ return { ...prev, messages };
+ });
+ },
+ [sessionId, state.agentMode, state.autoApprove]
+ );
+
// 初始化
useEffect(() => {
// 重置状态
@@ -659,5 +788,6 @@ export function useChat({ sessionId, onError, onSessionNotFound, onSessionUpdate
denyPermission,
setAgentMode,
setAutoApprove,
+ answerQuestion,
};
}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index b3828fc..b3d8ec9 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -176,6 +176,10 @@ export type {
FileSearchResponse,
// Agent Mode types
AgentModeType,
+ // Question types (for ask_user_question tool)
+ QuestionOption,
+ Question,
+ QuestionMessagePart,
} from './api/client.js';
// Primitives (shadcn/ui style)
@@ -222,6 +226,7 @@ export { Skeleton, MessageSkeleton, SessionSkeleton, FileSkeleton } from './comp
export { Markdown } from './components/Markdown.js';
export { CodeBlock, InlineCode } from './components/CodeBlock.js';
export { SubagentProgress, SubagentProgressCompact } from './components/SubagentProgress.js';
+export { AskUserQuestion } from './components/AskUserQuestion.js';
// Toast function (re-export from sonner)
export { toast } from 'sonner';
diff --git a/packages/web/src/pages/Chat.tsx b/packages/web/src/pages/Chat.tsx
index e061fd0..162be49 100644
--- a/packages/web/src/pages/Chat.tsx
+++ b/packages/web/src/pages/Chat.tsx
@@ -65,6 +65,7 @@ export function ChatPage({
setAutoApprove,
currentAgent,
currentSubagent,
+ answerQuestion,
} = useChat({
sessionId,
onError: (error) => {
@@ -291,13 +292,13 @@ export function ChatPage({
{messages.map((message) => (
-
+
))}
{/* 流式消息 - 复用 ChatMessage 组件 */}
{streamingMessage && (
-
+
)}
{/* 子 Agent 进度显示 */}