@steerable/agent-shell-web 0.6.53 → 0.6.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +4 -4
  2. package/src/brand.ts +1 -1
  3. package/src/components/AgentSidebar.tsx +60 -52
  4. package/src/components/ChatHeader.tsx +41 -25
  5. package/src/components/ConfirmDialog.tsx +4 -3
  6. package/src/components/CreateProjectModal.tsx +29 -19
  7. package/src/components/LocalLlmSettingsModal.tsx +4 -3
  8. package/src/components/SidebarRelease.tsx +2 -1
  9. package/src/components/TerminalPanel.tsx +3 -2
  10. package/src/components/TerminalView.tsx +12 -7
  11. package/src/components/chat/ApprovalModal.tsx +39 -25
  12. package/src/components/chat/ApprovalPromptProvider.tsx +1 -1
  13. package/src/components/chat/AskUserPromptProvider.tsx +1 -1
  14. package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
  15. package/src/components/chat/AssistantMessage.tsx +18 -13
  16. package/src/components/chat/ChatInput.tsx +56 -40
  17. package/src/components/chat/ChatProjectBadge.tsx +24 -19
  18. package/src/components/chat/EmptyChatHero.tsx +2 -1
  19. package/src/components/chat/ExecPolicyPicker.tsx +12 -11
  20. package/src/components/chat/ExecutedActionsCard.tsx +45 -33
  21. package/src/components/chat/FilePathCode.tsx +8 -3
  22. package/src/components/chat/InterruptedTurnCard.tsx +4 -3
  23. package/src/components/chat/LocalChatPanel.tsx +2 -1
  24. package/src/components/chat/Markdown.tsx +2 -1
  25. package/src/components/chat/MessageList.tsx +3 -2
  26. package/src/components/chat/ModelPicker.tsx +25 -16
  27. package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
  28. package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
  29. package/src/components/chat/PlanTodoList.tsx +2 -1
  30. package/src/components/chat/ProjectTrustBanner.tsx +19 -5
  31. package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
  32. package/src/components/chat/SessionTodoList.tsx +8 -7
  33. package/src/components/chat/SessionTreeModal.tsx +10 -9
  34. package/src/components/chat/StreamingStatus.tsx +4 -3
  35. package/src/components/chat/SuggestedReplies.tsx +3 -2
  36. package/src/components/chat/TaskOutcomeCards.tsx +8 -5
  37. package/src/components/chat/TaskPanelModal.tsx +31 -21
  38. package/src/components/chat/TaskProcessPanel.tsx +14 -13
  39. package/src/components/chat/TurnFilesCard.tsx +14 -13
  40. package/src/components/chat/UserMessage.tsx +12 -11
  41. package/src/components/chat/executed-actions-model.ts +47 -35
  42. package/src/components/chat/orchestration-flow-model.ts +16 -5
  43. package/src/components/chat/process-status.ts +8 -7
  44. package/src/components/chat/todo-list-model.ts +5 -4
  45. package/src/components/chat/turn-files.ts +8 -8
  46. package/src/components/settings/AgentCapabilityFields.tsx +24 -21
  47. package/src/components/settings/AgentsSettingsPanel.tsx +30 -25
  48. package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
  49. package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
  50. package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
  51. package/src/components/settings/InsightsSettingsPanel.tsx +43 -28
  52. package/src/components/settings/LlmSettingsPanel.tsx +104 -83
  53. package/src/components/settings/McpSettingsPanel.tsx +60 -34
  54. package/src/components/settings/ModelIdCombobox.tsx +2 -1
  55. package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
  56. package/src/components/settings/PortableSettingsPanel.tsx +83 -55
  57. package/src/components/settings/PythonRunnerSettingsPanel.tsx +22 -19
  58. package/src/components/settings/SecuritySettingsPanel.tsx +55 -31
  59. package/src/components/settings/SettingsNavMenu.tsx +3 -2
  60. package/src/components/settings/SettingsSaveButton.tsx +2 -1
  61. package/src/components/settings/SkillsSettingsPanel.tsx +36 -31
  62. package/src/components/settings/TelemetrySettingsPanel.tsx +15 -12
  63. package/src/components/settings/UsagePanel.tsx +19 -20
  64. package/src/components/settings/WebSearchSettingsPanel.tsx +23 -14
  65. package/src/components/settings/llm-vendors.ts +12 -7
  66. package/src/components/settings/model-capabilities.ts +16 -15
  67. package/src/components/settings/preset-choice-model.ts +6 -3
  68. package/src/fixtures/browser-dev-data.json +24 -24
  69. package/src/hooks/useProjects.ts +1 -1
  70. package/src/i18n/agent-label.ts +15 -0
  71. package/src/i18n/index.tsx +68 -0
  72. package/src/i18n/runtime.ts +117 -0
  73. package/src/i18n/translate.ts +65 -0
  74. package/src/layouts/AgentLayout.tsx +7 -6
  75. package/src/lib/app-release.ts +15 -10
  76. package/src/lib/attachments.ts +16 -7
  77. package/src/lib/browser-dev-electron-mock.ts +34 -34
  78. package/src/lib/chat-title.ts +3 -2
  79. package/src/lib/chat-transport.ts +6 -2
  80. package/src/lib/date-groups.ts +23 -15
  81. package/src/lib/electron-bridge.ts +4 -5
  82. package/src/lib/http-bridge.ts +4 -1
  83. package/src/lib/portable.ts +8 -7
  84. package/src/lib/show-thinking-content.ts +15 -3
  85. package/src/main.tsx +25 -2
  86. package/src/pages/AgentPage.tsx +17 -12
  87. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  88. package/src/pages/SettingsPage.tsx +48 -42
  89. package/src/vite-env.d.ts +3 -0
  90. package/vite.base.js +6 -2
@@ -63,6 +63,7 @@ import {
63
63
  } from "@/lib/pending-first-message";
64
64
  import { isOrchestrationSettingEnabled } from "@/lib/orchestration-settings";
65
65
  import { pickDefaultAgentId } from "@/brand";
66
+ import { t } from "@/i18n";
66
67
  import {
67
68
  appendAttachmentRefs,
68
69
  collectImageAttachments,
@@ -318,7 +319,7 @@ function AgentChatLoader({
318
319
  if (initialMessages === null) {
319
320
  return (
320
321
  <div className="flex h-full w-full items-center justify-center text-xs text-agent-muted-foreground">
321
- 加载对话历史…
322
+ {t("Loading chat history…")}
322
323
  </div>
323
324
  );
324
325
  }
@@ -1041,7 +1042,7 @@ function AgentChatView({
1041
1042
  const handleExecutePlan = useCallback(() => {
1042
1043
  handleModeChange("agent");
1043
1044
  setPlanReady(false);
1044
- void handleSubmit({ content: "请按照上面的计划开始执行。" });
1045
+ void handleSubmit({ content: t("Start executing the plan above.") });
1045
1046
  }, [handleModeChange, handleSubmit]);
1046
1047
 
1047
1048
  // 分享当前对话:截取整个聊天面板区域(含 header + 消息 + 输入框),
@@ -1154,7 +1155,7 @@ function AgentChatView({
1154
1155
  <div className="flex h-full w-full flex-col">
1155
1156
  {hydrationError && (
1156
1157
  <div className="border-b border-agent-border bg-agent-muted/60 px-2.5 py-1 text-xs text-agent-destructive">
1157
- 加载对话历史失败:{hydrationError}
1158
+ {t("Failed to load chat history: {error}", { error: hydrationError })}
1158
1159
  </div>
1159
1160
  )}
1160
1161
  <LocalChatPanel
@@ -1191,7 +1192,7 @@ function AgentChatView({
1191
1192
  }
1192
1193
  : undefined
1193
1194
  }
1194
- inputPlaceholder="向本地 Agent 发送消息…"
1195
+ inputPlaceholder={t("Message the local Agent…")}
1195
1196
  agents={ctx.agents}
1196
1197
  chats={ctx.chats}
1197
1198
  currentAgent={agent}
@@ -1269,7 +1270,9 @@ function AgentChatView({
1269
1270
  <div className="mx-2.5 mb-1 flex items-center justify-between gap-2 rounded-agent-md border border-amber-400/50 bg-amber-400/10 px-2.5 py-1.5 text-xs">
1270
1271
  <div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
1271
1272
  <LuListChecks className="h-4 w-4 shrink-0" />
1272
- <span>计划已生成。确认无误后可切换到 Agent 模式开始执行。</span>
1273
+ <span>
1274
+ {t("The plan is ready. Once it looks right, switch to Agent mode to execute it.")}
1275
+ </span>
1273
1276
  </div>
1274
1277
  <div className="flex items-center gap-2">
1275
1278
  <button
@@ -1278,7 +1281,7 @@ function AgentChatView({
1278
1281
  className="rounded-full px-2 py-1 text-agent-muted-foreground transition-colors hover:text-agent-foreground"
1279
1282
  data-testid="plan-dismiss"
1280
1283
  >
1281
- 忽略
1284
+ {t("Dismiss")}
1282
1285
  </button>
1283
1286
  <button
1284
1287
  type="button"
@@ -1286,7 +1289,7 @@ function AgentChatView({
1286
1289
  className="inline-flex items-center gap-1 rounded-full bg-agent-foreground px-3 py-1 font-medium text-agent-canvas transition hover:opacity-90"
1287
1290
  data-testid="plan-execute"
1288
1291
  >
1289
- 开始执行计划
1292
+ {t("Execute plan")}
1290
1293
  <LuArrowRight className="h-3.5 w-3.5" />
1291
1294
  </button>
1292
1295
  </div>
@@ -1416,7 +1419,7 @@ function EmptyChatGate() {
1416
1419
  ...(selectedProjectId && showProjectsChrome ? { projectId: selectedProjectId } : {}),
1417
1420
  ...(ctx.selectedAgentId ? { agentId: ctx.selectedAgentId } : {}),
1418
1421
  });
1419
- if (!id) throw new Error("创建对话失败,请重试");
1422
+ if (!id) throw new Error(t("Failed to create the chat. Please try again."));
1420
1423
  // 会话已建,把附件落进会话空间(与 LocalChatPanel 同一套语义):
1421
1424
  // 所有文件都写落盘路径引用(agent 用 local_read_file 读回);仅图片
1422
1425
  // 额外进 metadata.images 走多模态,非图片文件不会被图片逻辑吞掉。
@@ -1466,8 +1469,8 @@ function EmptyChatGate() {
1466
1469
  disabled={!isElectron() || isCreating}
1467
1470
  placeholder={
1468
1471
  mode === "plan"
1469
- ? "描述你的目标,Agent 将先制定计划…"
1470
- : "向本地 Agent 发送消息…"
1472
+ ? t("Describe your goal. The Agent will make a plan first…")
1473
+ : t("Message the local Agent…")
1471
1474
  }
1472
1475
  currentAgent={selectedAgent}
1473
1476
  agents={ctx.agents}
@@ -1516,7 +1519,7 @@ function EmptyChatGate() {
1516
1519
  </div>
1517
1520
  {isCreating && (
1518
1521
  <p className="text-xs text-agent-muted-foreground">
1519
- 正在创建对话…
1522
+ {t("Creating chat…")}
1520
1523
  </p>
1521
1524
  )}
1522
1525
  {createError && (
@@ -1526,7 +1529,9 @@ function EmptyChatGate() {
1526
1529
  )}
1527
1530
  {!isElectron() && (
1528
1531
  <p className="text-xs text-agent-destructive">
1529
- 浏览器预览模式 — 没有 Electron IPC 桥接,聊天列表与流式响应不可用。
1532
+ {t(
1533
+ "Browser preview mode: no Electron IPC bridge, so the chat list and streaming responses are unavailable.",
1534
+ )}
1530
1535
  </p>
1531
1536
  )}
1532
1537
  </div>
@@ -5,6 +5,7 @@ import { ChatHeader } from '@/components/ChatHeader';
5
5
  import type { ExecutedAction } from '@/components/chat/ExecutedActionsCard';
6
6
  import type { TurnBlock } from '@/components/chat/turn-timeline';
7
7
  import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
8
+ import { t } from '@/i18n';
8
9
 
9
10
  /**
10
11
  * ChatPanelPreviewPage — dev-only visual harness for `LocalChatPanel`.
@@ -22,10 +23,10 @@ import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
22
23
  const MOCK_AGENT: LocalChatAgent = {
23
24
  id: 'agent_local_default',
24
25
  slug: 'local-coder',
25
- name: '本地 Coder',
26
+ name: 'Local coder',
26
27
  icon: '🤖',
27
28
  color: '#0ea5e9',
28
- description: '本地代码助手 — 跑 shell / 工具演示 / 简单读写',
29
+ description: 'Local coding assistant — shell, tool demos, and simple file edits',
29
30
  rolePrompt: null,
30
31
  isBuiltin: true,
31
32
  };
@@ -34,7 +35,7 @@ const MOCK_CHAT: LocalChat = {
34
35
  id: 'preview-chat',
35
36
  projectId: null,
36
37
  userId: 'preview-user',
37
- title: '预览:检查服务连通性 + 跑测试套件',
38
+ title: 'Preview: check the service, then run the test suite',
38
39
  agentId: MOCK_AGENT.id,
39
40
  createdAt: new Date().toISOString(),
40
41
  updatedAt: new Date().toISOString(),
@@ -47,21 +48,21 @@ const MOCK_MESSAGES: ChatMessage[] = [
47
48
  {
48
49
  id: 'm1',
49
50
  role: 'user',
50
- content: '帮我看下本地服务是否连通,再跑一下本目录的测试套件。',
51
+ content: 'Check whether the local service is up, then run the test suite in this directory.',
51
52
  createdAt: new Date(Date.now() - 1000 * 60 * 5).toISOString(),
52
53
  },
53
54
  {
54
55
  id: 'm2',
55
56
  role: 'assistant',
56
57
  agentId: MOCK_AGENT.id,
57
- content: `好的,先检查服务连通性,然后跑测试。
58
+ content: `I'll check the service, then run the tests.
58
59
 
59
60
  \`\`\`bash
60
61
  $ check_connection
61
62
  ✓ Mock service @ 127.0.0.1:8765 reachable (latency 4ms)
62
63
  \`\`\`
63
64
 
64
- 连接正常。接下来跑测试套件:
65
+ The connection is fine. Next, the test suite:
65
66
 
66
67
  \`\`\`bash
67
68
  $ npm test
@@ -74,18 +75,17 @@ FAIL src/components/chat/AssistantMessage.test.tsx
74
75
  Tests: 1 failed, 2 passed, 3 total
75
76
  \`\`\`
76
77
 
77
- 有 1 个失败:\`AssistantMessage\` 的默认 agent 色没有从 props 传过来。建议在
78
- \`agentInitial\` 之前先 \`a.color ?? '#7c3aed'\`,让 fallback 优先于硬编码。
78
+ 1 failure: \`AssistantMessage\` did not take the agent color from props. Use \`a.color ?? '#7c3aed'\` before \`agentInitial\` so the fallback wins over the hardcoded color.
79
79
 
80
- 要我帮你直接改这个文件吗?`,
80
+ Want me to edit that file?`,
81
81
  createdAt: new Date(Date.now() - 1000 * 60 * 3).toISOString(),
82
82
  },
83
83
  ];
84
84
 
85
85
  const MOCK_TODOS = [
86
- { id: 'survey', content: '检查服务连通性', status: 'completed' as const },
87
- { id: 'test', content: '跑本目录测试套件', status: 'in_progress' as const },
88
- { id: 'fix', content: '修失败的断言颜色', status: 'pending' as const },
86
+ { id: 'survey', content: 'Check service connectivity', status: 'completed' as const },
87
+ { id: 'test', content: 'Run the test suite in this directory', status: 'in_progress' as const },
88
+ { id: 'fix', content: 'Fix the failing assertion color', status: 'pending' as const },
89
89
  ];
90
90
 
91
91
  const MOCK_TODO_ACTION: ExecutedAction = {
@@ -127,12 +127,12 @@ const MOCK_ACTIONS: ExecutedAction[] = [
127
127
  ];
128
128
 
129
129
  const MOCK_TIMELINE: TurnBlock[] = [
130
- { type: 'reasoning', content: '先检查服务连通性,再跑测试套件。' },
130
+ { type: 'reasoning', content: 'Check connectivity first, then run the test suite.' },
131
131
  { type: 'tools', actions: [MOCK_TODO_ACTION] },
132
132
  { type: 'tools', actions: [MOCK_ACTIONS[1]] },
133
- { type: 'text', content: '连接正常。接下来跑测试套件。' },
133
+ { type: 'text', content: 'The connection is fine. Next, the test suite.' },
134
134
  { type: 'tools', actions: [MOCK_ACTIONS[2]] },
135
- { type: 'reasoning', content: '测试失败,读一下失败文件再给建议。' },
135
+ { type: 'reasoning', content: 'The tests failed. Read the failing file, then suggest a fix.' },
136
136
  { type: 'tools', actions: [MOCK_ACTIONS[3]] },
137
137
  { type: 'text', content: MOCK_MESSAGES[1].content ?? '' },
138
138
  ];
@@ -142,33 +142,35 @@ const MOCK_TIMELINE: TurnBlock[] = [
142
142
  * • `static` — finished conversation; verifies copy buttons, action
143
143
  * cards, markdown rendering on history.
144
144
  * • `thinking` — in-flight assistant bubble before any content; verifies
145
- * StreamingStatus's "正在思考..." baseline.
145
+ * StreamingStatus's "Now thinking..." baseline.
146
146
  * • `tools-run` — in-flight bubble, round 2, 2 tools just ran; verifies
147
- * StreamingStatus's "已调用 N 个工具" + "Round N" badge interaction.
147
+ * StreamingStatus's "Called N tools" line and the Round N badge.
148
148
  * • `reasoning-stream` — reasoning tokens arrive over time; verifies the
149
149
  * 5-line peek stays visible and the list scrollbar sticks to the bottom.
150
150
  */
151
151
  type PreviewScene = 'static' | 'thinking' | 'tools-run' | 'reasoning-stream';
152
152
 
153
153
  const STREAMING_REASONING = [
154
- '先检查服务是否还能连上。',
155
- '如果端口通了,再跑本目录的测试套件。',
156
- '失败的话只读失败文件,不要整仓扫一遍。',
157
- '断言颜色对不上时优先看 AssistantMessage 的默认色。',
158
- '改完再复跑一次,确认没有带出新的失败。',
159
- '工具调用保持最少:连通性检查、跑测试、必要时读文件。',
160
- '最后用一两句说清楚结果和下一步。',
161
- '如果还在思考,后面的句子会被 5 行窗口裁掉。',
162
- '比较 a < b 时不要被 Markdown 当成 HTML 标签吃掉。',
163
- '滚动条应一直钉在最下面,不要随着 token 上下跳。',
154
+ 'Check whether the service is still reachable.',
155
+ 'If the port is open, run the test suite in this directory.',
156
+ 'If it fails, read only the failing file. Do not scan the whole repo.',
157
+ "When an assertion color is wrong, look at AssistantMessage's default color first.",
158
+ 'After the edit, run the tests again and check for new failures.',
159
+ 'Keep tool calls few: connectivity, tests, and a file read only if needed.',
160
+ 'Finish with one or two sentences on the result and the next step.',
161
+ 'If thinking is still streaming, later sentences are cut by the 5-line window.',
162
+ 'Comparing a < b must not be eaten as an HTML tag by Markdown.',
163
+ 'The scrollbar should stay pinned to the bottom and not jump with each token.',
164
164
  ].join('\n');
165
165
 
166
- const SCENES: { id: PreviewScene; label: string }[] = [
167
- { id: 'static', label: '静态历史' },
168
- { id: 'thinking', label: '流式 · 空内容' },
169
- { id: 'tools-run', label: '流式 · 工具已跑 · round 2' },
170
- { id: 'reasoning-stream', label: '流式 · 推理打字' },
171
- ];
166
+ function previewScenes(): { id: PreviewScene; label: string }[] {
167
+ return [
168
+ { id: 'static', label: t('Static history') },
169
+ { id: 'thinking', label: t('Streaming · empty content') },
170
+ { id: 'tools-run', label: t('Streaming · tools ran · round 2') },
171
+ { id: 'reasoning-stream', label: t('Streaming · reasoning typing') },
172
+ ];
173
+ }
172
174
 
173
175
  export function ChatPanelPreviewPage() {
174
176
  const [scene, setScene] = useState<PreviewScene>('static');
@@ -249,14 +251,14 @@ export function ChatPanelPreviewPage() {
249
251
  {
250
252
  type: 'reasoning',
251
253
  content: [
252
- '先检查服务是否还能连上。',
253
- '如果端口通了,再跑本目录的测试套件。',
254
- '失败的话只读失败文件,不要整仓扫一遍。',
255
- '断言颜色对不上时优先看 AssistantMessage 的默认色。',
256
- '改完再复跑一次,确认没有带出新的失败。',
257
- '工具调用保持最少:连通性检查、跑测试、必要时读文件。',
258
- '最后用一两句说清楚结果和下一步。',
259
- '如果还在思考,后面的句子会被 5 行窗口裁掉。',
254
+ 'Check whether the service is still reachable.',
255
+ 'If the port is open, run the test suite in this directory.',
256
+ 'If it fails, read only the failing file. Do not scan the whole repo.',
257
+ "When an assertion color is wrong, look at AssistantMessage's default color first.",
258
+ 'After the edit, run the tests again and check for new failures.',
259
+ 'Keep tool calls few: connectivity, tests, and a file read only if needed.',
260
+ 'Finish with one or two sentences on the result and the next step.',
261
+ 'If thinking is still streaming, later sentences are cut by the 5-line window.',
260
262
  ].join('\n'),
261
263
  },
262
264
  {
@@ -299,7 +301,7 @@ export function ChatPanelPreviewPage() {
299
301
  timelineByMsgId: { m2: MOCK_TIMELINE },
300
302
  durationByMessageId: { m2: 83_000 },
301
303
  currentRound: 1,
302
- suggestedReplies: ['修一下失败的断言颜色', '把测试再跑一遍', '解释这次失败的原因'],
304
+ suggestedReplies: ['Fix the failing assertion color', 'Run the tests again', 'Explain this failure'],
303
305
  };
304
306
  }
305
307
  }, [scene, streamedReasoning]);
@@ -311,7 +313,7 @@ export function ChatPanelPreviewPage() {
311
313
  /preview/chat (dev harness)
312
314
  </span>
313
315
  <div className="flex gap-1">
314
- {SCENES.map((s) => (
316
+ {previewScenes().map((s) => (
315
317
  <button
316
318
  key={s.id}
317
319
  type="button"
@@ -339,7 +341,7 @@ export function ChatPanelPreviewPage() {
339
341
  header={
340
342
  <ChatHeader chat={MOCK_CHAT} />
341
343
  }
342
- inputPlaceholder="预览模式 — 输入不会真的发送…"
344
+ inputPlaceholder={t('Preview mode: input is not actually sent…')}
343
345
  agents={[MOCK_AGENT]}
344
346
  currentAgent={MOCK_AGENT}
345
347
  executedActionsByMessageId={config.actionsByMsgId}
@@ -17,6 +17,7 @@ import {
17
17
  } from 'react-icons/lu';
18
18
  import { useOutletContext, useSearchParams } from 'react-router-dom';
19
19
  import { isElectron } from '@/lib/electron-bridge';
20
+ import { getLocale, t } from '@/i18n';
20
21
  import { useAppRelease } from '@/components/SidebarRelease';
21
22
  import { AgentsSettingsPanel } from '@/components/settings/AgentsSettingsPanel';
22
23
  import { AppearanceSettingsPanel } from '@/components/settings/AppearanceSettingsPanel';
@@ -49,24 +50,28 @@ type PluginTab = 'agents' | 'skills' | 'mcp' | 'web-search';
49
50
 
50
51
  const PLUGIN_TABS: readonly PluginTab[] = ['agents', 'skills', 'mcp', 'web-search'];
51
52
 
52
- const PLUGIN_TAB_META: Record<
53
- PluginTab,
54
- { label: string; noun: string; Icon: typeof LuBot }
55
- > = {
56
- agents: { label: '智能体', noun: '智能体', Icon: LuBot },
57
- skills: { label: 'Skills', noun: '技能', Icon: LuBlocks },
58
- mcp: { label: 'MCP', noun: 'MCP 服务', Icon: LuPlug },
59
- 'web-search': { label: '网络搜索', noun: '网络搜索', Icon: LuSearch },
60
- };
53
+ /** 每次渲染现取,文案跟随当前语言。 */
54
+ function pluginTabMeta(): Record<PluginTab, { label: string; noun: string; Icon: typeof LuBot }> {
55
+ return {
56
+ agents: { label: t('Agents'), noun: t('agents'), Icon: LuBot },
57
+ skills: { label: 'Skills', noun: t('skills'), Icon: LuBlocks },
58
+ mcp: { label: 'MCP', noun: t('MCP servers'), Icon: LuPlug },
59
+ 'web-search': { label: t('Web search'), noun: t('web search'), Icon: LuSearch },
60
+ };
61
+ }
61
62
 
62
63
  function isPluginTab(value: string | null): value is PluginTab {
63
64
  return (PLUGIN_TABS as readonly string[]).includes(value ?? '');
64
65
  }
65
66
 
66
67
  function pluginsSummary(tabs: readonly PluginTab[]): string {
67
- const nouns = tabs.map((id) => PLUGIN_TAB_META[id].noun);
68
- if (nouns.length <= 1) return `管理${nouns[0] ?? '插件'}。`;
69
- return `管理${nouns.slice(0, -1).join('、')}和${nouns[nouns.length - 1]}。`;
68
+ const meta = pluginTabMeta();
69
+ const nouns = tabs.map((id) => meta[id].noun);
70
+ const items =
71
+ nouns.length === 0
72
+ ? t('plugins')
73
+ : new Intl.ListFormat(getLocale(), { type: 'conjunction' }).format(nouns);
74
+ return t('Manage {items}.', { items });
70
75
  }
71
76
 
72
77
  function enabledPluginTabs(): PluginTab[] {
@@ -185,30 +190,30 @@ export function SettingsPage() {
185
190
  const list: SettingsNavItem[] = [];
186
191
 
187
192
  if (settingsChrome('appearance')) {
188
- list.push({ id: 'appearance', label: '界面', Icon: LuMonitor });
193
+ list.push({ id: 'appearance', label: t('Interface'), Icon: LuMonitor });
189
194
  }
190
- list.push({ id: 'orchestration', label: '协同编排', Icon: LuGitFork });
195
+ list.push({ id: 'orchestration', label: t('Orchestration'), Icon: LuGitFork });
191
196
 
192
197
  if (settingsChrome('llm')) {
193
- list.push({ id: 'llm', label: '本地模型设置', Icon: LuSettings });
198
+ list.push({ id: 'llm', label: t('Local model settings'), Icon: LuSettings });
194
199
  }
195
200
  if (settingsChrome('web-search')) {
196
- list.push({ id: 'web-search', label: '网络搜索', Icon: LuSearch });
201
+ list.push({ id: 'web-search', label: t('Web search'), Icon: LuSearch });
197
202
  }
198
203
  if (settingsChrome('usage')) {
199
- list.push({ id: 'usage', label: '用量与成本', Icon: LuChartBar });
204
+ list.push({ id: 'usage', label: t('Usage and cost'), Icon: LuChartBar });
200
205
  }
201
206
  if (settingsChrome('diagnose')) {
202
- list.push({ id: 'diagnose', label: '链路诊断', Icon: LuNetwork });
207
+ list.push({ id: 'diagnose', label: t('Connection diagnostics'), Icon: LuNetwork });
203
208
  }
204
209
  if (settingsChrome('security')) {
205
- list.push({ id: 'security', label: '安全', Icon: LuShieldCheck });
210
+ list.push({ id: 'security', label: t('Security'), Icon: LuShieldCheck });
206
211
  }
207
212
  if (settingsChrome('insights')) {
208
- list.push({ id: 'insights', label: '帮助改进产品', Icon: LuChartBar });
213
+ list.push({ id: 'insights', label: t('Help improve the product'), Icon: LuChartBar });
209
214
  }
210
215
  if (settingsChrome('telemetry')) {
211
- list.push({ id: 'telemetry', label: '遥测(OTLP)', Icon: LuActivity });
216
+ list.push({ id: 'telemetry', label: t('Telemetry (OTLP)'), Icon: LuActivity });
212
217
  }
213
218
 
214
219
  const packPanels = getPackSettingsPanels();
@@ -221,13 +226,13 @@ export function SettingsPage() {
221
226
  }
222
227
 
223
228
  if (isPortableEnabled()) {
224
- list.push({ id: 'portable', label: '备份与迁移', Icon: LuDownload });
229
+ list.push({ id: 'portable', label: t('Backup and migration'), Icon: LuDownload });
225
230
  }
226
231
  if (isElectron()) {
227
- list.push({ id: 'python-runner', label: 'Python 运行环境', Icon: LuCodeXml });
232
+ list.push({ id: 'python-runner', label: t('Python runtime'), Icon: LuCodeXml });
228
233
  }
229
234
  if (release.version) {
230
- list.push({ id: 'update', label: '软件更新', Icon: LuInfo });
235
+ list.push({ id: 'update', label: t('Software update'), Icon: LuInfo });
231
236
  }
232
237
 
233
238
  return list;
@@ -271,7 +276,8 @@ export function SettingsPage() {
271
276
  }
272
277
  }, [menuItems]);
273
278
 
274
- const title = section === 'plugins' ? '插件' : '设置';
279
+ const title = section === 'plugins' ? t('Plugins') : t('Settings');
280
+ const pluginMeta = pluginTabMeta();
275
281
 
276
282
  return (
277
283
  <div ref={pageContainerRef} className="flex h-full w-full flex-col overflow-hidden">
@@ -281,12 +287,12 @@ export function SettingsPage() {
281
287
  {section === 'plugins' && pluginTabs.length > 1 && (
282
288
  <div
283
289
  role="tablist"
284
- aria-label="插件"
290
+ aria-label={t('Plugins')}
285
291
  data-testid="plugins-tabs"
286
292
  className="flex min-w-0 items-center gap-1"
287
293
  >
288
294
  {pluginTabs.map((id) => {
289
- const meta = PLUGIN_TAB_META[id];
295
+ const meta = pluginMeta[id];
290
296
  const active = pluginTab === id;
291
297
  return (
292
298
  <button
@@ -343,7 +349,7 @@ export function SettingsPage() {
343
349
  <div ref={contentRef} className="min-w-0 flex-1 max-w-3xl space-y-4">
344
350
  {!isElectron() && (
345
351
  <p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
346
- 浏览器预览模式 — 没有 Electron IPC 桥接,部分设置不可用。
352
+ {t('Browser preview mode: no Electron IPC bridge, so some settings are unavailable.')}
347
353
  </p>
348
354
  )}
349
355
 
@@ -356,7 +362,7 @@ export function SettingsPage() {
356
362
  <section className="space-y-2" data-testid="settings-section-skills">
357
363
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
358
364
  <LuBlocks className="h-3.5 w-3.5 text-agent-muted-foreground" />
359
- 本地技能
365
+ {t('Local skills')}
360
366
  </h2>
361
367
  <SkillsSettingsPanel />
362
368
  </section>
@@ -365,7 +371,7 @@ export function SettingsPage() {
365
371
  <section className="space-y-2" data-testid="settings-section-mcp">
366
372
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
367
373
  <LuPlug className="h-3.5 w-3.5 text-agent-muted-foreground" />
368
- MCP 服务
374
+ {t('MCP servers')}
369
375
  </h2>
370
376
  <McpSettingsPanel />
371
377
  </section>
@@ -374,7 +380,7 @@ export function SettingsPage() {
374
380
  <section className="space-y-2" data-testid="settings-section-agents">
375
381
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
376
382
  <LuBot className="h-3.5 w-3.5 text-agent-muted-foreground" />
377
- 智能体
383
+ {t('Agents')}
378
384
  </h2>
379
385
  <AgentsSettingsPanel onCatalogChange={catalog?.refreshAgents} />
380
386
  </section>
@@ -383,7 +389,7 @@ export function SettingsPage() {
383
389
  <section className="space-y-2" data-testid="settings-section-web-search">
384
390
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
385
391
  <LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
386
- 网络搜索
392
+ {t('Web search')}
387
393
  </h2>
388
394
  <WebSearchSettingsPanel />
389
395
  </section>
@@ -397,7 +403,7 @@ export function SettingsPage() {
397
403
  <section className="space-y-2" data-testid="settings-section-appearance">
398
404
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
399
405
  <LuMonitor className="h-3.5 w-3.5 text-agent-muted-foreground" />
400
- 界面
406
+ {t('Interface')}
401
407
  </h2>
402
408
  <AppearanceSettingsPanel />
403
409
  </section>
@@ -406,7 +412,7 @@ export function SettingsPage() {
406
412
  <section className="space-y-2" data-testid="settings-section-orchestration">
407
413
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
408
414
  <LuSettings className="h-3.5 w-3.5 text-agent-muted-foreground" />
409
- 协同编排
415
+ {t('Orchestration')}
410
416
  </h2>
411
417
  <OrchestrationSettingsPanel />
412
418
  </section>
@@ -415,7 +421,7 @@ export function SettingsPage() {
415
421
  <section className="space-y-2" data-testid="settings-section-llm">
416
422
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
417
423
  <LuSettings className="h-3.5 w-3.5 text-agent-muted-foreground" />
418
- 本地模型设置
424
+ {t('Local model settings')}
419
425
  </h2>
420
426
  <LlmSettingsPanel
421
427
  ref={llmPanelRef}
@@ -429,7 +435,7 @@ export function SettingsPage() {
429
435
  <section className="space-y-2" data-testid="settings-section-web-search">
430
436
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
431
437
  <LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
432
- 网络搜索
438
+ {t('Web search')}
433
439
  </h2>
434
440
  <WebSearchSettingsPanel />
435
441
  </section>
@@ -439,7 +445,7 @@ export function SettingsPage() {
439
445
  <section className="space-y-2" data-testid="settings-section-usage">
440
446
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
441
447
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
442
- 用量与成本
448
+ {t('Usage and cost')}
443
449
  </h2>
444
450
  <UsagePanel />
445
451
  </section>
@@ -449,7 +455,7 @@ export function SettingsPage() {
449
455
  <section className="space-y-2" data-testid="settings-section-diagnose">
450
456
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
451
457
  <LuNetwork className="h-3.5 w-3.5 text-agent-muted-foreground" />
452
- 链路诊断
458
+ {t('Connection diagnostics')}
453
459
  </h2>
454
460
  <DiagnoseSettingsPanel />
455
461
  </section>
@@ -459,7 +465,7 @@ export function SettingsPage() {
459
465
  <section className="space-y-2" data-testid="settings-section-security">
460
466
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
461
467
  <LuShieldCheck className="h-3.5 w-3.5 text-agent-muted-foreground" />
462
- 安全
468
+ {t('Security')}
463
469
  </h2>
464
470
  <SecuritySettingsPanel />
465
471
  </section>
@@ -469,7 +475,7 @@ export function SettingsPage() {
469
475
  <section className="space-y-2" data-testid="settings-section-insights">
470
476
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
471
477
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
472
- 帮助改进产品
478
+ {t('Help improve the product')}
473
479
  </h2>
474
480
  <InsightsSettingsPanel />
475
481
  </section>
@@ -479,7 +485,7 @@ export function SettingsPage() {
479
485
  <section className="space-y-2" data-testid="settings-section-telemetry">
480
486
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
481
487
  <LuActivity className="h-3.5 w-3.5 text-agent-muted-foreground" />
482
- 遥测(OTLP)
488
+ {t('Telemetry (OTLP)')}
483
489
  </h2>
484
490
  <TelemetrySettingsPanel />
485
491
  </section>
@@ -507,7 +513,7 @@ export function SettingsPage() {
507
513
  <section className="space-y-2" data-testid="settings-section-portable">
508
514
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
509
515
  <LuDownload className="h-3.5 w-3.5 text-agent-muted-foreground" />
510
- 备份与迁移
516
+ {t('Backup and migration')}
511
517
  </h2>
512
518
  <PortableSettingsPanel />
513
519
  </section>
package/src/vite-env.d.ts CHANGED
@@ -14,6 +14,9 @@ interface ImportMetaEnv {
14
14
  readonly VITE_SETTINGS?: string;
15
15
  /** `'true'` 才打开对话与配置的导出/导入。缺省关。 */
16
16
  readonly VITE_PORTABLE?: string;
17
+ /** JSON 数组,产品声明的界面语言。缺省 `["en"]`。 */
18
+ readonly VITE_LOCALES?: string;
19
+ readonly VITE_DEFAULT_LOCALE?: string;
17
20
  }
18
21
 
19
22
  interface ImportMeta {
package/vite.base.js CHANGED
@@ -51,8 +51,8 @@ export function createProductViteConfig(options) {
51
51
  // 在注册时经 setBrandLogoUrl 注入(本包 src/brand.ts)。
52
52
  'import.meta.env.VITE_BRAND_NAME': JSON.stringify(brand?.displayName ?? 'Steerable Shell'),
53
53
  'import.meta.env.VITE_BRAND_TITLE': JSON.stringify(brand?.title ?? ''),
54
- 'import.meta.env.VITE_BRAND_TAGLINE': JSON.stringify(brand?.tagline ?? '一款本地桌面 AI 伙伴'),
55
- 'import.meta.env.VITE_BRAND_HOME_HINT': JSON.stringify(brand?.homeHint ?? '输入消息,直接开始一段新对话。'),
54
+ 'import.meta.env.VITE_BRAND_TAGLINE': JSON.stringify(brand?.tagline ?? 'A local desktop AI partner'),
55
+ 'import.meta.env.VITE_BRAND_HOME_HINT': JSON.stringify(brand?.homeHint ?? 'Type a message to start a new chat.'),
56
56
  'import.meta.env.VITE_DEFAULT_AGENT_ID': JSON.stringify(brand?.defaultAgentId ?? 'local-assistant'),
57
57
  // 宿主工具族 / 安全询问:与 node 侧 setProductConfig 读同一份 product.json。
58
58
  'import.meta.env.VITE_HOST_TOOLS': JSON.stringify(JSON.stringify(product.hostTools ?? {})),
@@ -60,6 +60,10 @@ export function createProductViteConfig(options) {
60
60
  'import.meta.env.VITE_CHAT_MODES': JSON.stringify(JSON.stringify(product.chatModes ?? ['agent', 'plan'])),
61
61
  'import.meta.env.VITE_SETTINGS': JSON.stringify(JSON.stringify(product.settings ?? {})),
62
62
  'import.meta.env.VITE_PORTABLE': JSON.stringify(product.portable === true ? 'true' : 'false'),
63
+ 'import.meta.env.VITE_LOCALES': JSON.stringify(JSON.stringify(Array.isArray(product.i18n?.locales) && product.i18n.locales.length > 0
64
+ ? product.i18n.locales
65
+ : ['en'])),
66
+ 'import.meta.env.VITE_DEFAULT_LOCALE': JSON.stringify(product.i18n?.defaultLocale ?? 'en'),
63
67
  },
64
68
  resolve: {
65
69
  alias: {