fix(acp): harden session-page connection and localize backend errors

- Session-page connect never triggers download/install; returns
  SdkNotInstalled immediately and prompts the user to install from
  Agent Settings instead
- Binary agents now accept any cached version via
  find_best_cached_binary_for_agent so stale caches still connect
- Bound Initialize handshake with a 60s timeout and convert it to
  AcpError::InitializeTimeout via a sentinel in run_connection
- Spawn background task owns ConnectionManager map insertion and
  removes the entry on exit through an RAII guard that survives
  panics, preventing leaked stale entries
- AcpError gains SdkNotInstalled and InitializeTimeout variants plus
  a stable code() identifier; AcpEvent::Error carries code so the
  frontend can render localized messages by key
- Frontend preflight now runs for all connect sources; error event
  handler switches on code to show translated text for
  initialize_timeout, sdk_not_installed, platform_not_supported,
  process_exited, spawn_failed and download_failed
- Remove ConnectionStatus::Downloading enum variant, all frontend
  branches, and i18n strings; drop obsolete autoLinkFailedTitle,
  autoLinkPreflightFailed, preflightCheckFailedDefault and
  preflightFailedTitle keys across 10 locales
- Add backendErrors.* translations in 10 languages
- Diagnostic logging: always log agent stderr plus binary
  path/size/args/env keys and Initialize timing; gate stdin/stdout
  JSON-RPC tracing behind CODEG_ACP_DEBUG to avoid persisting user
  content into OS log files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xintaofei
2026-04-12 03:36:08 +08:00
parent 5bda7d06e9
commit 1c1738298b
25 changed files with 595 additions and 255 deletions

View File

@@ -33,7 +33,7 @@ function normalizeErrorMessage(error: unknown): string {
return String(error)
}
function isExpectedAutoLinkError(error: unknown): boolean {
function isExpectedConnectError(error: unknown): boolean {
if (!error || typeof error !== "object") return false
return (error as { alerted?: unknown }).alerted === true
}
@@ -77,12 +77,10 @@ export function useConnectionLifecycle({
const modeLoading =
!hasCachedSelectors &&
(status === "connecting" ||
status === "downloading" ||
(isInteractiveStatus && !effectiveSelectorsReady))
const configOptionsLoading =
!hasCachedSelectors &&
(status === "connecting" ||
status === "downloading" ||
(isInteractiveStatus && !effectiveSelectorsReady))
// Gate for send button: block until the backend session is fully
// initialized (selectorsReady from the real backend event, not cache).
@@ -141,9 +139,7 @@ export function useConnectionLifecycle({
const s = statusRef.current
if (!s || s === "disconnected" || s === "error") {
connConnectRef
.current(agentTypeRef.current, workingDir, sessionIdRef.current, {
source: "auto_link",
})
.current(agentTypeRef.current, workingDir, sessionIdRef.current)
.then(() => {
if (!cancelled) {
setLastAutoConnectError(null)
@@ -157,7 +153,7 @@ export function useConnectionLifecycle({
message: normalizeErrorMessage(e),
})
}
if (!isExpectedAutoLinkError(e)) {
if (!isExpectedConnectError(e)) {
console.error("[ConnLifecycle] auto-connect:", e)
}
})
@@ -170,7 +166,7 @@ export function useConnectionLifecycle({
// Manage task status for connection progress
const taskIdRef = useRef<string | null>(null)
useEffect(() => {
if (status === "connecting" || status === "downloading") {
if (status === "connecting") {
if (!taskIdRef.current) {
const id = `acp-connect-${Date.now()}`
taskIdRef.current = id
@@ -271,10 +267,8 @@ export function useConnectionLifecycle({
touchActivity(contextKey)
if (!status || status === "disconnected" || status === "error") {
setLastAutoConnectError(null)
connConnect(agentType, workingDir, sessionId, {
source: "auto_link",
}).catch((e: unknown) => {
if (!isExpectedAutoLinkError(e)) {
connConnect(agentType, workingDir, sessionId).catch((e: unknown) => {
if (!isExpectedConnectError(e)) {
console.error("[ConnLifecycle] connect:", e)
}
})

View File

@@ -6,7 +6,6 @@ import {
useConnectionStore,
getCachedSelectors,
type ConnectionState,
type ConnectOptions,
type LiveMessage,
type PendingPermission,
type PendingQuestion,
@@ -45,8 +44,7 @@ export interface UseConnectionReturn {
connect: (
agentType: AgentType,
workingDir?: string,
sessionId?: string,
options?: ConnectOptions
sessionId?: string
) => Promise<void>
disconnect: () => Promise<void>
sendPrompt: (blocks: PromptInputBlock[]) => Promise<void>
@@ -96,12 +94,8 @@ export function useConnection(contextKey: string): UseConnectionReturn {
const error = connection?.error ?? null
const connect = useCallback(
(
agentType: AgentType,
workingDir?: string,
sessionId?: string,
options?: ConnectOptions
) => actions.connect(contextKey, agentType, workingDir, sessionId, options),
(agentType: AgentType, workingDir?: string, sessionId?: string) =>
actions.connect(contextKey, agentType, workingDir, sessionId),
[actions, contextKey]
)