@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
@@ -141,7 +141,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
141
141
  case '.tsv':
142
142
  case '.numbers':
143
143
  return {
144
- label: '电子表格',
144
+ label: 'Spreadsheet',
145
145
  extBadge: extClean,
146
146
  kind: 'spreadsheet',
147
147
  themeColor: 'emerald',
@@ -150,7 +150,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
150
150
  case '.ppt': // shell-neutral:allow — Office 幻灯片扩展名,不是产品品牌
151
151
  case '.key':
152
152
  return {
153
- label: '演示文稿',
153
+ label: 'Presentation',
154
154
  extBadge: extClean,
155
155
  kind: 'presentation',
156
156
  themeColor: 'amber',
@@ -160,14 +160,14 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
160
160
  case '.pages':
161
161
  case '.rtf':
162
162
  return {
163
- label: '文档',
163
+ label: 'Document',
164
164
  extBadge: extClean,
165
165
  kind: 'document',
166
166
  themeColor: 'blue',
167
167
  };
168
168
  case '.pdf':
169
169
  return {
170
- label: '文档',
170
+ label: 'Document',
171
171
  extBadge: 'PDF',
172
172
  kind: 'pdf',
173
173
  themeColor: 'rose',
@@ -179,7 +179,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
179
179
  case '.webp':
180
180
  case '.svg':
181
181
  return {
182
- label: '图像',
182
+ label: 'Image',
183
183
  extBadge: extClean,
184
184
  kind: 'image',
185
185
  themeColor: 'purple',
@@ -189,7 +189,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
189
189
  case '.gz':
190
190
  case '.7z':
191
191
  return {
192
- label: '压缩包',
192
+ label: 'Archive',
193
193
  extBadge: extClean,
194
194
  kind: 'archive',
195
195
  themeColor: 'amber',
@@ -197,14 +197,14 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
197
197
  case '.html':
198
198
  case '.htm':
199
199
  return {
200
- label: '网页',
200
+ label: 'Web page',
201
201
  extBadge: 'HTML',
202
202
  kind: 'document',
203
203
  themeColor: 'blue',
204
204
  };
205
205
  default:
206
206
  return {
207
- label: '文件',
207
+ label: 'File',
208
208
  extBadge: extClean,
209
209
  kind: 'file',
210
210
  themeColor: 'slate',
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
  import { isElectron } from '@/lib/electron-bridge';
4
5
  import {
5
6
  listChatAgentSkills,
@@ -43,16 +44,16 @@ const TOOL_POLICY_MODES: Array<{
43
44
  label: string;
44
45
  hint: string;
45
46
  }> = [
46
- { mode: 'all', label: '全部工具', hint: '不限制,可调用所有已接线的工具。' },
47
+ { mode: 'all', label: 'All tools', hint: 'No restriction. The agent can call every wired tool.' },
47
48
  {
48
49
  mode: 'allowlist',
49
- label: '仅允许勾选',
50
- hint: '只有勾选的工具进入工具列表,其余连名字都看不到。',
50
+ label: 'Only checked',
51
+ hint: 'Only checked tools enter the tool list. The agent cannot even see the names of the rest.',
51
52
  },
52
53
  {
53
54
  mode: 'denylist',
54
- label: '禁用勾选',
55
- hint: '勾选的工具被拒绝,其余照常可用。',
55
+ label: 'Block checked',
56
+ hint: 'Checked tools are refused. The rest stay available.',
56
57
  },
57
58
  ];
58
59
 
@@ -203,24 +204,26 @@ export function AgentCapabilityFields({
203
204
  <div className="space-y-3 rounded-agent-md border border-agent-border/50 bg-agent-canvas/60 p-3">
204
205
  <section className="space-y-1.5">
205
206
  <div className="flex items-center justify-between gap-2">
206
- <h5 className="text-[11px] font-semibold text-agent-foreground">技能</h5>
207
+ <h5 className="text-[11px] font-semibold text-agent-foreground">{t('Skills')}</h5>
207
208
  {value.skillIds.length > 0 && (
208
209
  <span className="text-[10px] text-agent-muted-foreground">
209
- 已选 {value.skillIds.length}
210
+ {t('{count} selected', { count: value.skillIds.length })}
210
211
  </span>
211
212
  )}
212
213
  </div>
213
214
  <p className="text-[10px] text-agent-muted-foreground">
214
- 勾选的技能正文会常驻该智能体的系统提示词,不必等触发条件命中。
215
+ {t(
216
+ "Checked skills stay in this agent's system prompt without waiting for their triggers to match.",
217
+ )}
215
218
  </p>
216
219
  {loading ? (
217
220
  <div className="flex items-center gap-2 py-3 text-[11px] text-agent-muted-foreground">
218
221
  <LuLoaderCircle className="h-3 w-3 animate-spin" />
219
- 获取技能目录...
222
+ {t('Loading skill catalog...')}
220
223
  </div>
221
224
  ) : skills.length === 0 ? (
222
225
  <p className="py-2 text-[11px] text-agent-muted-foreground">
223
- 暂无可勾选的技能,可在「Skill 设置」里导入。
226
+ {t('No skills to check yet. Import them in "Skill settings".')}
224
227
  </p>
225
228
  ) : (
226
229
  <>
@@ -231,7 +234,7 @@ export function AgentCapabilityFields({
231
234
  type="text"
232
235
  value={skillFilter}
233
236
  onChange={(event) => setSkillFilter(event.target.value)}
234
- placeholder="筛选技能"
237
+ placeholder={t('Filter skills')}
235
238
  className="h-7 w-full rounded-agent-md border border-agent-border bg-agent-canvas pl-7 pr-2 text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
236
239
  data-testid="agent-form-skill-filter"
237
240
  />
@@ -247,7 +250,7 @@ export function AgentCapabilityFields({
247
250
  }
248
251
  testId={`agent-form-skill-${skill.id}`}
249
252
  title={skill.displayName || skill.name}
250
- badge={skill.layer === 'eager' ? '常驻' : '按需'}
253
+ badge={skill.layer === 'eager' ? t('Always on') : t('On demand')}
251
254
  description={skill.description}
252
255
  />
253
256
  ))}
@@ -261,21 +264,21 @@ export function AgentCapabilityFields({
261
264
  onChange({ ...value, allowExternalSkills: !value.allowExternalSkills })
262
265
  }
263
266
  testId="agent-form-allow-external-skills"
264
- label="允许使用未勾选的技能"
265
- hint="关掉后这个智能体只能用上面勾选的技能,其余一律加载不到。"
267
+ label={t('Allow unchecked skills')}
268
+ hint={t('When off, this agent can only use the skills checked above. No other skill can load.')}
266
269
  />
267
270
  <SwitchRow
268
271
  checked={value.loadAllSkills}
269
272
  onChange={() => onChange({ ...value, loadAllSkills: !value.loadAllSkills })}
270
273
  testId="agent-form-load-all-skills"
271
- label="无视触发条件,加载全部技能"
272
- hint="内置「智能助手」就是这个模式。会显著增加每轮提示词长度。"
274
+ label={t('Ignore triggers and load all skills')}
275
+ hint={t('The built-in "Assistant" uses this mode. It makes every turn\'s prompt much longer.')}
273
276
  />
274
277
  </div>
275
278
  </section>
276
279
 
277
280
  <section className="space-y-1.5 border-t border-agent-border/40 pt-3">
278
- <h5 className="text-[11px] font-semibold text-agent-foreground">工具权限</h5>
281
+ <h5 className="text-[11px] font-semibold text-agent-foreground">{t('Tool permissions')}</h5>
279
282
  <div className="flex flex-wrap gap-1" data-testid="agent-form-tool-modes">
280
283
  {TOOL_POLICY_MODES.map((item) => (
281
284
  <button
@@ -290,12 +293,12 @@ export function AgentCapabilityFields({
290
293
  : 'bg-agent-muted text-agent-muted-foreground hover:text-agent-foreground'
291
294
  }`}
292
295
  >
293
- {item.label}
296
+ {t(item.label)}
294
297
  </button>
295
298
  ))}
296
299
  </div>
297
300
  {activeMode && (
298
- <p className="text-[10px] text-agent-muted-foreground">{activeMode.hint}</p>
301
+ <p className="text-[10px] text-agent-muted-foreground">{t(activeMode.hint)}</p>
299
302
  )}
300
303
  {policy.mode !== 'all' && (
301
304
  <>
@@ -304,7 +307,7 @@ export function AgentCapabilityFields({
304
307
  className="text-[10px] text-agent-destructive"
305
308
  data-testid="agent-form-tool-policy-empty"
306
309
  >
307
- 一个工具都没勾选——保存后按「全部工具」处理。
310
+ {t('No tool is checked. Saving treats this as "All tools".')}
308
311
  </p>
309
312
  )}
310
313
  <div className="relative">
@@ -313,7 +316,7 @@ export function AgentCapabilityFields({
313
316
  type="text"
314
317
  value={toolFilter}
315
318
  onChange={(event) => setToolFilter(event.target.value)}
316
- placeholder="筛选工具"
319
+ placeholder={t('Filter tools')}
317
320
  className="h-7 w-full rounded-agent-md border border-agent-border bg-agent-canvas pl-7 pr-2 text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
318
321
  data-testid="agent-form-tool-filter"
319
322
  />
@@ -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>