@steerable/agent-shell-web 0.6.53 → 0.6.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +4 -4
  2. package/src/brand.ts +1 -1
  3. package/src/components/AgentSidebar.tsx +60 -52
  4. package/src/components/ChatHeader.tsx +41 -25
  5. package/src/components/ConfirmDialog.tsx +4 -3
  6. package/src/components/CreateProjectModal.tsx +29 -19
  7. package/src/components/LocalLlmSettingsModal.tsx +4 -3
  8. package/src/components/SidebarRelease.tsx +2 -1
  9. package/src/components/TerminalPanel.tsx +3 -2
  10. package/src/components/TerminalView.tsx +12 -7
  11. package/src/components/chat/ApprovalModal.tsx +39 -25
  12. package/src/components/chat/ApprovalPromptProvider.tsx +1 -1
  13. package/src/components/chat/AskUserPromptProvider.tsx +1 -1
  14. package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
  15. package/src/components/chat/AssistantMessage.tsx +18 -13
  16. package/src/components/chat/ChatInput.tsx +56 -40
  17. package/src/components/chat/ChatProjectBadge.tsx +24 -19
  18. package/src/components/chat/EmptyChatHero.tsx +2 -1
  19. package/src/components/chat/ExecPolicyPicker.tsx +12 -11
  20. package/src/components/chat/ExecutedActionsCard.tsx +45 -33
  21. package/src/components/chat/FilePathCode.tsx +8 -3
  22. package/src/components/chat/InterruptedTurnCard.tsx +4 -3
  23. package/src/components/chat/LocalChatPanel.tsx +2 -1
  24. package/src/components/chat/Markdown.tsx +2 -1
  25. package/src/components/chat/MessageList.tsx +3 -2
  26. package/src/components/chat/ModelPicker.tsx +25 -16
  27. package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
  28. package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
  29. package/src/components/chat/PlanTodoList.tsx +2 -1
  30. package/src/components/chat/ProjectTrustBanner.tsx +19 -5
  31. package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
  32. package/src/components/chat/SessionTodoList.tsx +8 -7
  33. package/src/components/chat/SessionTreeModal.tsx +10 -9
  34. package/src/components/chat/StreamingStatus.tsx +4 -3
  35. package/src/components/chat/SuggestedReplies.tsx +3 -2
  36. package/src/components/chat/TaskOutcomeCards.tsx +8 -5
  37. package/src/components/chat/TaskPanelModal.tsx +31 -21
  38. package/src/components/chat/TaskProcessPanel.tsx +14 -13
  39. package/src/components/chat/TurnFilesCard.tsx +14 -13
  40. package/src/components/chat/UserMessage.tsx +12 -11
  41. package/src/components/chat/executed-actions-model.ts +47 -35
  42. package/src/components/chat/orchestration-flow-model.ts +16 -5
  43. package/src/components/chat/process-status.ts +8 -7
  44. package/src/components/chat/todo-list-model.ts +5 -4
  45. package/src/components/chat/turn-files.ts +8 -8
  46. package/src/components/settings/AgentCapabilityFields.tsx +24 -21
  47. package/src/components/settings/AgentsSettingsPanel.tsx +30 -25
  48. package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
  49. package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
  50. package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
  51. package/src/components/settings/InsightsSettingsPanel.tsx +43 -28
  52. package/src/components/settings/LlmSettingsPanel.tsx +104 -83
  53. package/src/components/settings/McpSettingsPanel.tsx +60 -34
  54. package/src/components/settings/ModelIdCombobox.tsx +2 -1
  55. package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
  56. package/src/components/settings/PortableSettingsPanel.tsx +83 -55
  57. package/src/components/settings/PythonRunnerSettingsPanel.tsx +22 -19
  58. package/src/components/settings/SecuritySettingsPanel.tsx +55 -31
  59. package/src/components/settings/SettingsNavMenu.tsx +3 -2
  60. package/src/components/settings/SettingsSaveButton.tsx +2 -1
  61. package/src/components/settings/SkillsSettingsPanel.tsx +36 -31
  62. package/src/components/settings/TelemetrySettingsPanel.tsx +15 -12
  63. package/src/components/settings/UsagePanel.tsx +19 -20
  64. package/src/components/settings/WebSearchSettingsPanel.tsx +23 -14
  65. package/src/components/settings/llm-vendors.ts +12 -7
  66. package/src/components/settings/model-capabilities.ts +16 -15
  67. package/src/components/settings/preset-choice-model.ts +6 -3
  68. package/src/fixtures/browser-dev-data.json +24 -24
  69. package/src/hooks/useProjects.ts +1 -1
  70. package/src/i18n/agent-label.ts +15 -0
  71. package/src/i18n/index.tsx +68 -0
  72. package/src/i18n/runtime.ts +117 -0
  73. package/src/i18n/translate.ts +65 -0
  74. package/src/layouts/AgentLayout.tsx +7 -6
  75. package/src/lib/app-release.ts +15 -10
  76. package/src/lib/attachments.ts +16 -7
  77. package/src/lib/browser-dev-electron-mock.ts +34 -34
  78. package/src/lib/chat-title.ts +3 -2
  79. package/src/lib/chat-transport.ts +6 -2
  80. package/src/lib/date-groups.ts +23 -15
  81. package/src/lib/electron-bridge.ts +4 -5
  82. package/src/lib/http-bridge.ts +4 -1
  83. package/src/lib/portable.ts +8 -7
  84. package/src/lib/show-thinking-content.ts +15 -3
  85. package/src/main.tsx +25 -2
  86. package/src/pages/AgentPage.tsx +17 -12
  87. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  88. package/src/pages/SettingsPage.tsx +48 -42
  89. package/src/vite-env.d.ts +3 -0
  90. package/vite.base.js +6 -2
@@ -8,6 +8,7 @@ import { LuFolder, LuFolderPlus, LuX } from 'react-icons/lu';
8
8
  import { BRAND_NAME } from '@/brand';
9
9
  import { ConfirmDialog } from '@/components/ConfirmDialog';
10
10
  import { getElectronBridge } from '@/lib/electron-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) => {
@@ -84,11 +85,11 @@ export function CreateProjectModal({
84
85
  }
85
86
  const picker = getElectronBridge()?.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>
@@ -7,6 +7,7 @@ import {
7
7
  getElectronBridge,
8
8
  type AppReleaseSnapshot,
9
9
  } from '@/lib/electron-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);
@@ -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
2
  import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
3
+ import { t } from '@/i18n';
3
4
 
4
5
  // 把 xterm 那一坨 (~300KB) 拆到独立 chunk —— 用户没打开终端就不下载。
5
6
  // 跟原 Next.js 版本里 `dynamic(() => import('./TerminalView'), {ssr:false})`
@@ -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>
@@ -7,6 +7,7 @@ import '@xterm/xterm/css/xterm.css';
7
7
  import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
8
8
  import { getElectronBridge } from '@/lib/electron-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
@@ -48,7 +49,7 @@ export function TerminalView({
48
49
  const terminal = bridge?.terminal;
49
50
  if (!terminal) {
50
51
  setStatus('error');
51
- setStatusMsg('window.electron.terminal 不可用');
52
+ setStatusMsg(t('window.electron.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
  />
@@ -3,6 +3,7 @@ import {
3
3
  type ApprovalDecisionKind,
4
4
  type ApprovalPromptRequest,
5
5
  } from '@/lib/electron-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>
@@ -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;
@@ -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;
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { LuCheck, LuChevronLeft, LuPencil, LuWand } from 'react-icons/lu';
3
+ import { t } from '@/i18n';
3
4
 
4
5
  /**
5
6
  * AskUserQuestionMenu — W8 结构化提问的分步菜单卡片。
@@ -94,7 +95,7 @@ function initialDraft(question: AskQuestion, answer: AnswerValue | undefined): {
94
95
  return {
95
96
  selected,
96
97
  customMode: customBits.length > 0,
97
- customText: customBits.join('、'),
98
+ customText: customBits.join(t(', ')),
98
99
  };
99
100
  }
100
101
  if (typeof answer === 'string' && answer.length > 0) {
@@ -129,7 +130,7 @@ function Footer({
129
130
  className="inline-flex items-center gap-1 rounded-agent-md border border-agent-border px-2.5 py-1.5 text-xs font-medium text-agent-foreground transition hover:bg-agent-muted"
130
131
  >
131
132
  <LuChevronLeft size={12} />
132
- 上一题
133
+ {t('Previous')}
133
134
  </button>
134
135
  )}
135
136
  {onAutoContinue && (
@@ -139,7 +140,7 @@ function Footer({
139
140
  className="inline-flex items-center gap-1 rounded-agent-md px-2.5 py-1.5 text-xs font-medium text-agent-muted-foreground transition hover:bg-agent-muted hover:text-agent-foreground"
140
141
  >
141
142
  <LuWand size={12} />
142
- 交给AI决定
143
+ {t('Let AI decide')}
143
144
  </button>
144
145
  )}
145
146
  </div>
@@ -175,7 +176,7 @@ function PrimaryButton({
175
176
  ].join(' ')}
176
177
  >
177
178
  <LuCheck size={12} />
178
- {isLast ? '提交' : '下一题'}
179
+ {isLast ? t('Submit') : t('Next')}
179
180
  </button>
180
181
  );
181
182
  }
@@ -352,7 +353,7 @@ function SelectQuestionStep({
352
353
  <LuCheck size={10} />
353
354
  </span>
354
355
  <LuPencil size={12} className="shrink-0" />
355
- <span className="min-w-0 flex-1">其他 / 自定义</span>
356
+ <span className="min-w-0 flex-1">{t('Other / custom')}</span>
356
357
  </button>
357
358
  );
358
359
  };
@@ -395,7 +396,7 @@ function SelectQuestionStep({
395
396
  commitMulti();
396
397
  }
397
398
  }}
398
- placeholder={question.placeholder || '输入你的补充...'}
399
+ placeholder={question.placeholder || t('Add your details...')}
399
400
  className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
400
401
  />
401
402
  )}
@@ -413,7 +414,7 @@ function SelectQuestionStep({
413
414
  commitCustom();
414
415
  }
415
416
  }}
416
- placeholder={question.placeholder || '输入你的回答...'}
417
+ placeholder={question.placeholder || t('Type your answer...')}
417
418
  className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
418
419
  />
419
420
  <button
@@ -424,7 +425,7 @@ function SelectQuestionStep({
424
425
  }}
425
426
  className="text-xs text-agent-muted-foreground transition hover:text-agent-foreground"
426
427
  >
427
- ← 返回选项
428
+ {t('← Back to options')}
428
429
  </button>
429
430
  </div>
430
431
  )}
@@ -434,7 +435,11 @@ function SelectQuestionStep({
434
435
  canGoBack={Boolean(onBack)}
435
436
  onBack={onBack}
436
437
  onAutoContinue={onAutoContinue}
437
- hint={question.multiSelect ? '↑↓ 移动,Enter 勾选' : '↑↓ 选择,Enter 确认'}
438
+ hint={
439
+ question.multiSelect
440
+ ? t('↑↓ to move, Enter to check')
441
+ : t('↑↓ to choose, Enter to confirm')
442
+ }
438
443
  >
439
444
  {question.multiSelect ? (
440
445
  <PrimaryButton
@@ -511,7 +516,7 @@ function TextQuestionStep({
511
516
  commit();
512
517
  }
513
518
  }}
514
- placeholder={question.placeholder || '输入你的回答...'}
519
+ placeholder={question.placeholder || t('Type your answer...')}
515
520
  className="w-full rounded-agent-md border border-agent-border bg-agent-canvas px-3 py-2 text-xs text-agent-foreground outline-none placeholder:text-agent-muted-foreground focus:border-agent-foreground focus:ring-1 focus:ring-agent-foreground"
516
521
  />
517
522
  </div>
@@ -520,7 +525,7 @@ function TextQuestionStep({
520
525
  canGoBack={Boolean(onBack)}
521
526
  onBack={onBack}
522
527
  onAutoContinue={onAutoContinue}
523
- hint="Enter 确认"
528
+ hint={t('Enter to confirm')}
524
529
  >
525
530
  <PrimaryButton disabled={!value.trim()} isLast={isLast} onClick={commit} />
526
531
  </Footer>
@@ -581,16 +586,16 @@ export function AskUserQuestionMenu({
581
586
  <div className="flex items-start justify-between gap-2 border-b border-agent-border px-3 py-2">
582
587
  <div className="min-w-0 flex-1">
583
588
  <p className="text-xs font-medium text-agent-foreground">
584
- {intro || '需要你的输入'}
589
+ {intro || t('Your input is needed')}
585
590
  </p>
586
591
  <p className="mt-0.5 text-[11px] text-agent-muted-foreground">
587
- 问题 {step + 1} / {normalized.length}
592
+ {t('Question {current} / {total}', { current: step + 1, total: normalized.length })}
588
593
  {bottomHint ? ` · ${bottomHint}` : ''}
589
594
  </p>
590
595
  </div>
591
596
  {current.type === 'password' || current.multiSelect ? (
592
597
  <span className="rounded-full border border-agent-border px-2 py-0.5 text-[10px] font-medium text-agent-muted-foreground">
593
- {current.type === 'password' ? '密码输入' : '可多选'}
598
+ {current.type === 'password' ? t('Password input') : t('Multiple choice')}
594
599
  </span>
595
600
  ) : null}
596
601
  </div>