@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
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { useEffect, useId } from 'react';
5
5
  import { createPortal } from 'react-dom';
6
+ import { t } from '@/i18n';
6
7
 
7
8
  export interface ConfirmDialogProps {
8
9
  open: boolean;
@@ -19,7 +20,7 @@ export function ConfirmDialog({
19
20
  open,
20
21
  title,
21
22
  description,
22
- confirmLabel = '删除',
23
+ confirmLabel = t('Delete'),
23
24
  pending = false,
24
25
  onCancel,
25
26
  onConfirm,
@@ -71,7 +72,7 @@ export function ConfirmDialog({
71
72
  className="h-9 rounded-full px-4 text-sm text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground disabled:opacity-40"
72
73
  data-testid={`${testId}-cancel`}
73
74
  >
74
- 取消
75
+ {t('Cancel')}
75
76
  </button>
76
77
  <button
77
78
  type="button"
@@ -80,7 +81,7 @@ export function ConfirmDialog({
80
81
  className="h-9 rounded-full bg-agent-destructive px-4 text-sm font-medium text-white transition-opacity hover:opacity-90 disabled:opacity-40"
81
82
  data-testid={`${testId}-confirm`}
82
83
  >
83
- {pending ? '删除中…' : confirmLabel}
84
+ {pending ? t('Deleting…') : confirmLabel}
84
85
  </button>
85
86
  </div>
86
87
  </div>
@@ -7,7 +7,8 @@ import { createPortal } from 'react-dom';
7
7
  import { LuFolder, LuFolderPlus, LuX } from 'react-icons/lu';
8
8
  import { BRAND_NAME } from '@/brand';
9
9
  import { ConfirmDialog } from '@/components/ConfirmDialog';
10
- import { getElectronBridge } from '@/lib/electron-bridge';
10
+ import { getHostBridge } from '@/lib/host-bridge';
11
+ import { t } from '@/i18n';
11
12
 
12
13
  export interface ProjectFormValues {
13
14
  name: string;
@@ -65,7 +66,7 @@ export function CreateProjectModal({
65
66
  if (typeof document === 'undefined') return null;
66
67
 
67
68
  const trimmed = name.trim();
68
- const homePreview = `~/Documents/${BRAND_NAME}/${trimmed || '项目名'}`;
69
+ const homePreview = `~/Documents/${BRAND_NAME}/${trimmed || t('project-name')}`;
69
70
  const titleId = isEdit ? 'edit-project-title' : 'create-project-title';
70
71
 
71
72
  const addSourceFolder = (folder: string) => {
@@ -82,13 +83,13 @@ export function CreateProjectModal({
82
83
  addSourceFolder(typed);
83
84
  return;
84
85
  }
85
- const picker = getElectronBridge()?.local?.selectDirectory;
86
+ const picker = getHostBridge()?.local?.selectDirectory;
86
87
  if (!picker) {
87
- setError('请输入文件夹路径');
88
+ setError(t('Enter a folder path'));
88
89
  return;
89
90
  }
90
91
  try {
91
- const result = await picker({ title: '添加源文件夹' });
92
+ const result = await picker({ title: t('Add source folder') });
92
93
  if (!result || result.canceled || result.filePaths.length === 0) return;
93
94
  addSourceFolder(result.filePaths[0]);
94
95
  } catch (err) {
@@ -137,13 +138,13 @@ export function CreateProjectModal({
137
138
  <div className="flex w-[440px] max-w-[92vw] flex-col overflow-hidden rounded-2xl border border-agent-border bg-agent-canvas shadow-2xl">
138
139
  <div className="flex items-center justify-between px-5 pb-1 pt-4">
139
140
  <h2 id={titleId} className="text-base font-semibold text-agent-foreground">
140
- {isEdit ? '编辑项目' : '新建项目'}
141
+ {isEdit ? t('Edit project') : t('New project')}
141
142
  </h2>
142
143
  <button
143
144
  type="button"
144
145
  onClick={onClose}
145
146
  className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
146
- aria-label="关闭"
147
+ aria-label={t('Close')}
147
148
  >
148
149
  <LuX className="h-4 w-4" />
149
150
  </button>
@@ -159,7 +160,7 @@ export function CreateProjectModal({
159
160
  onKeyDown={(event) => {
160
161
  if (event.key === 'Enter') void handleSubmit();
161
162
  }}
162
- placeholder="项目名称"
163
+ placeholder={t('Project name')}
163
164
  autoFocus
164
165
  className="h-full min-w-0 flex-1 bg-transparent text-sm text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
165
166
  data-testid={isEdit ? 'edit-project-name' : 'create-project-name'}
@@ -167,14 +168,14 @@ export function CreateProjectModal({
167
168
  </label>
168
169
  {!isEdit && (
169
170
  <p className="px-1 text-[11px] text-agent-muted-foreground">
170
- 默认位置:{homePreview}
171
+ {t('Default location: {path}', { path: homePreview })}
171
172
  </p>
172
173
  )}
173
174
 
174
175
  <div>
175
- <div className="mb-1 text-sm text-agent-foreground">源文件夹</div>
176
+ <div className="mb-1 text-sm text-agent-foreground">{t('Source folders')}</div>
176
177
  <p className="mb-2 text-[11px] text-agent-muted-foreground">
177
- 这些目录及其子目录可读写。
178
+ {t('These folders and their subfolders are readable and writable.')}
178
179
  </p>
179
180
  <div className="overflow-hidden rounded-2xl border border-agent-border">
180
181
  {sourceFolders.map((folder) => (
@@ -192,7 +193,7 @@ export function CreateProjectModal({
192
193
  setSourceFolders((prev) => prev.filter((item) => item !== folder))
193
194
  }
194
195
  className="rounded-full p-0.5 text-agent-muted-foreground hover:text-agent-foreground"
195
- aria-label={`移除 ${folder}`}
196
+ aria-label={t('Remove {folder}', { folder })}
196
197
  >
197
198
  <LuX className="h-3.5 w-3.5" />
198
199
  </button>
@@ -210,7 +211,7 @@ export function CreateProjectModal({
210
211
  void handleAddFolder();
211
212
  }
212
213
  }}
213
- placeholder="输入路径或点添加浏览"
214
+ placeholder={t('Type a path, or click Add to browse')}
214
215
  className="h-7 min-w-0 flex-1 bg-transparent text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground"
215
216
  data-testid={isEdit ? 'edit-project-folder-path' : 'create-project-folder-path'}
216
217
  />
@@ -220,7 +221,7 @@ export function CreateProjectModal({
220
221
  className="shrink-0 rounded-full px-2 py-1 text-xs font-medium text-agent-foreground hover:bg-agent-muted"
221
222
  data-testid={isEdit ? 'edit-project-add-folder' : 'create-project-add-folder'}
222
223
  >
223
- 添加
224
+ {t('Add')}
224
225
  </button>
225
226
  </div>
226
227
  </div>
@@ -239,11 +240,11 @@ export function CreateProjectModal({
239
240
  type="button"
240
241
  onClick={() => setDeleteConfirmOpen(true)}
241
242
  disabled={deleting || submitting}
242
- title="删除项目(会话保留为无项目对话)"
243
+ title={t('Delete project (its chats stay as chats without a project)')}
243
244
  className="mr-auto h-9 rounded-full px-3 text-sm text-agent-destructive transition-colors hover:bg-agent-destructive/10 disabled:opacity-40"
244
245
  data-testid="edit-project-delete"
245
246
  >
246
- 删除本地项目
247
+ {t('Delete local project')}
247
248
  </button>
248
249
  )}
249
250
  <button
@@ -253,7 +254,7 @@ export function CreateProjectModal({
253
254
  onDelete ? '' : 'ml-auto'
254
255
  }`}
255
256
  >
256
- 取消
257
+ {t('Cancel')}
257
258
  </button>
258
259
  <button
259
260
  type="button"
@@ -262,16 +263,25 @@ export function CreateProjectModal({
262
263
  className="h-9 rounded-full bg-agent-foreground px-4 text-sm font-medium text-agent-canvas transition-opacity disabled:opacity-40"
263
264
  data-testid={isEdit ? 'edit-project-submit' : 'create-project-submit'}
264
265
  >
265
- {submitting ? (isEdit ? '保存中…' : '创建中…') : isEdit ? '保存' : '创建项目'}
266
+ {submitting
267
+ ? isEdit
268
+ ? t('Saving…')
269
+ : t('Creating…')
270
+ : isEdit
271
+ ? t('Save')
272
+ : t('Create project')}
266
273
  </button>
267
274
  </div>
268
275
  </div>
269
276
  {onDelete && (
270
277
  <ConfirmDialog
271
278
  open={deleteConfirmOpen}
272
- title="删除项目"
273
- description={`确定删除项目「${trimmed || '未命名'}」?会话会保留为无项目对话。`}
274
- confirmLabel="删除项目"
279
+ title={t('Delete project')}
280
+ description={t(
281
+ 'Delete project "{name}"? Its chats will stay as chats without a project.',
282
+ { name: trimmed || t('Untitled') },
283
+ )}
284
+ confirmLabel={t('Delete project')}
275
285
  pending={deleting}
276
286
  onCancel={() => {
277
287
  if (deleting) return;
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
3
3
  import { LuX } from 'react-icons/lu';
4
4
  import type { LlmSettings } from '@/lib/local-api';
5
5
  import { LlmSettingsPanel } from '@/components/settings/LlmSettingsPanel';
6
+ import { t } from '@/i18n';
6
7
 
7
8
  interface LocalLlmSettingsModalProps {
8
9
  open: boolean;
@@ -43,12 +44,12 @@ export function LocalLlmSettingsModal({ open, onClose, onSaved }: LocalLlmSettin
43
44
  className="flex h-[560px] max-h-[90vh] w-[540px] max-w-[92vw] flex-col overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-2xl animate-fade-in"
44
45
  >
45
46
  <div className="flex h-9 flex-shrink-0 items-center justify-between border-b border-agent-border px-3">
46
- <h2 className="text-xs font-semibold text-agent-foreground">本地模型设置</h2>
47
+ <h2 className="text-xs font-semibold text-agent-foreground">{t('Local model settings')}</h2>
47
48
  <button
48
49
  type="button"
49
50
  onClick={onClose}
50
51
  className="rounded-full p-1 text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
51
- aria-label="关闭"
52
+ aria-label={t('Close')}
52
53
  >
53
54
  <LuX className="h-4 w-4" />
54
55
  </button>
@@ -62,7 +63,7 @@ export function LocalLlmSettingsModal({ open, onClose, onSaved }: LocalLlmSettin
62
63
  onClick={onClose}
63
64
  className="h-7 rounded-full px-3 text-xs font-medium text-agent-muted-foreground transition-colors hover:bg-agent-muted hover:text-agent-foreground"
64
65
  >
65
- 关闭
66
+ {t('Close')}
66
67
  </button>
67
68
  </div>
68
69
  </div>
@@ -4,16 +4,17 @@ import {
4
4
  type AppReleaseView,
5
5
  } from '@/lib/app-release';
6
6
  import {
7
- getElectronBridge,
7
+ getHostBridge,
8
8
  type AppReleaseSnapshot,
9
- } from '@/lib/electron-bridge';
9
+ } from '@/lib/host-bridge';
10
+ import { t } from '@/i18n';
10
11
 
11
12
  export function useAppRelease(): AppReleaseView & { run: () => void } {
12
13
  const [snap, setSnap] = useState<AppReleaseSnapshot | null>(null);
13
14
  const [confirmedCurrent, setConfirmedCurrent] = useState(false);
14
15
 
15
16
  useEffect(() => {
16
- const app = getElectronBridge()?.app;
17
+ const app = getHostBridge()?.app;
17
18
  if (!app) return;
18
19
  let alive = true;
19
20
  void app.snapshot().then((next) => {
@@ -31,7 +32,7 @@ export function useAppRelease(): AppReleaseView & { run: () => void } {
31
32
  }, []);
32
33
 
33
34
  const run = useCallback(() => {
34
- const app = getElectronBridge()?.app;
35
+ const app = getHostBridge()?.app;
35
36
  if (!app || !snap || !snap.enabled) return;
36
37
  if (snap.phase !== 'idle' && snap.phase !== 'error' && snap.phase !== 'ready') return;
37
38
  const task = snap.phase === 'ready' ? app.install() : app.check();
@@ -59,7 +60,7 @@ export function SidebarVersionLabel({ release }: { release: AppReleaseView }) {
59
60
  <span
60
61
  className="ml-auto text-[10px] tabular-nums text-agent-muted-foreground/70"
61
62
  data-testid="sidebar-app-version"
62
- title={`当前版本 ${release.version}`}
63
+ title={t('Current version {version}', { version: release.version })}
63
64
  >
64
65
  v{release.version}
65
66
  </span>
@@ -1,5 +1,6 @@
1
1
  import { Suspense, lazy, useEffect, useState } from 'react';
2
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
2
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
3
+ import { t } from '@/i18n';
3
4
 
4
5
  // 把 xterm 那一坨 (~300KB) 拆到独立 chunk —— 用户没打开终端就不下载。
5
6
  // 跟原 Next.js 版本里 `dynamic(() => import('./TerminalView'), {ssr:false})`
@@ -16,7 +17,7 @@ const TerminalView = lazy(() =>
16
17
  * xterm 视图,不杀 shell;重新打开时通过 `terminal:ensure` 的 replay
17
18
  * buffer 补回面板关闭期间的输出。
18
19
  *
19
- * Electron 检测保留给浏览器预览 / dev 模式(没有 `window.electron` 就
20
+ * Electron 检测保留给浏览器预览 / dev 模式(没有 `window.steerableHost` 就
20
21
  * 连不到本地 PTY)。延迟到首个 effect 再渲染,避免检测闪烁。
21
22
  */
22
23
  export function TerminalPanel({
@@ -32,7 +33,7 @@ export function TerminalPanel({
32
33
  const [hasElectron, setHasElectron] = useState<boolean | null>(null);
33
34
 
34
35
  useEffect(() => {
35
- setHasElectron(isElectron() && !!getElectronBridge()?.terminal);
36
+ setHasElectron(hasHostBridge() && !!getHostBridge()?.terminal);
36
37
  }, []);
37
38
 
38
39
  if (hasElectron === null) {
@@ -42,9 +43,9 @@ export function TerminalPanel({
42
43
  return (
43
44
  <div className="flex h-full w-full items-center justify-center bg-black p-4 text-center font-mono text-xs text-white">
44
45
  <div>
45
- <p className="mb-2">⚠️ 此面板需要桌面客户端或 BS server 连接。</p>
46
+ <p className="mb-2">⚠️ {t('This panel needs the desktop client or a BS server connection.')}</p>
46
47
  <p className="text-white/60">
47
- 当前页面没有可用的宿主 bridge,无法连接到本地 PTY。
48
+ {t('This page has no host bridge, so it cannot connect to a local PTY.')}
48
49
  </p>
49
50
  </div>
50
51
  </div>
@@ -5,15 +5,16 @@ import { WebLinksAddon } from '@xterm/addon-web-links';
5
5
  import { LuListTodo } from 'react-icons/lu';
6
6
  import '@xterm/xterm/css/xterm.css';
7
7
  import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
8
- import { getElectronBridge } from '@/lib/electron-bridge';
8
+ import { getHostBridge } from '@/lib/host-bridge';
9
9
  import { BRAND_NAME } from '@/brand';
10
+ import { t } from '@/i18n';
10
11
 
11
12
  /**
12
13
  * Renders an xterm.js terminal wired up to the main-process PTY via the
13
14
  * `terminal.*` bridge. Ported from `deeppath/apps/web/src/app/agent/terminal/
14
15
  * TerminalView.tsx`, with these adaptations:
15
16
  *
16
- * - `window.electron` direct access → `getElectronBridge()` helper so the
17
+ * - `window.steerableHost` direct access → `getHostBridge()` helper so the
17
18
  * "no bridge" path is type-safe.
18
19
  * - Removed `'use client'` directive (not a Next.js project).
19
20
  * - Container is `h-full` instead of `h-screen` so the component is
@@ -44,11 +45,11 @@ export function TerminalView({
44
45
 
45
46
  useEffect(() => {
46
47
  if (!containerRef.current) return;
47
- const bridge = getElectronBridge();
48
+ const bridge = getHostBridge();
48
49
  const terminal = bridge?.terminal;
49
50
  if (!terminal) {
50
51
  setStatus('error');
51
- setStatusMsg('window.electron.terminal 不可用');
52
+ setStatusMsg(t('The host terminal is unavailable'));
52
53
  return;
53
54
  }
54
55
 
@@ -199,23 +200,27 @@ export function TerminalView({
199
200
  }, []);
200
201
 
201
202
  const statusLabel =
202
- status === 'starting' ? '启动中…' : status === 'ready' ? '在线' : `错误:${statusMsg}`;
203
+ status === 'starting'
204
+ ? t('Starting…')
205
+ : status === 'ready'
206
+ ? t('Online')
207
+ : t('Error: {message}', { message: statusMsg });
203
208
 
204
209
  return (
205
210
  <div className="flex h-full w-full flex-col bg-[#0b0b0c]">
206
211
  <DockPanelHeader
207
- title={`${BRAND_NAME} · 终端 · ${statusLabel}`}
212
+ title={t('{brand} · Terminal · {status}', { brand: BRAND_NAME, status: statusLabel })}
208
213
  onClose={onClose}
209
- closeLabel="关闭终端面板"
214
+ closeLabel={t('Close terminal panel')}
210
215
  actions={
211
216
  onShowTaskProcess && (
212
217
  <DockHeaderButton
213
218
  icon={<LuListTodo className="h-3 w-3" />}
214
- label="后台任务"
219
+ label={t('Background tasks')}
215
220
  title={
216
221
  taskProcessTitle
217
- ? `切换到后台推理:${taskProcessTitle}`
218
- : '切换到后台推理'
222
+ ? t('Switch to background reasoning: {title}', { title: taskProcessTitle })
223
+ : t('Switch to background reasoning')
219
224
  }
220
225
  onClick={onShowTaskProcess}
221
226
  />
@@ -2,7 +2,8 @@ import { LuShieldAlert, LuTriangleAlert } from 'react-icons/lu';
2
2
  import {
3
3
  type ApprovalDecisionKind,
4
4
  type ApprovalPromptRequest,
5
- } from '@/lib/electron-bridge';
5
+ } from '@/lib/host-bridge';
6
+ import { t } from '@/i18n';
6
7
 
7
8
  /**
8
9
  * ApprovalPromptMenu — W4-1 审批代数的输入区 UI。
@@ -19,10 +20,10 @@ import {
19
20
  */
20
21
 
21
22
  const MODE_LABEL: Record<string, string> = {
22
- read: '只读',
23
- safe_write: '写入',
24
- destructive: '危险',
25
- other: '其他',
23
+ read: 'Read only',
24
+ safe_write: 'Write',
25
+ destructive: 'Dangerous',
26
+ other: 'Other',
26
27
  };
27
28
 
28
29
  function summarizeArguments(args: Record<string, unknown>): string {
@@ -45,15 +46,15 @@ interface DecisionButton {
45
46
  }
46
47
 
47
48
  const ALLOW_BUTTONS: DecisionButton[] = [
48
- { kind: 'allow_once', label: '允许一次', tone: 'allow' },
49
- { kind: 'allow_for_session', label: '本次会话允许', tone: 'allow' },
50
- { kind: 'allow_always', label: '始终允许', tone: 'allow' },
49
+ { kind: 'allow_once', label: 'Allow once', tone: 'allow' },
50
+ { kind: 'allow_for_session', label: 'Allow for this chat', tone: 'allow' },
51
+ { kind: 'allow_always', label: 'Always allow', tone: 'allow' },
51
52
  ];
52
53
 
53
54
  const DENY_BUTTONS: DecisionButton[] = [
54
- { kind: 'deny_once', label: '拒绝一次', tone: 'deny' },
55
- { kind: 'deny_for_session', label: '本次会话拒绝', tone: 'deny' },
56
- { kind: 'deny_always', label: '始终拒绝', tone: 'deny' },
55
+ { kind: 'deny_once', label: 'Deny once', tone: 'deny' },
56
+ { kind: 'deny_for_session', label: 'Deny for this chat', tone: 'deny' },
57
+ { kind: 'deny_always', label: 'Always deny', tone: 'deny' },
57
58
  ];
58
59
 
59
60
  function DecisionRow({
@@ -77,7 +78,7 @@ function DecisionRow({
77
78
  : 'rounded-agent-md border border-agent-border px-3 py-1.5 text-xs font-medium text-agent-foreground transition hover:bg-agent-muted'
78
79
  }
79
80
  >
80
- {b.label}
81
+ {t(b.label)}
81
82
  </button>
82
83
  ))}
83
84
  </div>
@@ -120,27 +121,29 @@ export function ApprovalPromptMenu({
120
121
  <div className="text-xs font-medium text-agent-foreground">
121
122
  {isEgress ? (
122
123
  <>
123
- Agent 请求访问外网
124
+ {t('Agent requests internet access')}
124
125
  <span className="mx-1 font-mono">
125
126
  {egressHost}
126
127
  {egressPort !== null ? `:${egressPort}` : ''}
127
128
  </span>
128
129
  </>
129
130
  ) : isSandboxEscalation ? (
130
- <>Agent 请求在工作区外执行命令</>
131
+ <>{t('Agent requests to run a command outside the workspace')}</>
131
132
  ) : (
132
133
  <>
133
- Agent 请求执行
134
+ {t('Agent requests to run')}
134
135
  <span className="mx-1 font-mono">{current.toolName}</span>
135
136
  <span className="text-agent-muted-foreground">
136
- ({MODE_LABEL[current.mode] ?? current.mode}操作)
137
+ {t('({mode} action)', {
138
+ mode: MODE_LABEL[current.mode] ? t(MODE_LABEL[current.mode]) : current.mode,
139
+ })}
137
140
  </span>
138
141
  </>
139
142
  )}
140
143
  </div>
141
144
  {pendingCount > 0 && (
142
145
  <div className="mt-0.5 text-[11px] text-agent-muted-foreground">
143
- 还有 {pendingCount} 个待审批
146
+ {t('{count} more awaiting approval', { count: pendingCount })}
144
147
  </div>
145
148
  )}
146
149
  </div>
@@ -152,23 +155,30 @@ export function ApprovalPromptMenu({
152
155
  </pre>
153
156
  {isEgress ? (
154
157
  <p className="mt-2 text-[11px] leading-relaxed text-agent-muted-foreground">
155
- 该域名不在出网白名单内。放行前请核对完整域名拼写(仿冒域名常
156
- 用相似拼写,如 cdn.jsdelivr.net.evil.com)。放行仅对本次会话
157
- 生效;要持久放行请把域名加入设置的出网白名单。
158
+ {t(
159
+ 'This domain is not on the egress allowlist. Check the full spelling before allowing it (lookalike domains often use similar spellings, such as cdn.jsdelivr.net.evil.com). Allowing it applies only to this chat. To allow it permanently, add the domain to the egress allowlist in Settings.',
160
+ )}
158
161
  </p>
159
162
  ) : isSandboxEscalation ? (
160
163
  <p className="mt-2 text-[11px] leading-relaxed text-agent-muted-foreground">
161
- 命令已在工作区沙箱中被拒绝。允许后只重试这条命令,不再限制写入范围。
164
+ {t(
165
+ 'The workspace sandbox blocked this command. Allowing it retries only this command, without limiting where it can write.',
166
+ )}
162
167
  </p>
163
168
  ) : (
164
169
  <>
165
170
  {current.category !== current.toolName && (
166
171
  <p className="mt-2 text-[11px] text-agent-muted-foreground">
167
- 持久化类别:{current.category}(会话/始终决定对该类别所有调用生效)
172
+ {t(
173
+ 'Saved category: {category} (a chat or always decision applies to every call in this category)',
174
+ { category: current.category },
175
+ )}
168
176
  </p>
169
177
  )}
170
178
  <p className="mt-2 text-[11px] leading-relaxed text-agent-muted-foreground">
171
- 在工作区沙箱中执行,范围外的写入会被拒绝。需要写到外面时,把沙箱切到「完整权限」后再试。
179
+ {t(
180
+ 'Runs in the workspace sandbox. Writes outside it are blocked. To write outside, switch the sandbox to "Full access" and try again.',
181
+ )}
172
182
  </p>
173
183
  </>
174
184
  )}
@@ -198,13 +208,17 @@ export function ApprovalPromptMenu({
198
208
  className="shrink-0 rounded-agent-md bg-agent-destructive px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
199
209
  data-testid="approval-abort"
200
210
  >
201
- 中止本轮
211
+ {t('Abort this turn')}
202
212
  </button>
203
213
  </div>
204
214
  <p className="text-[11px] leading-relaxed text-agent-muted-foreground">
205
215
  {isEgress
206
- ? '「会话」决定在本对话内记住(代理进程退出即失效)。不操作约 3 分钟后按拒绝处理。'
207
- : '「会话」决定在本对话内记住;「始终」决定写入本机 ~/.steerable/approvals.json,跨对话生效。不操作约 2 分钟后按拒绝处理。'}
216
+ ? t(
217
+ '"This chat" decisions are remembered in this chat until the agent process exits. With no action for about 3 minutes, the request is denied.',
218
+ )
219
+ : t(
220
+ '"This chat" decisions are remembered in this chat. "Always" decisions are written to ~/.steerable/approvals.json on this computer and apply across chats. With no action for about 2 minutes, the request is denied.',
221
+ )}
208
222
  </p>
209
223
  </div>
210
224
  </div>
@@ -9,10 +9,10 @@ import {
9
9
  type ReactNode,
10
10
  } from 'react';
11
11
  import {
12
- getElectronBridge,
12
+ getHostBridge,
13
13
  type ApprovalDecisionKind,
14
14
  type ApprovalPromptRequest,
15
- } from '@/lib/electron-bridge';
15
+ } from '@/lib/host-bridge';
16
16
  import { bindPromptToActiveChat, promptVisibleInChat } from './prompt-chat-scope';
17
17
 
18
18
  interface ApprovalPromptContextValue {
@@ -37,7 +37,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
37
37
  const decidedRequestIds = useRef(new Set<string>());
38
38
 
39
39
  useEffect(() => {
40
- const bridge = getElectronBridge();
40
+ const bridge = getHostBridge();
41
41
  if (!bridge?.approval) return;
42
42
  let active = true;
43
43
  const addRequests = (
@@ -67,7 +67,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
67
67
  if (active) addRequests(requests, 'prepend');
68
68
  })
69
69
  .catch((error) => {
70
- console.error('恢复待审批请求失败:', error);
70
+ console.error('Failed to restore pending approval requests:', error);
71
71
  });
72
72
  return () => {
73
73
  active = false;
@@ -76,7 +76,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
76
76
  }, []);
77
77
 
78
78
  const decide = useCallback((requestId: string, kind: ApprovalDecisionKind) => {
79
- const bridge = getElectronBridge();
79
+ const bridge = getHostBridge();
80
80
  decidedRequestIds.current.add(requestId);
81
81
  setQueue((previous) => previous.filter((request) => request.requestId !== requestId));
82
82
  void bridge?.approval?.decide({ requestId, kind });
@@ -8,7 +8,7 @@ import {
8
8
  useState,
9
9
  type ReactNode,
10
10
  } from 'react';
11
- import { getElectronBridge, type AskUserPromptRequest } from '@/lib/electron-bridge';
11
+ import { getHostBridge, type AskUserPromptRequest } from '@/lib/host-bridge';
12
12
  import { bindPromptToActiveChat, promptVisibleInChat } from './prompt-chat-scope';
13
13
 
14
14
  type AnswerValue = string | string[];
@@ -35,7 +35,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
35
35
  const answeredRequestIds = useRef(new Set<string>());
36
36
 
37
37
  useEffect(() => {
38
- const bridge = getElectronBridge();
38
+ const bridge = getHostBridge();
39
39
  if (!bridge?.askUser) return;
40
40
  let active = true;
41
41
  const addRequests = (
@@ -65,7 +65,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
65
65
  if (active) addRequests(requests, 'prepend');
66
66
  })
67
67
  .catch((error) => {
68
- console.error('恢复待回答问题失败:', error);
68
+ console.error('Failed to restore pending questions:', error);
69
69
  });
70
70
  return () => {
71
71
  active = false;
@@ -75,7 +75,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
75
75
 
76
76
  const answer = useCallback(
77
77
  (requestId: string, answers: Record<string, AnswerValue>) => {
78
- const bridge = getElectronBridge();
78
+ const bridge = getHostBridge();
79
79
  answeredRequestIds.current.add(requestId);
80
80
  setQueue((previous) => previous.filter((request) => request.requestId !== requestId));
81
81
  void bridge?.askUser?.answer({ requestId, answers });