@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
@@ -2,6 +2,7 @@ import { useState } from 'react';
2
2
  import { LuDownload, LuUpload } from 'react-icons/lu';
3
3
  import { hostToolChrome, settingsChrome } from '@/lib/host-tools';
4
4
  import { BRAND_NAME } from '@/brand';
5
+ import { t } from '@/i18n';
5
6
  import {
6
7
  applyClientSections,
7
8
  fetchChatDocument,
@@ -20,42 +21,61 @@ import {
20
21
  } from '@/lib/portable';
21
22
 
22
23
  const EXPORT_SECTIONS: Array<{ id: string; label: string; visible: () => boolean }> = [
23
- { id: 'appearance', label: '界面', visible: () => settingsChrome('appearance') },
24
- { id: 'execPolicy', label: '命令权限', visible: () => hostToolChrome('local-fs') },
25
- { id: 'llm', label: '本地模型', visible: () => settingsChrome('llm') },
26
- { id: 'webSearch', label: '网络搜索', visible: () => settingsChrome('web-search') },
27
- { id: 'insights', label: '帮助改进产品', visible: () => settingsChrome('insights') },
28
- { id: 'telemetry', label: '遥测', visible: () => settingsChrome('telemetry') },
29
- { id: 'mcp', label: 'MCP 服务', visible: () => settingsChrome('mcp') },
30
- { id: 'agents', label: '智能体', visible: () => settingsChrome('agents') },
31
- { id: 'skills', label: '技能引用', visible: () => settingsChrome('skills') },
24
+ { id: 'appearance', label: 'Interface', visible: () => settingsChrome('appearance') },
25
+ { id: 'execPolicy', label: 'Command permissions', visible: () => hostToolChrome('local-fs') },
26
+ { id: 'llm', label: 'Local model', visible: () => settingsChrome('llm') },
27
+ { id: 'webSearch', label: 'Web search', visible: () => settingsChrome('web-search') },
28
+ { id: 'insights', label: 'Help improve the product', visible: () => settingsChrome('insights') },
29
+ { id: 'telemetry', label: 'Telemetry', visible: () => settingsChrome('telemetry') },
30
+ { id: 'mcp', label: 'MCP servers', visible: () => settingsChrome('mcp') },
31
+ { id: 'agents', label: 'Agents', visible: () => settingsChrome('agents') },
32
+ { id: 'skills', label: 'Skill references', visible: () => settingsChrome('skills') },
32
33
  ];
33
34
 
34
35
  function projectClause(chat: { projectName?: string; projectCount?: number }): string | null {
35
36
  const count = chat.projectCount ?? 0;
36
37
  if (count <= 0) return null;
37
38
  if (count === 1 && chat.projectName) {
38
- return `项目「${chat.projectName}」(不含目录里的文件,同名项目沿用本机已有的)`;
39
+ return t(
40
+ 'Project "{name}" (files in its directory are not included; a project with the same name reuses the one on this computer)',
41
+ { name: chat.projectName },
42
+ );
39
43
  }
40
- return `${count} 个项目(不含目录里的文件,同名项目沿用本机已有的)`;
44
+ return t(
45
+ '{count} projects (files in their directories are not included; projects with the same name reuse the ones on this computer)',
46
+ { count },
47
+ );
41
48
  }
42
49
 
43
50
  function describeChat(preview: PortablePreview): string {
44
51
  const chat = preview.chat;
45
- if (!chat) return '将作为新会话导入,不覆盖现有对话。';
46
- const bits = [`${chat.messageCount} 条消息`];
47
- if (chat.attachmentCount > 0) bits.push(`${chat.attachmentCount} 个附件`);
48
- if (chat.omittedAttachmentCount > 0) bits.push(`${chat.omittedAttachmentCount} 个附件因过大未包含`);
49
- if (chat.truncated) bits.push('消息已达到导出上限');
52
+ if (!chat) return t('Will be imported as a new chat. Existing chats are not overwritten.');
53
+ const bits = [t('{count} messages', { count: chat.messageCount })];
54
+ if (chat.attachmentCount > 0) bits.push(t('{count} attachments', { count: chat.attachmentCount }));
55
+ if (chat.omittedAttachmentCount > 0) {
56
+ bits.push(t('{count} attachments left out because they are too large', { count: chat.omittedAttachmentCount }));
57
+ }
58
+ if (chat.truncated) bits.push(t('Messages reached the export limit'));
50
59
  const project = projectClause(chat);
51
60
  if (project) bits.push(project);
52
- if ((chat.count ?? 1) > 1) return `将新增 ${chat.count} 条对话,共 ${bits.join(',')}。不覆盖现有会话。`;
53
- return `将新增对话「${chat.title}」:${bits.join(',')}。不覆盖现有会话。`;
61
+ const details = bits.join(t(', '));
62
+ if ((chat.count ?? 1) > 1) {
63
+ return t('Will add {count} chats with {details} in total. Existing chats are not overwritten.', {
64
+ count: chat.count ?? 1,
65
+ details,
66
+ });
67
+ }
68
+ return t('Will add chat "{title}": {details}. Existing chats are not overwritten.', {
69
+ title: chat.title,
70
+ details,
71
+ });
54
72
  }
55
73
 
56
74
  function describeImport(preview: PortablePreview): string {
57
- if (preview.includeSecrets) return '这份配置包含 API Key。导入后会写入本机。';
58
- return '将按勾选覆盖对应设置。未出现的段保持不动。钥匙默认不在包里,导入后需要自己填写。';
75
+ if (preview.includeSecrets) return t('This config includes API keys. Importing writes them to this computer.');
76
+ return t(
77
+ 'Checked settings will be overwritten. Sections not in the file stay unchanged. Keys are not in the package by default, so you need to fill them in after importing.',
78
+ );
59
79
  }
60
80
 
61
81
  /**
@@ -113,7 +133,7 @@ export function PortableSettingsPanel() {
113
133
  const confirmExportChats = async () => {
114
134
  const chosen = chatRows.filter((row) => chatIds.has(row.id));
115
135
  if (chosen.length === 0) {
116
- setError('请至少选择一条对话');
136
+ setError(t('Select at least one chat'));
117
137
  return;
118
138
  }
119
139
  setBusy(true);
@@ -121,13 +141,13 @@ export function PortableSettingsPanel() {
121
141
  try {
122
142
  const chats = [];
123
143
  for (const row of chosen) chats.push(await fetchChatDocument(row.id));
124
- const saved = await saveJsonFile(safeDownloadName(BRAND_NAME, '对话'), {
144
+ const saved = await saveJsonFile(safeDownloadName(BRAND_NAME, t('Chats')), {
125
145
  kind: 'steerable-chats',
126
146
  schemaVersion: 1,
127
147
  exportedAt: new Date().toISOString(),
128
148
  chats,
129
149
  });
130
- setStatus(saved ? `已保存到 ${saved}` : '已取消');
150
+ setStatus(saved ? t('Saved to {path}', { path: saved }) : t('Canceled'));
131
151
  if (saved) setMode('idle');
132
152
  } catch (err) {
133
153
  setError(portableErrorMessage(err));
@@ -151,11 +171,11 @@ export function PortableSettingsPanel() {
151
171
  const fetched = await fetchConfigDocument(includeSecrets);
152
172
  const finished = finishConfigDocument(fetched, selected);
153
173
  if (Object.keys(finished.sections).length === 0) {
154
- setError('没有可导出的内容');
174
+ setError(t('Nothing to export'));
155
175
  return;
156
176
  }
157
- const saved = await saveJsonFile(safeDownloadName(BRAND_NAME, '配置'), finished);
158
- setStatus(saved ? `已保存到 ${saved}` : '已取消');
177
+ const saved = await saveJsonFile(safeDownloadName(BRAND_NAME, t('Config')), finished);
178
+ setStatus(saved ? t('Saved to {path}', { path: saved }) : t('Canceled'));
159
179
  if (saved) setMode('idle');
160
180
  } catch (err) {
161
181
  setError(portableErrorMessage(err));
@@ -182,14 +202,14 @@ export function PortableSettingsPanel() {
182
202
  setPreview(null);
183
203
  setDocument(null);
184
204
  setMode('idle');
185
- setError('这是配置包,请用「导入配置」');
205
+ setError(t('This is a config package. Use "Import config".'));
186
206
  return;
187
207
  }
188
208
  if (target === 'config' && isChat) {
189
209
  setPreview(null);
190
210
  setDocument(null);
191
211
  setMode('idle');
192
- setError('这是对话包,请用「导入对话」');
212
+ setError(t('This is a chat package. Use "Import chats".'));
193
213
  return;
194
214
  }
195
215
  setDocument(parsed.value);
@@ -222,13 +242,17 @@ export function PortableSettingsPanel() {
222
242
  const appliedLabels = sections
223
243
  .filter((section) => section.clientOnly || result.applied.includes(section.id))
224
244
  .map((section) => section.label);
225
- const parts = [`已导入${appliedLabels.length > 0 ? `:${appliedLabels.join('、')}` : ''}`];
245
+ const parts = [
246
+ appliedLabels.length > 0
247
+ ? t('Imported: {sections}', { sections: appliedLabels.join(t(', ')) })
248
+ : t('Imported'),
249
+ ];
226
250
  for (const item of result.skipped) parts.push(item.reason);
227
251
  for (const note of result.notes) parts.push(note);
228
252
  if (result.missingSkills.length > 0) {
229
- parts.push(`本机没有这些技能:${result.missingSkills.join('、')}`);
253
+ parts.push(t('These skills are not on this computer: {skills}', { skills: result.missingSkills.join(t(', ')) }));
230
254
  }
231
- setStatus(parts.join('。'));
255
+ setStatus(parts.join(t('. ')));
232
256
  setMode('idle');
233
257
  setPreview(null);
234
258
  setDocument(null);
@@ -246,7 +270,7 @@ export function PortableSettingsPanel() {
246
270
  try {
247
271
  const result = await importChatDocument(document);
248
272
  const count = preview.chat?.count ?? 1;
249
- setStatus(count > 1 ? `已导入 ${count} 条对话` : `已导入对话「${result.title}」`);
273
+ setStatus(count > 1 ? t('Imported {count} chats', { count }) : t('Imported chat "{title}"', { title: result.title }));
250
274
  setMode('idle');
251
275
  setPreview(null);
252
276
  setDocument(null);
@@ -269,7 +293,9 @@ export function PortableSettingsPanel() {
269
293
  data-testid="portable-settings-panel"
270
294
  >
271
295
  <p className="text-xs leading-relaxed text-agent-muted-foreground">
272
- 导出配置或对话记录,带到另一台电脑后再导入。对话会连同所属项目一起导出,不包含项目目录里的文件。对话作为新会话导入,不覆盖现有记录。API Key 默认不包含。
296
+ {t(
297
+ 'Export your config or chat history, take it to another computer, and import it there. Chats are exported with their projects, without the files in project directories. Chats are imported as new chats and do not overwrite existing history. API keys are not included by default.',
298
+ )}
273
299
  </p>
274
300
  <div className="flex flex-wrap gap-1.5">
275
301
  <button
@@ -279,7 +305,7 @@ export function PortableSettingsPanel() {
279
305
  data-testid="portable-export-config"
280
306
  >
281
307
  <LuDownload className="h-3.5 w-3.5" />
282
- 导出配置
308
+ {t('Export config')}
283
309
  </button>
284
310
  <button
285
311
  type="button"
@@ -289,7 +315,7 @@ export function PortableSettingsPanel() {
289
315
  data-testid="portable-export-chats"
290
316
  >
291
317
  <LuDownload className="h-3.5 w-3.5" />
292
- 导出对话
318
+ {t('Export chats')}
293
319
  </button>
294
320
  <button
295
321
  type="button"
@@ -299,7 +325,7 @@ export function PortableSettingsPanel() {
299
325
  data-testid="portable-import-config"
300
326
  >
301
327
  <LuUpload className="h-3.5 w-3.5" />
302
- 导入配置
328
+ {t('Import config')}
303
329
  </button>
304
330
  <button
305
331
  type="button"
@@ -309,13 +335,13 @@ export function PortableSettingsPanel() {
309
335
  data-testid="portable-import-chats"
310
336
  >
311
337
  <LuUpload className="h-3.5 w-3.5" />
312
- 导入对话
338
+ {t('Import chats')}
313
339
  </button>
314
340
  </div>
315
341
 
316
342
  {mode === 'export' && (
317
343
  <fieldset className="space-y-1.5" data-testid="portable-export-form">
318
- <legend className="text-xs font-medium text-agent-foreground">导出哪些段</legend>
344
+ <legend className="text-xs font-medium text-agent-foreground">{t('Sections to export')}</legend>
319
345
  {visible.map((item) => (
320
346
  <label key={item.id} className="flex items-center gap-1.5 text-xs text-agent-foreground">
321
347
  <input
@@ -324,7 +350,7 @@ export function PortableSettingsPanel() {
324
350
  onChange={() => toggle(item.id)}
325
351
  data-testid={`portable-section-${item.id}`}
326
352
  />
327
- {item.label}
353
+ {t(item.label)}
328
354
  </label>
329
355
  ))}
330
356
  <label className="flex items-center gap-1.5 text-xs text-agent-foreground">
@@ -334,10 +360,10 @@ export function PortableSettingsPanel() {
334
360
  onChange={(event) => setIncludeSecrets(event.target.checked)}
335
361
  data-testid="portable-include-secrets"
336
362
  />
337
- 包含 API Key
363
+ {t('Include API keys')}
338
364
  </label>
339
365
  <p className="text-[11px] leading-relaxed text-agent-muted-foreground">
340
- 勾选后文件等同于钥匙,不要发给别人,也不要提交到仓库。
366
+ {t('When checked, the file is as sensitive as a key. Do not send it to anyone or commit it to a repository.')}
341
367
  </p>
342
368
  <div className="flex gap-1.5">
343
369
  <button
@@ -347,14 +373,14 @@ export function PortableSettingsPanel() {
347
373
  className="h-7 rounded-full bg-agent-foreground px-2.5 text-xs text-agent-canvas disabled:opacity-60"
348
374
  data-testid="portable-export-confirm"
349
375
  >
350
- {busy ? '导出中' : '保存文件'}
376
+ {busy ? t('Exporting') : t('Save file')}
351
377
  </button>
352
378
  <button
353
379
  type="button"
354
380
  onClick={cancel}
355
381
  className="h-7 rounded-full px-2.5 text-xs text-agent-muted-foreground hover:bg-agent-foreground/5"
356
382
  >
357
- 取消
383
+ {t('Cancel')}
358
384
  </button>
359
385
  </div>
360
386
  </fieldset>
@@ -362,14 +388,16 @@ export function PortableSettingsPanel() {
362
388
 
363
389
  {mode === 'export-chats' && (
364
390
  <fieldset className="space-y-1.5" data-testid="portable-export-chats-form">
365
- <legend className="text-xs font-medium text-agent-foreground">导出哪些对话</legend>
391
+ <legend className="text-xs font-medium text-agent-foreground">{t('Chats to export')}</legend>
366
392
  <p className="text-[11px] leading-relaxed text-agent-muted-foreground">
367
- 这些对话所属的项目会一起写进文件,只保留名称和目录位置,不打包目录里的文件。
393
+ {t(
394
+ 'The projects these chats belong to are written into the file too, keeping only their names and directory locations. Files in the directories are not packaged.',
395
+ )}
368
396
  </p>
369
397
  {busy && chatRows.length === 0 ? (
370
- <p className="text-xs text-agent-muted-foreground">正在读取对话…</p>
398
+ <p className="text-xs text-agent-muted-foreground">{t('Loading chats…')}</p>
371
399
  ) : chatRows.length === 0 ? (
372
- <p className="text-xs text-agent-muted-foreground">还没有对话。</p>
400
+ <p className="text-xs text-agent-muted-foreground">{t('No chats yet.')}</p>
373
401
  ) : (
374
402
  <div className="max-h-48 space-y-1 overflow-y-auto">
375
403
  <label className="flex items-center gap-1.5 text-xs text-agent-foreground">
@@ -381,7 +409,7 @@ export function PortableSettingsPanel() {
381
409
  }}
382
410
  data-testid="portable-chats-all"
383
411
  />
384
- 全部({chatRows.length})
412
+ {t('All ({count})', { count: chatRows.length })}
385
413
  </label>
386
414
  {chatRows.map((row) => (
387
415
  <label key={row.id} className="flex items-center gap-1.5 text-xs text-agent-foreground">
@@ -404,14 +432,14 @@ export function PortableSettingsPanel() {
404
432
  className="h-7 rounded-full bg-agent-foreground px-2.5 text-xs text-agent-canvas disabled:opacity-60"
405
433
  data-testid="portable-export-chats-confirm"
406
434
  >
407
- {busy ? '导出中' : '保存文件'}
435
+ {busy ? t('Exporting') : t('Save file')}
408
436
  </button>
409
437
  <button
410
438
  type="button"
411
439
  onClick={cancel}
412
440
  className="h-7 rounded-full px-2.5 text-xs text-agent-muted-foreground hover:bg-agent-foreground/5"
413
441
  >
414
- 取消
442
+ {t('Cancel')}
415
443
  </button>
416
444
  </div>
417
445
  </fieldset>
@@ -428,14 +456,14 @@ export function PortableSettingsPanel() {
428
456
  className="h-7 rounded-full bg-agent-foreground px-2.5 text-xs text-agent-canvas disabled:opacity-60"
429
457
  data-testid="portable-import-chat-confirm"
430
458
  >
431
- {busy ? '导入中' : '导入对话'}
459
+ {busy ? t('Importing') : t('Import chats')}
432
460
  </button>
433
461
  <button
434
462
  type="button"
435
463
  onClick={cancel}
436
464
  className="h-7 rounded-full px-2.5 text-xs text-agent-muted-foreground hover:bg-agent-foreground/5"
437
465
  >
438
- 取消
466
+ {t('Cancel')}
439
467
  </button>
440
468
  </div>
441
469
  </div>
@@ -443,7 +471,7 @@ export function PortableSettingsPanel() {
443
471
 
444
472
  {mode === 'import' && preview && (
445
473
  <fieldset className="space-y-1.5" data-testid="portable-import-form">
446
- <legend className="text-xs font-medium text-agent-foreground">导入哪些段</legend>
474
+ <legend className="text-xs font-medium text-agent-foreground">{t('Sections to import')}</legend>
447
475
  <p className="text-[11px] leading-relaxed text-agent-muted-foreground">{describeImport(preview)}</p>
448
476
  {preview.sections.map((section) => (
449
477
  <label key={section.id} className="flex items-center gap-1.5 text-xs text-agent-foreground">
@@ -465,14 +493,14 @@ export function PortableSettingsPanel() {
465
493
  className="h-7 rounded-full bg-agent-foreground px-2.5 text-xs text-agent-canvas disabled:opacity-60"
466
494
  data-testid="portable-import-confirm"
467
495
  >
468
- {busy ? '导入中' : '导入配置'}
496
+ {busy ? t('Importing') : t('Import config')}
469
497
  </button>
470
498
  <button
471
499
  type="button"
472
500
  onClick={cancel}
473
501
  className="h-7 rounded-full px-2.5 text-xs text-agent-muted-foreground hover:bg-agent-foreground/5"
474
502
  >
475
- 取消
503
+ {t('Cancel')}
476
504
  </button>
477
505
  </div>
478
506
  </fieldset>
@@ -1,5 +1,6 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { LuCodeXml } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
  import {
4
5
  getElectronBridge,
5
6
  type PythonRunnerSnapshot,
@@ -72,33 +73,35 @@ export function PythonRunnerSettingsPanel() {
72
73
 
73
74
  const actionLabel =
74
75
  source === 'local'
75
- ? '使用此 Python'
76
+ ? t('Use this Python')
76
77
  : source === 'url'
77
- ? '从此地址下载'
78
- : '下载默认运行器';
78
+ ? t('Download from this URL')
79
+ : t('Download default runner');
79
80
  const phaseLabel =
80
81
  snapshot.phase === 'verifying'
81
- ? '正在校验'
82
+ ? t('Verifying')
82
83
  : snapshot.phase === 'extracting'
83
- ? '正在解压'
84
- : '正在下载';
84
+ ? t('Extracting')
85
+ : t('Downloading');
85
86
 
86
87
  return (
87
88
  <section className="space-y-2" data-testid="settings-section-python-runner">
88
89
  <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
89
90
  <LuCodeXml className="h-3.5 w-3.5 text-agent-muted-foreground" />
90
- Python 代码运行器
91
+ {t('Python code runner')}
91
92
  </h2>
92
93
  <div className="space-y-3 rounded-agent-md border border-agent-border bg-agent-card p-3 text-xs">
93
94
  <p className="leading-relaxed text-agent-muted-foreground">
94
- run_code 使用独立的 Python 解释器。下载和配置不会阻塞聊天,变更在重启应用后生效。
95
+ {t(
96
+ 'run_code uses a separate Python interpreter. Downloading and configuring it does not block chat. Changes take effect after you restart the app.',
97
+ )}
95
98
  </p>
96
99
 
97
100
  <div className="flex flex-wrap gap-3">
98
101
  {([
99
- ['default', '默认地址'],
100
- ['url', '自定义地址'],
101
- ['local', '本地 Python'],
102
+ ['default', 'Default URL'],
103
+ ['url', 'Custom URL'],
104
+ ['local', 'Local Python'],
102
105
  ] as const).map(([value, label]) => (
103
106
  <label key={value} className="flex items-center gap-1.5 text-agent-foreground">
104
107
  <input
@@ -109,7 +112,7 @@ export function PythonRunnerSettingsPanel() {
109
112
  disabled={busy}
110
113
  onChange={() => setSource(value)}
111
114
  />
112
- {label}
115
+ {t(label)}
113
116
  </label>
114
117
  ))}
115
118
  </div>
@@ -131,7 +134,7 @@ export function PythonRunnerSettingsPanel() {
131
134
  onChange={(event) => setUrl(event.target.value)}
132
135
  />
133
136
  <p className="text-agent-destructive">
134
- 自定义地址不会校验文件完整性,请仅使用可信来源。
137
+ {t('Custom URLs are not checked for file integrity. Use only trusted sources.')}
135
138
  </p>
136
139
  </div>
137
140
  ) : null}
@@ -142,7 +145,7 @@ export function PythonRunnerSettingsPanel() {
142
145
  type="text"
143
146
  value={localPath}
144
147
  disabled={busy}
145
- placeholder="Python 可执行文件的绝对路径"
148
+ placeholder={t('Absolute path to the Python executable')}
146
149
  className="h-8 min-w-0 flex-1 rounded-agent-sm border border-agent-border bg-agent-canvas px-2 text-agent-foreground outline-none focus:border-agent-foreground/40"
147
150
  onChange={(event) => setLocalPath(event.target.value)}
148
151
  />
@@ -152,7 +155,7 @@ export function PythonRunnerSettingsPanel() {
152
155
  className="h-8 shrink-0 rounded-agent-sm border border-agent-border px-3 text-agent-foreground hover:bg-agent-foreground/5 disabled:opacity-50"
153
156
  onClick={chooseLocal}
154
157
  >
155
- 选择…
158
+ {t('Choose…')}
156
159
  </button>
157
160
  </div>
158
161
  ) : null}
@@ -178,7 +181,7 @@ export function PythonRunnerSettingsPanel() {
178
181
  className="text-agent-foreground hover:underline"
179
182
  onClick={() => runner && update(runner.cancel())}
180
183
  >
181
- 取消
184
+ {t('Cancel')}
182
185
  </button>
183
186
  </div>
184
187
  </div>
@@ -203,7 +206,7 @@ export function PythonRunnerSettingsPanel() {
203
206
  className="h-8 rounded-agent-sm bg-agent-foreground px-3 font-medium text-agent-canvas hover:opacity-90"
204
207
  onClick={() => void runner?.restart()}
205
208
  >
206
- 重启应用
209
+ {t('Restart app')}
207
210
  </button>
208
211
  ) : null}
209
212
  </div>
@@ -218,11 +221,11 @@ export function PythonRunnerSettingsPanel() {
218
221
  </p>
219
222
  ) : null}
220
223
  {snapshot.restartRequired ? (
221
- <p className="font-medium text-agent-foreground">已保存,重启应用后生效。</p>
224
+ <p className="font-medium text-agent-foreground">{t('Saved. Takes effect after you restart the app.')}</p>
222
225
  ) : null}
223
226
  {snapshot.activeRunner ? (
224
227
  <p className="break-all text-agent-muted-foreground">
225
- 当前会话:{snapshot.activeRunner}
228
+ {t('Current session: {runner}', { runner: snapshot.activeRunner })}
226
229
  </p>
227
230
  ) : null}
228
231
  </div>
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuShieldCheck } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
  import { isElectron } from '@/lib/electron-bridge';
4
5
  import {
5
6
  getSidecarSandboxPosture,
@@ -23,44 +24,52 @@ type PostureReason = SidecarSandboxPosture['reason'];
23
24
 
24
25
  /** 收容失败、已拒绝启动(告警行首句)。 */
25
26
  const REFUSED_REASON_TEXT: Partial<Record<PostureReason, string>> = {
26
- platform_unsupported: '当前平台没有可用的进程沙箱后端',
27
- seatbelt_missing: '未找到 /usr/bin/sandbox-exec',
28
- profile_failed: '沙箱配置(Seatbelt profile)生成失败',
29
- wrap_failed: 'Linux 进程沙箱(bwrap / Landlock)未能包住 sidecar',
30
- helper_missing: '未找到 win-spawn-helper.exe',
27
+ platform_unsupported: 'No process sandbox backend is available on this platform',
28
+ seatbelt_missing: '/usr/bin/sandbox-exec not found',
29
+ profile_failed: 'Failed to generate the sandbox configuration (Seatbelt profile)',
30
+ wrap_failed: 'The Linux process sandbox (bwrap / Landlock) could not wrap the sidecar',
31
+ helper_missing: 'win-spawn-helper.exe not found',
31
32
  };
32
33
 
33
34
  /** 手动关闭原因的中文说明(中性行,不告警)。 */
34
35
  const OPT_OUT_REASON_TEXT: Partial<Record<PostureReason, string>> = {
35
- disabled_by_option: '已通过 sandbox: false 关闭',
36
- disabled_by_env: '已通过 STEERABLE_SIDECAR_SANDBOX=0 关闭',
36
+ disabled_by_option: 'Turned off with sandbox: false',
37
+ disabled_by_env: 'Turned off with STEERABLE_SIDECAR_SANDBOX=0',
37
38
  };
38
39
 
39
40
  function activeHeadline(backend: SidecarSandboxPosture['backend']): string {
40
41
  switch (backend) {
41
42
  case 'seatbelt':
42
- return 'Seatbelt · 部分强制';
43
+ return t('Seatbelt · partial enforcement');
43
44
  case 'bwrap':
44
- return 'bwrap · 部分强制';
45
+ return t('bwrap · partial enforcement');
45
46
  case 'landlock':
46
- return 'Landlock · 部分强制';
47
+ return t('Landlock · partial enforcement');
47
48
  case 'windows-restricted-token':
48
- return 'Windows 受限令牌 · 部分强制';
49
+ return t('Windows restricted token · partial enforcement');
49
50
  case 'none':
50
- return '未沙箱';
51
+ return t('Not sandboxed');
51
52
  }
52
53
  }
53
54
 
54
55
  function activeBody(backend: SidecarSandboxPosture['backend']): string {
55
56
  switch (backend) {
56
57
  case 'seatbelt':
57
- return 'sidecar 进程在 macOS Seatbelt 下运行:写入限制在 ~/.steerable 与临时目录。远程主机的出站网络只能按端口限制(Seatbelt 不识别主机名),故记为「部分」而非「完全」。';
58
+ return t(
59
+ 'The sidecar process runs under macOS Seatbelt: writes are limited to ~/.steerable and the temp directory. Outbound network to remote hosts can only be limited by port (Seatbelt does not recognize host names), so this is recorded as "partial" rather than "full".',
60
+ );
58
61
  case 'bwrap':
59
- return 'sidecar 进程在 bubblewrap 下运行:写入限制在 ~/.steerable 与临时目录。出站网络保持开启(sidecar 要访问 LLM),故记为「部分」。';
62
+ return t(
63
+ 'The sidecar process runs under bubblewrap: writes are limited to ~/.steerable and the temp directory. Outbound network stays open (the sidecar needs to reach the LLM), so this is recorded as "partial".',
64
+ );
60
65
  case 'landlock':
61
- return 'sidecar 进程在 Linux Landlock 下运行:写入限制在 ~/.steerable 与临时目录。出站网络保持开启(sidecar 要访问 LLM),故记为「部分」。';
66
+ return t(
67
+ 'The sidecar process runs under Linux Landlock: writes are limited to ~/.steerable and the temp directory. Outbound network stays open (the sidecar needs to reach the LLM), so this is recorded as "partial".',
68
+ );
62
69
  case 'windows-restricted-token':
63
- return 'sidecar 进程在 Windows 受限令牌 + Job Object 下运行:写入限制在 ~/.steerable。网络策略未由 helper 强制(无 WFP),故记为「部分」。';
70
+ return t(
71
+ 'The sidecar process runs under a Windows restricted token + Job Object: writes are limited to ~/.steerable. The helper does not enforce network policy (no WFP), so this is recorded as "partial".',
72
+ );
64
73
  case 'none':
65
74
  return '';
66
75
  }
@@ -71,20 +80,24 @@ function EgressPostureRow({ egress }: { egress: EgressPosture }) {
71
80
  if (egress.mode === 'per-host-proxy') {
72
81
  return (
73
82
  <p className="text-[11px] text-agent-muted-foreground">
74
- 出网管控:按主机白名单代理生效中;白名单外的请求会请你逐项放行(仅本次会话有效)。
83
+ {t(
84
+ 'Egress control: the per-host allowlist proxy is active. Requests outside the allowlist ask you to allow them one by one (valid for this session only).',
85
+ )}
75
86
  </p>
76
87
  );
77
88
  }
78
89
  if (egress.mode === 'disabled') {
79
90
  return (
80
91
  <p className="text-[11px] text-agent-muted-foreground">
81
- 出网管控:已关闭{egress.reason ? ` — ${egress.reason}` : ''}。
92
+ {t('Egress control: off{reason}.', { reason: egress.reason ? ` — ${egress.reason}` : '' })}
82
93
  </p>
83
94
  );
84
95
  }
85
96
  return (
86
97
  <p className="text-[11px] text-amber-600 dark:text-amber-400">
87
- 出网管控:已退回端口级(较弱的管控粒度){egress.reason ? ` — ${egress.reason}` : ''}。
98
+ {t('Egress control: fell back to port level (coarser control){reason}.', {
99
+ reason: egress.reason ? ` — ${egress.reason}` : '',
100
+ })}
88
101
  </p>
89
102
  );
90
103
  }
@@ -103,12 +116,12 @@ export function SecuritySettingsPanel() {
103
116
  const res = await getSidecarSandboxPosture();
104
117
  setPosture(res.posture ?? null);
105
118
  setEgress(res.egress ?? null);
106
- if (!res.posture) setUnavailable('无法读取沙箱状态');
119
+ if (!res.posture) setUnavailable(t('Could not read sandbox status'));
107
120
  } catch {
108
121
  // 503 = sidecar 未就绪且没有收容失败记录。
109
122
  setPosture(null);
110
123
  setEgress(null);
111
- setUnavailable('sidecar 未就绪 — 无法读取沙箱状态');
124
+ setUnavailable(t('Sidecar not ready — could not read sandbox status'));
112
125
  } finally {
113
126
  setLoading(false);
114
127
  }
@@ -125,35 +138,46 @@ export function SecuritySettingsPanel() {
125
138
  <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
126
139
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
127
140
  <LuShieldCheck className="h-3.5 w-3.5 text-agent-muted-foreground" />
128
- Sidecar 进程沙箱
141
+ {t('Sidecar process sandbox')}
129
142
  </h4>
130
143
 
131
144
  {!isElectron() ? (
132
- <p className="text-[11px] text-agent-muted-foreground">需要在桌面客户端中打开</p>
145
+ <p className="text-[11px] text-agent-muted-foreground">{t('Open this in the desktop app')}</p>
133
146
  ) : loading ? (
134
147
  <div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
135
148
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
136
- 读取沙箱状态...
149
+ {t('Loading sandbox status...')}
137
150
  </div>
138
151
  ) : unavailable ? (
139
152
  <p className="text-[11px] text-agent-muted-foreground">{unavailable}</p>
140
153
  ) : posture && refusedReason ? (
141
154
  <div className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive space-y-1.5">
142
- <p className="font-medium">无法收容、已拒绝启动 — {refusedReason}。</p>
155
+ <p className="font-medium">
156
+ {t('Cannot contain the process, so startup was refused — {reason}.', {
157
+ reason: t(refusedReason),
158
+ })}
159
+ </p>
143
160
  <p>
144
- 持有服务商 API 密钥的 sidecar 进程没有启动,因此也没有在无隔离状态下运行。
145
- 唯一裸跑入口是显式设置 STEERABLE_SIDECAR_SANDBOX=0。
161
+ {t(
162
+ 'The sidecar process that holds the provider API keys did not start, so it is not running without isolation either.',
163
+ )}{' '}
164
+ {t('The only way to run it unsandboxed is to set STEERABLE_SIDECAR_SANDBOX=0 explicitly.')}
146
165
  </p>
147
166
  <p>
148
- 如需隔离,请安装对应平台的收容后端(macOS Seatbelt、Linux bwrap/Landlock、Windows
149
- win-spawn-helper),或将本应用运行在容器内。
167
+ {t(
168
+ 'For isolation, install the containment backend for your platform (macOS Seatbelt, Linux bwrap/Landlock, Windows win-spawn-helper), or run this app in a container.',
169
+ )}
150
170
  </p>
151
171
  </div>
152
172
  ) : posture && optOutReason ? (
153
173
  <div className="rounded-agent-md border border-agent-border/60 bg-agent-canvas p-2.5 text-xs text-agent-muted-foreground space-y-1.5">
154
- <p className="font-medium text-agent-foreground">已手动关闭 — {optOutReason}。</p>
174
+ <p className="font-medium text-agent-foreground">
175
+ {t('Turned off manually — {reason}.', { reason: t(optOutReason) })}
176
+ </p>
155
177
  <p>
156
- sidecar 进程(持有服务商 API 密钥)无操作系统隔离运行。如需恢复,移除该设置并重启应用。
178
+ {t(
179
+ 'The sidecar process (which holds the provider API keys) runs without OS isolation. To restore it, remove the setting and restart the app.',
180
+ )}
157
181
  </p>
158
182
  </div>
159
183
  ) : posture ? (