@steerable/agent-shell-web 0.6.53 → 0.6.56

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 (100) hide show
  1. package/README.md +3 -4
  2. package/package.json +4 -4
  3. package/src/AppShell.tsx +3 -3
  4. package/src/brand.ts +1 -1
  5. package/src/components/AgentSidebar.tsx +64 -56
  6. package/src/components/ChatHeader.tsx +41 -25
  7. package/src/components/ConfirmDialog.tsx +4 -3
  8. package/src/components/CreateProjectModal.tsx +31 -21
  9. package/src/components/LocalLlmSettingsModal.tsx +4 -3
  10. package/src/components/SidebarRelease.tsx +6 -5
  11. package/src/components/TerminalPanel.tsx +6 -5
  12. package/src/components/TerminalView.tsx +15 -10
  13. package/src/components/chat/ApprovalModal.tsx +40 -26
  14. package/src/components/chat/ApprovalPromptProvider.tsx +5 -5
  15. package/src/components/chat/AskUserPromptProvider.tsx +4 -4
  16. package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
  17. package/src/components/chat/AssistantMessage.tsx +18 -13
  18. package/src/components/chat/ChatInput.tsx +56 -40
  19. package/src/components/chat/ChatProjectBadge.tsx +27 -22
  20. package/src/components/chat/EmptyChatHero.tsx +2 -1
  21. package/src/components/chat/ExecPolicyPicker.tsx +12 -11
  22. package/src/components/chat/ExecutedActionsCard.tsx +45 -33
  23. package/src/components/chat/FilePathCode.tsx +8 -3
  24. package/src/components/chat/InterruptedTurnCard.tsx +4 -3
  25. package/src/components/chat/LocalChatPanel.tsx +2 -1
  26. package/src/components/chat/Markdown.tsx +2 -1
  27. package/src/components/chat/MessageList.tsx +3 -2
  28. package/src/components/chat/ModelPicker.tsx +25 -16
  29. package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
  30. package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
  31. package/src/components/chat/PlanTodoList.tsx +2 -1
  32. package/src/components/chat/ProjectTrustBanner.tsx +19 -5
  33. package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
  34. package/src/components/chat/SessionTodoList.tsx +8 -7
  35. package/src/components/chat/SessionTreeModal.tsx +10 -9
  36. package/src/components/chat/StreamingStatus.tsx +4 -3
  37. package/src/components/chat/SuggestedReplies.tsx +3 -2
  38. package/src/components/chat/TaskOutcomeCards.tsx +8 -5
  39. package/src/components/chat/TaskPanelModal.tsx +33 -23
  40. package/src/components/chat/TaskProcessPanel.tsx +16 -15
  41. package/src/components/chat/TurnFilesCard.tsx +14 -13
  42. package/src/components/chat/UserMessage.tsx +12 -11
  43. package/src/components/chat/executed-actions-model.ts +47 -35
  44. package/src/components/chat/orchestration-flow-model.ts +16 -5
  45. package/src/components/chat/process-status.ts +8 -7
  46. package/src/components/chat/todo-list-model.ts +5 -4
  47. package/src/components/chat/turn-files.ts +8 -8
  48. package/src/components/chat/useChatTasks.ts +3 -3
  49. package/src/components/settings/AgentCapabilityFields.tsx +26 -23
  50. package/src/components/settings/AgentsSettingsPanel.tsx +33 -28
  51. package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
  52. package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
  53. package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
  54. package/src/components/settings/InsightsSettingsPanel.tsx +58 -43
  55. package/src/components/settings/LlmSettingsPanel.tsx +107 -86
  56. package/src/components/settings/McpSettingsPanel.tsx +72 -46
  57. package/src/components/settings/ModelIdCombobox.tsx +2 -1
  58. package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
  59. package/src/components/settings/PortableSettingsPanel.tsx +83 -55
  60. package/src/components/settings/PythonRunnerSettingsPanel.tsx +26 -23
  61. package/src/components/settings/SecuritySettingsPanel.tsx +58 -34
  62. package/src/components/settings/SettingsNavMenu.tsx +3 -2
  63. package/src/components/settings/SettingsSaveButton.tsx +2 -1
  64. package/src/components/settings/SkillsSettingsPanel.tsx +43 -38
  65. package/src/components/settings/TelemetrySettingsPanel.tsx +20 -17
  66. package/src/components/settings/UsagePanel.tsx +22 -23
  67. package/src/components/settings/WebSearchSettingsPanel.tsx +28 -19
  68. package/src/components/settings/llm-vendors.ts +12 -7
  69. package/src/components/settings/model-capabilities.ts +16 -15
  70. package/src/components/settings/preset-choice-model.ts +6 -3
  71. package/src/fixtures/browser-dev-data.json +24 -24
  72. package/src/hooks/useChatsAndAgents.ts +9 -9
  73. package/src/hooks/useProjects.ts +3 -3
  74. package/src/i18n/agent-label.ts +15 -0
  75. package/src/i18n/index.tsx +68 -0
  76. package/src/i18n/runtime.ts +117 -0
  77. package/src/i18n/translate.ts +65 -0
  78. package/src/layouts/AgentLayout.tsx +13 -12
  79. package/src/lib/app-release.ts +16 -11
  80. package/src/lib/attachments.ts +18 -9
  81. package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +42 -42
  82. package/src/lib/chat-title.ts +3 -2
  83. package/src/lib/chat-transport.ts +16 -12
  84. package/src/lib/date-groups.ts +23 -15
  85. package/src/lib/host-bridge.ts +371 -4
  86. package/src/lib/host-paste.ts +1 -1
  87. package/src/lib/http-bridge.ts +12 -9
  88. package/src/lib/insights.ts +2 -2
  89. package/src/lib/local-api.ts +5 -5
  90. package/src/lib/portable.ts +11 -10
  91. package/src/lib/show-thinking-content.ts +15 -3
  92. package/src/lib/slash-sources.ts +3 -3
  93. package/src/lib/tauri-bridge.ts +1 -1
  94. package/src/main.tsx +27 -4
  95. package/src/pages/AgentPage.tsx +39 -34
  96. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  97. package/src/pages/SettingsPage.tsx +51 -45
  98. package/src/vite-env.d.ts +3 -0
  99. package/vite.base.js +6 -2
  100. package/src/lib/electron-bridge.ts +0 -390
@@ -10,13 +10,14 @@ import {
10
10
  LuPlus,
11
11
  } from 'react-icons/lu';
12
12
  import { CreateProjectModal } from '@/components/CreateProjectModal';
13
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
13
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
14
14
  import {
15
15
  createProject,
16
16
  updateChatProject,
17
17
  updateProject,
18
18
  type LocalProject,
19
19
  } from '@/lib/local-api';
20
+ import { t } from '@/i18n';
20
21
 
21
22
  /**
22
23
  * 项目徽章 / 选择器(Codex 式 cwd 指示),渲染在输入框上方的 meta 行里,
@@ -113,8 +114,8 @@ function MenuSectionLabel({
113
114
  type="button"
114
115
  onClick={onAdd}
115
116
  className="flex h-5 w-5 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
116
- title="新建项目"
117
- aria-label="新建项目"
117
+ title={t('New project')}
118
+ aria-label={t('New project')}
118
119
  >
119
120
  <LuPlus className="h-3 w-3" />
120
121
  </button>
@@ -132,7 +133,7 @@ function CreateProjectMenuItem({ onClick }: { onClick: () => void }) {
132
133
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
133
134
  >
134
135
  <LuFolderPlus className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
135
- 新建项目
136
+ {t('New project')}
136
137
  </button>
137
138
  );
138
139
  }
@@ -218,9 +219,9 @@ export function ChatProjectBadge({
218
219
 
219
220
  const changeFolder = () =>
220
221
  run(async () => {
221
- if (!isElectron() || !project) return;
222
- const result = await getElectronBridge()!.local?.selectDirectory({
223
- title: `重新选择「${project.name}」绑定的文件夹`,
222
+ if (!hasHostBridge() || !project) return;
223
+ const result = await getHostBridge()!.local?.selectDirectory({
224
+ title: t('Choose a new folder for "{name}"', { name: project.name }),
224
225
  });
225
226
  if (!result || result.canceled || result.filePaths.length === 0) return;
226
227
  await updateProject(project.id, { folderPath: result.filePaths[0] });
@@ -230,7 +231,7 @@ export function ChatProjectBadge({
230
231
  return (
231
232
  <div className="relative">
232
233
  <BadgeButton
233
- label={project ? project.name : '选择项目'}
234
+ label={project ? project.name : t('Choose a project')}
234
235
  active={Boolean(project)}
235
236
  open={open}
236
237
  busy={busy}
@@ -240,8 +241,10 @@ export function ChatProjectBadge({
240
241
  }}
241
242
  title={
242
243
  project
243
- ? `${project.folderPath}\n点击管理项目归属 / 修改绑定目录`
244
- : '把当前对话关联到一个项目(文件写入限制在项目家目录、源文件夹,以及这些目录的子目录内)'
244
+ ? `${project.folderPath}\n${t('Click to manage the project or change its folder')}`
245
+ : t(
246
+ 'Link this chat to a project (file writes are limited to the project home folder, its source folders, and their subfolders)',
247
+ )
245
248
  }
246
249
  />
247
250
 
@@ -249,7 +252,7 @@ export function ChatProjectBadge({
249
252
  <MenuShell onClose={() => setOpen(false)}>
250
253
  {project ? (
251
254
  <>
252
- <MenuSectionLabel>当前项目</MenuSectionLabel>
255
+ <MenuSectionLabel>{t('Current project')}</MenuSectionLabel>
253
256
  <div
254
257
  className="flex items-center gap-2 rounded px-2 py-1.5 text-xs text-agent-foreground"
255
258
  title={project.folderPath}
@@ -268,7 +271,7 @@ export function ChatProjectBadge({
268
271
  </div>
269
272
  </>
270
273
  ) : (
271
- <MenuSectionLabel onAdd={openCreate}>关联到项目</MenuSectionLabel>
274
+ <MenuSectionLabel onAdd={openCreate}>{t('Link to a project')}</MenuSectionLabel>
272
275
  )}
273
276
 
274
277
  {otherProjects.length > 0 && (
@@ -276,7 +279,7 @@ export function ChatProjectBadge({
276
279
  {project && (
277
280
  <div className="mx-1 my-1 border-t border-agent-border/60" />
278
281
  )}
279
- {project && <MenuSectionLabel>移动到</MenuSectionLabel>}
282
+ {project && <MenuSectionLabel>{t('Move to')}</MenuSectionLabel>}
280
283
  <div className="max-h-40 overflow-y-auto">
281
284
  {otherProjects.map((p) => (
282
285
  <ProjectMenuRow
@@ -291,7 +294,7 @@ export function ChatProjectBadge({
291
294
  {!project && projects.length === 0 && (
292
295
  <>
293
296
  <div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
294
- 还没有项目
297
+ {t('No projects yet')}
295
298
  </div>
296
299
  <CreateProjectMenuItem onClick={openCreate} />
297
300
  </>
@@ -307,7 +310,7 @@ export function ChatProjectBadge({
307
310
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
308
311
  >
309
312
  <LuFolderPen className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
310
- 修改项目目录…
313
+ {t('Change project folder...')}
311
314
  </button>
312
315
  <button
313
316
  type="button"
@@ -316,7 +319,7 @@ export function ChatProjectBadge({
316
319
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
317
320
  >
318
321
  <LuFolderX className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
319
- 移出项目(变为无项目对话)
322
+ {t('Remove from project (becomes a chat without a project)')}
320
323
  </button>
321
324
  </>
322
325
  )}
@@ -375,21 +378,23 @@ export function ProjectPickerButton({
375
378
  return (
376
379
  <div className="relative">
377
380
  <BadgeButton
378
- label={selected ? selected.name : '选择项目'}
381
+ label={selected ? selected.name : t('Choose a project')}
379
382
  active={Boolean(selected)}
380
383
  open={open}
381
384
  busy={false}
382
385
  onClick={() => setOpen((v) => !v)}
383
386
  title={
384
387
  selected
385
- ? `${selected.folderPath}\n新对话将绑定到此项目`
386
- : '为新对话选择项目。不选则在文稿/<应用名>/conversations/ 下建立本对话工作区'
388
+ ? `${selected.folderPath}\n${t('New chats will be linked to this project')}`
389
+ : t(
390
+ 'Choose a project for the new chat. Without one, a workspace for this chat is created under Documents/<app name>/conversations/',
391
+ )
387
392
  }
388
393
  />
389
394
 
390
395
  {open && (
391
396
  <MenuShell onClose={() => setOpen(false)}>
392
- <MenuSectionLabel onAdd={openCreate}>新对话所属项目</MenuSectionLabel>
397
+ <MenuSectionLabel onAdd={openCreate}>{t('Project for new chat')}</MenuSectionLabel>
393
398
  <button
394
399
  type="button"
395
400
  role="menuitem"
@@ -400,7 +405,7 @@ export function ProjectPickerButton({
400
405
  className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs text-agent-foreground transition-colors hover:bg-agent-foreground/5"
401
406
  >
402
407
  <LuFolderX className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
403
- <span className="min-w-0 flex-1 truncate">不关联项目</span>
408
+ <span className="min-w-0 flex-1 truncate">{t('No project')}</span>
404
409
  {!selected && (
405
410
  <LuCheck className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
406
411
  )}
@@ -421,7 +426,7 @@ export function ProjectPickerButton({
421
426
  {projects.length === 0 && (
422
427
  <>
423
428
  <div className="px-2 py-1.5 text-[11px] text-agent-muted-foreground/70">
424
- 还没有项目
429
+ {t('No projects yet')}
425
430
  </div>
426
431
  <CreateProjectMenuItem onClick={openCreate} />
427
432
  </>
@@ -1,4 +1,5 @@
1
1
  import { BRAND_HOME_HINT, BRAND_NAME, getBrandLogoUrl } from '@/brand';
2
+ import { t } from '@/i18n';
2
3
 
3
4
  /**
4
5
  * 新对话落地页与空会话首屏的品牌区:侧栏同一枚 logo,加一句
@@ -16,7 +17,7 @@ export function EmptyChatHero() {
16
17
  draggable={false}
17
18
  />
18
19
  </h1>
19
- {hint ? <p className="mt-1.5 text-xs text-agent-muted-foreground">{hint}</p> : null}
20
+ {hint ? <p className="mt-1.5 text-xs text-agent-muted-foreground">{t(hint)}</p> : null}
20
21
  </div>
21
22
  );
22
23
  }
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { LuChevronDown, LuFolderLock, LuLockOpen } from 'react-icons/lu';
3
3
  import type { ExecPolicy } from '@/lib/exec-policy';
4
+ import { t } from '@/i18n';
4
5
 
5
6
  /**
6
7
  * ExecPolicyPicker — 输入框上的命令沙箱切换(类 Codex 底部权限档)。
@@ -17,14 +18,14 @@ const OPTIONS: Array<{
17
18
  }> = [
18
19
  {
19
20
  id: 'workspace',
20
- label: '工作区',
21
- description:
22
- '只能写入当前工作区,范围外的路径会被拒绝。',
21
+ label: 'Workspace',
22
+ description: 'Can only write to the current workspace. Paths outside it are blocked.',
23
23
  },
24
24
  {
25
25
  id: 'full',
26
- label: '完整权限',
27
- description: '关闭命令沙箱,可写本机任意路径。工具审批仍然生效。',
26
+ label: 'Full access',
27
+ description:
28
+ 'Turns off the command sandbox so any path on this computer can be written. Tool approvals still apply.',
28
29
  },
29
30
  ];
30
31
 
@@ -60,9 +61,9 @@ export function ExecPolicyPicker({
60
61
  disabled={disabled}
61
62
  aria-haspopup="listbox"
62
63
  aria-expanded={open}
63
- aria-label="命令沙箱"
64
+ aria-label={t('Command sandbox')}
64
65
  data-testid="exec-policy-picker"
65
- title={current.description}
66
+ title={t(current.description)}
66
67
  onClick={() => setOpen((next) => !next)}
67
68
  className={[
68
69
  'inline-flex h-6 max-w-[140px] items-center gap-1 rounded-full border px-1.5 text-[12px] leading-[1.45] transition-colors disabled:cursor-not-allowed disabled:opacity-70',
@@ -76,13 +77,13 @@ export function ExecPolicyPicker({
76
77
  ) : (
77
78
  <LuFolderLock className="h-3 w-3 shrink-0" />
78
79
  )}
79
- <span className="truncate">{current.label}</span>
80
+ <span className="truncate">{t(current.label)}</span>
80
81
  <LuChevronDown className="h-3 w-3 shrink-0 text-agent-muted-foreground" />
81
82
  </button>
82
83
  {open && (
83
84
  <div
84
85
  role="listbox"
85
- aria-label="命令沙箱"
86
+ aria-label={t('Command sandbox')}
86
87
  className="absolute bottom-full left-0 z-30 mb-1 w-64 overflow-hidden rounded-agent-md border border-agent-border bg-agent-canvas py-1 shadow-sm"
87
88
  >
88
89
  {OPTIONS.map((option) => {
@@ -106,10 +107,10 @@ export function ExecPolicyPicker({
106
107
  ].join(' ')}
107
108
  >
108
109
  <span className="text-[12px] font-medium leading-[1.45] text-agent-foreground">
109
- {option.label}
110
+ {t(option.label)}
110
111
  </span>
111
112
  <span className="text-[11px] leading-snug text-agent-muted-foreground">
112
- {option.description}
113
+ {t(option.description)}
113
114
  </span>
114
115
  </button>
115
116
  );
@@ -3,6 +3,7 @@ import { LuCircleCheck, LuCircleX, LuListTodo } from 'react-icons/lu';
3
3
  import { ToolExecutionCard } from '@steerable/agent-ui/cards';
4
4
  import type { ToolExecutionPayload } from '@steerable/agent-protocol';
5
5
  import type { LocalChatAgent } from '@/lib/local-api';
6
+ import { t } from '@/i18n';
6
7
  import { UnifiedDiff } from './UnifiedDiff';
7
8
  import {
8
9
  summarizeRunCodeAction,
@@ -117,11 +118,11 @@ function sandboxBadge(sandbox: ExecutedAction['sandbox']): string | null {
117
118
  if (!sandbox) return null;
118
119
  switch (sandbox.enforcement) {
119
120
  case 'full':
120
- return '[沙箱]';
121
+ return t('[Sandboxed]');
121
122
  case 'partial':
122
- return '[沙箱·部分]';
123
+ return t('[Sandboxed · partial]');
123
124
  default:
124
- return '[未沙箱]';
125
+ return t('[Not sandboxed]');
125
126
  }
126
127
  }
127
128
 
@@ -252,7 +253,7 @@ function renderDelegateArgs(
252
253
  {profile ? (
253
254
  <section>
254
255
  <div className="mb-0.5 text-[10px] uppercase tracking-wider text-agent-muted-foreground">
255
- 子代理
256
+ {t('Subagent')}
256
257
  </div>
257
258
  <div className="text-[11px]">{displayName}</div>
258
259
  </section>
@@ -260,7 +261,7 @@ function renderDelegateArgs(
260
261
  {task ? (
261
262
  <section>
262
263
  <div className="mb-0.5 text-[10px] uppercase tracking-wider text-agent-muted-foreground">
263
- 任务
264
+ {t('Task')}
264
265
  </div>
265
266
  <pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
266
267
  {task}
@@ -277,7 +278,7 @@ function renderDelegateOutput(output: unknown): ReactNode {
277
278
  return (
278
279
  <div className="space-y-1.5">
279
280
  {kvBlock(
280
- '回报',
281
+ t('Report'),
281
282
  <pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
282
283
  {env.message}
283
284
  </pre>,
@@ -286,7 +287,7 @@ function renderDelegateOutput(output: unknown): ReactNode {
286
287
  );
287
288
  }
288
289
  if (env.error || env.success === false) {
289
- return <div className="text-[11px] text-agent-muted-foreground">没有文字回报</div>;
290
+ return <div className="text-[11px] text-agent-muted-foreground">{t('No text report')}</div>;
290
291
  }
291
292
  return renderActionOutput(output);
292
293
  }
@@ -303,11 +304,11 @@ function TaskLead() {
303
304
  );
304
305
  }
305
306
 
306
- const TASK_STATUS_ZH: Record<string, string> = {
307
- running: '已启动',
308
- blocked: '等待依赖',
309
- completed: '已完成',
310
- failed: '失败',
307
+ const TASK_STATUS_LABELS: Record<string, string> = {
308
+ running: 'Started',
309
+ blocked: 'Waiting on dependencies',
310
+ completed: 'Completed',
311
+ failed: 'Failed',
311
312
  };
312
313
 
313
314
  function kvBlock(label: string, value: ReactNode): ReactNode {
@@ -331,17 +332,22 @@ function renderTaskArgs(tool: string, args: unknown): ReactNode {
331
332
  <div className="space-y-1.5">
332
333
  {parsed.task
333
334
  ? kvBlock(
334
- '任务',
335
+ t('Task'),
335
336
  <pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
336
337
  {parsed.task}
337
338
  </pre>,
338
339
  )
339
340
  : null}
340
341
  {parsed.dependsOn.length > 0
341
- ? kvBlock('依赖', parsed.dependsOn.map(shortTaskId).join('、'))
342
+ ? kvBlock(t('Dependencies'), parsed.dependsOn.map(shortTaskId).join(t(', ')))
342
343
  : null}
343
344
  {parsed.worktree
344
- ? kvBlock('工作区', parsed.worktreeName ? `隔离 · ${parsed.worktreeName}` : '隔离')
345
+ ? kvBlock(
346
+ t('Workspace'),
347
+ parsed.worktreeName
348
+ ? t('Isolated · {name}', { name: parsed.worktreeName })
349
+ : t('Isolated'),
350
+ )
345
351
  : null}
346
352
  </div>
347
353
  );
@@ -352,10 +358,10 @@ function renderTaskArgs(tool: string, args: unknown): ReactNode {
352
358
  if (!taskId && !message) return renderActionOutput(args);
353
359
  return (
354
360
  <div className="space-y-1.5">
355
- {taskId ? kvBlock('编号', <span className="font-mono">{shortTaskId(taskId)}</span>) : null}
361
+ {taskId ? kvBlock(t('ID'), <span className="font-mono">{shortTaskId(taskId)}</span>) : null}
356
362
  {message
357
363
  ? kvBlock(
358
- '消息',
364
+ t('Message'),
359
365
  <pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
360
366
  {message}
361
367
  </pre>,
@@ -375,32 +381,35 @@ function renderTaskOutput(output: unknown): ReactNode {
375
381
  {parsed.items.map((item, index) => (
376
382
  <div key={item.taskId ?? `task-${index}`} className="space-y-1.5">
377
383
  {item.taskId
378
- ? kvBlock('编号', <span className="font-mono">{shortTaskId(item.taskId)}</span>)
384
+ ? kvBlock(t('ID'), <span className="font-mono">{shortTaskId(item.taskId)}</span>)
379
385
  : null}
380
- {item.status ? kvBlock('状态', TASK_STATUS_ZH[item.status] ?? item.status) : null}
386
+ {item.status ? kvBlock(
387
+ t('Status'),
388
+ TASK_STATUS_LABELS[item.status] ? t(TASK_STATUS_LABELS[item.status]) : item.status,
389
+ ) : null}
381
390
  {item.task
382
391
  ? kvBlock(
383
- '任务',
392
+ t('Task'),
384
393
  <pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
385
394
  {item.task}
386
395
  </pre>,
387
396
  )
388
397
  : null}
389
- {item.worktreeLabel ? kvBlock('工作区', item.worktreeLabel) : null}
390
- {item.hint ? kvBlock('说明', item.hint) : null}
398
+ {item.worktreeLabel ? kvBlock(t('Workspace'), item.worktreeLabel) : null}
399
+ {item.hint ? kvBlock(t('Note'), item.hint) : null}
391
400
  {item.answer
392
401
  ? kvBlock(
393
- '结果',
402
+ t('Result'),
394
403
  <pre className="whitespace-pre-wrap break-words rounded bg-agent-muted/40 px-2 py-1.5">
395
404
  {item.answer}
396
405
  </pre>,
397
406
  )
398
407
  : null}
399
- {item.error ? kvBlock('原因', item.error) : null}
408
+ {item.error ? kvBlock(t('Reason'), item.error) : null}
400
409
  </div>
401
410
  ))}
402
411
  {parsed.hint && parsed.items.every((item) => item.hint !== parsed.hint)
403
- ? kvBlock('说明', parsed.hint)
412
+ ? kvBlock(t('Note'), parsed.hint)
404
413
  : null}
405
414
  </div>
406
415
  );
@@ -419,7 +428,7 @@ function toolCardExtras(
419
428
  const { profile } = parseDelegateSubagent(action.arguments);
420
429
  const displayName = delegateSubagentDisplayName(profile, agents);
421
430
  return {
422
- label: `委派 · ${displayName}`,
431
+ label: t('Delegate · {name}', { name: displayName }),
423
432
  lead: <DelegateLead name={displayName} color={delegateColor(profile, agents)} />,
424
433
  renderArgs: (args) => renderDelegateArgs(args, agents),
425
434
  renderOutput: renderDelegateOutput,
@@ -453,16 +462,16 @@ function inspectActivate(
453
462
  chatId,
454
463
  recordId: child.recordId,
455
464
  live: child.status === 'running',
456
- title: task ?? '子代理任务',
465
+ title: task ?? t('Subagent task'),
457
466
  }),
458
- activateTitle: '查看子代理执行过程',
467
+ activateTitle: t('View subagent run'),
459
468
  };
460
469
  }
461
470
  const target = inspectableTaskFromAction(action.tool, action.arguments, action.result);
462
471
  if (!target) return {};
463
472
  return {
464
473
  onActivate: () => onInspectTask({ id: target.id, chatId, title: target.title }),
465
- activateTitle: '查看后台执行过程',
474
+ activateTitle: t('View background run'),
466
475
  };
467
476
  }
468
477
 
@@ -558,15 +567,18 @@ export function ExecutedActionsCard({
558
567
  <LuCircleX className="h-3.5 w-3.5 text-agent-destructive" />
559
568
  )}
560
569
  <span className="font-medium text-agent-foreground">
561
- 已自动执行 {expanded.length} 个操作
570
+ {t('Ran {count} actions automatically', { count: expanded.length })}
562
571
  </span>
563
572
  </div>
564
573
  <span className="text-[11px] text-agent-muted-foreground">
565
574
  {runningCount > 0
566
- ? `${runningCount} 执行中`
575
+ ? t('{count} running', { count: runningCount })
567
576
  : failureCount > 0
568
- ? `${successCount} 成功 · ${failureCount} 失败`
569
- : `全部成功`}
577
+ ? t('{success} succeeded · {failed} failed', {
578
+ success: successCount,
579
+ failed: failureCount,
580
+ })
581
+ : t('All succeeded')}
570
582
  </span>
571
583
  </div>
572
584
  <div className="space-y-px">
@@ -3,6 +3,7 @@ import { openLocalPath, type ResolvedLocalPath } from '@/lib/local-api';
3
3
  import { hostToolChrome } from '@/lib/host-tools';
4
4
  import { peekResolvedPath, subscribeResolvedPath } from './path-mentions';
5
5
  import { splitTurnFilePath } from './turn-files';
6
+ import { t } from '@/i18n';
6
7
 
7
8
  /**
8
9
  * 行内代码里的文件路径:后端确认存在后变成可点击,点击用系统默认应用打开。
@@ -48,9 +49,9 @@ export function FilePathCode({ candidate, chatId, children, ...rest }: FilePathC
48
49
  setOpenError(null);
49
50
  try {
50
51
  const result = await openLocalPath(resolved.path);
51
- if (!result.success) setOpenError(result.error || '打开失败');
52
+ if (!result.success) setOpenError(result.error || t('Failed to open'));
52
53
  } catch (err) {
53
- setOpenError(err instanceof Error ? err.message : '打开失败');
54
+ setOpenError(err instanceof Error ? err.message : t('Failed to open'));
54
55
  }
55
56
  };
56
57
 
@@ -59,7 +60,11 @@ export function FilePathCode({ candidate, chatId, children, ...rest }: FilePathC
59
60
  type="button"
60
61
  data-file-path-chip=""
61
62
  onClick={handleClick}
62
- title={openError ? `${resolved.path}(${openError})` : `点击打开 ${resolved.path}`}
63
+ title={
64
+ openError
65
+ ? t('{path} ({error})', { path: resolved.path, error: openError })
66
+ : t('Click to open {path}', { path: resolved.path })
67
+ }
63
68
  className={`${INLINE_CODE_CLASS} cursor-pointer underline decoration-dotted underline-offset-2 transition-colors hover:bg-agent-accent/15 hover:text-agent-accent ${
64
69
  openError ? 'text-red-600 dark:text-red-400' : ''
65
70
  }`}
@@ -12,6 +12,7 @@
12
12
  * 写下的终态,不会进入本卡片:前者是用户的明确意图,后者已有错误展示。
13
13
  */
14
14
  import { LuTriangleAlert } from 'react-icons/lu';
15
+ import { t } from '@/i18n';
15
16
 
16
17
  export interface InterruptedTurnCardProps {
17
18
  /** 点击「继续上次回复」——触发 resume 流(不再追加用户消息)。 */
@@ -28,7 +29,7 @@ export function InterruptedTurnCard({ onContinue, onDismiss }: InterruptedTurnCa
28
29
  >
29
30
  <div className="flex items-center gap-2 text-amber-700 dark:text-amber-300">
30
31
  <LuTriangleAlert className="h-4 w-4 shrink-0" />
31
- <span>上次回复因应用退出或异常中断,未能完成。</span>
32
+ <span>{t('The last reply was interrupted because the app quit or crashed, so it did not finish.')}</span>
32
33
  </div>
33
34
  <div className="flex shrink-0 items-center gap-2">
34
35
  <button
@@ -36,14 +37,14 @@ export function InterruptedTurnCard({ onContinue, onDismiss }: InterruptedTurnCa
36
37
  onClick={onDismiss}
37
38
  className="rounded px-1.5 py-0.5 text-amber-700/80 transition-colors hover:text-amber-900 dark:text-amber-300/80 dark:hover:text-amber-100"
38
39
  >
39
- 忽略
40
+ {t('Dismiss')}
40
41
  </button>
41
42
  <button
42
43
  type="button"
43
44
  onClick={onContinue}
44
45
  className="inline-flex items-center gap-1 rounded-full bg-agent-foreground px-3 py-1 font-medium text-agent-canvas transition hover:opacity-90"
45
46
  >
46
- 继续上次回复
47
+ {t('Continue last reply')}
47
48
  </button>
48
49
  </div>
49
50
  </div>
@@ -20,6 +20,7 @@ import type { TurnBlock } from './turn-timeline';
20
20
  import type { TurnFile } from './turn-files';
21
21
  import type { LlmSpeedSnapshot } from './process-status';
22
22
  import { ComposerStatusStack } from './composer-status-stack';
23
+ import { t } from '@/i18n';
23
24
  import { SessionTodoList } from './SessionTodoList';
24
25
  import { resolveLatestSessionTodos } from './todo-list-model';
25
26
  import { SessionOrchestrationFlow } from './SessionOrchestrationFlow';
@@ -418,7 +419,7 @@ export function LocalChatPanel({
418
419
  disabled={disabled}
419
420
  placeholder={
420
421
  mode === 'plan'
421
- ? '描述你的目标,Agent 将先制定计划…'
422
+ ? t('Describe your goal. The Agent will make a plan first...')
422
423
  : inputPlaceholder
423
424
  }
424
425
  currentAgent={currentAgent}
@@ -11,6 +11,7 @@ import ReactMarkdown, { type Components } from 'react-markdown';
11
11
  import remarkGfm from 'remark-gfm';
12
12
  import rehypeRaw from 'rehype-raw';
13
13
  import { LuBlocks, LuMessageSquare, LuPlug } from 'react-icons/lu';
14
+ import { t } from '@/i18n';
14
15
  import type { LocalChat, LocalChatAgent } from '@/lib/local-api';
15
16
  import {
16
17
  resolveSlashTool,
@@ -314,7 +315,7 @@ function renderTextWithMentions(
314
315
  );
315
316
  }
316
317
 
317
- const chat = chats.find((c) => (c.title || '未命名对话') === name);
318
+ const chat = chats.find((c) => (c.title || t('Untitled chat')) === name);
318
319
  if (chat) {
319
320
  return (
320
321
  <span
@@ -10,6 +10,7 @@ import {
10
10
  import { LuArrowDown } from 'react-icons/lu';
11
11
  import type { ChatMessage } from '@steerable/agent-protocol';
12
12
  import type { LocalChat, LocalChatAgent, LocalTask } from '@/lib/local-api';
13
+ import { t } from '@/i18n';
13
14
  import { UserMessage } from './UserMessage';
14
15
  import { AssistantMessage } from './AssistantMessage';
15
16
  import { InterruptedTurnCard } from './InterruptedTurnCard';
@@ -467,8 +468,8 @@ export function MessageList({
467
468
  scrollToBottom('smooth');
468
469
  }}
469
470
  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"
470
- title="回到底部"
471
- aria-label="回到底部"
471
+ title={t('Back to bottom')}
472
+ aria-label={t('Back to bottom')}
472
473
  >
473
474
  <LuArrowDown className="h-4 w-4" />
474
475
  </button>