mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-03-17 02:43:09 +00:00
feat: add structured questions (AskUserQuestion) to assistant chat
Add interactive multiple-choice question support to the project assistant, allowing it to present clickable options when clarification is needed. Backend changes: - Add ask_user MCP tool to feature_mcp.py with input validation - Add mcp__features__ask_user to assistant allowed tools list - Intercept ask_user tool calls in _query_claude() to yield question messages - Add answer WebSocket message handler in assistant_chat router - Document ask_user tool in assistant system prompt Frontend changes: - Add AssistantChatQuestionMessage type and update server message union - Add currentQuestions state and sendAnswer() to useAssistantChat hook - Handle question WebSocket messages by attaching to last assistant message - Render QuestionOptions component between messages and input area - Disable text input while structured questions are active Flow: Claude calls ask_user → backend intercepts → WebSocket question message → frontend renders QuestionOptions → user clicks options → answer sent back → Claude receives formatted answer and continues conversation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2
ui/package-lock.json
generated
2
ui/package-lock.json
generated
@@ -55,7 +55,7 @@
|
||||
},
|
||||
"..": {
|
||||
"name": "autoforge-ai",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"license": "AGPL-3.0",
|
||||
"bin": {
|
||||
"autoforge": "bin/autoforge.js"
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Send, Loader2, Wifi, WifiOff, Plus, History } from 'lucide-react'
|
||||
import { useAssistantChat } from '../hooks/useAssistantChat'
|
||||
import { ChatMessage as ChatMessageComponent } from './ChatMessage'
|
||||
import { ConversationHistory } from './ConversationHistory'
|
||||
import { QuestionOptions } from './QuestionOptions'
|
||||
import type { ChatMessage } from '../lib/types'
|
||||
import { isSubmitEnter } from '../lib/keyboard'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -52,8 +53,10 @@ export function AssistantChat({
|
||||
isLoading,
|
||||
connectionStatus,
|
||||
conversationId: activeConversationId,
|
||||
currentQuestions,
|
||||
start,
|
||||
sendMessage,
|
||||
sendAnswer,
|
||||
clearMessages,
|
||||
} = useAssistantChat({
|
||||
projectName,
|
||||
@@ -268,6 +271,16 @@ export function AssistantChat({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Structured questions from assistant */}
|
||||
{currentQuestions && (
|
||||
<div className="border-t border-border bg-background">
|
||||
<QuestionOptions
|
||||
questions={currentQuestions}
|
||||
onSubmit={sendAnswer}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Input area */}
|
||||
<div className="border-t border-border p-4 bg-card">
|
||||
<div className="flex gap-2">
|
||||
@@ -277,13 +290,13 @@ export function AssistantChat({
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Ask about the codebase..."
|
||||
disabled={isLoading || isLoadingConversation || connectionStatus !== 'connected'}
|
||||
disabled={isLoading || isLoadingConversation || connectionStatus !== 'connected' || !!currentQuestions}
|
||||
className="flex-1 resize-none min-h-[44px] max-h-[120px]"
|
||||
rows={1}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={!inputValue.trim() || isLoading || isLoadingConversation || connectionStatus !== 'connected'}
|
||||
disabled={!inputValue.trim() || isLoading || isLoadingConversation || connectionStatus !== 'connected' || !!currentQuestions}
|
||||
title="Send message"
|
||||
>
|
||||
{isLoading ? (
|
||||
@@ -294,7 +307,7 @@ export function AssistantChat({
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
{currentQuestions ? 'Select an option above to continue' : 'Press Enter to send, Shift+Enter for new line'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import type { ChatMessage, AssistantChatServerMessage } from "../lib/types";
|
||||
import type { ChatMessage, AssistantChatServerMessage, SpecQuestion } from "../lib/types";
|
||||
|
||||
type ConnectionStatus = "disconnected" | "connecting" | "connected" | "error";
|
||||
|
||||
@@ -17,8 +17,10 @@ interface UseAssistantChatReturn {
|
||||
isLoading: boolean;
|
||||
connectionStatus: ConnectionStatus;
|
||||
conversationId: number | null;
|
||||
currentQuestions: SpecQuestion[] | null;
|
||||
start: (conversationId?: number | null) => void;
|
||||
sendMessage: (content: string) => void;
|
||||
sendAnswer: (answers: Record<string, string | string[]>) => void;
|
||||
disconnect: () => void;
|
||||
clearMessages: () => void;
|
||||
}
|
||||
@@ -36,6 +38,7 @@ export function useAssistantChat({
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("disconnected");
|
||||
const [conversationId, setConversationId] = useState<number | null>(null);
|
||||
const [currentQuestions, setCurrentQuestions] = useState<SpecQuestion[] | null>(null);
|
||||
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const currentAssistantMessageRef = useRef<string | null>(null);
|
||||
@@ -204,6 +207,25 @@ export function useAssistantChat({
|
||||
break;
|
||||
}
|
||||
|
||||
case "question": {
|
||||
// Claude is asking structured questions via ask_user tool
|
||||
setCurrentQuestions(data.questions);
|
||||
setIsLoading(false);
|
||||
|
||||
// Attach questions to the last assistant message for display context
|
||||
setMessages((prev) => {
|
||||
const lastMessage = prev[prev.length - 1];
|
||||
if (lastMessage?.role === "assistant" && lastMessage.isStreaming) {
|
||||
return [
|
||||
...prev.slice(0, -1),
|
||||
{ ...lastMessage, isStreaming: false, questions: data.questions },
|
||||
];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "conversation_created": {
|
||||
setConversationId(data.conversation_id);
|
||||
break;
|
||||
@@ -327,6 +349,49 @@ export function useAssistantChat({
|
||||
[onError],
|
||||
);
|
||||
|
||||
const sendAnswer = useCallback(
|
||||
(answers: Record<string, string | string[]>) => {
|
||||
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
|
||||
onError?.("Not connected");
|
||||
return;
|
||||
}
|
||||
|
||||
// Format answers as display text for user message
|
||||
const answerParts: string[] = [];
|
||||
for (const [, value] of Object.entries(answers)) {
|
||||
if (Array.isArray(value)) {
|
||||
answerParts.push(value.join(", "));
|
||||
} else {
|
||||
answerParts.push(value);
|
||||
}
|
||||
}
|
||||
const displayText = answerParts.join("; ");
|
||||
|
||||
// Add user message to chat
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: generateId(),
|
||||
role: "user",
|
||||
content: displayText,
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
setCurrentQuestions(null);
|
||||
setIsLoading(true);
|
||||
|
||||
// Send structured answer to server
|
||||
wsRef.current.send(
|
||||
JSON.stringify({
|
||||
type: "answer",
|
||||
answers,
|
||||
}),
|
||||
);
|
||||
},
|
||||
[onError],
|
||||
);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
reconnectAttempts.current = maxReconnectAttempts; // Prevent reconnection
|
||||
if (pingIntervalRef.current) {
|
||||
@@ -350,8 +415,10 @@ export function useAssistantChat({
|
||||
isLoading,
|
||||
connectionStatus,
|
||||
conversationId,
|
||||
currentQuestions,
|
||||
start,
|
||||
sendMessage,
|
||||
sendAnswer,
|
||||
disconnect,
|
||||
clearMessages,
|
||||
};
|
||||
|
||||
@@ -465,6 +465,11 @@ export interface AssistantChatConversationCreatedMessage {
|
||||
conversation_id: number
|
||||
}
|
||||
|
||||
export interface AssistantChatQuestionMessage {
|
||||
type: 'question'
|
||||
questions: SpecQuestion[]
|
||||
}
|
||||
|
||||
export interface AssistantChatPongMessage {
|
||||
type: 'pong'
|
||||
}
|
||||
@@ -472,6 +477,7 @@ export interface AssistantChatPongMessage {
|
||||
export type AssistantChatServerMessage =
|
||||
| AssistantChatTextMessage
|
||||
| AssistantChatToolCallMessage
|
||||
| AssistantChatQuestionMessage
|
||||
| AssistantChatResponseDoneMessage
|
||||
| AssistantChatErrorMessage
|
||||
| AssistantChatConversationCreatedMessage
|
||||
|
||||
Reference in New Issue
Block a user