From 1cc67c244e9d7c99f996a6b08ba864972a3c77ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 05:42:45 -0400 Subject: [PATCH] Q178: the Chinese UI labels stored values and sign-in prompts, and shortcuts follow the platform Co-Authored-By: Claude Opus 5.5 --- src/main/app/ipc-handlers.ts | 4 +- .../services/oauth/anthropic-claude-flow.ts | 2 +- .../services/oauth/github-copilot-flow.ts | 2 +- .../services/oauth/google-antigravity-flow.ts | 2 +- src/main/services/oauth/kimi-code-flow.ts | 2 +- src/main/services/oauth/oauth-types.ts | 5 +- src/main/services/oauth/openai-codex-flow.ts | 2 +- src/main/services/provider-oauth-service.ts | 2 +- src/renderer/src/i18n/labels.ts | 59 +++++++++++++++++++ src/renderer/src/i18n/zh.ts | 33 +++++++++++ src/renderer/src/pages/ApprovalsPage.tsx | 3 +- src/renderer/src/pages/HomePage.tsx | 3 +- src/renderer/src/pages/RunsPage.tsx | 7 ++- .../src/pages/approvals/ApprovalDialogs.tsx | 5 +- src/renderer/src/pages/chat/ChatComposer.tsx | 3 +- .../src/pages/chat/ChatMessageRow.tsx | 3 +- src/renderer/src/pages/chat/ChatTimeline.tsx | 3 +- .../src/pages/library/DocumentsPanel.tsx | 5 +- .../src/pages/office/AgentDetailCard.tsx | 4 +- .../src/pages/runs/RunRequestList.tsx | 3 +- .../pages/settings/ProviderRequestPanel.tsx | 3 +- .../src/pages/settings/ProvidersPanel.tsx | 3 +- src/renderer/src/pages/types.ts | 9 --- .../src/pages/work/TaskDetailDialog.tsx | 9 +-- .../src/shared/ProviderOAuthConnect.tsx | 25 +++++--- src/renderer/src/shared/Shell.tsx | 3 +- src/shared/desktop-api.ts | 3 +- tests/e2e/ceo-chat.spec.ts | 3 + 28 files changed, 158 insertions(+), 52 deletions(-) create mode 100644 src/renderer/src/i18n/labels.ts diff --git a/src/main/app/ipc-handlers.ts b/src/main/app/ipc-handlers.ts index 44241bee..e6f0e513 100644 --- a/src/main/app/ipc-handlers.ts +++ b/src/main/app/ipc-handlers.ts @@ -294,10 +294,10 @@ export function registerIpcHandlers(deps: IpcDeps): void { const secret = await providerOAuth.run( { providerKey: input.providerKey, enterpriseDomain: input.enterpriseDomain ?? null }, { - onAuth: ({ url, instructions }) => { + onAuth: ({ url, userCode }) => { const browserUrl = normalizeExternalBrowserUrl(url); void shell.openExternal(browserUrl); - emit({ phase: "authorize", url: browserUrl, instructions }); + emit({ phase: "authorize", url: browserUrl, userCode }); }, onProgress: (message) => emit({ phase: "progress", message }), }, diff --git a/src/main/services/oauth/anthropic-claude-flow.ts b/src/main/services/oauth/anthropic-claude-flow.ts index a3f885cf..1156433c 100644 --- a/src/main/services/oauth/anthropic-claude-flow.ts +++ b/src/main/services/oauth/anthropic-claude-flow.ts @@ -75,7 +75,7 @@ export const anthropicClaudeFlow: OAuthFlow = { const { verifier, challenge } = generatePkce(); const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal }); try { - callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), instructions: "Complete the Claude sign-in in your browser." }); + callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), userCode: null }); const result = await server.waitForCode(); if (result.state !== verifier) throw new Error("OAuth state mismatch."); return await exchangeCode(result.code, result.state, verifier, callbacks.signal); diff --git a/src/main/services/oauth/github-copilot-flow.ts b/src/main/services/oauth/github-copilot-flow.ts index ef3c7dbb..c6fa93a7 100644 --- a/src/main/services/oauth/github-copilot-flow.ts +++ b/src/main/services/oauth/github-copilot-flow.ts @@ -216,7 +216,7 @@ export const githubCopilotFlow: OAuthFlow = { const device = await startDeviceFlow(domain, callbacks.signal); callbacks.onAuth({ url: device.verification_uri, - instructions: `Enter code ${device.user_code} at ${device.verification_uri}`, + userCode: device.user_code, }); const githubToken = await pollForGitHubAccessToken(domain, device, callbacks.signal); const credentials = await refreshGitHubCopilotToken(githubToken, enterpriseDomain ?? undefined, callbacks.signal); diff --git a/src/main/services/oauth/google-antigravity-flow.ts b/src/main/services/oauth/google-antigravity-flow.ts index 1bbc225b..c5302cc1 100644 --- a/src/main/services/oauth/google-antigravity-flow.ts +++ b/src/main/services/oauth/google-antigravity-flow.ts @@ -140,7 +140,7 @@ export const antigravityFlow: OAuthFlow = { const { verifier, challenge } = generatePkce(); const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal }); try { - callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), instructions: "Complete the Google sign-in in your browser." }); + callbacks.onAuth({ url: buildAuthorizeUrl(challenge, verifier), userCode: null }); const result = await server.waitForCode(); if (result.state !== verifier) throw new Error("OAuth state mismatch."); const tokens = await exchangeCode(result.code, verifier, clientSecret, callbacks.signal); diff --git a/src/main/services/oauth/kimi-code-flow.ts b/src/main/services/oauth/kimi-code-flow.ts index 5f646946..1fb841ce 100644 --- a/src/main/services/oauth/kimi-code-flow.ts +++ b/src/main/services/oauth/kimi-code-flow.ts @@ -183,7 +183,7 @@ export const kimiCodeFlow: OAuthFlow = { const device = await requestDeviceAuthorization(callbacks.signal); callbacks.onAuth({ url: device.verification_uri_complete, - instructions: `Approve code ${device.user_code} in your browser to link your Kimi plan.`, + userCode: device.user_code, }); return pollForToken(device, callbacks.signal); }, diff --git a/src/main/services/oauth/oauth-types.ts b/src/main/services/oauth/oauth-types.ts index d5a95add..afc9ad8a 100644 --- a/src/main/services/oauth/oauth-types.ts +++ b/src/main/services/oauth/oauth-types.ts @@ -15,9 +15,8 @@ export interface OAuthCredentials { } export interface OAuthLoginCallbacks { - /** Called once the authorization URL is ready. `instructions` carries the - * device user-code (device flow) or a short note (authorization-code flow). */ - onAuth: (info: { url: string; instructions?: string }) => void; + /** Called once the authorization URL is ready; `userCode` is the device-flow code the user enters, null for a browser redirect. */ + onAuth: (info: { url: string; userCode: string | null }) => void; onProgress?: (message: string) => void; signal?: AbortSignal; /** GitHub Enterprise domain for Copilot; null/empty means github.com. */ diff --git a/src/main/services/oauth/openai-codex-flow.ts b/src/main/services/oauth/openai-codex-flow.ts index 5875ab31..4569162c 100644 --- a/src/main/services/oauth/openai-codex-flow.ts +++ b/src/main/services/oauth/openai-codex-flow.ts @@ -99,7 +99,7 @@ export const openaiCodexFlow: OAuthFlow = { const state = generatePkce().verifier; const server = await startOAuthCallbackServer({ port: CALLBACK_PORT, path: CALLBACK_PATH, signal: callbacks.signal }); try { - callbacks.onAuth({ url: buildAuthorizeUrl(challenge, state), instructions: "Complete the ChatGPT sign-in in your browser." }); + callbacks.onAuth({ url: buildAuthorizeUrl(challenge, state), userCode: null }); const result = await server.waitForCode(); if (result.state !== state) throw new Error("OAuth state mismatch."); const tokens = await exchangeCode(result.code, verifier, callbacks.signal); diff --git a/src/main/services/provider-oauth-service.ts b/src/main/services/provider-oauth-service.ts index 0c5fe419..2f712bde 100644 --- a/src/main/services/provider-oauth-service.ts +++ b/src/main/services/provider-oauth-service.ts @@ -2,7 +2,7 @@ import { getOAuthFlow } from "./oauth/oauth-flows"; import type { IntegrationKeyStore } from "./integration-key-store"; export interface ProviderOAuthHooks { - onAuth: (info: { url: string; instructions?: string }) => void; + onAuth: (info: { url: string; userCode: string | null }) => void; onProgress?: (message: string) => void; } diff --git a/src/renderer/src/i18n/labels.ts b/src/renderer/src/i18n/labels.ts new file mode 100644 index 00000000..eb718533 --- /dev/null +++ b/src/renderer/src/i18n/labels.ts @@ -0,0 +1,59 @@ +import { t } from "./i18n"; + +/** A stored value with no label yet shows as stored and is logged, so the gap is visible instead of hidden. */ +function labelOf(kind: string, labels: Record, value: string): string { + const label = labels[value]; + if (label !== undefined) return label; + console.warn(`[labels] no ${kind} label for "${value}"`); + return value; +} + +const APPROVAL_KINDS: Record = { + task_risk: t("High-risk task"), + task_execution: t("Workspace access"), + agent_hire: t("Hire"), + deliverable_review: t("Deliverable review"), + goal_review: t("Goal review"), + chat_escalation: t("Escalation"), + board_force_approval: t("Board override"), + social_publish: t("Social post"), +}; +export const approvalKindLabel = (kind: string) => labelOf("approval kind", APPROVAL_KINDS, kind); + +const APPROVAL_STATES: Record = { pending: t("Pending"), approved: t("Approved"), declined: t("Declined"), superseded: t("Superseded") }; +export const approvalStateLabel = (state: string) => labelOf("approval state", APPROVAL_STATES, state); + +const RUN_TRIGGERS: Record = { heartbeat: t("Heartbeat"), manual: t("Manual"), chat_mention: t("Chat mention"), bootstrap: t("Company launch") }; +export const runTriggerLabel = (trigger: string) => labelOf("run trigger", RUN_TRIGGERS, trigger); + +const REQUEST_STATES: Record = { pending: t("Pending"), streaming: t("Streaming"), succeeded: t("Succeeded"), failed: t("Failed"), cancelled: t("Cancelled") }; +export const requestStateLabel = (state: string) => labelOf("request state", REQUEST_STATES, state); + +const RISKS: Record = { low: t("Low"), medium: t("Medium"), high: t("High") }; +export const riskLabel = (risk: string) => labelOf("risk", RISKS, risk); + +const ROLE_LEVELS: Record = { staff: t("Staff"), lead: t("Lead"), director: t("Director"), c_suite: t("C-suite") }; +export const roleLevelLabel = (level: string) => labelOf("rank", ROLE_LEVELS, level); + +const DOCUMENT_STATUSES: Record = { draft: t("Draft"), review: t("In review"), approved: t("Approved"), archived: t("Archived") }; +export const DOCUMENT_STATUS_VALUES = Object.keys(DOCUMENT_STATUSES); +export const documentStatusLabel = (status: string) => labelOf("document status", DOCUMENT_STATUSES, status); + +const CREDENTIAL_SOURCES: Record = { manual_secret: t("Key entered here"), provider_oauth_login: t("Signed in") }; +export const credentialSourceLabel = (source: string) => labelOf("credential source", CREDENTIAL_SOURCES, source); + +const TASK_EVENTS: Record = { + created: t("Created"), updated: t("Edited"), deleted: t("Deleted"), status_changed: t("Status changed"), stalled: t("Stalled"), + review_requested: t("Review requested"), review_closed: t("Review closed"), blocked: t("Blocked"), unblocked: t("Unblocked"), +}; +export const taskEventLabel = (action: string) => labelOf("task event", TASK_EVENTS, action); + +const FILE_KINDS: Record = { md: "Markdown", pdf: "PDF", img: t("Image"), sheet: t("Spreadsheet"), doc: t("Document"), file: t("File") }; +/** Other kinds are file extensions, shown as the format name. */ +export const fileKindLabel = (kind: string) => FILE_KINDS[kind] ?? kind.toUpperCase(); + +/** The platform's own notation, matching the keys the handlers accept (Cmd on macOS, Ctrl elsewhere). */ +export function shortcutLabel(key: "K" | "Enter"): string { + if (window.agentCompany.platform === "darwin") return key === "Enter" ? "⌘↩" : "⌘K"; + return `Ctrl+${key}`; +} diff --git a/src/renderer/src/i18n/zh.ts b/src/renderer/src/i18n/zh.ts index c77b46be..a8e5db2c 100644 --- a/src/renderer/src/i18n/zh.ts +++ b/src/renderer/src/i18n/zh.ts @@ -731,4 +731,37 @@ export const zh: Record = { "Queued sessions that will fail when they start: {count}.": "开始时将失败的排队会话:{count}。", "Disconnecting…": "正在断开…", "{label} is disconnected.": "{label} 已断开。", + "Archived": "已归档", + "Board override": "董事会强制批准", + "C-suite": "高管", + "Cancelled": "已取消", + "Chat mention": "聊天提及", + "Company launch": "公司启动", + "Deleted": "已删除", + "Deliverable review": "交付物审核", + "Draft": "草稿", + "Edited": "已编辑", + "Enter code {code} at {url}": "在 {url} 输入代码 {code}", + "Escalation": "升级", + "Finish signing in in the browser window that opened.": "请在打开的浏览器窗口中完成登录。", + "Goal review": "目标审查", + "Heartbeat": "心跳", + "High-risk task": "高风险任务", + "Image": "图片", + "Key entered here": "在此输入的密钥", + "Review closed": "审核已结束", + "Review requested": "已请求审核", + "Sign in again": "重新登录", + "Sign in with {label}": "使用 {label} 登录", + "Signed in": "已登录", + "Signed in to {label}": "已登录 {label}", + "Social post": "社交发布", + "Spreadsheet": "表格", + "Stalled": "已停滞", + "Status changed": "状态已变更", + "Streaming": "传输中", + "Succeeded": "成功", + "Unblocked": "已解除阻塞", + "Waiting for {label} sign-in": "等待 {label} 登录", + "Workspace access": "工作区访问", }; diff --git a/src/renderer/src/pages/ApprovalsPage.tsx b/src/renderer/src/pages/ApprovalsPage.tsx index 58bff8c7..44d69b28 100644 --- a/src/renderer/src/pages/ApprovalsPage.tsx +++ b/src/renderer/src/pages/ApprovalsPage.tsx @@ -1,3 +1,4 @@ +import { approvalKindLabel } from "../i18n/labels"; import { decideEach, type BatchResult } from "./approvals/decide-each"; import { DECIDED_PAGE, decidedWindow } from "./approvals/decided-window"; import { ApprovalDetailDialog, ApprovalRecordDialog } from "./approvals/ApprovalDialogs"; @@ -278,7 +279,7 @@ function ApprovalRow({
{tone.label} · - {approval.kind} + {approvalKindLabel(approval.kind)}
{approval.title}
diff --git a/src/renderer/src/pages/HomePage.tsx b/src/renderer/src/pages/HomePage.tsx index 8a0249fe..ddf09069 100644 --- a/src/renderer/src/pages/HomePage.tsx +++ b/src/renderer/src/pages/HomePage.tsx @@ -1,3 +1,4 @@ +import { runTriggerLabel } from "../i18n/labels"; import { goalProgress, type GoalPace } from "./home/goal-progress"; import { newestRuns } from "./runs/newest-runs"; import { isBusinessTask } from "./work/task-helpers"; @@ -241,7 +242,7 @@ function RunActivityCard({ {agent && }
{task?.title ?? run.taskId}
-
{agent?.name ?? t("Agent")} · {run.trigger}
+
{agent?.name ?? t("Agent")} · {runTriggerLabel(run.trigger)}
{runStateLabel(run.state)} {formatRelative(run.createdAt)} diff --git a/src/renderer/src/pages/RunsPage.tsx b/src/renderer/src/pages/RunsPage.tsx index cecb5e52..95503c06 100644 --- a/src/renderer/src/pages/RunsPage.tsx +++ b/src/renderer/src/pages/RunsPage.tsx @@ -1,3 +1,4 @@ +import { requestStateLabel, runTriggerLabel } from "../i18n/labels"; import { RunRequestList } from "./runs/RunRequestList"; import { useRunRequests } from "./runs/use-run-requests"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -117,7 +118,7 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) { {task?.title ?? run.taskId}
- {agent?.name ?? t("Agent")} · {run.trigger} + {agent?.name ?? t("Agent")} · {runTriggerLabel(run.trigger)}
{runLabel(run.state)} @@ -206,7 +207,7 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: { {t("Agent")}
{agent?.name ?? "—"} ({agent?.role ?? "—"})
{t("Provider")}
{provider?.label ?? "—"}
{t("Model")}
{run.runModel ?? t("Chosen when the session starts")}
- {t("Trigger")}
{run.trigger}
+ {t("Trigger")}
{runTriggerLabel(run.trigger)}
{t("Created")}
{formatRelative(run.createdAt)}
{run.startedAt && (<>{t("Started")}
{formatRelative(run.startedAt)}
)} {run.completedAt && (<>{t("Completed")}
{formatRelative(run.completedAt)}
)} @@ -242,7 +243,7 @@ function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
{t("Request ID")}
{request.id}
{t("Model")}
{request.model}
- {t("State")}
{request.state}
+ {t("State")}
{requestStateLabel(request.state)}
{t("Input chars")}
{request.inputCharCount.toLocaleString()}
{t("Output chars")}
{request.outputCharCount.toLocaleString()}
{t("Tokens")}
{request.inputTokens === null ? t("not reported") : t("{input} in · {output} out", { input: request.inputTokens.toLocaleString(), output: (request.outputTokens ?? 0).toLocaleString() })}
diff --git a/src/renderer/src/pages/approvals/ApprovalDialogs.tsx b/src/renderer/src/pages/approvals/ApprovalDialogs.tsx index 10c488aa..1f18a1c4 100644 --- a/src/renderer/src/pages/approvals/ApprovalDialogs.tsx +++ b/src/renderer/src/pages/approvals/ApprovalDialogs.tsx @@ -1,3 +1,4 @@ +import { approvalKindLabel } from "../../i18n/labels"; import { useState } from "react"; import { Button, IconButton, Pill } from "../../shared/ui"; import type { ApprovalRecord } from "../../../../shared/domain"; @@ -45,7 +46,7 @@ export function ApprovalDetailDialog({
{tone.label} · - {approval.kind} + {approvalKindLabel(approval.kind)}

{approval.details} @@ -112,7 +113,7 @@ export function ApprovalRecordDialog({ approval, requester, decider, drift, onCl

{outcome} - {approval.kind} + {approvalKindLabel(approval.kind)} {drift && {drift === "changed" ? t("Changed since approval") : t("Removed since approval")}}

{approval.details}

diff --git a/src/renderer/src/pages/chat/ChatComposer.tsx b/src/renderer/src/pages/chat/ChatComposer.tsx index 1642894d..7c7bdab2 100644 --- a/src/renderer/src/pages/chat/ChatComposer.tsx +++ b/src/renderer/src/pages/chat/ChatComposer.tsx @@ -1,3 +1,4 @@ +import { shortcutLabel } from "../../i18n/labels"; import { useEffect, useRef, useState, type FormEvent, type MutableRefObject } from "react"; import { Button, Icon, IconButton } from "../../shared/ui"; import type { AgentRecord, EmployeeRecord, FileObjectRecord } from "../../../../shared/domain"; @@ -108,7 +109,7 @@ export function ChatComposer({ ))}
- Ctrl+Enter + {shortcutLabel("Enter")}
diff --git a/src/renderer/src/pages/chat/ChatMessageRow.tsx b/src/renderer/src/pages/chat/ChatMessageRow.tsx index a735732e..3d115ce6 100644 --- a/src/renderer/src/pages/chat/ChatMessageRow.tsx +++ b/src/renderer/src/pages/chat/ChatMessageRow.tsx @@ -1,3 +1,4 @@ +import { fileKindLabel } from "../../i18n/labels"; import { useState, type FormEvent, type ReactNode } from "react"; import { Avatar, Button, Icon, IconButton } from "../../shared/ui"; import type { @@ -260,7 +261,7 @@ function AttachmentCard({ file, handoff = false, onOpen }: { file: FileObjectRec )} -
{file.kind.toUpperCase()}
+
{fileKindLabel(file.kind)}
{canOpen && } diff --git a/src/renderer/src/pages/chat/ChatTimeline.tsx b/src/renderer/src/pages/chat/ChatTimeline.tsx index 40b9963c..d9146e7d 100644 --- a/src/renderer/src/pages/chat/ChatTimeline.tsx +++ b/src/renderer/src/pages/chat/ChatTimeline.tsx @@ -1,3 +1,4 @@ +import { riskLabel } from "../../i18n/labels"; import { useState, type ReactNode } from "react"; import { Button, Icon, Pill } from "../../shared/ui"; import type { @@ -149,7 +150,7 @@ function ApprovalInline({ approval, onApprove, onDecline }: { approval: Approval
{t("Approval needed")} - {approval.risk} + {riskLabel(approval.risk)}
{approval.title}
{approval.details}
diff --git a/src/renderer/src/pages/library/DocumentsPanel.tsx b/src/renderer/src/pages/library/DocumentsPanel.tsx index 2327d56b..68a51203 100644 --- a/src/renderer/src/pages/library/DocumentsPanel.tsx +++ b/src/renderer/src/pages/library/DocumentsPanel.tsx @@ -1,3 +1,4 @@ +import { DOCUMENT_STATUS_VALUES, documentStatusLabel } from "../../i18n/labels"; import { useEffect, useState, type FormEvent } from "react"; import type { DocumentRecord, TaskRecord } from "../../../../shared/domain"; import type { DocumentVersionRecord } from "../../../../shared/commands"; @@ -98,7 +99,7 @@ export function DocumentsPanel({

{shown?.title ?? selected.title}

{selected.docType} - {shown?.status ?? selected.status} + {documentStatusLabel(shown?.status ?? selected.status)}
{producedBy &&

{t("Produced by task: {title}", { title: producedBy })}

}
@@ -124,7 +125,7 @@ export function DocumentsPanel({