feat(acp): surface Claude API retry state in chat input

Enable raw Claude SDK forwarding for ACP sessions and emit only system/api_retry events to the frontend.

Show a localized single-line retry banner with loading under the conversation input, including error details and retry progress.
This commit is contained in:
xintaofei
2026-04-14 14:59:32 +08:00
parent 77e46d80f8
commit f9923df1fe
17 changed files with 492 additions and 17 deletions

View File

@@ -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<serde_json::Map<String, serde_json::Value>> {
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<PlanEntryInfo> {
.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<AcpEvent> {
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, &notification) {
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());
}
}

View File

@@ -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,

View File

@@ -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 (
<div className="flex h-full min-h-0 flex-col">
<div className="flex-1 min-h-0">{children}</div>
@@ -145,6 +207,17 @@ export function ConversationShell({
/>
)}
{claudeApiRetry && (
<div className="border-t border-destructive/20 bg-destructive/5 px-4 py-2 text-xs text-destructive">
<div className="flex items-center gap-2 font-medium">
<Loader2 className="h-3.5 w-3.5 animate-spin" />
<span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
{retryLineText}
</span>
</div>
</div>
)}
{error && (
<div className="px-4 py-2 text-xs text-destructive bg-destructive/5 border-t border-destructive/20">
{error}

View File

@@ -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}

View File

@@ -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<string, unknown> | null {
return value as Record<string, unknown>
}
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<AcpEvent, { type: "claude_sdk_message" }>
): 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({

View File

@@ -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,

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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