@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
@@ -3,6 +3,7 @@ import { LuArrowDown, LuLoaderCircle, LuTerminal } from 'react-icons/lu';
3
3
  import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
4
4
  import { getChildProcess, getTaskProcess } from '@/lib/local-api';
5
5
  import { getElectronBridge } from '@/lib/electron-bridge';
6
+ import { t } from '@/i18n';
6
7
  import { Markdown } from '@/components/chat/Markdown';
7
8
  import { TurnProcessGroup } from '@/components/chat/TurnProcessGroup';
8
9
  import {
@@ -168,11 +169,11 @@ export function TaskProcessPanel({
168
169
  }, [inspected.id, childRecordId]);
169
170
 
170
171
  const statusLabel = live
171
- ? '正在推理'
172
+ ? t('Reasoning')
172
173
  : stale
173
- ? '进程已中断(表上仍是运行中)'
174
- : '已结束';
175
- const panelTitle = `${childRecordId ? '子代理推理' : '后台推理'} · ${statusLabel}`;
174
+ ? t('Process interrupted (still marked running in the table)')
175
+ : t('Finished');
176
+ const panelTitle = `${childRecordId ? t('Subagent reasoning') : t('Background reasoning')} · ${statusLabel}`;
176
177
 
177
178
  return (
178
179
  <div
@@ -182,13 +183,13 @@ export function TaskProcessPanel({
182
183
  <DockPanelHeader
183
184
  title={panelTitle}
184
185
  onClose={onClose}
185
- closeLabel="关闭过程面板"
186
+ closeLabel={t('Close process panel')}
186
187
  actions={
187
188
  onShowTerminal && (
188
189
  <DockHeaderButton
189
190
  icon={<LuTerminal className="h-3 w-3" />}
190
- label="终端"
191
- title="切换到终端"
191
+ label={t('Terminal')}
192
+ title={t('Switch to terminal')}
192
193
  onClick={onShowTerminal}
193
194
  />
194
195
  )
@@ -208,7 +209,7 @@ export function TaskProcessPanel({
208
209
  {loading ? (
209
210
  <div className="flex items-center gap-2 text-xs text-agent-muted-foreground">
210
211
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
211
- 正在加载推理过程…
212
+ {t('Loading reasoning process...')}
212
213
  </div>
213
214
  ) : error ? (
214
215
  <div className="text-xs text-red-600 dark:text-red-300">{error}</div>
@@ -223,10 +224,10 @@ export function TaskProcessPanel({
223
224
  emptyFallback={
224
225
  <div className="text-xs text-agent-muted-foreground">
225
226
  {stale
226
- ? '任务流已随进程结束,没有留下可回放的推理记录。'
227
+ ? t('The task stream ended with the process and left no reasoning record to replay.')
227
228
  : live
228
- ? '任务已启动,推理过程会显示在这里。'
229
- : '没有可显示的推理过程。'}
229
+ ? t('The task has started. Its reasoning process appears here.')
230
+ : t('No reasoning process to show.')}
230
231
  </div>
231
232
  }
232
233
  renderAnswer={(block) => (
@@ -246,8 +247,8 @@ export function TaskProcessPanel({
246
247
  scrollToBottom('smooth');
247
248
  }}
248
249
  className="absolute bottom-2 right-2 flex h-7 w-7 items-center justify-center rounded-full border border-agent-border bg-agent-canvas text-agent-foreground shadow-md transition-colors hover:bg-agent-foreground/5"
249
- title="回到底部"
250
- aria-label="回到底部"
250
+ title={t('Back to bottom')}
251
+ aria-label={t('Back to bottom')}
251
252
  >
252
253
  <LuArrowDown className="h-4 w-4" />
253
254
  </button>
@@ -16,6 +16,7 @@ import {
16
16
  } from 'react-icons/lu';
17
17
  import { openLocalPath } from '@/lib/local-api';
18
18
  import { hostToolChrome } from '@/lib/host-tools';
19
+ import { t } from '@/i18n';
19
20
  import {
20
21
  formatFileSize,
21
22
  formatIntermediateDisplayPath,
@@ -67,7 +68,7 @@ export function TurnFilesCard({ files, executedActions }: TurnFilesCardProps) {
67
68
  try {
68
69
  const result = await openLocalPath(filePath);
69
70
  if (!result.success) {
70
- setOpenError({ path: filePath, message: result.error || '打开失败' });
71
+ setOpenError({ path: filePath, message: result.error || t('Could not open') });
71
72
  }
72
73
  } catch (err) {
73
74
  setOpenError({
@@ -197,7 +198,7 @@ function DeliverableCard({
197
198
  </button>
198
199
  <div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-[11px] text-agent-muted-foreground">
199
200
  <span className="truncate" title={file.description}>
200
- {file.description ?? `${meta.label} · ${meta.extBadge}`}
201
+ {file.description ?? `${t(meta.label)} · ${meta.extBadge}`}
201
202
  </span>
202
203
  {size && (
203
204
  <>
@@ -221,13 +222,13 @@ function DeliverableCard({
221
222
  {busy ? (
222
223
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin text-agent-muted-foreground" />
223
224
  ) : null}
224
- <span>打开方式</span>
225
+ <span>{t('Open with')}</span>
225
226
  </button>
226
227
  <button
227
228
  type="button"
228
229
  onClick={() => setDropdownOpen(!dropdownOpen)}
229
230
  disabled={!allowOpen}
230
- aria-label="更多操作"
231
+ aria-label={t('More actions')}
231
232
  className="border-l border-agent-border px-1.5 py-1 text-agent-muted-foreground hover:bg-agent-muted/50 hover:text-agent-foreground rounded-r-agent-md transition-colors"
232
233
  >
233
234
  <LuChevronDown className="h-3.5 w-3.5" />
@@ -246,7 +247,7 @@ function DeliverableCard({
246
247
  className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-agent-foreground hover:bg-agent-foreground/5"
247
248
  >
248
249
  <LuExternalLink className="h-3.5 w-3.5 text-agent-muted-foreground" />
249
- <span>用默认应用打开</span>
250
+ <span>{t('Open with default app')}</span>
250
251
  </button>
251
252
  <button
252
253
  type="button"
@@ -257,7 +258,7 @@ function DeliverableCard({
257
258
  className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-agent-foreground hover:bg-agent-foreground/5"
258
259
  >
259
260
  <LuFolderOpen className="h-3.5 w-3.5 text-agent-muted-foreground" />
260
- <span>在文件夹中显示</span>
261
+ <span>{t('Show in folder')}</span>
261
262
  </button>
262
263
  <button
263
264
  type="button"
@@ -272,7 +273,7 @@ function DeliverableCard({
272
273
  ) : (
273
274
  <LuCopy className="h-3.5 w-3.5 text-agent-muted-foreground" />
274
275
  )}
275
- <span>{copied ? '已复制完整路径' : '复制文件路径'}</span>
276
+ <span>{copied ? t('Copied full path') : t('Copy file path')}</span>
276
277
  </button>
277
278
  </div>
278
279
  )}
@@ -281,7 +282,7 @@ function DeliverableCard({
281
282
 
282
283
  {error && (
283
284
  <div className="mt-2 px-1 text-[11px] text-agent-destructive" role="status">
284
- 打开失败:{error}
285
+ {t('Could not open: {error}', { error })}
285
286
  </div>
286
287
  )}
287
288
  </div>
@@ -370,7 +371,7 @@ function EditedFilesCard({
370
371
  </div>
371
372
  <div className="min-w-0">
372
373
  <div className="truncate text-xs font-medium text-agent-foreground">
373
- 修改了 {files.length} 个文件
374
+ {t('Changed {count} files', { count: files.length })}
374
375
  </div>
375
376
  {(totalAdditions > 0 || totalDeletions > 0) && (
376
377
  <div className="flex items-center gap-1.5 font-mono text-[11px] tabular-nums text-agent-muted-foreground">
@@ -388,10 +389,10 @@ function EditedFilesCard({
388
389
  <button
389
390
  type="button"
390
391
  disabled
391
- title="撤销本轮文件改动"
392
+ title={t('Undo file changes from this turn')}
392
393
  className="flex items-center gap-1 px-1.5 py-0.5 text-xs text-agent-muted-foreground/50 cursor-default"
393
394
  >
394
- <span>撤销</span>
395
+ <span>{t('Undo')}</span>
395
396
  <LuUndo2 className="h-3 w-3" />
396
397
  </button>
397
398
  <button
@@ -400,7 +401,7 @@ function EditedFilesCard({
400
401
  onClick={() => setExpanded(!expanded)}
401
402
  className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-0.5 text-xs font-medium text-agent-foreground hover:bg-agent-muted/50 transition-colors"
402
403
  >
403
- 查看详情
404
+ {t('View details')}
404
405
  </button>
405
406
  </div>
406
407
  </div>
@@ -457,7 +458,7 @@ function EditedFilesCard({
457
458
 
458
459
  {error && (
459
460
  <div className="px-3 pb-1 text-[11px] text-agent-destructive" role="status">
460
- 打开失败:{error}
461
+ {t('Could not open: {error}', { error })}
461
462
  </div>
462
463
  )}
463
464
  </li>
@@ -3,6 +3,7 @@ import { motion } from 'framer-motion';
3
3
  import { LuChevronDown, LuChevronUp, LuCheck, LuCopy, LuCornerDownRight } from 'react-icons/lu';
4
4
  import type { ChatMessage } from '@steerable/agent-protocol';
5
5
  import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
6
+ import { t } from '@/i18n';
6
7
  import { useSlashSources } from '@/lib/slash-sources';
7
8
  import { Markdown } from './Markdown';
8
9
  import { getFriendlyDate } from './timestamp';
@@ -68,7 +69,7 @@ export function UserMessage({
68
69
  <div className="mb-1 flex items-center gap-1 text-[11px] font-medium text-agent-muted-foreground select-none">
69
70
  <LuCornerDownRight className="h-3 w-3 text-agent-primary/80" />
70
71
  <span className="rounded bg-agent-primary/10 px-1 py-0.5 text-[10px] text-agent-primary font-medium">
71
- 追加
72
+ {t('Appended')}
72
73
  </span>
73
74
  </div>
74
75
  )}
@@ -110,12 +111,12 @@ export function UserMessage({
110
111
  >
111
112
  {isExpanded ? (
112
113
  <>
113
- 收起
114
+ {t('Collapse')}
114
115
  <LuChevronUp className="h-3 w-3" />
115
116
  </>
116
117
  ) : (
117
118
  <>
118
- 展开全部
119
+ {t('Expand all')}
119
120
  <LuChevronDown className="h-3 w-3" />
120
121
  </>
121
122
  )}
@@ -127,14 +128,14 @@ export function UserMessage({
127
128
  type="button"
128
129
  onClick={() => void copy()}
129
130
  className="inline-flex items-center gap-0.5 rounded text-xs text-agent-muted-foreground opacity-0 transition-all duration-200 hover:text-agent-foreground focus:opacity-100 group-hover/message:opacity-100"
130
- title={copied ? '已复制' : '复制消息'}
131
- aria-label={copied ? '已复制' : '复制消息'}
131
+ title={copied ? t('Copied') : t('Copy message')}
132
+ aria-label={copied ? t('Copied') : t('Copy message')}
132
133
  >
133
134
  {copied ? (
134
135
  <>
135
136
  <LuCheck className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
136
137
  <span className="text-emerald-600 dark:text-emerald-400">
137
- 已复制
138
+ {t('Copied')}
138
139
  </span>
139
140
  </>
140
141
  ) : (
@@ -200,12 +201,12 @@ export function UserMessage({
200
201
  >
201
202
  {isExpanded ? (
202
203
  <>
203
- 收起
204
+ {t('Collapse')}
204
205
  <LuChevronUp className="h-3 w-3" />
205
206
  </>
206
207
  ) : (
207
208
  <>
208
- 展开全部
209
+ {t('Expand all')}
209
210
  <LuChevronDown className="h-3 w-3" />
210
211
  </>
211
212
  )}
@@ -217,14 +218,14 @@ export function UserMessage({
217
218
  type="button"
218
219
  onClick={() => void copy()}
219
220
  className="inline-flex items-center gap-0.5 rounded text-xs text-agent-muted-foreground opacity-0 transition-all duration-200 hover:text-agent-foreground focus:opacity-100 group-hover/message:opacity-100"
220
- title={copied ? '已复制' : '复制消息'}
221
- aria-label={copied ? '已复制' : '复制消息'}
221
+ title={copied ? t('Copied') : t('Copy message')}
222
+ aria-label={copied ? t('Copied') : t('Copy message')}
222
223
  >
223
224
  {copied ? (
224
225
  <>
225
226
  <LuCheck className="h-3 w-3 text-emerald-600 dark:text-emerald-400" />
226
227
  <span className="text-emerald-600 dark:text-emerald-400">
227
- 已复制
228
+ {t('Copied')}
228
229
  </span>
229
230
  </>
230
231
  ) : (
@@ -8,6 +8,8 @@
8
8
  * result_count / status / bytes / truncated 这些小字段原样保留。
9
9
  */
10
10
 
11
+ import { t } from '@/i18n';
12
+ import { agentLabel } from '@/i18n/agent-label';
11
13
  import { findAgentForProfile } from './orchestration-children-model';
12
14
 
13
15
  interface WebToolResultData {
@@ -61,19 +63,21 @@ export function summarizeWebAction(
61
63
 
62
64
  if (tool === 'web_search') {
63
65
  const query = typeof obj.query === 'string' ? obj.query : '';
64
- const head = `搜索“${query.length > 40 ? `${query.slice(0, 37)}…` : query}”`;
66
+ const head = t('Search "{query}"', {
67
+ query: query.length > 40 ? `${query.slice(0, 37)}…` : query,
68
+ });
65
69
  if (failed || !data) return head;
66
70
  const count = typeof data.result_count === 'number' ? data.result_count : null;
67
- return count === null ? head : `${head} → ${count} 条结果`;
71
+ return count === null ? head : t('{head} → {count} results', { head, count });
68
72
  }
69
73
  if (tool === 'web_fetch') {
70
- const head = `抓取 ${shortUrl(obj.url) ?? '未知地址'}`;
74
+ const head = t('Fetch {url}', { url: shortUrl(obj.url) ?? t('unknown address') });
71
75
  if (failed || !data) return head;
72
76
  const parts: string[] = [];
73
77
  if (typeof data.status === 'number') parts.push(String(data.status));
74
78
  const size = formatBytes(data.bytes);
75
79
  if (size) parts.push(size);
76
- if (data.truncated === true) parts.push('已截断');
80
+ if (data.truncated === true) parts.push(t('truncated'));
77
81
  return parts.length > 0 ? `${head} → ${parts.join(' · ')}` : head;
78
82
  }
79
83
  return null;
@@ -99,7 +103,7 @@ export function summarizeRunCodeAction(
99
103
  const description =
100
104
  typeof obj.description === 'string' && obj.description.trim()
101
105
  ? obj.description.trim()
102
- : '程序';
106
+ : t('Program');
103
107
  const head =
104
108
  description.length > 40 ? `${description.slice(0, 37)}…` : description;
105
109
  const data = resultData(result);
@@ -110,10 +114,10 @@ export function summarizeRunCodeAction(
110
114
  result &&
111
115
  typeof result === 'object' &&
112
116
  (result as { success?: unknown }).success === false;
113
- if (failed) return `程序「${head}」失败`;
117
+ if (failed) return t('Program "{name}" failed', { name: head });
114
118
  return calls.length > 0
115
- ? `程序「${head}」· ${calls.length} 个内层工具`
116
- : `程序「${head}」`;
119
+ ? t('Program "{name}" · {count} inner tools', { name: head, count: calls.length })
120
+ : t('Program "{name}"', { name: head });
117
121
  }
118
122
 
119
123
  export interface ExecutedActionLike {
@@ -129,12 +133,17 @@ export interface DelegateSubagentView {
129
133
  }
130
134
 
131
135
  const BUILTIN_DELEGATE_LABELS: Record<string, string> = {
132
- explore: '探索',
133
- research: '调研',
134
- coder: '编程',
135
- 'general-purpose': '通用',
136
+ explore: 'Explore',
137
+ research: 'Research',
138
+ coder: 'Coding',
139
+ 'general-purpose': 'General',
136
140
  };
137
141
 
142
+ function builtinDelegateLabel(profile: string): string {
143
+ const source = BUILTIN_DELEGATE_LABELS[profile];
144
+ return source ? t(source) : profile;
145
+ }
146
+
138
147
  const BUILTIN_DELEGATE_COLORS: Record<string, string> = {
139
148
  explore: '#0ea5e9',
140
149
  research: '#2563eb',
@@ -157,7 +166,7 @@ function participantFromProfile(
157
166
  const match = findAgentForProfile(profile, agents);
158
167
  return {
159
168
  key: match?.id ?? profile,
160
- name: match?.name ?? BUILTIN_DELEGATE_LABELS[profile] ?? profile,
169
+ name: match ? agentLabel(match) : builtinDelegateLabel(profile),
161
170
  color: match?.color || BUILTIN_DELEGATE_COLORS[profile] || '#7c3aed',
162
171
  };
163
172
  }
@@ -187,7 +196,7 @@ export function collectTurnParticipants(
187
196
  const pushAgent = (agent: AgentRef) => {
188
197
  push({
189
198
  key: agent.id,
190
- name: agent.name,
199
+ name: agentLabel(agent),
191
200
  color: agent.color || '#7c3aed',
192
201
  });
193
202
  };
@@ -196,7 +205,7 @@ export function collectTurnParticipants(
196
205
  if (parent && !mentioned.includes(parent.id)) {
197
206
  push({
198
207
  key: parent.id,
199
- name: parent.name,
208
+ name: agentLabel(parent),
200
209
  color: parent.color || '#7c3aed',
201
210
  });
202
211
  }
@@ -209,7 +218,7 @@ export function collectTurnParticipants(
209
218
  if (parent) {
210
219
  push({
211
220
  key: parent.id,
212
- name: parent.name,
221
+ name: agentLabel(parent),
213
222
  color: parent.color || '#7c3aed',
214
223
  });
215
224
  }
@@ -285,10 +294,10 @@ export function delegateSubagentDisplayName(
285
294
  profile: string | null,
286
295
  agents: ReadonlyArray<{ id: string; slug: string | null; name: string }>,
287
296
  ): string {
288
- if (!profile) return '子代理';
297
+ if (!profile) return t('Subagent');
289
298
  const match = findAgentForProfile(profile, agents);
290
- if (match) return match.name;
291
- return BUILTIN_DELEGATE_LABELS[profile] ?? profile;
299
+ if (match) return agentLabel(match);
300
+ return builtinDelegateLabel(profile);
292
301
  }
293
302
 
294
303
  /** 任务正文作行摘要;没有任务时返回 null,调用方不再回落 JSON。 */
@@ -374,22 +383,25 @@ export function parseToolEnvelope(result: unknown): ToolEnvelope {
374
383
  };
375
384
  }
376
385
 
377
- const DELEGATE_STATUS_ZH: Record<string, string> = {
378
- budget_exhausted: '额度耗尽',
379
- failed: '失败',
380
- cancelled: '已取消',
386
+ const DELEGATE_STATUS_LABELS: Record<string, string> = {
387
+ budget_exhausted: 'budget exhausted',
388
+ failed: 'failure',
389
+ cancelled: 'cancellation',
381
390
  };
382
391
 
383
392
  /** 把 sidecar 的英文失败句收成行头/错误栏可读的中文。 */
384
393
  export function humanizeDelegateError(error: string | null): string | null {
385
394
  if (!error) return null;
386
- if (error.includes('orchestration_budget_exceeded')) return '编排额度已用尽';
395
+ if (error.includes('orchestration_budget_exceeded')) return t('Orchestration budget used up');
387
396
  const ended = error.match(/sub-agent ended with status:\s*(\S+)/i);
388
397
  if (ended) {
389
398
  const status = ended[1].replace(/[.,;]+$/, '');
390
- return `子代理因${DELEGATE_STATUS_ZH[status] ?? status}结束`;
399
+ const label = DELEGATE_STATUS_LABELS[status];
400
+ return t('Subagent ended due to {reason}', { reason: label ? t(label) : status });
401
+ }
402
+ if (error.includes('budget_exhausted')) {
403
+ return t('Subagent ended due to {reason}', { reason: t('budget exhausted') });
391
404
  }
392
- if (error.includes('budget_exhausted')) return '子代理因额度耗尽结束';
393
405
  return error;
394
406
  }
395
407
 
@@ -475,7 +487,7 @@ export interface InspectTaskInput {
475
487
  }
476
488
 
477
489
  export function inspectTaskTitle(input: InspectTaskInput): string {
478
- return input.title ?? input.task ?? '后台任务';
490
+ return input.title ?? input.task ?? t('Background task');
479
491
  }
480
492
 
481
493
  /** 工具行能打开右侧「后台推理」时抽出 taskId 和标题。 */
@@ -493,7 +505,7 @@ export function inspectableTaskFromAction(
493
505
  parseTaskRun(args).task
494
506
  ?? parsed.items[0]?.task
495
507
  ?? parsed.items[0]?.hint
496
- ?? '后台任务';
508
+ ?? t('Background task');
497
509
  return { id, title };
498
510
  }
499
511
 
@@ -539,13 +551,13 @@ export function isTaskFamilyTool(tool: string): tool is TaskFamilyTool {
539
551
  export function taskToolLabel(tool: string, args: unknown): string | null {
540
552
  switch (tool) {
541
553
  case 'task_run':
542
- return parseTaskRun(args).worktree ? '后台任务 · 隔离' : '后台任务';
554
+ return parseTaskRun(args).worktree ? t('Background task · Isolated') : t('Background task');
543
555
  case 'task_send':
544
- return '转达任务';
556
+ return t('Relay to task');
545
557
  case 'task_status':
546
- return '查询任务';
558
+ return t('Check tasks');
547
559
  case 'task_result':
548
- return '收取结果';
560
+ return t('Collect result');
549
561
  default:
550
562
  return null;
551
563
  }
@@ -569,8 +581,8 @@ export function summarizeTaskAction(
569
581
  const parsed = parseTaskRun(args);
570
582
  if (!parsed.task) return null;
571
583
  const head = clip(parsed.task);
572
- if (taskRunStatus(result) === 'blocked') return `${head} · 等待依赖`;
573
- if (parsed.worktree) return `${head} · 隔离工作区`;
584
+ if (taskRunStatus(result) === 'blocked') return t('{head} · Waiting on dependencies', { head });
585
+ if (parsed.worktree) return t('{head} · Isolated workspace', { head });
574
586
  return head;
575
587
  }
576
588
  if (tool === 'task_send') {
@@ -581,7 +593,7 @@ export function summarizeTaskAction(
581
593
  const id = stringField(asObject(args), 'taskId');
582
594
  if (id) return shortTaskId(id);
583
595
  const total = asObject(result)?.total;
584
- return typeof total === 'number' ? `${total} 个任务` : '本会话';
596
+ return typeof total === 'number' ? t('{count} tasks', { count: total }) : t('This chat');
585
597
  }
586
598
  if (tool === 'task_result') {
587
599
  const id = stringField(asObject(args), 'taskId');
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { ChildInfo } from './orchestration-children-model';
9
9
  import { parseToolEnvelope } from './executed-actions-model';
10
+ import { t } from '@/i18n';
10
11
 
11
12
  export const ORCHESTRATION_TOOL_NAMES = [
12
13
  'agent_spawn',
@@ -102,11 +103,21 @@ function buildOrchestrationFlow(nodes: OrchestrationChildNode[]): OrchestrationF
102
103
 
103
104
  let summaryCopy = '';
104
105
  if (isAllCompleted) {
105
- summaryCopy = `${completedCount}/${totalCount} 全部完成`;
106
+ summaryCopy = t('{completed}/{total} all done', {
107
+ completed: completedCount,
108
+ total: totalCount,
109
+ });
106
110
  } else if (hasActive) {
107
- summaryCopy = `协同执行中 (${completedCount}/${totalCount} 完成)...`;
111
+ summaryCopy = t('Running together ({completed}/{total} done)...', {
112
+ completed: completedCount,
113
+ total: totalCount,
114
+ });
108
115
  } else {
109
- summaryCopy = `${completedCount} 完成 · ${totalCount - completedCount} 结束 (${totalCount} 个子任务)`;
116
+ summaryCopy = t('{completed} done · {ended} ended ({total} subtasks)', {
117
+ completed: completedCount,
118
+ ended: totalCount - completedCount,
119
+ total: totalCount,
120
+ });
110
121
  }
111
122
 
112
123
  return {
@@ -226,7 +237,7 @@ export function extractOrchestrationFlow(
226
237
 
227
238
  if (envelope.success === false || envelope.error) {
228
239
  node.status = 'failed';
229
- node.error = envelope.error || '派发失败';
240
+ node.error = envelope.error || t('Dispatch failed');
230
241
  }
231
242
  } else if (action.tool === 'agent_wait') {
232
243
  const childId = typeof rawArgs.childId === 'string' ? rawArgs.childId.trim() : '';
@@ -260,7 +271,7 @@ export function extractOrchestrationFlow(
260
271
  node.status = 'completed';
261
272
  } else if (waitStatus === 'failed' || waitStatus === 'error' || envelope.success === false) {
262
273
  node.status = 'failed';
263
- node.error = envelope.error || '执行失败';
274
+ node.error = envelope.error || t('Run failed');
264
275
  } else if (waitStatus === 'cancelled') {
265
276
  node.status = 'cancelled';
266
277
  } else if (waitStatus === 'interrupted') {
@@ -4,6 +4,7 @@
4
4
  * output stage (reasoning + reply), excluding tool-wait gaps.
5
5
  */
6
6
 
7
+ import { t } from '@/i18n';
7
8
  import { formatElapsedCompact } from './elapsed';
8
9
  import {
9
10
  countProcessReasoning,
@@ -75,16 +76,16 @@ export function processStatusLabel(input: {
75
76
  elapsedMs?: number;
76
77
  }): string {
77
78
  const { process, isStreaming, elapsedMs } = input;
78
- if (isStreaming) return '工作中';
79
+ if (isStreaming) return t('Working');
79
80
 
80
81
  const parts: string[] = [];
81
82
  const thinks = countProcessReasoning(process);
82
83
  const tools = countProcessTools(process);
83
84
  const elapsed = formatElapsedPart(false, elapsedMs);
84
- if (elapsed) parts.push(`工作了 ${elapsed}`);
85
- if (thinks > 0) parts.push(`思考 ${thinks} 次`);
86
- if (tools > 0) parts.push(`工具调用 ${tools} 次`);
87
- if (parts.length === 0) return '执行过程';
85
+ if (elapsed) parts.push(t('Worked {elapsed}', { elapsed }));
86
+ if (thinks > 0) parts.push(t('Thought {count} times', { count: thinks }));
87
+ if (tools > 0) parts.push(t('{count} tool calls', { count: tools }));
88
+ if (parts.length === 0) return t('Run process');
88
89
  return parts.join(' · ');
89
90
  }
90
91
 
@@ -96,9 +97,9 @@ export function thinkingFoldLabel(input: {
96
97
  }): string {
97
98
  const elapsed = formatElapsedPart(input.isLive, input.elapsedMs);
98
99
  if (input.isLive) {
99
- return elapsed ? `思考中 · ${elapsed}` : '思考中';
100
+ return elapsed ? t('Thinking · {elapsed}', { elapsed }) : t('Thinking...');
100
101
  }
101
- return elapsed ? `思考 · ${elapsed}` : '思考';
102
+ return elapsed ? t('Thought for {elapsed}', { elapsed }) : t('Thinking');
102
103
  }
103
104
 
104
105
  /** Reasoning + reply tokens for the message-footer tok/s. */
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { ExecutedAction } from './ExecutedActionsCard';
10
10
  import type { TurnBlock } from './turn-timeline';
11
+ import { t } from '@/i18n';
11
12
 
12
13
  export const TODO_STATUSES = ['pending', 'in_progress', 'completed'] as const;
13
14
  export type TodoStatus = (typeof TODO_STATUSES)[number];
@@ -108,16 +109,16 @@ export function summarizeTodoWriteAction(
108
109
  ): string | null {
109
110
  if (tool !== 'todo_write') return null;
110
111
  const todos = extractTodosFromAction({ tool, arguments: args, result });
111
- if (!todos) return '任务清单';
112
+ if (!todos) return t('Task list');
112
113
  const { completed, total, inProgress } = summarizeTodos(todos);
113
114
  const current = todos.find((todo) => todo.status === 'in_progress');
114
115
  if (inProgress > 0 && current) {
115
116
  const label =
116
117
  current.content.length > 28 ? `${current.content.slice(0, 25)}…` : current.content;
117
- return `任务清单 ${completed}/${total} · ${label}`;
118
+ return t('Task list {completed}/{total} · {label}', { completed, total, label });
118
119
  }
119
- if (completed === total) return `任务清单 ${completed}/${total} 已完成`;
120
- return `任务清单 ${completed}/${total}`;
120
+ if (completed === total) return t('Task list {completed}/{total} done', { completed, total });
121
+ return t('Task list {completed}/{total}', { completed, total });
121
122
  }
122
123
 
123
124
  export function latestTodosFromActions(