@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
@@ -1,4 +1,5 @@
1
1
  import React from 'react';
2
+ import { t } from '@/i18n';
2
3
 
3
4
  export interface SettingsNavItem {
4
5
  id: string;
@@ -21,10 +22,10 @@ export function SettingsNavMenu({
21
22
  <aside
22
23
  className="sticky top-0 h-fit w-44 shrink-0 py-1 select-none"
23
24
  data-testid="settings-side-nav"
24
- aria-label="设置目录导航"
25
+ aria-label={t('Settings section navigation')}
25
26
  >
26
27
  <div className="mb-2 px-2.5 text-[11px] font-semibold tracking-wider text-agent-muted-foreground uppercase">
27
- 设置导航
28
+ {t('Settings navigation')}
28
29
  </div>
29
30
  <nav className="space-y-0.5">
30
31
  {items.map((item) => {
@@ -1,4 +1,5 @@
1
1
  import { LuCheck, LuLoaderCircle } from 'react-icons/lu';
2
+ import { t } from '@/i18n';
2
3
 
3
4
  interface SettingsSaveButtonProps {
4
5
  saving: boolean;
@@ -29,7 +30,7 @@ export function SettingsSaveButton({
29
30
  ) : savedOk ? (
30
31
  <LuCheck className="h-3.5 w-3.5" />
31
32
  ) : null}
32
- 保存
33
+ {t('Save')}
33
34
  </button>
34
35
  );
35
36
  }
@@ -5,6 +5,7 @@ import {
5
5
  LuLoaderCircle,
6
6
  LuTrash2,
7
7
  } from 'react-icons/lu';
8
+ import { t } from '@/i18n';
8
9
  import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
9
10
  import { getPackHiddenSlashSkills } from '@/packs/registry';
10
11
 
@@ -68,7 +69,7 @@ export function SkillsSettingsPanel() {
68
69
  });
69
70
  setSkills(res.skills || []);
70
71
  } catch (err) {
71
- console.error('获取技能列表失败:', err);
72
+ console.error('Failed to load skill list:', err);
72
73
  setError(err instanceof Error ? err.message : String(err));
73
74
  } finally {
74
75
  setSkillsLoading(false);
@@ -84,20 +85,20 @@ export function SkillsSettingsPanel() {
84
85
  try {
85
86
  const bridge = getElectronBridge();
86
87
  if (bridge?.local?.selectDirectory) {
87
- setImportStatus('正在打开文件夹选择器...');
88
+ setImportStatus(t('Opening folder picker...'));
88
89
  const result = await bridge.local.selectDirectory();
89
90
  if (result && !result.canceled && result.filePaths.length > 0) {
90
91
  const selectedPath = result.filePaths[0];
91
92
  setImportPath(selectedPath);
92
- setImportStatus(`已选择路径: ${selectedPath}`);
93
+ setImportStatus(t('Selected path: {path}', { path: selectedPath }));
93
94
  } else {
94
- setImportStatus('已取消选择文件夹');
95
+ setImportStatus(t('Folder selection canceled'));
95
96
  }
96
97
  } else {
97
- setError('当前版本的客户端不支持文件夹选择器');
98
+ setError(t('This version of the app does not support the folder picker'));
98
99
  }
99
100
  } catch (err) {
100
- console.error('选择文件夹失败:', err);
101
+ console.error('Failed to select folder:', err);
101
102
  setError(err instanceof Error ? err.message : String(err));
102
103
  }
103
104
  };
@@ -107,7 +108,7 @@ export function SkillsSettingsPanel() {
107
108
  if (!trimmed) return;
108
109
  setImporting(true);
109
110
  setError(null);
110
- setImportStatus('正在导入本地技能...');
111
+ setImportStatus(t('Importing local skill...'));
111
112
  try {
112
113
  const res = await getElectronBridge()!.localBackend.request<{ success: boolean; name: string }>({
113
114
  method: 'POST',
@@ -116,16 +117,16 @@ export function SkillsSettingsPanel() {
116
117
  });
117
118
  if (res.success) {
118
119
  setImportPath('');
119
- setImportStatus(`导入成功! 已添加技能 "${res.name}"`);
120
+ setImportStatus(t('Import succeeded. Added skill "{name}"', { name: res.name }));
120
121
  await fetchSkills();
121
122
  } else {
122
- setError('导入技能失败');
123
- setImportStatus('导入失败: 无法将技能拷贝到运行目录');
123
+ setError(t('Failed to import skill'));
124
+ setImportStatus(t('Import failed: could not copy the skill to the runtime directory'));
124
125
  }
125
126
  } catch (err) {
126
127
  const errMsg = err instanceof Error ? err.message : String(err);
127
128
  setError(errMsg);
128
- setImportStatus(`导入出错: ${errMsg}`);
129
+ setImportStatus(t('Import error: {error}', { error: errMsg }));
129
130
  } finally {
130
131
  setImporting(false);
131
132
  }
@@ -143,7 +144,7 @@ export function SkillsSettingsPanel() {
143
144
  if (res.success) {
144
145
  await fetchSkills();
145
146
  } else {
146
- setError('删除技能失败');
147
+ setError(t('Failed to delete skill'));
147
148
  }
148
149
  } catch (err) {
149
150
  setError(err instanceof Error ? err.message : String(err));
@@ -158,11 +159,15 @@ export function SkillsSettingsPanel() {
158
159
  <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
159
160
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
160
161
  <LuBlocks className="h-3.5 w-3.5 text-agent-muted-foreground" />
161
- 导入本地技能目录 (Import Local Skill Directory)
162
+ {t('Import local skill directory')}
162
163
  </h4>
163
164
  <p className="text-[11px] text-agent-muted-foreground">
164
- 写到当前项目(或工作目录)下 <code>skills/技能名/</code> 的技能会自动出现在下方列表,无需导入。
165
- 也可以在这里导入其它本地目录(包含 <code>SKILL.md</code> 的文件夹,例如:<code>my-team/sql-tools</code>)。
165
+ {t('Skills saved in the current project (or working directory) under')}{' '}
166
+ <code>skills/{t('skill-name')}/</code>{' '}
167
+ {t('appear in the list below automatically. No import needed.')}{' '}
168
+ {t('You can also import other local directories here (any folder with')}{' '}
169
+ <code>SKILL.md</code> {t('inside, for example')} <code>my-team/sql-tools</code>
170
+ {t(').')}
166
171
  </p>
167
172
  <div className="flex gap-2">
168
173
  <div className="relative flex-1 flex items-center">
@@ -170,7 +175,7 @@ export function SkillsSettingsPanel() {
170
175
  type="text"
171
176
  value={importPath}
172
177
  onChange={(e) => setImportPath(e.target.value)}
173
- placeholder="请输入技能路径,或点击右侧选择文件夹"
178
+ placeholder={t('Enter a skill path, or click the button on the right to choose a folder')}
174
179
  className="w-full pl-3 pr-8 h-8 text-xs bg-agent-canvas text-agent-foreground border border-agent-border rounded-agent-md focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
175
180
  disabled={importing}
176
181
  data-testid="skills-import-path"
@@ -180,7 +185,7 @@ export function SkillsSettingsPanel() {
180
185
  onClick={handleBrowseFolder}
181
186
  disabled={importing}
182
187
  className="absolute right-2.5 text-agent-muted-foreground hover:text-agent-foreground transition-colors"
183
- title="选择本地文件夹"
188
+ title={t('Choose a local folder')}
184
189
  data-testid="skills-import-browse"
185
190
  >
186
191
  <LuFolderOpen className="h-4 w-4" />
@@ -200,7 +205,7 @@ export function SkillsSettingsPanel() {
200
205
  {importing ? (
201
206
  <LuLoaderCircle className="h-3 w-3 animate-spin" />
202
207
  ) : (
203
- '导入'
208
+ t('Import')
204
209
  )}
205
210
  </button>
206
211
  </div>
@@ -217,16 +222,16 @@ export function SkillsSettingsPanel() {
217
222
  {/* Skills List */}
218
223
  <div className="space-y-2">
219
224
  <h4 className="text-xs font-semibold text-agent-muted-foreground uppercase tracking-wide">
220
- 已加载技能
225
+ {t('Loaded skills')}
221
226
  </h4>
222
227
  {skillsLoading ? (
223
228
  <div className="flex items-center gap-2 py-4 text-xs text-agent-muted-foreground">
224
229
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
225
- 获取已加载技能...
230
+ {t('Fetching loaded skills...')}
226
231
  </div>
227
232
  ) : skills.length === 0 ? (
228
233
  <div className="text-center py-6 text-xs text-agent-muted-foreground">
229
- 暂无已加载技能
234
+ {t('No loaded skills yet')}
230
235
  </div>
231
236
  ) : (
232
237
  <div className="divide-y divide-agent-border/40 pr-1">
@@ -257,38 +262,38 @@ export function SkillsSettingsPanel() {
257
262
  )}
258
263
  {isBuiltin && (
259
264
  <span className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left">
260
- 内置
265
+ {t('Built-in')}
261
266
  </span>
262
267
  )}
263
268
  {isWorkspace && (
264
269
  <span
265
270
  className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
266
- title="来自项目或工作目录的 skills/ 文件夹,写进去即出现在此列表"
271
+ title={t('From the skills/ folder of the project or working directory. Anything saved there appears in this list.')}
267
272
  >
268
- 工作区
273
+ {t('Workspace')}
269
274
  </span>
270
275
  )}
271
276
  {skill.layer === 'eager' ? (
272
277
  <span
273
278
  className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
274
- title="常驻层:正文始终注入系统提示词"
279
+ title={t('Always-on layer: the body is always injected into the system prompt')}
275
280
  >
276
- 常驻
281
+ {t('Always on')}
277
282
  </span>
278
283
  ) : (
279
284
  <span
280
285
  className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
281
- title="按需层:仅在目录中列出,模型经 skill 工具按需加载"
286
+ title={t('On-demand layer: only listed in the catalog. The model loads it through the skill tool when needed.')}
282
287
  >
283
- 按需
288
+ {t('On demand')}
284
289
  </span>
285
290
  )}
286
291
  {skill.modelInvocable === false && (
287
292
  <span
288
293
  className="px-1 py-0.2 rounded bg-agent-muted text-[9px] text-agent-muted-foreground font-medium flex-shrink-0 scale-95 origin-left"
289
- title="disable-model-invocation:模型不可调用,只能用 /名称 手动触发"
294
+ title={t('disable-model-invocation: the model cannot call it. Trigger it manually with /name.')}
290
295
  >
291
- 仅手动
296
+ {t('Manual only')}
292
297
  </span>
293
298
  )}
294
299
  </div>
@@ -304,7 +309,7 @@ export function SkillsSettingsPanel() {
304
309
  onClick={() => handleDeleteSkill(skill.name)}
305
310
  disabled={deletingName === skill.name}
306
311
  className="text-agent-muted-foreground hover:text-agent-destructive p-1 rounded-full hover:bg-agent-muted transition-colors"
307
- title="卸载此技能"
312
+ title={t('Uninstall this skill')}
308
313
  data-testid={`skill-uninstall-${skill.name}`}
309
314
  >
310
315
  {deletingName === skill.name ? (
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuActivity } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
  import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
4
5
 
5
6
  /**
@@ -70,8 +71,10 @@ export function TelemetrySettingsPanel() {
70
71
  setEndpoint(saved.endpoint ?? '');
71
72
  setStatus(
72
73
  saved.endpoint
73
- ? `已保存 — 遥测开启(${saved.privacyMode === 'full' ? '完整(已脱敏)' : '仅元数据'})`
74
- : '已保存 — 未配置 collector 地址,遥测保持关闭',
74
+ ? t('Saved. Telemetry is on ({mode})', {
75
+ mode: saved.privacyMode === 'full' ? t('Full (redacted)') : t('Metadata only'),
76
+ })
77
+ : t('Saved. No collector URL is set, so telemetry stays off'),
75
78
  );
76
79
  } catch (err) {
77
80
  setError(err instanceof Error ? err.message : String(err));
@@ -91,32 +94,32 @@ export function TelemetrySettingsPanel() {
91
94
  {loading ? (
92
95
  <div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
93
96
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
94
- 读取遥测设置...
97
+ {t('Loading telemetry settings...')}
95
98
  </div>
96
99
  ) : (
97
100
  <>
98
101
  <div className="space-y-1.5">
99
102
  <label className="text-[11px] font-medium text-agent-muted-foreground">
100
- Collector 地址(/v1/traces)
103
+ {t('Collector URL (/v1/traces)')}
101
104
  </label>
102
105
  <input
103
106
  type="text"
104
107
  value={endpoint}
105
108
  onChange={(e) => setEndpoint(e.target.value)}
106
- placeholder="http://127.0.0.1:4318/v1/traces(留空 = 关闭遥测)"
109
+ placeholder={t('http://127.0.0.1:4318/v1/traces (leave blank = telemetry off)')}
107
110
  className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 font-mono text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
108
111
  />
109
112
  </div>
110
113
 
111
114
  <div className="space-y-1.5">
112
115
  <label className="text-[11px] font-medium text-agent-muted-foreground">
113
- 隐私档位
116
+ {t('Privacy level')}
114
117
  </label>
115
118
  <div className="flex gap-2">
116
119
  {(
117
120
  [
118
- { value: 'metadata', label: '仅元数据', hint: '结构/时延/状态,不含内容' },
119
- { value: 'full', label: '完整(已脱敏)', hint: '含 payload,密钥已打码' },
121
+ { value: 'metadata', label: 'Metadata only', hint: 'Structure, latency, and status. No content.' },
122
+ { value: 'full', label: 'Full (redacted)', hint: 'Includes payloads. Secrets are masked.' },
120
123
  ] as const
121
124
  ).map((opt) => (
122
125
  <button
@@ -129,8 +132,8 @@ export function TelemetrySettingsPanel() {
129
132
  : 'border-agent-border bg-agent-canvas hover:bg-agent-muted/40'
130
133
  }`}
131
134
  >
132
- <div className="text-xs font-medium text-agent-foreground">{opt.label}</div>
133
- <div className="text-[10px] text-agent-muted-foreground">{opt.hint}</div>
135
+ <div className="text-xs font-medium text-agent-foreground">{t(opt.label)}</div>
136
+ <div className="text-[10px] text-agent-muted-foreground">{t(opt.hint)}</div>
134
137
  </button>
135
138
  ))}
136
139
  </div>
@@ -138,7 +141,7 @@ export function TelemetrySettingsPanel() {
138
141
 
139
142
  <div className="flex items-center justify-between gap-2">
140
143
  <p className="text-[10px] text-agent-muted-foreground">
141
- 无论哪一档,密钥/令牌在导出前都会被脱敏;留空地址则完全不导出。
144
+ {t('At either level, keys and tokens are redacted before export. Leave the URL blank to export nothing.')}
142
145
  </p>
143
146
  <button
144
147
  type="button"
@@ -150,7 +153,7 @@ export function TelemetrySettingsPanel() {
150
153
  : 'bg-agent-foreground text-agent-canvas hover:opacity-90'
151
154
  }`}
152
155
  >
153
- {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : '保存'}
156
+ {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : t('Save')}
154
157
  </button>
155
158
  </div>
156
159
 
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuChartBar, LuRefreshCw } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
  import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
4
5
 
5
6
  /**
@@ -40,11 +41,7 @@ interface UsageSummaryWire {
40
41
  };
41
42
  }
42
43
 
43
- const WINDOW_OPTIONS = [
44
- { days: 7, label: '近 7 天' },
45
- { days: 30, label: '近 30 天' },
46
- { days: 90, label: '近 90 天' },
47
- ] as const;
44
+ const WINDOW_OPTIONS = [{ days: 7 }, { days: 30 }, { days: 90 }] as const;
48
45
 
49
46
  function formatTokens(n: number): string {
50
47
  if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
@@ -95,7 +92,7 @@ export function UsagePanel() {
95
92
  <div className="flex items-center justify-between">
96
93
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
97
94
  <LuChartBar className="h-3.5 w-3.5 text-agent-muted-foreground" />
98
- 用量与成本
95
+ {t('Usage and cost')}
99
96
  </h4>
100
97
  <div className="flex items-center gap-1.5">
101
98
  {WINDOW_OPTIONS.map((opt) => (
@@ -109,7 +106,7 @@ export function UsagePanel() {
109
106
  : 'bg-agent-canvas text-agent-muted-foreground border border-agent-border hover:bg-agent-muted/40'
110
107
  }`}
111
108
  >
112
- {opt.label}
109
+ {t('Last {days} days', { days: opt.days })}
113
110
  </button>
114
111
  ))}
115
112
  <button
@@ -117,7 +114,7 @@ export function UsagePanel() {
117
114
  onClick={() => void fetchSummary(days)}
118
115
  disabled={loading}
119
116
  className="h-6 w-6 rounded-full border border-agent-border bg-agent-canvas text-agent-muted-foreground hover:bg-agent-muted/40 flex items-center justify-center"
120
- title="刷新"
117
+ title={t('Refresh')}
121
118
  >
122
119
  <LuRefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} />
123
120
  </button>
@@ -127,30 +124,30 @@ export function UsagePanel() {
127
124
  {loading && !summary ? (
128
125
  <div className="flex items-center gap-2 py-4 text-xs text-agent-muted-foreground">
129
126
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
130
- 读取用量...
127
+ {t('Loading usage...')}
131
128
  </div>
132
129
  ) : !summary || summary.byModel.length === 0 ? (
133
130
  <p className="text-[11px] text-agent-muted-foreground py-2">
134
- 该时间窗内暂无用量记录。完成一轮对话后,这里会按模型聚合 token 与成本。
131
+ {t('No usage in this time window yet. After a conversation turn finishes, tokens and cost are grouped by model here.')}
135
132
  </p>
136
133
  ) : (
137
134
  <>
138
135
  {/* 总计卡片 */}
139
136
  <div className="grid grid-cols-4 gap-2">
140
137
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
141
- <div className="text-[10px] text-agent-muted-foreground">总轮次</div>
138
+ <div className="text-[10px] text-agent-muted-foreground">{t('Total turns')}</div>
142
139
  <div className="text-xs font-semibold text-agent-foreground">{summary.totals.turns}</div>
143
140
  </div>
144
141
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
145
- <div className="text-[10px] text-agent-muted-foreground">总 Token</div>
142
+ <div className="text-[10px] text-agent-muted-foreground">{t('Total tokens')}</div>
146
143
  <div className="text-xs font-semibold text-agent-foreground">{formatTokens(summary.totals.totalTokens)}</div>
147
144
  </div>
148
145
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
149
- <div className="text-[10px] text-agent-muted-foreground">缓存命中</div>
146
+ <div className="text-[10px] text-agent-muted-foreground">{t('Cache hits')}</div>
150
147
  <div className="text-xs font-semibold text-agent-foreground">{formatTokens(summary.totals.cachedPromptTokens)}</div>
151
148
  </div>
152
149
  <div className="rounded-agent-md border border-agent-border bg-agent-canvas px-2.5 py-2">
153
- <div className="text-[10px] text-agent-muted-foreground">估算成本</div>
150
+ <div className="text-[10px] text-agent-muted-foreground">{t('Estimated cost')}</div>
154
151
  <div className="text-xs font-semibold text-agent-foreground">{formatCost(summary.totals.costUsd)}</div>
155
152
  </div>
156
153
  </div>
@@ -160,11 +157,11 @@ export function UsagePanel() {
160
157
  <table className="w-full text-[11px]">
161
158
  <thead>
162
159
  <tr className="bg-agent-muted/40 text-agent-muted-foreground">
163
- <th className="text-left font-medium px-2.5 py-1.5">模型</th>
164
- <th className="text-right font-medium px-2.5 py-1.5">轮次</th>
165
- <th className="text-right font-medium px-2.5 py-1.5">输入</th>
166
- <th className="text-right font-medium px-2.5 py-1.5">输出</th>
167
- <th className="text-right font-medium px-2.5 py-1.5">成本</th>
160
+ <th className="text-left font-medium px-2.5 py-1.5">{t('Model')}</th>
161
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Turns')}</th>
162
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Input')}</th>
163
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Output')}</th>
164
+ <th className="text-right font-medium px-2.5 py-1.5">{t('Cost')}</th>
168
165
  </tr>
169
166
  </thead>
170
167
  <tbody>
@@ -184,7 +181,9 @@ export function UsagePanel() {
184
181
  </div>
185
182
 
186
183
  <p className="text-[10px] text-agent-muted-foreground">
187
- 成本按 framework 单价表估算后按约 1 美元 = 7.2 元换算为人民币,只统计有单价的模型;"—" 表示该模型无单价(本地/未知)。缓存命中为命中 prompt 缓存的 token 数。
184
+ {t(
185
+ 'Cost is estimated from the framework price table and converted to RMB at about 1 USD = 7.2 CNY. Only models with a price are counted; "—" means the model has no price (local or unknown). Cache hits are the tokens served from the prompt cache.',
186
+ )}
188
187
  </p>
189
188
  </>
190
189
  )}
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
  import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
4
5
 
5
6
  /**
@@ -71,11 +72,15 @@ export function WebSearchSettingsPanel() {
71
72
  setProvider(nextProvider);
72
73
  setApiKey(saved?.apiKey ?? '');
73
74
  if (nextProvider === 'ddg') {
74
- setStatus('已保存 — 重启应用后 sidecar 会注册免费搜索(DuckDuckGo)');
75
+ setStatus(t('Saved. After you restart the app, the sidecar registers free search (DuckDuckGo).'));
75
76
  } else if (saved?.apiKey) {
76
- setStatus('已保存 — 重启应用后 sidecar 会注册 web_search');
77
+ setStatus(t('Saved. After you restart the app, the sidecar registers web_search.'));
77
78
  } else {
78
- setStatus('已保存 — 未配置则模型没有搜索工具(OpenAI 可用聊天凭证走托管搜索)');
79
+ setStatus(
80
+ t(
81
+ 'Saved. Without a key the model has no search tool (OpenAI can use the chat credentials for hosted search).',
82
+ ),
83
+ );
79
84
  }
80
85
  } catch (err) {
81
86
  setError(err instanceof Error ? err.message : String(err));
@@ -89,25 +94,25 @@ export function WebSearchSettingsPanel() {
89
94
  <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
90
95
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
91
96
  <LuSearch className="h-3.5 w-3.5 text-agent-muted-foreground" />
92
- 网络搜索
97
+ {t('Web search')}
93
98
  </h4>
94
99
 
95
100
  {loading ? (
96
101
  <div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
97
102
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
98
- 读取搜索设置...
103
+ {t('Loading search settings...')}
99
104
  </div>
100
105
  ) : (
101
106
  <>
102
107
  <div className="space-y-1.5">
103
108
  <label className="text-[11px] font-medium text-agent-muted-foreground">
104
- 搜索后端
109
+ {t('Search backend')}
105
110
  </label>
106
111
  <div className="flex gap-2">
107
112
  {(
108
113
  [
109
- { value: 'ddg', label: '免费', hint: 'DuckDuckGo,无需 Key' },
110
- { value: 'tavily', label: 'Tavily', hint: '需 API Key,结果更稳' },
114
+ { value: 'ddg', label: 'Free', hint: 'DuckDuckGo, no key needed' },
115
+ { value: 'tavily', label: 'Tavily', hint: 'Needs an API key, more reliable results' },
111
116
  ] as const
112
117
  ).map((opt) => (
113
118
  <button
@@ -121,8 +126,8 @@ export function WebSearchSettingsPanel() {
121
126
  : 'border-agent-border bg-agent-canvas hover:bg-agent-muted/40'
122
127
  }`}
123
128
  >
124
- <div className="text-xs font-medium text-agent-foreground">{opt.label}</div>
125
- <div className="text-[10px] text-agent-muted-foreground">{opt.hint}</div>
129
+ <div className="text-xs font-medium text-agent-foreground">{t(opt.label)}</div>
130
+ <div className="text-[10px] text-agent-muted-foreground">{t(opt.hint)}</div>
126
131
  </button>
127
132
  ))}
128
133
  </div>
@@ -137,7 +142,7 @@ export function WebSearchSettingsPanel() {
137
142
  type="password"
138
143
  value={apiKey}
139
144
  onChange={(e) => setApiKey(e.target.value)}
140
- placeholder="tvly-...(留空 = 不注册 Tavily)"
145
+ placeholder={t('tvly-... (leave blank = do not register Tavily)')}
141
146
  className="h-8 w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 font-mono text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
142
147
  autoComplete="off"
143
148
  />
@@ -147,8 +152,12 @@ export function WebSearchSettingsPanel() {
147
152
  <div className="flex items-center justify-between gap-2">
148
153
  <p className="text-[10px] text-agent-muted-foreground">
149
154
  {provider === 'ddg'
150
- ? '免费后端走 DuckDuckGo 公开搜索页,无需 Key。质量与稳定性不如 Tavily;国内网络可能需要系统代理。Harbor 评测仍关闭网络工具。'
151
- : 'GLM / OpenRouter / DeepSeek 需要这把钥;OpenAI(api.openai.com)可用聊天凭证走托管搜索,不必另配。Harbor 评测仍关闭网络工具。'}
155
+ ? t(
156
+ 'The free backend uses the public DuckDuckGo search page and needs no key. Quality and reliability are lower than Tavily, and networks in mainland China may need a system proxy. Harbor evaluations still turn off network tools.',
157
+ )
158
+ : t(
159
+ 'GLM / OpenRouter / DeepSeek need this key. OpenAI (api.openai.com) can use the chat credentials for hosted search with no extra setup. Harbor evaluations still turn off network tools.',
160
+ )}
152
161
  </p>
153
162
  <button
154
163
  type="button"
@@ -160,7 +169,7 @@ export function WebSearchSettingsPanel() {
160
169
  : 'bg-agent-foreground text-agent-canvas hover:opacity-90'
161
170
  }`}
162
171
  >
163
- {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : '保存'}
172
+ {saving ? <LuLoaderCircle className="h-3 w-3 animate-spin" /> : t('Save')}
164
173
  </button>
165
174
  </div>
166
175
 
@@ -3,6 +3,7 @@
3
3
  * 常用置顶、中文标签、以及 sidecar 未就绪时的本地回退。
4
4
  */
5
5
 
6
+ import { t } from '@/i18n';
6
7
  import type {
7
8
  CatalogProviderDescriptor,
8
9
  GatewayModelEntry,
@@ -42,14 +43,14 @@ export const VENDOR_LABELS: Record<string, string> = {
42
43
  anthropic: 'Anthropic',
43
44
  google: 'Google Gemini',
44
45
  openrouter: 'OpenRouter',
45
- 'alibaba-cn': '阿里云百炼',
46
- 'moonshotai-cn': '月之暗面 Kimi',
47
- zhipuai: '智谱 GLM',
46
+ 'alibaba-cn': 'Alibaba Cloud Model Studio',
47
+ 'moonshotai-cn': 'Moonshot AI Kimi',
48
+ zhipuai: 'Zhipu GLM',
48
49
  groq: 'Groq',
49
50
  xai: 'xAI',
50
51
  lmstudio: 'LM Studio',
51
- ollama: 'Ollama (本地)',
52
- custom: '自定义 OpenAI 兼容',
52
+ ollama: 'Ollama (local)',
53
+ custom: 'Custom OpenAI-compatible',
53
54
  };
54
55
 
55
56
  /** sidecar 未就绪时仍能选常用服务商。 */
@@ -155,7 +156,8 @@ const LOCAL_VENDORS: VendorOption[] = [
155
156
  ];
156
157
 
157
158
  export function vendorLabel(id: string): string {
158
- return VENDOR_LABELS[id] ?? id;
159
+ const label = VENDOR_LABELS[id];
160
+ return label ? t(label) : id;
159
161
  }
160
162
 
161
163
  export function llmProviderFromWireKind(wireKind: string): LlmProvider {
@@ -190,7 +192,10 @@ export function mergeVendorOptions(catalog: CatalogProviderDescriptor[]): Vendor
190
192
  featured: featured.has(row.id),
191
193
  }));
192
194
  const seen = new Set(fromCatalog.map((v) => v.id));
193
- const extras = LOCAL_VENDORS.filter((v) => !seen.has(v.id));
195
+ const extras = LOCAL_VENDORS.filter((v) => !seen.has(v.id)).map((v) => ({
196
+ ...v,
197
+ label: vendorLabel(v.id),
198
+ }));
194
199
  const featuredOrder = [...FEATURED_VENDOR_IDS, 'ollama', 'custom'];
195
200
  const rank = (id: string) => {
196
201
  const idx = featuredOrder.indexOf(id);