diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index cf97da2..cf83bf8 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -1,5 +1,5 @@ use std::collections::{BTreeMap, HashMap, HashSet}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Arc; use sacp::schema::McpServerStdio; @@ -706,6 +706,47 @@ fn resolve_working_dir(working_dir: Option<&str>) -> PathBuf { } } +fn claude_raw_sdk_session_meta( + agent_type: AgentType, +) -> Option> { + if agent_type != AgentType::ClaudeCode { + return None; + } + + let mut claude_code = serde_json::Map::new(); + claude_code.insert( + "emitRawSDKMessages".to_string(), + serde_json::Value::Bool(true), + ); + + let mut meta = serde_json::Map::new(); + meta.insert( + "claudeCode".to_string(), + serde_json::Value::Object(claude_code), + ); + Some(meta) +} + +fn build_new_session_request(agent_type: AgentType, cwd: &Path) -> NewSessionRequest { + let mut req = NewSessionRequest::new(cwd.to_path_buf()); + if let Some(meta) = claude_raw_sdk_session_meta(agent_type) { + req = req.meta(meta); + } + req +} + +fn build_load_session_request( + agent_type: AgentType, + session_id: SessionId, + cwd: &Path, +) -> LoadSessionRequest { + let mut req = LoadSessionRequest::new(session_id, cwd.to_path_buf()); + if let Some(meta) = claude_raw_sdk_session_meta(agent_type) { + req = req.meta(meta); + } + req +} + /// The main ACP connection loop. async fn run_connection( agent: AcpAgent, @@ -926,7 +967,8 @@ async fn run_connection( if let Some(sid) = session_id { // Load existing session via session/load - let load_req = LoadSessionRequest::new(SessionId::new(sid.clone()), &cwd); + let load_req = + build_load_session_request(agent_type, SessionId::new(sid.clone()), &cwd); let load_result = cx.send_request_to(Agent, load_req).block_task().await; match load_result { @@ -963,7 +1005,11 @@ async fn run_connection( Ok(()) }) .await - .otherwise_ignore(); + .otherwise(async |dispatch| { + maybe_emit_claude_sdk_ext_notification(&cid, &h, dispatch); + Ok(()) + }) + .await; } } if drained > 0 { @@ -1042,7 +1088,7 @@ async fn run_connection( ); } let new_resp = cx - .send_request_to(Agent, NewSessionRequest::new(cwd.clone())) + .send_request_to(Agent, build_new_session_request(agent_type, &cwd)) .block_task() .await?; let fallback_sid = new_resp.session_id.0.to_string(); @@ -1099,7 +1145,7 @@ async fn run_connection( } else { // Create new session let new_resp = cx - .send_request_to(Agent, NewSessionRequest::new(cwd.clone())) + .send_request_to(Agent, build_new_session_request(agent_type, &cwd)) .block_task() .await?; let sid = new_resp.session_id.0.to_string(); @@ -1778,7 +1824,11 @@ async fn run_conversation_loop<'a>( }, ) .await - .otherwise_ignore(); + .otherwise(async |dispatch| { + maybe_emit_claude_sdk_ext_notification(&cid, &h, dispatch); + Ok(()) + }) + .await; } Ok(_) => {} Err(e) => { @@ -1879,7 +1929,11 @@ async fn run_conversation_loop<'a>( }, ) .await - .otherwise_ignore() + .otherwise(async |dispatch| { + maybe_emit_claude_sdk_ext_notification(&cid, &h, dispatch); + Ok(()) + }) + .await { eprintln!("[ACP] Ignoring dispatch parse error: {e}"); } @@ -2347,6 +2401,58 @@ fn map_plan_entries(plan: &Plan) -> Vec { .collect() } +fn parse_claude_sdk_message_params( + params: &serde_json::Value, +) -> Option<(String, serde_json::Value)> { + let obj = params.as_object()?; + let session_id = obj.get("sessionId")?.as_str()?.to_string(); + let message = obj.get("message")?.clone(); + Some((session_id, message)) +} + +fn is_claude_api_retry_message(message: &serde_json::Value) -> bool { + let obj = match message.as_object() { + Some(obj) => obj, + None => return false, + }; + let message_type = obj.get("type").and_then(|v| v.as_str()); + let message_subtype = obj.get("subtype").and_then(|v| v.as_str()); + matches!(message_type, Some("system")) && matches!(message_subtype, Some("api_retry")) +} + +fn map_claude_sdk_ext_notification( + connection_id: &str, + notification: &UntypedMessage, +) -> Option { + if notification.method() != "_claude/sdkMessage" { + return None; + } + + let (session_id, message) = parse_claude_sdk_message_params(notification.params())?; + if !is_claude_api_retry_message(&message) { + return None; + } + Some(AcpEvent::ClaudeSdkMessage { + connection_id: connection_id.to_string(), + session_id, + message, + }) +} + +fn maybe_emit_claude_sdk_ext_notification( + connection_id: &str, + emitter: &EventEmitter, + dispatch: Dispatch, +) { + let Dispatch::Notification(notification) = dispatch else { + return; + }; + + if let Some(event) = map_claude_sdk_ext_notification(connection_id, ¬ification) { + crate::web::event_bridge::emit_event(emitter, "acp://event", event); + } +} + /// Fix null fields in `usage_update` notifications that would otherwise fail deserialization. /// /// Some ACP agents send `"used": null` in usage_update notifications, but the @@ -2524,3 +2630,110 @@ fn emit_conversation_update( } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn claude_raw_sdk_meta_enabled_only_for_claude() { + let claude_meta = claude_raw_sdk_session_meta(AgentType::ClaudeCode) + .expect("Claude must have raw SDK meta"); + assert_eq!( + claude_meta + .get("claudeCode") + .and_then(|v| v.get("emitRawSDKMessages")) + .and_then(|v| v.as_bool()), + Some(true) + ); + + assert!(claude_raw_sdk_session_meta(AgentType::Codex).is_none()); + } + + #[test] + fn map_claude_sdk_ext_notification_maps_valid_payload() { + let raw = UntypedMessage::new( + "_claude/sdkMessage", + serde_json::json!({ + "sessionId": "session-123", + "message": { + "type": "system", + "subtype": "api_retry", + "attempt": 3, + "max_retries": 10 + } + }), + ) + .unwrap(); + + let event = map_claude_sdk_ext_notification("conn-1", &raw) + .expect("valid sdk payload should map"); + + match event { + AcpEvent::ClaudeSdkMessage { + connection_id, + session_id, + message, + } => { + assert_eq!(connection_id, "conn-1"); + assert_eq!(session_id, "session-123"); + assert_eq!(message.get("type").and_then(|v| v.as_str()), Some("system")); + } + _ => panic!("expected ClaudeSdkMessage"), + } + } + + #[test] + fn map_claude_sdk_ext_notification_rejects_non_api_retry() { + let non_retry = UntypedMessage::new( + "_claude/sdkMessage", + serde_json::json!({ + "sessionId": "session-123", + "message": {"type": "system", "subtype": "status"} + }), + ) + .unwrap(); + assert!(map_claude_sdk_ext_notification("conn-1", &non_retry).is_none()); + } + + #[test] + fn map_claude_sdk_ext_notification_rejects_invalid_payload() { + let wrong_method = UntypedMessage::new( + "_other/method", + serde_json::json!({"sessionId": "s", "message": {}}), + ) + .unwrap(); + assert!(map_claude_sdk_ext_notification("conn-1", &wrong_method).is_none()); + + let missing_fields = UntypedMessage::new( + "_claude/sdkMessage", + serde_json::json!({"sessionId": 1}), + ) + .unwrap(); + assert!(map_claude_sdk_ext_notification("conn-1", &missing_fields).is_none()); + } + + #[test] + fn build_new_session_request_sets_claude_raw_meta() { + let cwd = std::path::PathBuf::from("/tmp/codeg"); + let req = build_new_session_request(AgentType::ClaudeCode, &cwd); + + assert_eq!( + req.meta + .as_ref() + .and_then(|m| m.get("claudeCode")) + .and_then(|v| v.get("emitRawSDKMessages")) + .and_then(|v| v.as_bool()), + Some(true) + ); + } + + #[test] + fn build_load_session_request_skips_meta_for_non_claude() { + let cwd = std::path::PathBuf::from("/tmp/codeg"); + let req = + build_load_session_request(AgentType::Codex, SessionId::new("abc".to_string()), &cwd); + + assert!(req.meta.is_none()); + } +} diff --git a/src-tauri/src/acp/types.rs b/src-tauri/src/acp/types.rs index b31e985..e64a4e6 100644 --- a/src-tauri/src/acp/types.rs +++ b/src-tauri/src/acp/types.rs @@ -47,6 +47,12 @@ pub enum AcpEvent { ContentDelta { connection_id: String, text: String }, /// Agent thinking/reasoning Thinking { connection_id: String, text: String }, + /// Raw SDK message forwarded from Claude ACP extension notification + ClaudeSdkMessage { + connection_id: String, + session_id: String, + message: serde_json::Value, + }, /// Agent initiated a tool call ToolCall { connection_id: String, diff --git a/src/components/chat/conversation-shell.tsx b/src/components/chat/conversation-shell.tsx index ba9a248..50e0a62 100644 --- a/src/components/chat/conversation-shell.tsx +++ b/src/components/chat/conversation-shell.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from "react" +import { useTranslations } from "next-intl" import type { AgentType, ConnectionStatus, @@ -11,8 +12,10 @@ import type { import type { PendingPermission, PendingQuestion, + ClaudeApiRetryState, } from "@/contexts/acp-connections-context" import type { QueuedMessage } from "@/hooks/use-message-queue" +import { Loader2 } from "lucide-react" import { ChatInput } from "@/components/chat/chat-input" import { PermissionDialog } from "@/components/chat/permission-dialog" import { QuestionDialog } from "@/components/chat/question-dialog" @@ -23,6 +26,7 @@ interface ConversationShellProps { defaultPath?: string agentName?: string error: string | null + claudeApiRetry: ClaudeApiRetryState | null pendingPermission: PendingPermission | null pendingQuestion: PendingQuestion | null onFocus: () => void @@ -64,6 +68,7 @@ export function ConversationShell({ defaultPath, agentName, error, + claudeApiRetry, pendingPermission, pendingQuestion, onFocus, @@ -98,6 +103,63 @@ export function ConversationShell({ onCancelQueueEdit, onForkSend, }: ConversationShellProps) { + const tAcp = useTranslations("Folder.chat.acpConnections") + const retry = claudeApiRetry + const retryAttemptRaw = retry?.attempt + const retryMaxRaw = retry?.maxRetries + const retryDelayMsRaw = retry?.retryDelayMs + const retryErrorStatusRaw = retry?.errorStatus + + const retryAttempt = + retryAttemptRaw !== null && retryAttemptRaw !== undefined + ? Math.trunc(retryAttemptRaw) + : null + const retryMax = + retryMaxRaw !== null && retryMaxRaw !== undefined + ? Math.trunc(retryMaxRaw) + : null + const retryDelaySeconds = + retryDelayMsRaw !== null && retryDelayMsRaw !== undefined + ? (retryDelayMsRaw / 1000).toFixed(1) + : null + const errorLabel = retry?.error ?? tAcp("claudeApiRetry.fallbackError") + const statusLabel = + retryErrorStatusRaw !== null && retryErrorStatusRaw !== undefined + ? tAcp("claudeApiRetry.httpStatus", { + status: Math.trunc(retryErrorStatusRaw), + }) + : "" + const retryLabel = + retryAttempt !== null && retryMax !== null + ? tAcp("claudeApiRetry.retryingWithMax", { + attempt: retryAttempt, + max: retryMax, + }) + : retryAttempt !== null + ? tAcp("claudeApiRetry.retryingAttempt", { + attempt: retryAttempt, + }) + : tAcp("claudeApiRetry.retrying") + const delayLabel = + retryDelaySeconds !== null + ? tAcp("claudeApiRetry.nextRetryIn", { + seconds: retryDelaySeconds, + }) + : null + const retryLineText = + delayLabel !== null + ? tAcp("claudeApiRetry.lineWithDelay", { + error: errorLabel, + status: statusLabel, + retry: retryLabel, + delay: delayLabel, + }) + : tAcp("claudeApiRetry.line", { + error: errorLabel, + status: statusLabel, + retry: retryLabel, + }) + return (
{children}
@@ -145,6 +207,17 @@ export function ConversationShell({ /> )} + {claudeApiRetry && ( +
+
+ + + {retryLineText} + +
+
+ )} + {error && (
{error} diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx index 47635ab..7a50e01 100644 --- a/src/components/conversations/conversation-detail-panel.tsx +++ b/src/components/conversations/conversation-detail-panel.tsx @@ -877,6 +877,7 @@ const ConversationTabView = memo(function ConversationTabView({ defaultPath={workingDirForConnection} agentName={AGENT_LABELS[selectedAgent]} error={conn.error} + claudeApiRetry={conn.claudeApiRetry} pendingPermission={conn.pendingPermission} pendingQuestion={conn.pendingQuestion} onFocus={handleFocus} diff --git a/src/contexts/acp-connections-context.tsx b/src/contexts/acp-connections-context.tsx index c52cfed..a76ed5b 100644 --- a/src/contexts/acp-connections-context.tsx +++ b/src/contexts/acp-connections-context.tsx @@ -77,6 +77,15 @@ export interface PendingQuestion { question: string } +export interface ClaudeApiRetryState { + sessionId: string + attempt: number | null + maxRetries: number | null + error: string | null + errorStatus: number | null + retryDelayMs: number | null +} + export type LiveContentBlock = | { type: "text"; text: string } | { type: "thinking"; text: string } @@ -108,6 +117,7 @@ export interface ConnectionState { liveMessage: LiveMessage | null pendingPermission: PendingPermission | null pendingQuestion: PendingQuestion | null + claudeApiRetry: ClaudeApiRetryState | null error: string | null } @@ -221,6 +231,11 @@ type Action = contextKey: string entries: PlanEntryInfo[] } + | { + type: "CLAUDE_API_RETRY" + contextKey: string + retry: ClaudeApiRetryState | null + } | { type: "ERROR"; contextKey: string; message: string } | { type: "AVAILABLE_COMMANDS" @@ -264,6 +279,37 @@ function asRecord(value: unknown): Record | null { return value as Record } +function asFiniteNumber(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) { + return value + } + if (typeof value === "string" && value.trim().length > 0) { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : null + } + return null +} + +function parseClaudeApiRetryEvent( + event: Extract +): ClaudeApiRetryState | null { + const message = asRecord(event.message) + if (!message) return null + if (message.type !== "system" || message.subtype !== "api_retry") return null + + return { + sessionId: + typeof message.session_id === "string" + ? message.session_id + : event.session_id, + attempt: asFiniteNumber(message.attempt), + maxRetries: asFiniteNumber(message.max_retries), + error: typeof message.error === "string" ? message.error : null, + errorStatus: asFiniteNumber(message.error_status), + retryDelayMs: asFiniteNumber(message.retry_delay_ms), + } +} + function extractPermissionToolCallId(toolCall: unknown): string | null { const record = asRecord(toolCall) if (!record) return null @@ -554,6 +600,7 @@ function connectionsReducer( liveMessage: null, pendingPermission: null, pendingQuestion: null, + claudeApiRetry: null, error: null, }) return next @@ -581,7 +628,11 @@ function connectionsReducer( startedAt: Date.now(), } updated.pendingQuestion = null + updated.claudeApiRetry = null updated.error = null + } else if (conn.status === "prompting") { + // Prompt cycle ended: clear in-flight Claude API retry banner. + updated.claudeApiRetry = null } next.set(action.contextKey, updated) return next @@ -1086,12 +1137,24 @@ function connectionsReducer( return next } + case "CLAUDE_API_RETRY": { + const conn = state.get(action.contextKey) + if (!conn) return state + const next = new Map(state) + next.set(action.contextKey, { + ...conn, + claudeApiRetry: action.retry, + }) + return next + } + case "ERROR": { const conn = state.get(action.contextKey) if (!conn) return state const next = new Map(state) next.set(action.contextKey, { ...conn, + claudeApiRetry: null, error: action.message, }) return next @@ -1582,6 +1645,14 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { case "thinking": enqueueStreamingAction({ type: "THINKING", contextKey, text: e.text }) break + case "claude_sdk_message": + flushStreamingQueue() + dispatch({ + type: "CLAUDE_API_RETRY", + contextKey, + retry: parseClaudeApiRetryEvent(e), + }) + break case "tool_call": flushStreamingQueue() dispatch({ diff --git a/src/hooks/use-connection.ts b/src/hooks/use-connection.ts index c73f295..6bf9129 100644 --- a/src/hooks/use-connection.ts +++ b/src/hooks/use-connection.ts @@ -5,6 +5,7 @@ import { useAcpActions, useConnectionStore, getCachedSelectors, + type ClaudeApiRetryState, type ConnectionState, type LiveMessage, type PendingPermission, @@ -40,6 +41,7 @@ export interface UseConnectionReturn { liveMessage: LiveMessage | null pendingPermission: PendingPermission | null pendingQuestion: PendingQuestion | null + claudeApiRetry: ClaudeApiRetryState | null error: string | null connect: ( agentType: AgentType, @@ -91,6 +93,7 @@ export function useConnection(contextKey: string): UseConnectionReturn { const liveMessage = connection?.liveMessage ?? null const pendingPermission = connection?.pendingPermission ?? null const pendingQuestion = connection?.pendingQuestion ?? null + const claudeApiRetry = connection?.claudeApiRetry ?? null const error = connection?.error ?? null const connect = useCallback( @@ -146,6 +149,7 @@ export function useConnection(contextKey: string): UseConnectionReturn { liveMessage, pendingPermission, pendingQuestion, + claudeApiRetry, error, connect, disconnect, @@ -169,6 +173,7 @@ export function useConnection(contextKey: string): UseConnectionReturn { liveMessage, pendingPermission, pendingQuestion, + claudeApiRetry, error, connect, disconnect, diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index ed30577..f06dc0e 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "أداة", "eventErrorTitle": "خطأ الوكيل", "notificationTurnComplete": "{agent} أنهى الاستجابة", - "notificationError": "{agent} خطأ: {message}" + "notificationError": "{agent} خطأ: {message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "إعادة المحاولة {attempt}/{max}", + "retryingAttempt": "إعادة المحاولة رقم {attempt}", + "retrying": "جاري إعادة المحاولة", + "nextRetryIn": "المحاولة التالية خلال {seconds}ث", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}، {delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 45a7405..7cc5ff3 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "Werkzeug", "eventErrorTitle": "Agentenfehler", "notificationTurnComplete": "{agent} hat die Antwort abgeschlossen", - "notificationError": "{agent} Fehler: {message}" + "notificationError": "{agent} Fehler: {message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "erneuter Versuch {attempt}/{max}", + "retryingAttempt": "erneuter Versuch {attempt}", + "retrying": "erneuter Versuch", + "nextRetryIn": "nächster in {seconds}s", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}, {delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 9a39519..33550f0 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "Tool", "eventErrorTitle": "Agent Error", "notificationTurnComplete": "{agent} has finished responding", - "notificationError": "{agent} error: {message}" + "notificationError": "{agent} error: {message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "retrying {attempt}/{max}", + "retryingAttempt": "retrying attempt {attempt}", + "retrying": "retrying", + "nextRetryIn": "next in {seconds}s", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}, {delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index a664f2d..aca3a9f 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "Herramienta", "eventErrorTitle": "Error del agente", "notificationTurnComplete": "{agent} ha terminado de responder", - "notificationError": "{agent} error: {message}" + "notificationError": "{agent} error: {message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "reintentando {attempt}/{max}", + "retryingAttempt": "reintentando intento {attempt}", + "retrying": "reintentando", + "nextRetryIn": "siguiente en {seconds}s", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}, {delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 3186a85..4cb1290 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "Outil", "eventErrorTitle": "Erreur de l'agent", "notificationTurnComplete": "{agent} a terminé de répondre", - "notificationError": "{agent} erreur : {message}" + "notificationError": "{agent} erreur : {message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "nouvelle tentative {attempt}/{max}", + "retryingAttempt": "nouvelle tentative {attempt}", + "retrying": "nouvelle tentative", + "nextRetryIn": "prochaine dans {seconds}s", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}, {delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 048f1df..a2caadc 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "ツール", "eventErrorTitle": "エージェントエラー", "notificationTurnComplete": "{agent} の応答が完了しました", - "notificationError": "{agent} エラー:{message}" + "notificationError": "{agent} エラー:{message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "再試行中 {attempt}/{max}", + "retryingAttempt": "再試行中({attempt} 回目)", + "retrying": "再試行中", + "nextRetryIn": "{seconds}秒後に再試行", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}、{delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 5c34ea9..07dcdac 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "도구", "eventErrorTitle": "에이전트 오류", "notificationTurnComplete": "{agent} 응답이 완료되었습니다", - "notificationError": "{agent} 오류: {message}" + "notificationError": "{agent} 오류: {message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "재시도 중 {attempt}/{max}", + "retryingAttempt": "{attempt}번째 재시도 중", + "retrying": "재시도 중", + "nextRetryIn": "{seconds}초 후 재시도", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}, {delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 3f0cce9..2b29ddb 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "Ferramenta", "eventErrorTitle": "Erro do agente", "notificationTurnComplete": "{agent} terminou de responder", - "notificationError": "{agent} erro: {message}" + "notificationError": "{agent} erro: {message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "tentando novamente {attempt}/{max}", + "retryingAttempt": "tentando novamente tentativa {attempt}", + "retrying": "tentando novamente", + "nextRetryIn": "próxima em {seconds}s", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry}, {delay}", + "httpStatus": " (HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 0f90876..e7c448a 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "工具", "eventErrorTitle": "Agent 错误", "notificationTurnComplete": "{agent} 已完成响应", - "notificationError": "{agent} 错误:{message}" + "notificationError": "{agent} 错误:{message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "正在重试 {attempt}/{max}", + "retryingAttempt": "正在重试(第 {attempt} 次)", + "retrying": "正在重试", + "nextRetryIn": "{seconds} 秒后重试", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry},{delay}", + "httpStatus": "(HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 6021138..c2d1bdf 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1449,7 +1449,17 @@ "toolFallbackTitle": "工具", "eventErrorTitle": "Agent 錯誤", "notificationTurnComplete": "{agent} 已完成回應", - "notificationError": "{agent} 錯誤:{message}" + "notificationError": "{agent} 錯誤:{message}", + "claudeApiRetry": { + "fallbackError": "authentication_failed", + "retryingWithMax": "正在重試 {attempt}/{max}", + "retryingAttempt": "正在重試(第 {attempt} 次)", + "retrying": "正在重試", + "nextRetryIn": "{seconds} 秒後重試", + "line": "{error}{status} · {retry}", + "lineWithDelay": "{error}{status} · {retry},{delay}", + "httpStatus": "(HTTP {status})" + } }, "connectionLifecycle": { "tasks": { diff --git a/src/lib/types.ts b/src/lib/types.ts index c395de5..9e4d171 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -362,6 +362,12 @@ export interface SessionUsageUpdateInfo { export type AcpEvent = | { type: "content_delta"; connection_id: string; text: string } | { type: "thinking"; connection_id: string; text: string } + | { + type: "claude_sdk_message" + connection_id: string + session_id: string + message: unknown + } | { type: "tool_call" connection_id: string