From 931f69c421e51021ce765577c32edf15e9194eff Mon Sep 17 00:00:00 2001 From: xintaofei Date: Sat, 7 Mar 2026 14:52:09 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E4=BC=9A=E8=AF=9D=E5=8E=BB?= =?UTF-8?q?=E5=A4=9A=E8=AF=AD=E8=A8=80=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../message/content-parts-renderer.tsx | 128 +++++++++++- src/contexts/acp-connections-context.tsx | 193 ++++++++++-------- src/i18n/messages/en.json | 45 +++- src/i18n/messages/zh-CN.json | 45 +++- src/i18n/messages/zh-TW.json | 45 +++- 5 files changed, 357 insertions(+), 99 deletions(-) diff --git a/src/components/message/content-parts-renderer.tsx b/src/components/message/content-parts-renderer.tsx index e55a5e0..c4bca38 100644 --- a/src/components/message/content-parts-renderer.tsx +++ b/src/components/message/content-parts-renderer.tsx @@ -1080,6 +1080,107 @@ function sanitizeLiveTitle(title: string | null | undefined): string | null { return trimmed } +function localizeDerivedToolTitle( + title: string | null, + t: (key: string, values?: Record) => string +): string | null { + if (!title) return null + + if (title === "Edit") return t("title.edit") + if (title === "Command") return t("title.command") + if (title === "TodoWrite") return t("title.todoWrite") + if (title === "Read") return t("title.read") + if (title === "Write") return t("title.write") + if (title === "NotebookEdit") return t("title.notebookEdit") + + const editFilesMatch = title.match(/^Edit \((\d+) files\)$/) + if (editFilesMatch) { + return t("title.editFiles", { count: Number(editFilesMatch[1]) }) + } + + const editWithTarget = title.match(/^Edit (.+)$/) + if (editWithTarget) { + return t("title.editWithTarget", { target: editWithTarget[1] }) + } + + const readWithTarget = title.match(/^Read (.+)$/) + if (readWithTarget) { + return t("title.readWithTarget", { target: readWithTarget[1] }) + } + + const writeWithTarget = title.match(/^Write (.+)$/) + if (writeWithTarget) { + return t("title.writeWithTarget", { target: writeWithTarget[1] }) + } + + const notebookEditWithTarget = title.match(/^NotebookEdit (.+)$/) + if (notebookEditWithTarget) { + return t("title.notebookEditWithTarget", { + target: notebookEditWithTarget[1], + }) + } + + const globWithPattern = title.match(/^Glob (.+)$/) + if (globWithPattern) { + return t("title.globWithPattern", { pattern: globWithPattern[1] }) + } + + const grepWithPattern = title.match(/^Grep (.+)$/) + if (grepWithPattern) { + return t("title.grepWithPattern", { pattern: grepWithPattern[1] }) + } + + const taskCreateWithSubject = title.match(/^TaskCreate: (.+)$/) + if (taskCreateWithSubject) { + return t("title.taskCreateWithSubject", { + subject: taskCreateWithSubject[1], + }) + } + + const taskUpdateWithStatus = title.match(/^TaskUpdate #([^ ]+)(?: → (.+))?$/) + if (taskUpdateWithStatus) { + const id = taskUpdateWithStatus[1] + const status = taskUpdateWithStatus[2] + if (status) { + return t("title.taskUpdateWithStatus", { id, status }) + } + return t("title.taskUpdate", { id }) + } + + const webFetchWithUrl = title.match(/^WebFetch (.+)$/) + if (webFetchWithUrl) { + return t("title.webFetchWithUrl", { url: webFetchWithUrl[1] }) + } + + const webSearchWithQuery = title.match(/^WebSearch: (.+)$/) + if (webSearchWithQuery) { + return t("title.webSearchWithQuery", { query: webSearchWithQuery[1] }) + } + + const todosProgress = title.match(/^Todos \((\d+)\/(\d+)\)$/) + if (todosProgress) { + return t("title.todosProgress", { + done: Number(todosProgress[1]), + total: Number(todosProgress[2]), + }) + } + + const skillWithName = title.match(/^Skill: (.+)$/) + if (skillWithName) { + return t("title.skillWithName", { name: skillWithName[1] }) + } + + const genericWithContext = title.match(/^([^:]+): (.+)$/) + if (genericWithContext) { + return t("title.genericWithContext", { + tool: genericWithContext[1], + context: genericWithContext[2], + }) + } + + return title +} + // ── Specialized tool input renderers ───────────────────────────────── /** Edit tool: file path + unified diff view */ @@ -1906,23 +2007,28 @@ const ToolCallPart = memo(function ToolCallPart({ const isCommandLikeTool = isCommandTool || toolNameLower === "apply_patch" const isRunning = part.state === "input-available" || part.state === "input-streaming" - const title = useMemo( - () => + const title = useMemo(() => { + const rawTitle = deriveToolTitle( normalizedToolName, part.input, part.output ?? part.errorText ?? null ) ?? sanitizeLiveTitle(part.displayTitle) ?? - null, - [ - normalizedToolName, - part.input, - part.output, - part.errorText, - part.displayTitle, - ] - ) + null + return localizeDerivedToolTitle(rawTitle, ((key, values) => + t(key as never, values as never)) as ( + key: string, + values?: Record + ) => string) + }, [ + normalizedToolName, + part.input, + part.output, + part.errorText, + part.displayTitle, + t, + ]) const lineChangeStats = useMemo(() => { if (toolNameLower !== "edit" && toolNameLower !== "apply_patch") { return null diff --git a/src/contexts/acp-connections-context.tsx b/src/contexts/acp-connections-context.tsx index 79e088e..a048281 100644 --- a/src/contexts/acp-connections-context.tsx +++ b/src/contexts/acp-connections-context.tsx @@ -9,6 +9,7 @@ import { useRef, type ReactNode, } from "react" +import { useTranslations } from "next-intl" import { listen, type UnlistenFn } from "@tauri-apps/api/event" import { acpConnect, @@ -939,69 +940,10 @@ function isAlertedError(error: unknown): error is AlertedError { return (error as { alerted?: unknown }).alerted === true } -function buildOpenAgentsSettingsAction(agentType?: AgentType): AlertAction { - const payload = - typeof agentType === "string" - ? JSON.stringify({ - section: "agents", - agentType, - }) - : "agents" - return { - label: "打开 Agents 管理", - kind: "open_agents_settings", - payload, - } -} - -const AGENTS_SETUP_HINT = "点击前往设置 > Agents 管理安装。" - -function buildSdkNotInstalledMessage(agentLabel: string): string { - return `${agentLabel} SDK 尚未安装` -} - -function buildInstallGuidanceMessage(raw: string): string { - const normalized = raw.trim().replace(/[。.!?,,;;::]+$/u, "") - if (!normalized) return AGENTS_SETUP_HINT - if (normalized.includes("设置 > Agents 管理安装")) { - return `${normalized}。` - } - return `${normalized},${AGENTS_SETUP_HINT}` -} - -function buildAutoLinkBlockedReason(agent: AcpAgentInfo | null): string { - if (!agent) { - return "无法读取当前 Agent 配置。" - } - - const agentLabel = AGENT_LABELS[agent.agent_type] - if (!agent.enabled) { - return `${agentLabel} 已在 Agents 管理中禁用,请先启用后再连接。` - } - - if (!agent.available) { - return `${agentLabel} 当前平台不可用。` - } - - if (agent.installed_version) { - return "" - } - - switch (agent.distribution_type) { - case "binary": - return `${buildSdkNotInstalledMessage(agentLabel)}。` - case "npx": - return `${buildSdkNotInstalledMessage(agentLabel)}。` - case "uvx": - return `${buildSdkNotInstalledMessage(agentLabel)}。` - default: - return `${buildSdkNotInstalledMessage(agentLabel)}。` - } -} - // ── Provider ── export function AcpConnectionsProvider({ children }: { children: ReactNode }) { + const t = useTranslations("Folder.chat.acpConnections") const { pushAlert } = useAlertContext() const pushAlertRef = useRef(pushAlert) useEffect(() => { @@ -1022,6 +964,64 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { // Guard against concurrent connect() calls const connectingKeysRef = useRef(new Set()) + type AutoLinkBlockState = + | { kind: "none"; reason: "" } + | { + kind: "missing_config" | "disabled" | "unavailable" | "sdk_missing" + reason: string + } + + const buildOpenAgentsSettingsAction = useCallback( + (agentType?: AgentType): AlertAction => { + const payload = + typeof agentType === "string" + ? JSON.stringify({ + section: "agents", + agentType, + }) + : "agents" + return { + label: t("actions.openAgentsSettings"), + kind: "open_agents_settings", + payload, + } + }, + [t] + ) + + const resolveAutoLinkBlockState = useCallback( + (agent: AcpAgentInfo | null): AutoLinkBlockState => { + if (!agent) { + return { kind: "missing_config", reason: t("blocked.missingConfig") } + } + + const agentLabel = AGENT_LABELS[agent.agent_type] + if (!agent.enabled) { + return { + kind: "disabled", + reason: t("blocked.disabled", { agent: agentLabel }), + } + } + + if (!agent.available) { + return { + kind: "unavailable", + reason: t("blocked.unavailable", { agent: agentLabel }), + } + } + + if (agent.installed_version) { + return { kind: "none", reason: "" } + } + + return { + kind: "sdk_missing", + reason: t("blocked.sdkMissing", { agent: agentLabel }), + } + }, + [t] + ) + // Activity tracking (no re-renders) const lastActivityRef = useRef(new Map()) const streamingQueueRef = useRef([]) @@ -1456,34 +1456,43 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { configuredAgent = agents.find((agent) => agent.agent_type === agentType) ?? null } catch (error) { - const reason = `无法读取 Agent 配置:${normalizeErrorMessage(error)}` + const reason = t("unableReadAgentConfig", { + message: normalizeErrorMessage(error), + }) + const autoLinkFailedTitle = t("autoLinkFailedTitle", { + agent: AGENT_LABELS[agentType], + }) pushAlertRef.current( "error", - `${AGENT_LABELS[agentType]} 自动链接失败`, - `${reason}\n${AGENTS_SETUP_HINT}`, + autoLinkFailedTitle, + `${reason}\n${t("agentsSetupHint")}`, [buildOpenAgentsSettingsAction(agentType)] ) throw createAlertedError(reason) } - const blockedReason = buildAutoLinkBlockedReason(configuredAgent) - if (blockedReason) { - const sdkNotInstalled = blockedReason.includes("SDK 尚未安装") - const detail = sdkNotInstalled - ? buildInstallGuidanceMessage(blockedReason) - : `${blockedReason}\n${AGENTS_SETUP_HINT}` + const blocked = resolveAutoLinkBlockState(configuredAgent) + if (blocked.kind !== "none") { + const autoLinkFailedTitle = t("autoLinkFailedTitle", { + agent: AGENT_LABELS[agentType], + }) + const detail = + blocked.kind === "sdk_missing" + ? t("withSetupHint", { + message: blocked.reason, + hint: t("agentsSetupHint"), + }) + : `${blocked.reason}\n${t("agentsSetupHint")}` pushAlertRef.current( "error", - sdkNotInstalled - ? buildSdkNotInstalledMessage(AGENT_LABELS[agentType]) - : `${AGENT_LABELS[agentType]} 自动链接失败`, + blocked.kind === "sdk_missing" + ? blocked.reason + : autoLinkFailedTitle, detail, [buildOpenAgentsSettingsAction(agentType)] ) throw createAlertedError( - sdkNotInstalled - ? buildSdkNotInstalledMessage(AGENT_LABELS[agentType]) - : blockedReason + blocked.kind === "sdk_missing" ? blocked.reason : blocked.reason ) } } @@ -1501,11 +1510,11 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { const message = detail.trim().length > 0 ? detail - : "预检查未通过,请检查 Agent 配置。" + : t("preflightCheckFailedDefault") pushAlertRef.current( "error", - `${preflight.agent_name} 自动链接失败`, - `${message}\n${AGENTS_SETUP_HINT}`, + t("autoLinkFailedTitle", { agent: preflight.agent_name }), + `${message}\n${t("agentsSetupHint")}`, [buildOpenAgentsSettingsAction(agentType)] ) throw createAlertedError(message) @@ -1524,7 +1533,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { // Add retry action fixes.push({ - label: "Retry", + label: t("actions.retry"), kind: "retry_connection", payload: JSON.stringify({ agentType, @@ -1536,7 +1545,7 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { pushAlertRef.current( "error", - `${preflight.agent_name} preflight failed`, + t("preflightFailedTitle", { agent: preflight.agent_name }), detail, fixes ) @@ -1547,11 +1556,13 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { if (isAlertedError(e)) { throw e } - const reason = `自动链接预检查失败:${normalizeErrorMessage(e)}` + const reason = t("autoLinkPreflightFailed", { + message: normalizeErrorMessage(e), + }) pushAlertRef.current( "error", - `${AGENT_LABELS[agentType]} 自动链接失败`, - `${reason}\n${AGENTS_SETUP_HINT}`, + t("autoLinkFailedTitle", { agent: AGENT_LABELS[agentType] }), + `${reason}\n${t("agentsSetupHint")}`, [buildOpenAgentsSettingsAction(agentType)] ) throw createAlertedError(reason) @@ -1601,14 +1612,26 @@ export function AcpConnectionsProvider({ children }: { children: ReactNode }) { } catch (err) { if (!isAlertedError(err)) { const message = normalizeErrorMessage(err) - pushAlertRef.current("error", `${agentType} 连接失败`, message) + pushAlertRef.current( + "error", + t("connectFailedTitle", { agent: agentType }), + message + ) } throw err } finally { connectingKeysRef.current.delete(contextKey) } }, - [consumeBufferedEvents, dispatch, handleMappedEvent, waitForListenerReady] + [ + buildOpenAgentsSettingsAction, + consumeBufferedEvents, + dispatch, + handleMappedEvent, + resolveAutoLinkBlockState, + t, + waitForListenerReady, + ] ) const disconnect = useCallback( diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index e343fb8..2e21121 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -1061,6 +1061,26 @@ "diffDescriptionConflict": "{path} · disk vs unsaved" }, "chat": { + "acpConnections": { + "actions": { + "openAgentsSettings": "Open Agents settings", + "retry": "Retry" + }, + "agentsSetupHint": "Open Settings > Agents to manage installation.", + "withSetupHint": "{message}\n{hint}", + "blocked": { + "missingConfig": "Unable to read current Agent configuration.", + "disabled": "{agent} is disabled in Agents settings. Enable it before connecting.", + "unavailable": "{agent} is unavailable on the current platform.", + "sdkMissing": "{agent} SDK is not installed" + }, + "unableReadAgentConfig": "Unable to read Agent config: {message}", + "autoLinkFailedTitle": "{agent} auto-link failed", + "preflightCheckFailedDefault": "Preflight checks failed. Check Agent settings.", + "preflightFailedTitle": "{agent} preflight failed", + "autoLinkPreflightFailed": "Auto-link preflight failed: {message}", + "connectFailedTitle": "{agent} connection failed" + }, "connectionLifecycle": { "tasks": { "connectingTitle": "Connecting to {agent}", @@ -1201,7 +1221,30 @@ "promptLabel": "Prompt", "subjectLabel": "Subject", "taskLabel": "Task", - "nameLabel": "Name:" + "nameLabel": "Name:", + "title": { + "edit": "Edit", + "command": "Command", + "todoWrite": "TodoWrite", + "read": "Read", + "write": "Write", + "notebookEdit": "NotebookEdit", + "editFiles": "Edit ({count} files)", + "editWithTarget": "Edit {target}", + "readWithTarget": "Read {target}", + "writeWithTarget": "Write {target}", + "notebookEditWithTarget": "NotebookEdit {target}", + "globWithPattern": "Glob {pattern}", + "grepWithPattern": "Grep {pattern}", + "taskCreateWithSubject": "TaskCreate: {subject}", + "taskUpdateWithStatus": "TaskUpdate #{id} -> {status}", + "taskUpdate": "TaskUpdate #{id}", + "webFetchWithUrl": "WebFetch {url}", + "webSearchWithQuery": "WebSearch: {query}", + "todosProgress": "Todos ({done}/{total})", + "skillWithName": "Skill: {name}", + "genericWithContext": "{tool}: {context}" + } } }, "diffPreview": { diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index fc1c44b..4dfc23e 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -1061,6 +1061,26 @@ "diffDescriptionConflict": "{path} · 磁盘与未保存内容" }, "chat": { + "acpConnections": { + "actions": { + "openAgentsSettings": "打开 Agents 管理", + "retry": "重试" + }, + "agentsSetupHint": "点击前往设置 > Agents 管理安装。", + "withSetupHint": "{message}\n{hint}", + "blocked": { + "missingConfig": "无法读取当前 Agent 配置。", + "disabled": "{agent} 已在 Agents 管理中禁用,请先启用后再连接。", + "unavailable": "{agent} 当前平台不可用。", + "sdkMissing": "{agent} SDK 尚未安装" + }, + "unableReadAgentConfig": "无法读取 Agent 配置:{message}", + "autoLinkFailedTitle": "{agent} 自动链接失败", + "preflightCheckFailedDefault": "预检查未通过,请检查 Agent 配置。", + "preflightFailedTitle": "{agent} 预检查失败", + "autoLinkPreflightFailed": "自动链接预检查失败:{message}", + "connectFailedTitle": "{agent} 连接失败" + }, "connectionLifecycle": { "tasks": { "connectingTitle": "正在连接 {agent}", @@ -1201,7 +1221,30 @@ "promptLabel": "提示词", "subjectLabel": "主题", "taskLabel": "任务", - "nameLabel": "名称:" + "nameLabel": "名称:", + "title": { + "edit": "编辑", + "command": "命令", + "todoWrite": "待办", + "read": "读取", + "write": "写入", + "notebookEdit": "Notebook 编辑", + "editFiles": "编辑({count} 个文件)", + "editWithTarget": "编辑 {target}", + "readWithTarget": "读取 {target}", + "writeWithTarget": "写入 {target}", + "notebookEditWithTarget": "Notebook 编辑 {target}", + "globWithPattern": "Glob {pattern}", + "grepWithPattern": "Grep {pattern}", + "taskCreateWithSubject": "创建任务:{subject}", + "taskUpdateWithStatus": "更新任务 #{id} -> {status}", + "taskUpdate": "更新任务 #{id}", + "webFetchWithUrl": "抓取网页 {url}", + "webSearchWithQuery": "网页搜索:{query}", + "todosProgress": "待办({done}/{total})", + "skillWithName": "Skill:{name}", + "genericWithContext": "{tool}:{context}" + } } }, "diffPreview": { diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index f268cab..092eb65 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -1061,6 +1061,26 @@ "diffDescriptionConflict": "{path} · 磁碟與未儲存內容" }, "chat": { + "acpConnections": { + "actions": { + "openAgentsSettings": "打開 Agents 管理", + "retry": "重試" + }, + "agentsSetupHint": "點擊前往設定 > Agents 管理安裝。", + "withSetupHint": "{message}\n{hint}", + "blocked": { + "missingConfig": "無法讀取目前 Agent 設定。", + "disabled": "{agent} 已在 Agents 管理中停用,請先啟用後再連線。", + "unavailable": "{agent} 目前平台不可用。", + "sdkMissing": "{agent} SDK 尚未安裝" + }, + "unableReadAgentConfig": "無法讀取 Agent 設定:{message}", + "autoLinkFailedTitle": "{agent} 自動連結失敗", + "preflightCheckFailedDefault": "預檢查未通過,請檢查 Agent 設定。", + "preflightFailedTitle": "{agent} 預檢查失敗", + "autoLinkPreflightFailed": "自動連結預檢查失敗:{message}", + "connectFailedTitle": "{agent} 連線失敗" + }, "connectionLifecycle": { "tasks": { "connectingTitle": "正在連線 {agent}", @@ -1201,7 +1221,30 @@ "promptLabel": "提示詞", "subjectLabel": "主題", "taskLabel": "任務", - "nameLabel": "名稱:" + "nameLabel": "名稱:", + "title": { + "edit": "編輯", + "command": "命令", + "todoWrite": "待辦", + "read": "讀取", + "write": "寫入", + "notebookEdit": "Notebook 編輯", + "editFiles": "編輯({count} 個檔案)", + "editWithTarget": "編輯 {target}", + "readWithTarget": "讀取 {target}", + "writeWithTarget": "寫入 {target}", + "notebookEditWithTarget": "Notebook 編輯 {target}", + "globWithPattern": "Glob {pattern}", + "grepWithPattern": "Grep {pattern}", + "taskCreateWithSubject": "建立任務:{subject}", + "taskUpdateWithStatus": "更新任務 #{id} -> {status}", + "taskUpdate": "更新任務 #{id}", + "webFetchWithUrl": "擷取網頁 {url}", + "webSearchWithQuery": "網頁搜尋:{query}", + "todosProgress": "待辦({done}/{total})", + "skillWithName": "Skill:{name}", + "genericWithContext": "{tool}:{context}" + } } }, "diffPreview": {