@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
@@ -1,4 +1,5 @@
1
1
  import { motion } from 'framer-motion';
2
+ import { t } from '@/i18n';
2
3
 
3
4
  /**
4
5
  * StreamingStatus — informative placeholder for an assistant bubble that has
@@ -71,12 +72,12 @@ export function StreamingStatus({
71
72
 
72
73
  function selectLabel(round: number, actionCount: number): string {
73
74
  if (round >= 2 && actionCount === 0) {
74
- return `Round ${round} · 继续推理...`;
75
+ return t('Round {round} · Continuing reasoning...', { round });
75
76
  }
76
77
  if (actionCount > 0) {
77
- return `已调用 ${actionCount} 个工具,正在分析...`;
78
+ return t('Called {count} tools, analyzing...', { count: actionCount });
78
79
  }
79
- return '正在思考...';
80
+ return t('Now thinking...');
80
81
  }
81
82
 
82
83
  function Dots() {
@@ -1,4 +1,5 @@
1
1
  import { LuSparkles } from 'react-icons/lu';
2
+ import { t } from '@/i18n';
2
3
 
3
4
  /**
4
5
  * WorkBuddy-style follow-up chips: next-turn user inputs rendered under the
@@ -22,8 +23,8 @@ export function SuggestedReplies({
22
23
  >
23
24
  <div
24
25
  className="mr-0.5 inline-flex items-center select-none"
25
- title="建议"
26
- aria-label="建议"
26
+ title={t('Suggestions')}
27
+ aria-label={t('Suggestions')}
27
28
  >
28
29
  <LuSparkles className="h-3 w-3 text-agent-muted-foreground/80" />
29
30
  </div>
@@ -10,6 +10,7 @@
10
10
  * 不写库。worktree 的合并/丢弃仍归任务弹层——卡片只通知,不重复那套操作。
11
11
  */
12
12
  import { LuCheck, LuGitBranch, LuX } from 'react-icons/lu';
13
+ import { t } from '@/i18n';
13
14
  import type { LocalTask } from '@/lib/local-api';
14
15
 
15
16
  /** 同时挤在消息列尾部的卡片上限;更多的只留一行汇总指回角标。 */
@@ -41,7 +42,9 @@ export function TaskOutcomeCards({ tasks, onInspect, onDismiss }: TaskOutcomeCar
41
42
  ))}
42
43
  {overflow > 0 && (
43
44
  <div className="px-1 text-[11px] text-agent-muted-foreground">
44
- 另有 {overflow} 个后台任务已结束 — 见标题栏的任务按钮。
45
+ {t('{count} more background tasks finished. See the tasks button in the title bar.', {
46
+ count: overflow,
47
+ })}
45
48
  </div>
46
49
  )}
47
50
  </div>
@@ -86,7 +89,7 @@ function TaskOutcomeCard({
86
89
  : 'text-emerald-700 dark:text-emerald-300'
87
90
  }
88
91
  >
89
- {failed ? '后台任务失败:' : '后台任务已完成:'}
92
+ {failed ? t('Background task failed: ') : t('Background task completed: ')}
90
93
  <span className="font-medium">{task.task}</span>
91
94
  </div>
92
95
  {detail && (
@@ -100,7 +103,7 @@ function TaskOutcomeCard({
100
103
  <span className="min-w-0 truncate font-mono text-[10px]">
101
104
  {task.worktreeBranch}
102
105
  </span>
103
- <span>待合并 — 在标题栏的任务按钮里处理。</span>
106
+ <span>{t('Awaiting merge. Handle it from the tasks button in the title bar.')}</span>
104
107
  </div>
105
108
  )}
106
109
  </div>
@@ -111,14 +114,14 @@ function TaskOutcomeCard({
111
114
  onClick={onDismiss}
112
115
  className="rounded px-1.5 py-0.5 text-agent-muted-foreground transition-colors hover:text-agent-foreground"
113
116
  >
114
- 忽略
117
+ {t('Dismiss')}
115
118
  </button>
116
119
  <button
117
120
  type="button"
118
121
  onClick={onInspect}
119
122
  className="rounded-full bg-agent-foreground px-3 py-1 font-medium text-agent-canvas transition hover:opacity-90"
120
123
  >
121
- 查看过程
124
+ {t('View process')}
122
125
  </button>
123
126
  </div>
124
127
  </div>
@@ -18,7 +18,8 @@ import {
18
18
  mergeTaskWorktree,
19
19
  type LocalTask,
20
20
  } from '@/lib/local-api';
21
- import { getElectronBridge } from '@/lib/electron-bridge';
21
+ import { getHostBridge } from '@/lib/host-bridge';
22
+ import { t } from '@/i18n';
22
23
 
23
24
  /**
24
25
  * TaskPanelModal — 跨 turn 后台任务面板(4.6a/4.6c)。
@@ -80,7 +81,7 @@ export function TaskPanelModal({
80
81
  // 任务终态推送 → 重拉列表。preload 只透传 chatId/taskId/status,
81
82
  // 记录本体以 SQLite 为准。
82
83
  useEffect(() => {
83
- const bridge = getElectronBridge();
84
+ const bridge = getHostBridge();
84
85
  if (!bridge?.onTaskUpdated) return;
85
86
  return bridge.onTaskUpdated((payload) => {
86
87
  if (payload.chatId === chatId) void refresh();
@@ -121,14 +122,14 @@ export function TaskPanelModal({
121
122
  }
122
123
  };
123
124
 
124
- const runningCount = tasks?.filter((t) => t.status === 'running').length ?? 0;
125
+ const runningCount = tasks?.filter((task) => task.status === 'running').length ?? 0;
125
126
 
126
127
  return createPortal(
127
128
  <div
128
129
  className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
129
130
  role="dialog"
130
131
  aria-modal="true"
131
- aria-label="后台任务"
132
+ aria-label={t('Background tasks')}
132
133
  onMouseDown={(e) => {
133
134
  if (e.target === e.currentTarget) onClose();
134
135
  }}
@@ -138,10 +139,15 @@ export function TaskPanelModal({
138
139
  <div className="flex items-center gap-2 border-b border-agent-border px-3 py-2">
139
140
  <LuListTodo className="h-4 w-4 shrink-0 text-agent-muted-foreground" />
140
141
  <span className="min-w-0 flex-1 truncate text-xs font-medium text-agent-foreground">
141
- 后台任务
142
+ {t('Background tasks')}
142
143
  {tasks && tasks.length > 0 && (
143
144
  <span className="ml-1.5 text-xs font-normal text-agent-muted-foreground">
144
- {tasks.length} 个任务{runningCount > 0 ? `,${runningCount} 个运行中` : ''}
145
+ {runningCount > 0
146
+ ? t('{count} tasks, {running} running', {
147
+ count: tasks.length,
148
+ running: runningCount,
149
+ })
150
+ : t('{count} tasks', { count: tasks.length })}
145
151
  </span>
146
152
  )}
147
153
  </span>
@@ -149,7 +155,7 @@ export function TaskPanelModal({
149
155
  type="button"
150
156
  onClick={onClose}
151
157
  className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
152
- aria-label="关闭"
158
+ aria-label={t('Close')}
153
159
  >
154
160
  <LuX className="h-3.5 w-3.5" />
155
161
  </button>
@@ -159,11 +165,13 @@ export function TaskPanelModal({
159
165
  {loading ? (
160
166
  <div className="flex items-center justify-center gap-2 px-3 py-6 text-xs text-agent-muted-foreground">
161
167
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
162
- 正在加载任务…
168
+ {t('Loading tasks...')}
163
169
  </div>
164
170
  ) : !tasks || tasks.length === 0 ? (
165
171
  <div className="px-3 py-6 text-center text-xs text-agent-muted-foreground">
166
- 暂无后台任务 — 让助手用 task_run 启动一个跨轮任务后会出现在这里。
172
+ {t(
173
+ 'No background tasks yet. When the assistant starts a cross-turn task with task_run, it appears here.',
174
+ )}
167
175
  </div>
168
176
  ) : (
169
177
  tasks.map((task) => {
@@ -190,7 +198,7 @@ export function TaskPanelModal({
190
198
  type="button"
191
199
  onClick={() => onInspect?.(task)}
192
200
  className="min-w-0 flex-1 truncate text-left text-xs font-medium text-agent-foreground hover:underline"
193
- title="在右侧查看推理过程"
201
+ title={t('View the reasoning process on the right')}
194
202
  >
195
203
  {task.task}
196
204
  </button>
@@ -202,9 +210,9 @@ export function TaskPanelModal({
202
210
  <LuGitBranch className="h-3 w-3" />
203
211
  {task.worktreeBranch}
204
212
  {task.worktreeState === 'merged'
205
- ? ' · 已合并'
213
+ ? ` · ${t('Merged')}`
206
214
  : task.worktreeState === 'discarded'
207
- ? ' · 已丢弃'
215
+ ? ` · ${t('Discarded')}`
208
216
  : ''}
209
217
  </span>
210
218
  )}
@@ -212,7 +220,7 @@ export function TaskPanelModal({
212
220
  type="button"
213
221
  onClick={() => setExpandedId(expanded ? null : task.id)}
214
222
  className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-agent-muted-foreground transition-colors hover:bg-agent-foreground/5 hover:text-agent-foreground"
215
- aria-label={expanded ? '收起' : '展开'}
223
+ aria-label={expanded ? t('Collapse') : t('Expand')}
216
224
  aria-expanded={expanded}
217
225
  >
218
226
  {expanded ? (
@@ -225,12 +233,12 @@ export function TaskPanelModal({
225
233
 
226
234
  <div className="mt-1 pl-5 text-[10px] text-agent-muted-foreground/80">
227
235
  {task.status === 'running'
228
- ? '运行中'
236
+ ? t('In progress')
229
237
  : task.status === 'blocked'
230
- ? '等待依赖'
238
+ ? t('Waiting on dependencies')
231
239
  : task.status === 'completed'
232
- ? '已完成'
233
- : '失败'}
240
+ ? t('Completed')
241
+ : t('Failed')}
234
242
  {' · '}
235
243
  {formatTime(task.updatedAt)}
236
244
  </div>
@@ -250,10 +258,10 @@ export function TaskPanelModal({
250
258
  !task.error && (
251
259
  <div className="text-xs text-agent-muted-foreground">
252
260
  {task.status === 'running'
253
- ? '任务还在运行,完成后结果会出现在这里。'
261
+ ? t('The task is still running. The result appears here when it finishes.')
254
262
  : task.status === 'blocked'
255
- ? '等待依赖任务完成,就绪后会自动开始。'
256
- : '无结果。'}
263
+ ? t('Waiting for dependent tasks to finish. It starts automatically when ready.')
264
+ : t('No result.')}
257
265
  </div>
258
266
  )
259
267
  )}
@@ -271,7 +279,7 @@ export function TaskPanelModal({
271
279
  ) : (
272
280
  <LuGitMerge className="h-3 w-3" />
273
281
  )}
274
- 合并到主仓
282
+ {t('Merge into main repo')}
275
283
  </button>
276
284
  <button
277
285
  type="button"
@@ -281,7 +289,7 @@ export function TaskPanelModal({
281
289
  data-task-discard
282
290
  >
283
291
  <LuTrash2 className="h-3 w-3" />
284
- 丢弃
292
+ {t('Discard')}
285
293
  </button>
286
294
  </div>
287
295
  )}
@@ -299,7 +307,9 @@ export function TaskPanelModal({
299
307
  </div>
300
308
 
301
309
  <div className="border-t border-agent-border px-3 py-1.5 text-[10px] text-agent-muted-foreground/80">
302
- 任务由后台智能体独立执行。点任务名可在右侧看推理过程;Esc 关闭。
310
+ {t(
311
+ 'Background agents run tasks on their own. Click a task name to see its reasoning on the right. Press Esc to close.',
312
+ )}
303
313
  </div>
304
314
  </div>
305
315
  </div>,
@@ -2,7 +2,8 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
2
2
  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
- import { getElectronBridge } from '@/lib/electron-bridge';
5
+ import { getHostBridge } from '@/lib/host-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 {
@@ -153,7 +154,7 @@ export function TaskProcessPanel({
153
154
 
154
155
  useEffect(() => {
155
156
  if (childRecordId) return;
156
- const bridge = getElectronBridge();
157
+ const bridge = getHostBridge();
157
158
  if (!bridge?.onTaskProcess) return;
158
159
  return bridge.onTaskProcess((payload) => {
159
160
  if (payload.taskId !== inspected.id) return;
@@ -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
  ) : (