Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
import { App } from "./App";
import { __resetComposerBehaviorForTests } from "./app/composerBehavior";
import { __resetEscapeLayersForTests } from "./app/escapeLayer";
import { __setLocaleForTests } from "./app/i18n";
import type {
AnyRpcNotification,
DesktopRuntime,
Expand Down Expand Up @@ -1505,6 +1506,7 @@ describe("desktop command center", () => {
localStorage.clear();
__resetComposerBehaviorForTests();
__resetEscapeLayersForTests();
__setLocaleForTests("en");
});
afterEach(() => {
cleanup();
Expand Down
117 changes: 117 additions & 0 deletions desktop/src/app/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,29 @@ const ZH_CN: Record<string, string> = {
"settings.composer.queue": "排队为下一个 Turn",
"settings.composer.note":
"仅在忙碌时生效;Cmd/Ctrl+Enter 执行另一种行为。空闲时 Enter 始终发送。本设置立即生效,仅保存在本机。",
// Updates card
"settings.updates.eyebrow": "签名发布通道",
"settings.updates.title": "应用更新",
"settings.updates.check": "检查更新",
"settings.updates.checking": "正在检查…",
"settings.updates.install": "安装",
"settings.updates.preparing": "准备中…",
"settings.updates.downloading": "下载中…",
"settings.updates.installing": "安装中…",
"settings.updates.upToDate": "当前已是最新版本。",
"settings.updates.available":
"DeepCode {{version}} 可用。安装前会验证软件包签名。",
"settings.updates.idle":
"仅在手动请求时检查更新。开发版本可能未配置发布通道。",
"settings.updates.checkingNote": "正在检查签名发布通道。",
// Diagnostics card
"settings.diagnostics.eyebrow": "故障排除",
"settings.diagnostics.title": "诊断",
"settings.diagnostics.export": "导出报告",
"settings.diagnostics.exporting": "导出中…",
"settings.diagnostics.runChecks": "运行检查",
"settings.diagnostics.savedTo": "已保存脱敏诊断报告至 {{path}}",
"settings.diagnostics.noProject": "未选择项目",
// Sidebar
"sidebar.threads": "会话",
"sidebar.automations": "自动化",
Expand All @@ -67,12 +90,106 @@ const ZH_CN: Record<string, string> = {
"sidebar.mcp": "MCP",
"sidebar.settings": "设置",
"sidebar.projects": "项目",
"sidebar.newThread": "新建会话",
"sidebar.searchSessions": "搜索会话",
"sidebar.openFolder": "打开本地文件夹",
"sidebar.openFolderHint": "CLI 中的会话将在此显示。",
"sidebar.noResults": "无匹配会话",
"sidebar.noResultsHint": "可按标题、项目或工作空间路径搜索。",
"sidebar.noSessions": "暂无会话",
"sidebar.showMore": "显示更多 {{count}} 个",
"sidebar.showLess": "收起",
"sidebar.previousSessions": "历史会话",
"sidebar.folderUnavailable": "原始文件夹不可用",
"sidebar.localAgent": "本地智能体",
"sidebar.agentReady": "本地智能体就绪",
"sidebar.sharedHistory": "共享会话历史",
// Composer high-traffic strings
"composer.hint.send": "↵ 发送",
"composer.hint.steerQueue": "↵ 引导 · ⌘↵ 排队",
"composer.hint.queueSteer": "↵ 排队 · ⌘↵ 引导",
"composer.hint.newline": "⇧↵ 换行",
"composer.queueNext": "排队下一个",
// Thread header
"thread.startThread": "开始本地编码会话",
"thread.folderUnavailable": "文件夹不可用",
"thread.noLocalFolder": "无本地文件夹",
"thread.previousSessions": "历史会话",
"thread.trustFolder": "信任文件夹",
"thread.trusted": "已信任",
"thread.trustedTooltip": "允许在此文件夹中执行",
"thread.folderUnavailableTooltip": "原始会话文件夹不再可用",
"thread.paper": "论文",
"thread.paperTooltip": "创建 Paper2Code 会话",
"thread.fork": "分叉",
"thread.forkTooltip": "分叉到独立工作树",
"thread.review": "审查",
"thread.closeReview": "关闭审查面板",
"thread.openReview": "打开审查面板",
// Approval card
"approval.label": "需要审批",
"approval.decision": "决定: {{status}}",
"approval.allowContinue": "允许 {{tool}} 继续?",
"approval.reviewOperation": "在智能体继续之前,请审查此操作。",
"approval.sensitiveOperation": "敏感操作",
"approval.reviewArguments": "查看参数",
"approval.allowOnce": "允许一次",
"approval.allowSession": "允许本次会话",
"approval.deny": "拒绝",
// Runtime notice
"runtime.offline": "本地应用服务器不可用。",
"runtime.restart": "重启服务",
// Goal rail
"goal.setGoal": "设定目标",
"goal.setGoalHint": "在普通 Turn 之间保持一个持久的目标",
"goal.sessionGoal": "会话目标",
"goal.turn": "Turn",
"goal.turns": "Turns",
"goal.tokens": "tokens",
"goal.continue": "继续",
"goal.editGoal": "编辑目标",
"goal.pause": "暂停",
"goal.pauseTooltip": "暂停自动继续;不会中断当前 Turn",
"goal.editReopen": "编辑并重新开启",
"goal.resume": "恢复",
"goal.edit": "编辑",
"goal.newGoal": "新建目标",
"goal.objectiveHint":
"目标编辑保持同一 Goal 身份,并到达活跃的 Turn。",
"goal.outcome": "目标结果",
"goal.tokenBudget": "Token 预算",
"goal.tokenBudgetOptional": "可选",
"goal.noLimit": "无限制",
"goal.skills": "技能",
"goal.cancel": "取消",
"goal.saveResume": "保存并恢复",
"goal.saveGoal": "保存目标",
"goal.startGoal": "开始目标",
"goal.clearGoal": "清除目标",
"goal.clearConfirm": "确定清除此会话目标?",
"goal.completionOutcome": "完成结果",
"goal.blockedOutcome": "阻塞结果",
"goal.relatedActivity": "相关活动",
"goal.noSkills": "未选择目标相关技能。",
"goal.activeTime": "{{duration}} 活跃时间",
"goal.noUsage": "尚无已完成的 Goal Turn 用量",
"goal.tokenBudgetLabel": "{{budget}} token 预算",
"goal.noTokenBudget": "无 token 预算",
"goal.decidingTurn": "决定 Turn",
"goal.goalDescription":
"目标 {{id}} 附属于此会话。普通后续对话引导工作方向,而不重写目标。",
"goal.closeEditor": "关闭目标编辑器",
"goal.describeOutcome": "描述 DeepCode 应达成的完整目标。",
// Inspector
"inspector.label": "检查器",
"inspector.views": "检查器视图",
"inspector.closeReview": "关闭审查面板",
"inspector.tab.changes": "变更",
"inspector.tab.files": "文件",
"inspector.tab.artifacts": "产物",
"inspector.tab.tests": "测试",
"inspector.tab.terminal": "终端",
"inspector.tab.details": "详情",
};

function readLocale(): Locale {
Expand Down
6 changes: 4 additions & 2 deletions desktop/src/components/RuntimeNotice.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AlertTriangle, RotateCw, X } from "lucide-react";
import { useTranslation } from "react-i18next";

import type { BridgeError, SidecarStatus } from "../rpc/contracts";
import styles from "./RuntimeNotice.module.css";
Expand All @@ -18,9 +19,10 @@ export function RuntimeNotice({
onRestart,
onDismissError,
}: RuntimeNoticeProps) {
const { t } = useTranslation();
if (!error && runtime.phase !== "crashed" && runtime.phase !== "stopped") return null;
const serviceOffline = runtime.phase === "crashed" || runtime.phase === "stopped";
const message = error?.message ?? runtime.message ?? "The local App Server is unavailable.";
const message = error?.message ?? runtime.message ?? t("runtime.offline", "The local App Server is unavailable.");
return (
<div className={styles.notice} role="alert">
<AlertTriangle size={18} aria-hidden="true" />
Expand All @@ -42,7 +44,7 @@ export function RuntimeNotice({
{serviceOffline ? (
<button type="button" onClick={onRestart} disabled={busy}>
<RotateCw size={14} />
Restart service
{t("runtime.restart", "Restart service")}
</button>
) : null}
</div>
Expand Down
28 changes: 15 additions & 13 deletions desktop/src/features/execution/ApprovalCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
TerminalSquare,
Wrench,
} from "lucide-react";
import { useTranslation } from "react-i18next";

import type { Approval, ApprovalDecision } from "../../generated/app-server";
import styles from "./ApprovalCard.module.css";
Expand All @@ -15,17 +16,17 @@ interface ApprovalCardProps {
onRespond(approvalId: string, decision: ApprovalDecision): void;
}

function requestDetail(approval: Approval): string {
function requestDetail(approval: Approval, t: (key: string, defaultValue: string, options?: Record<string, string>) => string): string {
const tool = approval.request.toolName;
const reason = approval.request.reason;
if (typeof reason === "string" && reason) return reason;
if (typeof tool === "string" && tool) return `Allow ${tool} to continue?`;
return "Review this operation before the agent continues.";
if (typeof tool === "string" && tool) return t("approval.allowContinue", "Allow {{tool}} to continue?", { tool });
return t("approval.reviewOperation", "Review this operation before the agent continues.");
}

function requestTool(approval: Approval): string {
function requestTool(approval: Approval, t: (key: string, defaultValue: string) => string): string {
const tool = approval.request.toolName;
return typeof tool === "string" && tool.trim() ? tool : "Sensitive operation";
return typeof tool === "string" && tool.trim() ? tool : t("approval.sensitiveOperation", "Sensitive operation");
}

function requestArguments(approval: Approval): string | null {
Expand Down Expand Up @@ -61,27 +62,28 @@ function CategoryIcon({ category }: { category: Approval["category"] }) {
}

export function ApprovalCard({ approval, busy, onRespond }: ApprovalCardProps) {
const { t } = useTranslation();
if (approval.status !== "pending") {
return (
<p className={styles.resolution}>
Decision: {approval.status.replaceAll("_", " ")}
{t("approval.decision", "Decision: {{status}}", { status: approval.status.replaceAll("_", " ") })}
</p>
);
}
const argumentsPreview = requestArguments(approval);
return (
<div className={styles.card} aria-label="Approval required">
<div className={styles.card} aria-label={t("approval.label", "Approval required")}>
<div className={styles.operation}>
<CategoryIcon category={approval.category} />
<span>
<strong>{requestTool(approval)}</strong>
<strong>{requestTool(approval, t)}</strong>
<small>{approval.category.replaceAll("_", " ")}</small>
</span>
</div>
<p>{requestDetail(approval)}</p>
<p>{requestDetail(approval, t)}</p>
{argumentsPreview ? (
<details className={styles.arguments}>
<summary>Review arguments</summary>
<summary>{t("approval.reviewArguments", "Review arguments")}</summary>
<pre>{argumentsPreview}</pre>
</details>
) : null}
Expand All @@ -91,22 +93,22 @@ export function ApprovalCard({ approval, busy, onRespond }: ApprovalCardProps) {
onClick={() => onRespond(approval.id, "approved_once")}
disabled={busy}
>
Allow once
{t("approval.allowOnce", "Allow once")}
</button>
<button
type="button"
onClick={() => onRespond(approval.id, "approved_session")}
disabled={busy}
>
Allow for Session
{t("approval.allowSession", "Allow for Session")}
</button>
<button
className={styles.danger}
type="button"
onClick={() => onRespond(approval.id, "denied")}
disabled={busy}
>
Deny
{t("approval.deny", "Deny")}
</button>
</div>
</div>
Expand Down
Loading