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:
@@ -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, ¬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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user