+
+
+ {t("title")}
+
+
+
+ {question.question}
+
+
+
+
+
+ )
+}
diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx
index 528b344..d61e210 100644
--- a/src/components/conversations/conversation-detail-panel.tsx
+++ b/src/components/conversations/conversation-detail-panel.tsx
@@ -572,6 +572,36 @@ const ConversationTabView = memo(function ConversationTabView({
[connConnect, connDisconnect, workingDirForConnection]
)
+ const handleAnswerQuestion = useCallback(
+ (answer: string) => {
+ if (connStatus !== "connected") return
+ const optimisticTurn: MessageTurn = {
+ id: `optimistic-${crypto.randomUUID()}`,
+ role: "user",
+ blocks: [{ type: "text", text: answer }],
+ timestamp: new Date().toISOString(),
+ }
+ appendOptimisticTurn(
+ effectiveConversationId,
+ optimisticTurn,
+ optimisticTurn.id
+ )
+ setSendSignal((prev) => prev + 1)
+ setSyncState(effectiveConversationId, "awaiting_persist")
+ lifecycleSend(
+ { blocks: [{ type: "text", text: answer }], displayText: answer },
+ null
+ )
+ },
+ [
+ appendOptimisticTurn,
+ connStatus,
+ effectiveConversationId,
+ lifecycleSend,
+ setSyncState,
+ ]
+ )
+
const showDraftHeader = !hasPersistedConversation
const isWelcomeMode = showDraftHeader && !hasSentMessage
@@ -596,10 +626,12 @@ const ConversationTabView = memo(function ConversationTabView({
defaultPath={workingDirForConnection}
error={conn.error}
pendingPermission={conn.pendingPermission}
+ pendingQuestion={conn.pendingQuestion}
onFocus={handleFocus}
onSend={handleSend}
onCancel={handleCancel}
onRespondPermission={handleRespondPermission}
+ onAnswerQuestion={handleAnswerQuestion}
modes={connectionModes}
configOptions={connectionConfigOptions}
modeLoading={modeLoading}
diff --git a/src/contexts/acp-connections-context.tsx b/src/contexts/acp-connections-context.tsx
index dd58c4d..8ee0010 100644
--- a/src/contexts/acp-connections-context.tsx
+++ b/src/contexts/acp-connections-context.tsx
@@ -12,6 +12,7 @@ import {
import { useTranslations } from "next-intl"
import { listen, type UnlistenFn } from "@tauri-apps/api/event"
import { disposeTauriListener } from "@/lib/tauri-listener"
+import { inferLiveToolName } from "@/lib/tool-call-normalization"
import {
acpConnect,
acpListAgents,
@@ -63,6 +64,11 @@ export interface PendingPermission {
options: PermissionOptionInfo[]
}
+export interface PendingQuestion {
+ tool_call_id: string
+ question: string
+}
+
export type LiveContentBlock =
| { type: "text"; text: string }
| { type: "thinking"; text: string }
@@ -92,6 +98,7 @@ export interface ConnectionState {
usage: SessionUsageUpdateInfo | null
liveMessage: LiveMessage | null
pendingPermission: PendingPermission | null
+ pendingQuestion: PendingQuestion | null
error: string | null
}
@@ -147,6 +154,12 @@ type Action =
options: PermissionOptionInfo[]
}
| { type: "PERMISSION_CLEARED"; contextKey: string }
+ | {
+ type: "SET_PENDING_QUESTION"
+ contextKey: string
+ pendingQuestion: PendingQuestion
+ }
+ | { type: "CLEAR_PENDING_QUESTION"; contextKey: string }
| { type: "SESSION_STARTED"; contextKey: string; sessionId: string }
| {
type: "SESSION_MODES"
@@ -259,6 +272,23 @@ function extractPermissionToolKind(toolCall: unknown): string | null {
return null
}
+function extractQuestionText(rawInput: string | null): string | null {
+ if (!rawInput) return null
+ try {
+ const parsed = JSON.parse(rawInput)
+ if (
+ parsed &&
+ typeof parsed === "object" &&
+ typeof parsed.question === "string"
+ ) {
+ return parsed.question
+ }
+ } catch {
+ // not JSON, try using rawInput as-is if it looks like a question
+ }
+ return null
+}
+
function sameModes(
a: SessionModeStateInfo | null,
b: SessionModeStateInfo
@@ -451,6 +481,7 @@ function connectionsReducer(
usage: null,
liveMessage: null,
pendingPermission: null,
+ pendingQuestion: null,
error: null,
})
return next
@@ -477,6 +508,7 @@ function connectionsReducer(
content: [],
startedAt: Date.now(),
}
+ updated.pendingQuestion = null
updated.error = null
}
next.set(action.contextKey, updated)
@@ -741,6 +773,28 @@ function connectionsReducer(
return next
}
+ case "SET_PENDING_QUESTION": {
+ const conn = state.get(action.contextKey)
+ if (!conn) return state
+ const next = new Map(state)
+ next.set(action.contextKey, {
+ ...conn,
+ pendingQuestion: action.pendingQuestion,
+ })
+ return next
+ }
+
+ case "CLEAR_PENDING_QUESTION": {
+ const conn = state.get(action.contextKey)
+ if (!conn) return state
+ const next = new Map(state)
+ next.set(action.contextKey, {
+ ...conn,
+ pendingQuestion: null,
+ })
+ return next
+ }
+
case "SESSION_STARTED": {
const conn = state.get(action.contextKey)
if (!conn) return state
@@ -1398,14 +1452,43 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) {
entries: e.entries,
})
break
- case "turn_complete":
+ case "turn_complete": {
flushStreamingQueue()
dispatch({
type: "STATUS_CHANGED",
contextKey,
status: "connected",
})
+ // Detect pending question from tool calls in the completed turn
+ const turnConn = storeRef.current.connections.get(contextKey)
+ if (turnConn?.liveMessage) {
+ const blocks = turnConn.liveMessage.content
+ for (let i = blocks.length - 1; i >= 0; i--) {
+ const block = blocks[i]
+ if (block.type !== "tool_call") continue
+ const normalized = inferLiveToolName({
+ title: block.info.title,
+ kind: block.info.kind,
+ rawInput: block.info.raw_input,
+ })
+ if (normalized === "question") {
+ const questionText = extractQuestionText(block.info.raw_input)
+ if (questionText) {
+ dispatch({
+ type: "SET_PENDING_QUESTION",
+ contextKey,
+ pendingQuestion: {
+ tool_call_id: block.info.tool_call_id,
+ question: questionText,
+ },
+ })
+ }
+ break
+ }
+ }
+ }
break
+ }
case "error":
flushStreamingQueue()
dispatch({ type: "ERROR", contextKey, message: e.message })
diff --git a/src/hooks/use-connection.ts b/src/hooks/use-connection.ts
index c39e943..c31c604 100644
--- a/src/hooks/use-connection.ts
+++ b/src/hooks/use-connection.ts
@@ -8,6 +8,7 @@ import {
type ConnectOptions,
type LiveMessage,
type PendingPermission,
+ type PendingQuestion,
} from "@/contexts/acp-connections-context"
import type {
AgentType,
@@ -36,6 +37,7 @@ export interface UseConnectionReturn {
availableCommands: AvailableCommandInfo[] | null
liveMessage: LiveMessage | null
pendingPermission: PendingPermission | null
+ pendingQuestion: PendingQuestion | null
error: string | null
connect: (
agentType: AgentType,
@@ -81,6 +83,7 @@ export function useConnection(contextKey: string): UseConnectionReturn {
const availableCommands = connection?.availableCommands ?? null
const liveMessage = connection?.liveMessage ?? null
const pendingPermission = connection?.pendingPermission ?? null
+ const pendingQuestion = connection?.pendingQuestion ?? null
const error = connection?.error ?? null
const connect = useCallback(
@@ -137,6 +140,7 @@ export function useConnection(contextKey: string): UseConnectionReturn {
availableCommands,
liveMessage,
pendingPermission,
+ pendingQuestion,
error,
connect,
disconnect,
@@ -157,6 +161,7 @@ export function useConnection(contextKey: string): UseConnectionReturn {
availableCommands,
liveMessage,
pendingPermission,
+ pendingQuestion,
error,
connect,
disconnect,
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json
index 27bb466..cacd3df 100644
--- a/src/i18n/messages/ar.json
+++ b/src/i18n/messages/ar.json
@@ -1175,6 +1175,11 @@
"plan": "الخطة",
"targetMode": "وضع الهدف: {mode}"
},
+ "questionDialog": {
+ "title": "الوكيل يطرح سؤالاً",
+ "placeholder": "اكتب إجابتك...",
+ "send": "إرسال"
+ },
"messageBranch": {
"previousBranchAria": "الفرع السابق",
"nextBranchAria": "الفرع التالي",
diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json
index 8a7a168..b132d56 100644
--- a/src/i18n/messages/de.json
+++ b/src/i18n/messages/de.json
@@ -1175,6 +1175,11 @@
"plan": "Arbeitsplan",
"targetMode": "Zielmodus: {mode}"
},
+ "questionDialog": {
+ "title": "Agent stellt eine Frage",
+ "placeholder": "Antwort eingeben...",
+ "send": "Senden"
+ },
"messageBranch": {
"previousBranchAria": "Vorheriger Branch",
"nextBranchAria": "Nächster Branch",
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 5cab814..556a45d 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -1175,6 +1175,11 @@
"plan": "Plan",
"targetMode": "Target mode: {mode}"
},
+ "questionDialog": {
+ "title": "Agent is asking a question",
+ "placeholder": "Type your answer...",
+ "send": "Send"
+ },
"messageBranch": {
"previousBranchAria": "Previous branch",
"nextBranchAria": "Next branch",
diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json
index ef5d4b7..a06a9e6 100644
--- a/src/i18n/messages/es.json
+++ b/src/i18n/messages/es.json
@@ -1175,6 +1175,11 @@
"plan": "Plan de trabajo",
"targetMode": "Modo objetivo: {mode}"
},
+ "questionDialog": {
+ "title": "El agente está haciendo una pregunta",
+ "placeholder": "Escribe tu respuesta...",
+ "send": "Enviar"
+ },
"messageBranch": {
"previousBranchAria": "Rama anterior",
"nextBranchAria": "Rama siguiente",
diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json
index 8f6627b..c2fc8cf 100644
--- a/src/i18n/messages/fr.json
+++ b/src/i18n/messages/fr.json
@@ -1175,6 +1175,11 @@
"plan": "Plan de travail",
"targetMode": "Mode cible : {mode}"
},
+ "questionDialog": {
+ "title": "L'agent pose une question",
+ "placeholder": "Tapez votre réponse...",
+ "send": "Envoyer"
+ },
"messageBranch": {
"previousBranchAria": "Branche précédente",
"nextBranchAria": "Branche suivante",
diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json
index 8d16e5a..748ea06 100644
--- a/src/i18n/messages/ja.json
+++ b/src/i18n/messages/ja.json
@@ -1175,6 +1175,11 @@
"plan": "計画",
"targetMode": "対象モード: {mode}"
},
+ "questionDialog": {
+ "title": "エージェントが質問しています",
+ "placeholder": "回答を入力...",
+ "send": "送信"
+ },
"messageBranch": {
"previousBranchAria": "前のブランチ",
"nextBranchAria": "次のブランチ",
diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json
index b0a675a..ae10334 100644
--- a/src/i18n/messages/ko.json
+++ b/src/i18n/messages/ko.json
@@ -1175,6 +1175,11 @@
"plan": "계획",
"targetMode": "대상 모드: {mode}"
},
+ "questionDialog": {
+ "title": "에이전트가 질문하고 있습니다",
+ "placeholder": "답변을 입력하세요...",
+ "send": "전송"
+ },
"messageBranch": {
"previousBranchAria": "이전 브랜치",
"nextBranchAria": "다음 브랜치",
diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json
index 91a31c5..7e38f1c 100644
--- a/src/i18n/messages/pt.json
+++ b/src/i18n/messages/pt.json
@@ -1175,6 +1175,11 @@
"plan": "Plano",
"targetMode": "Modo de destino: {mode}"
},
+ "questionDialog": {
+ "title": "O agente está fazendo uma pergunta",
+ "placeholder": "Digite sua resposta...",
+ "send": "Enviar"
+ },
"messageBranch": {
"previousBranchAria": "Branch anterior",
"nextBranchAria": "Próxima branch",
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json
index ebdfe4b..cfb3bc4 100644
--- a/src/i18n/messages/zh-CN.json
+++ b/src/i18n/messages/zh-CN.json
@@ -1175,6 +1175,11 @@
"plan": "计划",
"targetMode": "目标模式:{mode}"
},
+ "questionDialog": {
+ "title": "代理正在提问",
+ "placeholder": "输入你的回答...",
+ "send": "发送"
+ },
"messageBranch": {
"previousBranchAria": "上一分支",
"nextBranchAria": "下一分支",
diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json
index 4d2ef2d..7716659 100644
--- a/src/i18n/messages/zh-TW.json
+++ b/src/i18n/messages/zh-TW.json
@@ -1175,6 +1175,11 @@
"plan": "計畫",
"targetMode": "目標模式:{mode}"
},
+ "questionDialog": {
+ "title": "代理正在提問",
+ "placeholder": "輸入你的回答...",
+ "send": "傳送"
+ },
"messageBranch": {
"previousBranchAria": "上一個分支",
"nextBranchAria": "下一個分支",
diff --git a/src/lib/tool-call-normalization.ts b/src/lib/tool-call-normalization.ts
index 8456fa8..607bae5 100644
--- a/src/lib/tool-call-normalization.ts
+++ b/src/lib/tool-call-normalization.ts
@@ -51,6 +51,8 @@ const EXACT_TOOL_NAME_ALIASES: Record