@steerable/agent-shell-web 0.6.52 → 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 (93) 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 +138 -42
  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 +193 -71
  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 +24 -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 +19 -6
  82. package/src/lib/host-file-drop.ts +68 -0
  83. package/src/lib/host-paste.ts +38 -9
  84. package/src/lib/http-bridge.ts +4 -1
  85. package/src/lib/portable.ts +8 -7
  86. package/src/lib/show-thinking-content.ts +15 -3
  87. package/src/lib/tauri-bridge.ts +5 -0
  88. package/src/main.tsx +25 -2
  89. package/src/pages/AgentPage.tsx +17 -12
  90. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  91. package/src/pages/SettingsPage.tsx +48 -42
  92. package/src/vite-env.d.ts +3 -0
  93. package/vite.base.js +6 -2
@@ -6,6 +6,7 @@ import {
6
6
  LuTrash2,
7
7
  } from 'react-icons/lu';
8
8
  import { ConfirmDialog } from '@/components/ConfirmDialog';
9
+ import { t } from '@/i18n';
9
10
  import { isElectron } from '@/lib/electron-bridge';
10
11
  import {
11
12
  archiveChatAgent,
@@ -61,15 +62,15 @@ function agentInitial(agent: LocalChatAgent): string {
61
62
  function capabilitySummary(agent: LocalChatAgent): string[] {
62
63
  const labels: string[] = [];
63
64
  const skillCount = agent.skillIds?.length ?? 0;
64
- if (skillCount > 0) labels.push(`技能 ${skillCount}`);
65
- if (agent.allowExternalSkills === false) labels.push('仅限所选技能');
66
- if (agent.loadAllSkills) labels.push('全部技能');
65
+ if (skillCount > 0) labels.push(t('Skills {count}', { count: skillCount }));
66
+ if (agent.allowExternalSkills === false) labels.push(t('Selected skills only'));
67
+ if (agent.loadAllSkills) labels.push(t('All skills'));
67
68
  const policy = agent.toolPolicy;
68
69
  if (policy && policy.mode !== 'all' && policy.tools.length > 0) {
69
70
  labels.push(
70
71
  policy.mode === 'allowlist'
71
- ? `仅 ${policy.tools.length} 个工具`
72
- : `禁用 ${policy.tools.length} 个工具`,
72
+ ? t('Only {count} tools', { count: policy.tools.length })
73
+ : t('{count} tools blocked', { count: policy.tools.length }),
73
74
  );
74
75
  }
75
76
  return labels;
@@ -158,7 +159,7 @@ export function AgentsSettingsPanel({
158
159
  name: trimmedName,
159
160
  color,
160
161
  description: description.trim() || null,
161
- rolePrompt: rolePrompt.trim() || `你是 **${trimmedName}**。`,
162
+ rolePrompt: rolePrompt.trim() || t('You are **{name}**.', { name: trimmedName }),
162
163
  skillIds: capability.skillIds,
163
164
  allowExternalSkills: capability.allowExternalSkills,
164
165
  loadAllSkills: capability.loadAllSkills,
@@ -203,20 +204,21 @@ export function AgentsSettingsPanel({
203
204
  return (
204
205
  <div className="space-y-3">
205
206
  <p className="text-[11px] text-agent-muted-foreground">
206
- 自定义智能体会出现在输入框上方的专家选择器里。角色设定会作为对话的人设前言,
207
- 勾选的技能与工具权限在每一轮真实生效。内置智能体可以改文案和能力,但不能删除。
207
+ {t(
208
+ 'Custom agents appear in the expert picker above the input box. The role prompt opens the conversation as its persona, and the checked skills and tool permissions apply on every turn. Built-in agents can have their text and capabilities edited, but cannot be deleted.',
209
+ )}
208
210
  </p>
209
211
 
210
212
  {formOpen ? (
211
213
  <div className="space-y-2 rounded-agent-md border border-agent-border/60 bg-agent-muted/30 p-2.5">
212
214
  <h4 className="text-xs font-semibold text-agent-foreground">
213
- {editingId ? '编辑智能体' : '新建智能体'}
215
+ {editingId ? t('Edit agent') : t('New agent')}
214
216
  </h4>
215
217
  <input
216
218
  type="text"
217
219
  value={name}
218
220
  onChange={(event) => setName(event.target.value)}
219
- placeholder="名称,如 地质顾问"
221
+ placeholder={t('Name, e.g. Geology advisor')}
220
222
  maxLength={40}
221
223
  className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
222
224
  data-testid="agent-form-name"
@@ -225,13 +227,13 @@ export function AgentsSettingsPanel({
225
227
  type="text"
226
228
  value={description}
227
229
  onChange={(event) => setDescription(event.target.value)}
228
- placeholder="一句话简介(可选)"
230
+ placeholder={t('One-line summary (optional)')}
229
231
  maxLength={200}
230
232
  className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
231
233
  data-testid="agent-form-description"
232
234
  />
233
235
  <div>
234
- <p className="mb-1.5 text-[10px] font-medium text-agent-muted-foreground">颜色</p>
236
+ <p className="mb-1.5 text-[10px] font-medium text-agent-muted-foreground">{t('Color')}</p>
235
237
  <div className="flex flex-wrap gap-1.5" data-testid="agent-form-colors">
236
238
  {AGENT_COLORS.map((item) => (
237
239
  <button
@@ -245,7 +247,7 @@ export function AgentsSettingsPanel({
245
247
  }`}
246
248
  style={{ backgroundColor: item }}
247
249
  title={item}
248
- aria-label={`颜色 ${item}`}
250
+ aria-label={t('Color {color}', { color: item })}
249
251
  />
250
252
  ))}
251
253
  </div>
@@ -253,7 +255,7 @@ export function AgentsSettingsPanel({
253
255
  <textarea
254
256
  value={rolePrompt}
255
257
  onChange={(event) => setRolePrompt(event.target.value)}
256
- placeholder="角色设定。例如:你是地质顾问,回答简洁,先给结论再给依据。"
258
+ placeholder={t('Role prompt. For example: You are a geology advisor. Answer concisely, giving the conclusion first and then the evidence.')}
257
259
  rows={6}
258
260
  className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
259
261
  data-testid="agent-form-role"
@@ -268,7 +270,7 @@ export function AgentsSettingsPanel({
268
270
  }}
269
271
  className="h-8 rounded-full px-4 text-xs font-medium text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
270
272
  >
271
- 取消
273
+ {t('Cancel')}
272
274
  </button>
273
275
  <button
274
276
  type="button"
@@ -281,7 +283,7 @@ export function AgentsSettingsPanel({
281
283
  : 'bg-agent-foreground text-agent-canvas hover:opacity-90'
282
284
  }`}
283
285
  >
284
- {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : '保存'}
286
+ {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : t('Save')}
285
287
  </button>
286
288
  </div>
287
289
  </div>
@@ -293,13 +295,13 @@ export function AgentsSettingsPanel({
293
295
  data-testid="agent-add"
294
296
  >
295
297
  <LuPlus className="h-3.5 w-3.5" />
296
- 添加智能体
298
+ {t('Add agent')}
297
299
  </button>
298
300
  )}
299
301
 
300
302
  <div className="space-y-2">
301
303
  <h4 className="text-xs font-semibold uppercase tracking-wide text-agent-muted-foreground">
302
- 已有智能体
304
+ {t('Existing agents')}
303
305
  {agents.length > 0 && (
304
306
  <span className="ml-1.5 font-normal normal-case">· {agents.length}</span>
305
307
  )}
@@ -307,11 +309,11 @@ export function AgentsSettingsPanel({
307
309
  {loading ? (
308
310
  <div className="flex items-center gap-2 py-4 text-xs text-agent-muted-foreground">
309
311
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
310
- 获取智能体...
312
+ {t('Loading agents...')}
311
313
  </div>
312
314
  ) : agents.length === 0 ? (
313
315
  <div className="py-6 text-center text-xs text-agent-muted-foreground">
314
- 暂无智能体
316
+ {t('No agents yet')}
315
317
  </div>
316
318
  ) : (
317
319
  <div className="divide-y divide-agent-border/40 pr-1">
@@ -334,7 +336,7 @@ export function AgentsSettingsPanel({
334
336
  </span>
335
337
  {agent.isBuiltin && (
336
338
  <span className="flex-shrink-0 rounded bg-agent-muted px-1 py-0.5 text-[9px] font-medium text-agent-muted-foreground">
337
- 内置
339
+ {t('Built-in')}
338
340
  </span>
339
341
  )}
340
342
  </div>
@@ -364,7 +366,7 @@ export function AgentsSettingsPanel({
364
366
  type="button"
365
367
  onClick={() => openEdit(agent)}
366
368
  className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
367
- title="编辑"
369
+ title={t('Edit')}
368
370
  data-testid={`agent-edit-${agent.id}`}
369
371
  >
370
372
  <LuPencil className="h-3.5 w-3.5" />
@@ -377,7 +379,7 @@ export function AgentsSettingsPanel({
377
379
  }
378
380
  disabled={archivingId === agent.id}
379
381
  className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-destructive"
380
- title="删除"
382
+ title={t('Delete')}
381
383
  data-testid={`agent-archive-${agent.id}`}
382
384
  >
383
385
  {archivingId === agent.id ? (
@@ -404,10 +406,13 @@ export function AgentsSettingsPanel({
404
406
  )}
405
407
  <ConfirmDialog
406
408
  open={pendingArchive !== null}
407
- title="删除智能体"
409
+ title={t('Delete agent')}
408
410
  description={
409
411
  pendingArchive
410
- ? `确定删除「${pendingArchive.name}」?它会从专家选择器中移除,已有会话不受影响。`
412
+ ? t(
413
+ 'Delete "{name}"? It will be removed from the expert picker. Existing conversations are not affected.',
414
+ { name: pendingArchive.name },
415
+ )
411
416
  : ''
412
417
  }
413
418
  pending={pendingArchive !== null && archivingId === pendingArchive.id}
@@ -1,5 +1,6 @@
1
1
  import { LuInfo } from 'react-icons/lu';
2
2
  import { useAppRelease } from '@/components/SidebarRelease';
3
+ import { t } from '@/i18n';
3
4
 
4
5
  /**
5
6
  * 综合设置里的版本与更新。侧栏只显示版本号;检查、下载和安装在这里。
@@ -17,11 +18,11 @@ export function AppUpdateSettingsPanel() {
17
18
  <section className="space-y-2" data-testid="settings-section-update">
18
19
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
19
20
  <LuInfo className="h-3.5 w-3.5 text-agent-muted-foreground" />
20
- 关于
21
+ {t('About')}
21
22
  </h2>
22
23
  <div className="flex items-center gap-3 rounded-agent-md border border-agent-border bg-agent-card p-2.5">
23
24
  <div className="min-w-0 flex-1">
24
- <p className="text-xs font-medium text-agent-foreground">当前版本</p>
25
+ <p className="text-xs font-medium text-agent-foreground">{t('Current version')}</p>
25
26
  <p
26
27
  className="mt-1 text-xs tabular-nums text-agent-muted-foreground"
27
28
  data-testid="settings-app-version"
@@ -3,54 +3,98 @@ import {
3
3
  persistThinkingDisplay,
4
4
  useThinkingDisplay,
5
5
  } from '@/lib/show-thinking-content';
6
+ import { localeEndonym, supportedLocales, useI18n } from '@/i18n';
6
7
 
7
8
  /**
8
9
  * 界面偏好:思考过程正文按「隐藏 / 显示5行 / 完整显示」三档。
9
- * 立即写入 localStorage,不走设置页顶部的模型保存按钮。
10
+ * 产品声明了多于一种语言时,在这里切换。立即写入 localStorage。
10
11
  */
11
12
  export function AppearanceSettingsPanel() {
13
+ const { t, locale, setLocale } = useI18n();
12
14
  const mode = useThinkingDisplay();
13
15
  const current = THINKING_DISPLAY_OPTIONS.find((item) => item.mode === mode);
16
+ const locales = supportedLocales();
14
17
 
15
18
  return (
16
- <div
17
- className="space-y-2 rounded-agent-md border border-agent-border bg-agent-card p-2.5"
18
- data-testid="appearance-settings-panel"
19
- >
20
- <div>
21
- <p className="text-xs font-medium text-agent-foreground">显示思考内容</p>
22
- <p className="mt-1 text-xs leading-relaxed text-agent-muted-foreground">
23
- {current?.hint} 这三档只控制思考正文,不影响工具卡片和工作行。工作行只显示「工作中」或结束后的次数与用时。
24
- </p>
25
- </div>
19
+ <div className="space-y-2">
20
+ {locales.length > 1 ? (
21
+ <div
22
+ className="space-y-2 rounded-agent-md border border-agent-border bg-agent-card p-2.5"
23
+ data-testid="locale-settings"
24
+ >
25
+ <p className="text-xs font-medium text-agent-foreground">{t('Language')}</p>
26
+ <div
27
+ className="flex w-full rounded-full border border-agent-border bg-agent-canvas p-0.5"
28
+ role="radiogroup"
29
+ aria-label={t('Language')}
30
+ data-testid="locale-toggle"
31
+ >
32
+ {locales.map((item) => {
33
+ const selected = locale === item;
34
+ return (
35
+ <button
36
+ key={item}
37
+ type="button"
38
+ role="radio"
39
+ aria-checked={selected}
40
+ data-testid={`locale-option-${item}`}
41
+ onClick={() => setLocale(item)}
42
+ className={[
43
+ 'h-7 flex-1 rounded-full px-2 text-xs transition-colors',
44
+ selected
45
+ ? 'bg-agent-foreground/10 text-agent-foreground'
46
+ : 'text-agent-muted-foreground hover:text-agent-foreground',
47
+ ].join(' ')}
48
+ >
49
+ {localeEndonym(item)}
50
+ </button>
51
+ );
52
+ })}
53
+ </div>
54
+ </div>
55
+ ) : null}
26
56
  <div
27
- className="flex w-full rounded-full border border-agent-border bg-agent-canvas p-0.5"
28
- role="radiogroup"
29
- aria-label="显示思考内容"
30
- data-testid="thinking-display-toggle"
57
+ className="space-y-2 rounded-agent-md border border-agent-border bg-agent-card p-2.5"
58
+ data-testid="appearance-settings-panel"
31
59
  >
32
- {THINKING_DISPLAY_OPTIONS.map((item) => {
33
- const selected = mode === item.mode;
34
- return (
35
- <button
36
- key={item.mode}
37
- type="button"
38
- role="radio"
39
- aria-checked={selected}
40
- aria-label={item.label}
41
- data-testid={`thinking-display-${item.mode}`}
42
- onClick={() => persistThinkingDisplay(item.mode)}
43
- className={[
44
- 'h-7 flex-1 rounded-full px-2 text-xs transition-colors',
45
- selected
46
- ? 'bg-agent-foreground/10 text-agent-foreground'
47
- : 'text-agent-muted-foreground hover:text-agent-foreground',
48
- ].join(' ')}
49
- >
50
- {item.label}
51
- </button>
52
- );
53
- })}
60
+ <div>
61
+ <p className="text-xs font-medium text-agent-foreground">{t('Show thinking')}</p>
62
+ <p className="mt-1 text-xs leading-relaxed text-agent-muted-foreground">
63
+ {current ? t(current.hint) : null}{' '}
64
+ {t(
65
+ 'These options only control the thinking text. Tool cards and work rows stay as they are. A work row shows "Working", or the count and elapsed time after it finishes.',
66
+ )}
67
+ </p>
68
+ </div>
69
+ <div
70
+ className="flex w-full rounded-full border border-agent-border bg-agent-canvas p-0.5"
71
+ role="radiogroup"
72
+ aria-label={t('Show thinking')}
73
+ data-testid="thinking-display-toggle"
74
+ >
75
+ {THINKING_DISPLAY_OPTIONS.map((item) => {
76
+ const selected = mode === item.mode;
77
+ return (
78
+ <button
79
+ key={item.mode}
80
+ type="button"
81
+ role="radio"
82
+ aria-checked={selected}
83
+ aria-label={t(item.label)}
84
+ data-testid={`thinking-display-${item.mode}`}
85
+ onClick={() => persistThinkingDisplay(item.mode)}
86
+ className={[
87
+ 'h-7 flex-1 rounded-full px-2 text-xs transition-colors',
88
+ selected
89
+ ? 'bg-agent-foreground/10 text-agent-foreground'
90
+ : 'text-agent-muted-foreground hover:text-agent-foreground',
91
+ ].join(' ')}
92
+ >
93
+ {t(item.label)}
94
+ </button>
95
+ );
96
+ })}
97
+ </div>
54
98
  </div>
55
99
  </div>
56
100
  );
@@ -1,6 +1,7 @@
1
1
  import { useState } from 'react';
2
2
  import { LuCircleCheck, LuCircleX, LuLoader, LuPlay } from 'react-icons/lu';
3
3
  import { diagnoseLlmConnection, type DiagnoseResult } from '@/lib/local-api';
4
+ import { t } from '@/i18n';
4
5
 
5
6
  /**
6
7
  * 设置页「链路诊断」面板:一键探测当前 LLM 配置的连通性
@@ -29,8 +30,10 @@ export function DiagnoseSettingsPanel() {
29
30
  return (
30
31
  <div className="space-y-2 rounded-agent-md border border-agent-border bg-agent-card p-2.5">
31
32
  <p className="text-xs text-agent-muted-foreground">
32
- 诊断当前模型服务的网络连通性,包括 DNS 解析、TCP 连接、TLS 握手、HTTP 接口与对话补全。
33
- 诊断在宿主进程内执行,会同时报告系统/环境代理配置。
33
+ {t(
34
+ 'Diagnose network connectivity to the current model service, including DNS resolution, TCP connection, TLS handshake, the HTTP endpoint, and chat completion.',
35
+ )}{' '}
36
+ {t('The diagnosis runs in the host process and also reports system and environment proxy settings.')}
34
37
  </p>
35
38
 
36
39
  <button
@@ -42,19 +45,19 @@ export function DiagnoseSettingsPanel() {
42
45
  {running ? (
43
46
  <>
44
47
  <LuLoader className="h-3.5 w-3.5 animate-spin" />
45
- 诊断中…
48
+ {t('Diagnosing…')}
46
49
  </>
47
50
  ) : (
48
51
  <>
49
52
  <LuPlay className="h-3.5 w-3.5" />
50
- 开始诊断
53
+ {t('Start diagnosis')}
51
54
  </>
52
55
  )}
53
56
  </button>
54
57
 
55
58
  {error && (
56
59
  <p className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive">
57
- 诊断请求失败:{error}
60
+ {t('Diagnosis request failed: {error}', { error })}
58
61
  </p>
59
62
  )}
60
63
 
@@ -64,12 +67,12 @@ export function DiagnoseSettingsPanel() {
64
67
  {result.ok ? (
65
68
  <span className="inline-flex items-center gap-1 text-agent-success">
66
69
  <LuCircleCheck className="h-4 w-4" />
67
- 全部通过
70
+ {t('All checks passed')}
68
71
  </span>
69
72
  ) : (
70
73
  <span className="inline-flex items-center gap-1 text-agent-destructive">
71
74
  <LuCircleX className="h-4 w-4" />
72
- 诊断失败
75
+ {t('Diagnosis failed')}
73
76
  </span>
74
77
  )}
75
78
  </div>
@@ -98,7 +101,7 @@ export function DiagnoseSettingsPanel() {
98
101
 
99
102
  {result.ambientProxies.length > 0 && (
100
103
  <div className="rounded-agent-sm border border-agent-border bg-agent-muted/30 p-2.5 text-xs">
101
- <p className="font-medium text-agent-foreground">检测到的代理配置</p>
104
+ <p className="font-medium text-agent-foreground">{t('Detected proxy settings')}</p>
102
105
  <ul className="mt-1 list-inside list-disc space-y-0.5 text-agent-muted-foreground">
103
106
  {result.ambientProxies.map((proxy) => (
104
107
  <li key={proxy}>{proxy}</li>
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuChartBar } from 'react-icons/lu';
3
3
  import { BRAND_NAME } from '@/brand';
4
+ import { t } from '@/i18n';
4
5
  import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
5
6
 
6
7
  type InsightsWire = {
@@ -71,13 +72,13 @@ export function InsightsConsentBanner() {
71
72
  className="flex-shrink-0 border-t border-agent-border bg-agent-muted px-2.5 py-2"
72
73
  data-testid="insights-consent-banner"
73
74
  role="region"
74
- aria-label="帮助改进产品同意"
75
+ aria-label={t('Consent to help improve the product')}
75
76
  >
76
77
  <div className="flex items-center justify-between gap-3">
77
78
  <div className="min-w-0">
78
- <p className="text-xs font-medium text-agent-foreground">帮助改进{BRAND_NAME}</p>
79
+ <p className="text-xs font-medium text-agent-foreground">{t('Help improve {brand}', { brand: BRAND_NAME })}</p>
79
80
  <p className="mt-1 text-xs leading-relaxed text-agent-muted-foreground">
80
- 同意上传数据到服务器,帮助改进产品。之后可在设置里更改。
81
+ {t('Agree to upload data to the server to help improve the product. You can change this later in Settings.')}
81
82
  </p>
82
83
  </div>
83
84
  <div className="flex shrink-0 gap-2">
@@ -87,7 +88,7 @@ export function InsightsConsentBanner() {
87
88
  onClick={() => void save(true)}
88
89
  className="h-8 whitespace-nowrap rounded-full bg-agent-foreground px-3.5 text-xs text-agent-canvas disabled:opacity-50"
89
90
  >
90
- 保存
91
+ {t('Save')}
91
92
  </button>
92
93
  <button
93
94
  type="button"
@@ -95,7 +96,7 @@ export function InsightsConsentBanner() {
95
96
  onClick={() => void save(false)}
96
97
  className="h-8 whitespace-nowrap rounded-full border border-agent-border bg-agent-canvas px-3.5 text-xs text-agent-foreground disabled:opacity-50"
97
98
  >
98
- 取消
99
+ {t('Cancel')}
99
100
  </button>
100
101
  </div>
101
102
  </div>
@@ -152,7 +153,7 @@ export function InsightsSettingsPanel() {
152
153
  },
153
154
  });
154
155
  setData({ ...empty, ...res, profile: { ...empty.profile, ...res.profile } });
155
- setStatus('已保存。未勾选的种类只留在本机。');
156
+ setStatus(t('Saved. Unchecked categories stay on this device only.'));
156
157
  } catch (err) {
157
158
  setError(err instanceof Error ? err.message : String(err));
158
159
  } finally {
@@ -170,11 +171,11 @@ export function InsightsSettingsPanel() {
170
171
  });
171
172
  const text = `${JSON.stringify(bundle, null, 2)}\n`;
172
173
  const saved = await getElectronBridge()!.local?.saveTextFile?.({
173
- title: '导出本地洞察记录',
174
+ title: t('Export local insight records'),
174
175
  defaultPath: `deeppath-insights-${new Date().toISOString().slice(0, 10)}.json`,
175
176
  content: text,
176
177
  });
177
- if (saved?.canceled === false) setStatus(`已保存到 ${saved.filePath}`);
178
+ if (saved?.canceled === false) setStatus(t('Saved to {path}', { path: saved.filePath ?? '' }));
178
179
  } catch (err) {
179
180
  setError(err instanceof Error ? err.message : String(err));
180
181
  }
@@ -188,7 +189,11 @@ export function InsightsSettingsPanel() {
188
189
  method: 'POST',
189
190
  path: '/api/v2/insights/upload-local',
190
191
  });
191
- setStatus(res.ok ? `已上传到${BRAND_NAME}服务器` : '上传失败,记录仍在本机,可改导出文件发送');
192
+ setStatus(
193
+ res.ok
194
+ ? t('Uploaded to the {brand} server', { brand: BRAND_NAME })
195
+ : t('Upload failed. The records are still on this device. You can export a file and send it instead.'),
196
+ );
192
197
  await load();
193
198
  } catch (err) {
194
199
  setError(err instanceof Error ? err.message : String(err));
@@ -202,15 +207,17 @@ export function InsightsSettingsPanel() {
202
207
  <div className="rounded-agent-md border border-agent-border/60 bg-agent-muted/30 p-2.5 space-y-2">
203
208
  <h4 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
204
209
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
205
- 帮助改进产品(行为 / 对话 / 用户信息 分开确认)
210
+ {t('Help improve the product (behavior / conversations / user info are confirmed separately)')}
206
211
  </h4>
207
212
  <p className="text-[11px] text-agent-muted-foreground">
208
- 即使用户不同意上传,记录也会留在本机。可导出 JSON 发给产品团队,或点一次「现在上传」。
213
+ {t(
214
+ 'Even if the user does not agree to upload, records stay on this device. You can export JSON and send it to the product team, or click "Upload now" once.',
215
+ )}
209
216
  </p>
210
217
  {!isElectron() ? (
211
- <p className="text-[11px] text-agent-muted-foreground">需要在桌面客户端中打开</p>
218
+ <p className="text-[11px] text-agent-muted-foreground">{t('Open this in the desktop app')}</p>
212
219
  ) : loading ? (
213
- <p className="text-[11px] text-agent-muted-foreground">读取中…</p>
220
+ <p className="text-[11px] text-agent-muted-foreground">{t('Loading…')}</p>
214
221
  ) : (
215
222
  <>
216
223
  <label className="flex items-start gap-2 text-xs">
@@ -220,8 +227,8 @@ export function InsightsSettingsPanel() {
220
227
  onChange={(e) => setData((d) => ({ ...d, shareBehavior: e.target.checked }))}
221
228
  />
222
229
  <span>
223
- 自动上传<strong>行为</strong>
224
- <span className="block text-[10px] text-agent-muted-foreground">打开、发送、设置卡住等,不含问答正文</span>
230
+ {t('Auto-upload')} <strong>{t('behavior')}</strong>
231
+ <span className="block text-[10px] text-agent-muted-foreground">{t('Opens, sends, stuck settings, and so on. No question or answer text.')}</span>
225
232
  </span>
226
233
  </label>
227
234
  <label className="flex items-start gap-2 text-xs">
@@ -231,8 +238,8 @@ export function InsightsSettingsPanel() {
231
238
  onChange={(e) => setData((d) => ({ ...d, shareConversation: e.target.checked }))}
232
239
  />
233
240
  <span>
234
- 自动上传<strong>对话</strong>
235
- <span className="block text-[10px] text-agent-muted-foreground">提问与回答(已脱敏密钥和用户目录)</span>
241
+ {t('Auto-upload')} <strong>{t('conversations')}</strong>
242
+ <span className="block text-[10px] text-agent-muted-foreground">{t('Questions and answers (keys and user directories are redacted)')}</span>
236
243
  </span>
237
244
  </label>
238
245
  <label className="flex items-start gap-2 text-xs">
@@ -242,21 +249,21 @@ export function InsightsSettingsPanel() {
242
249
  onChange={(e) => setData((d) => ({ ...d, shareProfile: e.target.checked }))}
243
250
  />
244
251
  <span>
245
- 自动上传<strong>用户信息</strong>
246
- <span className="block text-[10px] text-agent-muted-foreground">与上面两项分开,不勾选则称呼/邮箱只留本机</span>
252
+ {t('Auto-upload')} <strong>{t('user info')}</strong>
253
+ <span className="block text-[10px] text-agent-muted-foreground">{t('Separate from the two above. If unchecked, your name and email stay on this device only.')}</span>
247
254
  </span>
248
255
  </label>
249
256
  <div className="grid grid-cols-2 gap-2">
250
257
  {(
251
258
  [
252
- ['displayName', '称呼'],
253
- ['email', '邮箱'],
254
- ['company', '公司/团队'],
255
- ['note', '想告诉我们的'],
259
+ ['displayName', 'Name'],
260
+ ['email', 'Email'],
261
+ ['company', 'Company/team'],
262
+ ['note', 'Anything to tell us'],
256
263
  ] as const
257
264
  ).map(([key, label]) => (
258
265
  <label key={key} className="space-y-1 text-[11px] text-agent-muted-foreground">
259
- {label}
266
+ {t(label)}
260
267
  <input
261
268
  value={data.profile[key]}
262
269
  onChange={(e) =>
@@ -268,7 +275,15 @@ export function InsightsSettingsPanel() {
268
275
  ))}
269
276
  </div>
270
277
  <p className="text-[10px] text-agent-muted-foreground">
271
- 本机已记 行为 {stats.events} / 对话 {stats.turns} / 资料 {stats.profile},待上传 {stats.pending}
278
+ {t(
279
+ 'Recorded on this device: behavior {events} / conversations {turns} / profile {profile}, pending upload {pending}',
280
+ {
281
+ events: stats.events,
282
+ turns: stats.turns,
283
+ profile: stats.profile,
284
+ pending: stats.pending,
285
+ },
286
+ )}
272
287
  {data.installId ? ` · ${data.installId.slice(0, 8)}` : ''}
273
288
  </p>
274
289
  <div className="flex flex-wrap gap-2">
@@ -278,21 +293,21 @@ export function InsightsSettingsPanel() {
278
293
  onClick={() => void save()}
279
294
  className="h-8 rounded-full bg-agent-foreground px-4 text-xs text-agent-canvas"
280
295
  >
281
- 保存
296
+ {t('Save')}
282
297
  </button>
283
298
  <button
284
299
  type="button"
285
300
  onClick={() => void exportFile()}
286
301
  className="h-8 rounded-full border border-agent-border px-4 text-xs"
287
302
  >
288
- 导出文件发给开发者
303
+ {t('Export a file for the developers')}
289
304
  </button>
290
305
  <button
291
306
  type="button"
292
307
  onClick={() => void uploadNow()}
293
308
  className="h-8 rounded-full border border-agent-border px-4 text-xs"
294
309
  >
295
- 现在上传本地记录
310
+ {t('Upload local records now')}
296
311
  </button>
297
312
  </div>
298
313
  </>