继续会话去多语言处理

This commit is contained in:
xintaofei
2026-03-07 14:52:09 +08:00
parent 17a03ef95c
commit 931f69c421
5 changed files with 357 additions and 99 deletions

View File

@@ -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, unknown>) => 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, unknown>
) => string)
}, [
normalizedToolName,
part.input,
part.output,
part.errorText,
part.displayTitle,
t,
])
const lineChangeStats = useMemo(() => {
if (toolNameLower !== "edit" && toolNameLower !== "apply_patch") {
return null

View File

@@ -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<string>())
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<string, number>())
const streamingQueueRef = useRef<StreamingAction[]>([])
@@ -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(

View File

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

View File

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

View File

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