@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
@@ -8,6 +8,8 @@
8
8
  * result_count / status / bytes / truncated 这些小字段原样保留。
9
9
  */
10
10
 
11
+ import { t } from '@/i18n';
12
+ import { agentLabel } from '@/i18n/agent-label';
11
13
  import { findAgentForProfile } from './orchestration-children-model';
12
14
 
13
15
  interface WebToolResultData {
@@ -61,19 +63,21 @@ export function summarizeWebAction(
61
63
 
62
64
  if (tool === 'web_search') {
63
65
  const query = typeof obj.query === 'string' ? obj.query : '';
64
- const head = `搜索“${query.length > 40 ? `${query.slice(0, 37)}…` : query}”`;
66
+ const head = t('Search "{query}"', {
67
+ query: query.length > 40 ? `${query.slice(0, 37)}…` : query,
68
+ });
65
69
  if (failed || !data) return head;
66
70
  const count = typeof data.result_count === 'number' ? data.result_count : null;
67
- return count === null ? head : `${head} → ${count} 条结果`;
71
+ return count === null ? head : t('{head} → {count} results', { head, count });
68
72
  }
69
73
  if (tool === 'web_fetch') {
70
- const head = `抓取 ${shortUrl(obj.url) ?? '未知地址'}`;
74
+ const head = t('Fetch {url}', { url: shortUrl(obj.url) ?? t('unknown address') });
71
75
  if (failed || !data) return head;
72
76
  const parts: string[] = [];
73
77
  if (typeof data.status === 'number') parts.push(String(data.status));
74
78
  const size = formatBytes(data.bytes);
75
79
  if (size) parts.push(size);
76
- if (data.truncated === true) parts.push('已截断');
80
+ if (data.truncated === true) parts.push(t('truncated'));
77
81
  return parts.length > 0 ? `${head} → ${parts.join(' · ')}` : head;
78
82
  }
79
83
  return null;
@@ -99,7 +103,7 @@ export function summarizeRunCodeAction(
99
103
  const description =
100
104
  typeof obj.description === 'string' && obj.description.trim()
101
105
  ? obj.description.trim()
102
- : '程序';
106
+ : t('Program');
103
107
  const head =
104
108
  description.length > 40 ? `${description.slice(0, 37)}…` : description;
105
109
  const data = resultData(result);
@@ -110,10 +114,10 @@ export function summarizeRunCodeAction(
110
114
  result &&
111
115
  typeof result === 'object' &&
112
116
  (result as { success?: unknown }).success === false;
113
- if (failed) return `程序「${head}」失败`;
117
+ if (failed) return t('Program "{name}" failed', { name: head });
114
118
  return calls.length > 0
115
- ? `程序「${head}」· ${calls.length} 个内层工具`
116
- : `程序「${head}」`;
119
+ ? t('Program "{name}" · {count} inner tools', { name: head, count: calls.length })
120
+ : t('Program "{name}"', { name: head });
117
121
  }
118
122
 
119
123
  export interface ExecutedActionLike {
@@ -129,12 +133,17 @@ export interface DelegateSubagentView {
129
133
  }
130
134
 
131
135
  const BUILTIN_DELEGATE_LABELS: Record<string, string> = {
132
- explore: '探索',
133
- research: '调研',
134
- coder: '编程',
135
- 'general-purpose': '通用',
136
+ explore: 'Explore',
137
+ research: 'Research',
138
+ coder: 'Coding',
139
+ 'general-purpose': 'General',
136
140
  };
137
141
 
142
+ function builtinDelegateLabel(profile: string): string {
143
+ const source = BUILTIN_DELEGATE_LABELS[profile];
144
+ return source ? t(source) : profile;
145
+ }
146
+
138
147
  const BUILTIN_DELEGATE_COLORS: Record<string, string> = {
139
148
  explore: '#0ea5e9',
140
149
  research: '#2563eb',
@@ -157,7 +166,7 @@ function participantFromProfile(
157
166
  const match = findAgentForProfile(profile, agents);
158
167
  return {
159
168
  key: match?.id ?? profile,
160
- name: match?.name ?? BUILTIN_DELEGATE_LABELS[profile] ?? profile,
169
+ name: match ? agentLabel(match) : builtinDelegateLabel(profile),
161
170
  color: match?.color || BUILTIN_DELEGATE_COLORS[profile] || '#7c3aed',
162
171
  };
163
172
  }
@@ -187,7 +196,7 @@ export function collectTurnParticipants(
187
196
  const pushAgent = (agent: AgentRef) => {
188
197
  push({
189
198
  key: agent.id,
190
- name: agent.name,
199
+ name: agentLabel(agent),
191
200
  color: agent.color || '#7c3aed',
192
201
  });
193
202
  };
@@ -196,7 +205,7 @@ export function collectTurnParticipants(
196
205
  if (parent && !mentioned.includes(parent.id)) {
197
206
  push({
198
207
  key: parent.id,
199
- name: parent.name,
208
+ name: agentLabel(parent),
200
209
  color: parent.color || '#7c3aed',
201
210
  });
202
211
  }
@@ -209,7 +218,7 @@ export function collectTurnParticipants(
209
218
  if (parent) {
210
219
  push({
211
220
  key: parent.id,
212
- name: parent.name,
221
+ name: agentLabel(parent),
213
222
  color: parent.color || '#7c3aed',
214
223
  });
215
224
  }
@@ -285,10 +294,10 @@ export function delegateSubagentDisplayName(
285
294
  profile: string | null,
286
295
  agents: ReadonlyArray<{ id: string; slug: string | null; name: string }>,
287
296
  ): string {
288
- if (!profile) return '子代理';
297
+ if (!profile) return t('Subagent');
289
298
  const match = findAgentForProfile(profile, agents);
290
- if (match) return match.name;
291
- return BUILTIN_DELEGATE_LABELS[profile] ?? profile;
299
+ if (match) return agentLabel(match);
300
+ return builtinDelegateLabel(profile);
292
301
  }
293
302
 
294
303
  /** 任务正文作行摘要;没有任务时返回 null,调用方不再回落 JSON。 */
@@ -374,22 +383,25 @@ export function parseToolEnvelope(result: unknown): ToolEnvelope {
374
383
  };
375
384
  }
376
385
 
377
- const DELEGATE_STATUS_ZH: Record<string, string> = {
378
- budget_exhausted: '额度耗尽',
379
- failed: '失败',
380
- cancelled: '已取消',
386
+ const DELEGATE_STATUS_LABELS: Record<string, string> = {
387
+ budget_exhausted: 'budget exhausted',
388
+ failed: 'failure',
389
+ cancelled: 'cancellation',
381
390
  };
382
391
 
383
392
  /** 把 sidecar 的英文失败句收成行头/错误栏可读的中文。 */
384
393
  export function humanizeDelegateError(error: string | null): string | null {
385
394
  if (!error) return null;
386
- if (error.includes('orchestration_budget_exceeded')) return '编排额度已用尽';
395
+ if (error.includes('orchestration_budget_exceeded')) return t('Orchestration budget used up');
387
396
  const ended = error.match(/sub-agent ended with status:\s*(\S+)/i);
388
397
  if (ended) {
389
398
  const status = ended[1].replace(/[.,;]+$/, '');
390
- return `子代理因${DELEGATE_STATUS_ZH[status] ?? status}结束`;
399
+ const label = DELEGATE_STATUS_LABELS[status];
400
+ return t('Subagent ended due to {reason}', { reason: label ? t(label) : status });
401
+ }
402
+ if (error.includes('budget_exhausted')) {
403
+ return t('Subagent ended due to {reason}', { reason: t('budget exhausted') });
391
404
  }
392
- if (error.includes('budget_exhausted')) return '子代理因额度耗尽结束';
393
405
  return error;
394
406
  }
395
407
 
@@ -475,7 +487,7 @@ export interface InspectTaskInput {
475
487
  }
476
488
 
477
489
  export function inspectTaskTitle(input: InspectTaskInput): string {
478
- return input.title ?? input.task ?? '后台任务';
490
+ return input.title ?? input.task ?? t('Background task');
479
491
  }
480
492
 
481
493
  /** 工具行能打开右侧「后台推理」时抽出 taskId 和标题。 */
@@ -493,7 +505,7 @@ export function inspectableTaskFromAction(
493
505
  parseTaskRun(args).task
494
506
  ?? parsed.items[0]?.task
495
507
  ?? parsed.items[0]?.hint
496
- ?? '后台任务';
508
+ ?? t('Background task');
497
509
  return { id, title };
498
510
  }
499
511
 
@@ -539,13 +551,13 @@ export function isTaskFamilyTool(tool: string): tool is TaskFamilyTool {
539
551
  export function taskToolLabel(tool: string, args: unknown): string | null {
540
552
  switch (tool) {
541
553
  case 'task_run':
542
- return parseTaskRun(args).worktree ? '后台任务 · 隔离' : '后台任务';
554
+ return parseTaskRun(args).worktree ? t('Background task · Isolated') : t('Background task');
543
555
  case 'task_send':
544
- return '转达任务';
556
+ return t('Relay to task');
545
557
  case 'task_status':
546
- return '查询任务';
558
+ return t('Check tasks');
547
559
  case 'task_result':
548
- return '收取结果';
560
+ return t('Collect result');
549
561
  default:
550
562
  return null;
551
563
  }
@@ -569,8 +581,8 @@ export function summarizeTaskAction(
569
581
  const parsed = parseTaskRun(args);
570
582
  if (!parsed.task) return null;
571
583
  const head = clip(parsed.task);
572
- if (taskRunStatus(result) === 'blocked') return `${head} · 等待依赖`;
573
- if (parsed.worktree) return `${head} · 隔离工作区`;
584
+ if (taskRunStatus(result) === 'blocked') return t('{head} · Waiting on dependencies', { head });
585
+ if (parsed.worktree) return t('{head} · Isolated workspace', { head });
574
586
  return head;
575
587
  }
576
588
  if (tool === 'task_send') {
@@ -581,7 +593,7 @@ export function summarizeTaskAction(
581
593
  const id = stringField(asObject(args), 'taskId');
582
594
  if (id) return shortTaskId(id);
583
595
  const total = asObject(result)?.total;
584
- return typeof total === 'number' ? `${total} 个任务` : '本会话';
596
+ return typeof total === 'number' ? t('{count} tasks', { count: total }) : t('This chat');
585
597
  }
586
598
  if (tool === 'task_result') {
587
599
  const id = stringField(asObject(args), 'taskId');
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { ChildInfo } from './orchestration-children-model';
9
9
  import { parseToolEnvelope } from './executed-actions-model';
10
+ import { t } from '@/i18n';
10
11
 
11
12
  export const ORCHESTRATION_TOOL_NAMES = [
12
13
  'agent_spawn',
@@ -102,11 +103,21 @@ function buildOrchestrationFlow(nodes: OrchestrationChildNode[]): OrchestrationF
102
103
 
103
104
  let summaryCopy = '';
104
105
  if (isAllCompleted) {
105
- summaryCopy = `${completedCount}/${totalCount} 全部完成`;
106
+ summaryCopy = t('{completed}/{total} all done', {
107
+ completed: completedCount,
108
+ total: totalCount,
109
+ });
106
110
  } else if (hasActive) {
107
- summaryCopy = `协同执行中 (${completedCount}/${totalCount} 完成)...`;
111
+ summaryCopy = t('Running together ({completed}/{total} done)...', {
112
+ completed: completedCount,
113
+ total: totalCount,
114
+ });
108
115
  } else {
109
- summaryCopy = `${completedCount} 完成 · ${totalCount - completedCount} 结束 (${totalCount} 个子任务)`;
116
+ summaryCopy = t('{completed} done · {ended} ended ({total} subtasks)', {
117
+ completed: completedCount,
118
+ ended: totalCount - completedCount,
119
+ total: totalCount,
120
+ });
110
121
  }
111
122
 
112
123
  return {
@@ -226,7 +237,7 @@ export function extractOrchestrationFlow(
226
237
 
227
238
  if (envelope.success === false || envelope.error) {
228
239
  node.status = 'failed';
229
- node.error = envelope.error || '派发失败';
240
+ node.error = envelope.error || t('Dispatch failed');
230
241
  }
231
242
  } else if (action.tool === 'agent_wait') {
232
243
  const childId = typeof rawArgs.childId === 'string' ? rawArgs.childId.trim() : '';
@@ -260,7 +271,7 @@ export function extractOrchestrationFlow(
260
271
  node.status = 'completed';
261
272
  } else if (waitStatus === 'failed' || waitStatus === 'error' || envelope.success === false) {
262
273
  node.status = 'failed';
263
- node.error = envelope.error || '执行失败';
274
+ node.error = envelope.error || t('Run failed');
264
275
  } else if (waitStatus === 'cancelled') {
265
276
  node.status = 'cancelled';
266
277
  } else if (waitStatus === 'interrupted') {
@@ -4,6 +4,7 @@
4
4
  * output stage (reasoning + reply), excluding tool-wait gaps.
5
5
  */
6
6
 
7
+ import { t } from '@/i18n';
7
8
  import { formatElapsedCompact } from './elapsed';
8
9
  import {
9
10
  countProcessReasoning,
@@ -75,16 +76,16 @@ export function processStatusLabel(input: {
75
76
  elapsedMs?: number;
76
77
  }): string {
77
78
  const { process, isStreaming, elapsedMs } = input;
78
- if (isStreaming) return '工作中';
79
+ if (isStreaming) return t('Working');
79
80
 
80
81
  const parts: string[] = [];
81
82
  const thinks = countProcessReasoning(process);
82
83
  const tools = countProcessTools(process);
83
84
  const elapsed = formatElapsedPart(false, elapsedMs);
84
- if (elapsed) parts.push(`工作了 ${elapsed}`);
85
- if (thinks > 0) parts.push(`思考 ${thinks} 次`);
86
- if (tools > 0) parts.push(`工具调用 ${tools} 次`);
87
- if (parts.length === 0) return '执行过程';
85
+ if (elapsed) parts.push(t('Worked {elapsed}', { elapsed }));
86
+ if (thinks > 0) parts.push(t('Thought {count} times', { count: thinks }));
87
+ if (tools > 0) parts.push(t('{count} tool calls', { count: tools }));
88
+ if (parts.length === 0) return t('Run process');
88
89
  return parts.join(' · ');
89
90
  }
90
91
 
@@ -96,9 +97,9 @@ export function thinkingFoldLabel(input: {
96
97
  }): string {
97
98
  const elapsed = formatElapsedPart(input.isLive, input.elapsedMs);
98
99
  if (input.isLive) {
99
- return elapsed ? `思考中 · ${elapsed}` : '思考中';
100
+ return elapsed ? t('Thinking · {elapsed}', { elapsed }) : t('Thinking...');
100
101
  }
101
- return elapsed ? `思考 · ${elapsed}` : '思考';
102
+ return elapsed ? t('Thought for {elapsed}', { elapsed }) : t('Thinking');
102
103
  }
103
104
 
104
105
  /** Reasoning + reply tokens for the message-footer tok/s. */
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { ExecutedAction } from './ExecutedActionsCard';
10
10
  import type { TurnBlock } from './turn-timeline';
11
+ import { t } from '@/i18n';
11
12
 
12
13
  export const TODO_STATUSES = ['pending', 'in_progress', 'completed'] as const;
13
14
  export type TodoStatus = (typeof TODO_STATUSES)[number];
@@ -108,16 +109,16 @@ export function summarizeTodoWriteAction(
108
109
  ): string | null {
109
110
  if (tool !== 'todo_write') return null;
110
111
  const todos = extractTodosFromAction({ tool, arguments: args, result });
111
- if (!todos) return '任务清单';
112
+ if (!todos) return t('Task list');
112
113
  const { completed, total, inProgress } = summarizeTodos(todos);
113
114
  const current = todos.find((todo) => todo.status === 'in_progress');
114
115
  if (inProgress > 0 && current) {
115
116
  const label =
116
117
  current.content.length > 28 ? `${current.content.slice(0, 25)}…` : current.content;
117
- return `任务清单 ${completed}/${total} · ${label}`;
118
+ return t('Task list {completed}/{total} · {label}', { completed, total, label });
118
119
  }
119
- if (completed === total) return `任务清单 ${completed}/${total} 已完成`;
120
- return `任务清单 ${completed}/${total}`;
120
+ if (completed === total) return t('Task list {completed}/{total} done', { completed, total });
121
+ return t('Task list {completed}/{total}', { completed, total });
121
122
  }
122
123
 
123
124
  export function latestTodosFromActions(
@@ -141,7 +141,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
141
141
  case '.tsv':
142
142
  case '.numbers':
143
143
  return {
144
- label: '电子表格',
144
+ label: 'Spreadsheet',
145
145
  extBadge: extClean,
146
146
  kind: 'spreadsheet',
147
147
  themeColor: 'emerald',
@@ -150,7 +150,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
150
150
  case '.ppt': // shell-neutral:allow — Office 幻灯片扩展名,不是产品品牌
151
151
  case '.key':
152
152
  return {
153
- label: '演示文稿',
153
+ label: 'Presentation',
154
154
  extBadge: extClean,
155
155
  kind: 'presentation',
156
156
  themeColor: 'amber',
@@ -160,14 +160,14 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
160
160
  case '.pages':
161
161
  case '.rtf':
162
162
  return {
163
- label: '文档',
163
+ label: 'Document',
164
164
  extBadge: extClean,
165
165
  kind: 'document',
166
166
  themeColor: 'blue',
167
167
  };
168
168
  case '.pdf':
169
169
  return {
170
- label: '文档',
170
+ label: 'Document',
171
171
  extBadge: 'PDF',
172
172
  kind: 'pdf',
173
173
  themeColor: 'rose',
@@ -179,7 +179,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
179
179
  case '.webp':
180
180
  case '.svg':
181
181
  return {
182
- label: '图像',
182
+ label: 'Image',
183
183
  extBadge: extClean,
184
184
  kind: 'image',
185
185
  themeColor: 'purple',
@@ -189,7 +189,7 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
189
189
  case '.gz':
190
190
  case '.7z':
191
191
  return {
192
- label: '压缩包',
192
+ label: 'Archive',
193
193
  extBadge: extClean,
194
194
  kind: 'archive',
195
195
  themeColor: 'amber',
@@ -197,14 +197,14 @@ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
197
197
  case '.html':
198
198
  case '.htm':
199
199
  return {
200
- label: '网页',
200
+ label: 'Web page',
201
201
  extBadge: 'HTML',
202
202
  kind: 'document',
203
203
  themeColor: 'blue',
204
204
  };
205
205
  default:
206
206
  return {
207
- label: '文件',
207
+ label: 'File',
208
208
  extBadge: extClean,
209
209
  kind: 'file',
210
210
  themeColor: 'slate',
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
12
  import { listChatTasks, type LocalTask } from '@/lib/local-api';
13
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
13
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
14
14
 
15
15
  export interface ChatTaskSummary {
16
16
  total: number;
@@ -68,7 +68,7 @@ export function useChatTasks(chatId: string | null): ChatTasksState {
68
68
  const seenRef = useRef<Map<string, LocalTask['status']> | null>(null);
69
69
 
70
70
  const refresh = useCallback(async () => {
71
- if (!chatId || !isElectron()) {
71
+ if (!chatId || !hasHostBridge()) {
72
72
  setTasks(NO_TASKS);
73
73
  return;
74
74
  }
@@ -105,7 +105,7 @@ export function useChatTasks(chatId: string | null): ChatTasksState {
105
105
 
106
106
  useEffect(() => {
107
107
  if (!chatId) return;
108
- const bridge = getElectronBridge();
108
+ const bridge = getHostBridge();
109
109
  if (!bridge?.onTaskUpdated) return;
110
110
  return bridge.onTaskUpdated((payload) => {
111
111
  if (payload.chatId === chatId) void refresh();
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
3
- import { isElectron } from '@/lib/electron-bridge';
3
+ import { t } from '@/i18n';
4
+ import { hasHostBridge } from '@/lib/host-bridge';
4
5
  import {
5
6
  listChatAgentSkills,
6
7
  listChatAgentTools,
@@ -43,16 +44,16 @@ const TOOL_POLICY_MODES: Array<{
43
44
  label: string;
44
45
  hint: string;
45
46
  }> = [
46
- { mode: 'all', label: '全部工具', hint: '不限制,可调用所有已接线的工具。' },
47
+ { mode: 'all', label: 'All tools', hint: 'No restriction. The agent can call every wired tool.' },
47
48
  {
48
49
  mode: 'allowlist',
49
- label: '仅允许勾选',
50
- hint: '只有勾选的工具进入工具列表,其余连名字都看不到。',
50
+ label: 'Only checked',
51
+ hint: 'Only checked tools enter the tool list. The agent cannot even see the names of the rest.',
51
52
  },
52
53
  {
53
54
  mode: 'denylist',
54
- label: '禁用勾选',
55
- hint: '勾选的工具被拒绝,其余照常可用。',
55
+ label: 'Block checked',
56
+ hint: 'Checked tools are refused. The rest stay available.',
56
57
  },
57
58
  ];
58
59
 
@@ -153,7 +154,7 @@ export function AgentCapabilityFields({
153
154
  const [toolFilter, setToolFilter] = useState('');
154
155
 
155
156
  const fetchCatalogs = useCallback(async () => {
156
- if (!isElectron()) return;
157
+ if (!hasHostBridge()) return;
157
158
  setLoading(true);
158
159
  try {
159
160
  const [skillRes, toolRes] = await Promise.all([
@@ -203,24 +204,26 @@ export function AgentCapabilityFields({
203
204
  <div className="space-y-3 rounded-agent-md border border-agent-border/50 bg-agent-canvas/60 p-3">
204
205
  <section className="space-y-1.5">
205
206
  <div className="flex items-center justify-between gap-2">
206
- <h5 className="text-[11px] font-semibold text-agent-foreground">技能</h5>
207
+ <h5 className="text-[11px] font-semibold text-agent-foreground">{t('Skills')}</h5>
207
208
  {value.skillIds.length > 0 && (
208
209
  <span className="text-[10px] text-agent-muted-foreground">
209
- 已选 {value.skillIds.length}
210
+ {t('{count} selected', { count: value.skillIds.length })}
210
211
  </span>
211
212
  )}
212
213
  </div>
213
214
  <p className="text-[10px] text-agent-muted-foreground">
214
- 勾选的技能正文会常驻该智能体的系统提示词,不必等触发条件命中。
215
+ {t(
216
+ "Checked skills stay in this agent's system prompt without waiting for their triggers to match.",
217
+ )}
215
218
  </p>
216
219
  {loading ? (
217
220
  <div className="flex items-center gap-2 py-3 text-[11px] text-agent-muted-foreground">
218
221
  <LuLoaderCircle className="h-3 w-3 animate-spin" />
219
- 获取技能目录...
222
+ {t('Loading skill catalog...')}
220
223
  </div>
221
224
  ) : skills.length === 0 ? (
222
225
  <p className="py-2 text-[11px] text-agent-muted-foreground">
223
- 暂无可勾选的技能,可在「Skill 设置」里导入。
226
+ {t('No skills to check yet. Import them in "Skill settings".')}
224
227
  </p>
225
228
  ) : (
226
229
  <>
@@ -231,7 +234,7 @@ export function AgentCapabilityFields({
231
234
  type="text"
232
235
  value={skillFilter}
233
236
  onChange={(event) => setSkillFilter(event.target.value)}
234
- placeholder="筛选技能"
237
+ placeholder={t('Filter skills')}
235
238
  className="h-7 w-full rounded-agent-md border border-agent-border bg-agent-canvas pl-7 pr-2 text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
236
239
  data-testid="agent-form-skill-filter"
237
240
  />
@@ -247,7 +250,7 @@ export function AgentCapabilityFields({
247
250
  }
248
251
  testId={`agent-form-skill-${skill.id}`}
249
252
  title={skill.displayName || skill.name}
250
- badge={skill.layer === 'eager' ? '常驻' : '按需'}
253
+ badge={skill.layer === 'eager' ? t('Always on') : t('On demand')}
251
254
  description={skill.description}
252
255
  />
253
256
  ))}
@@ -261,21 +264,21 @@ export function AgentCapabilityFields({
261
264
  onChange({ ...value, allowExternalSkills: !value.allowExternalSkills })
262
265
  }
263
266
  testId="agent-form-allow-external-skills"
264
- label="允许使用未勾选的技能"
265
- hint="关掉后这个智能体只能用上面勾选的技能,其余一律加载不到。"
267
+ label={t('Allow unchecked skills')}
268
+ hint={t('When off, this agent can only use the skills checked above. No other skill can load.')}
266
269
  />
267
270
  <SwitchRow
268
271
  checked={value.loadAllSkills}
269
272
  onChange={() => onChange({ ...value, loadAllSkills: !value.loadAllSkills })}
270
273
  testId="agent-form-load-all-skills"
271
- label="无视触发条件,加载全部技能"
272
- hint="内置「智能助手」就是这个模式。会显著增加每轮提示词长度。"
274
+ label={t('Ignore triggers and load all skills')}
275
+ hint={t('The built-in "Assistant" uses this mode. It makes every turn\'s prompt much longer.')}
273
276
  />
274
277
  </div>
275
278
  </section>
276
279
 
277
280
  <section className="space-y-1.5 border-t border-agent-border/40 pt-3">
278
- <h5 className="text-[11px] font-semibold text-agent-foreground">工具权限</h5>
281
+ <h5 className="text-[11px] font-semibold text-agent-foreground">{t('Tool permissions')}</h5>
279
282
  <div className="flex flex-wrap gap-1" data-testid="agent-form-tool-modes">
280
283
  {TOOL_POLICY_MODES.map((item) => (
281
284
  <button
@@ -290,12 +293,12 @@ export function AgentCapabilityFields({
290
293
  : 'bg-agent-muted text-agent-muted-foreground hover:text-agent-foreground'
291
294
  }`}
292
295
  >
293
- {item.label}
296
+ {t(item.label)}
294
297
  </button>
295
298
  ))}
296
299
  </div>
297
300
  {activeMode && (
298
- <p className="text-[10px] text-agent-muted-foreground">{activeMode.hint}</p>
301
+ <p className="text-[10px] text-agent-muted-foreground">{t(activeMode.hint)}</p>
299
302
  )}
300
303
  {policy.mode !== 'all' && (
301
304
  <>
@@ -304,7 +307,7 @@ export function AgentCapabilityFields({
304
307
  className="text-[10px] text-agent-destructive"
305
308
  data-testid="agent-form-tool-policy-empty"
306
309
  >
307
- 一个工具都没勾选——保存后按「全部工具」处理。
310
+ {t('No tool is checked. Saving treats this as "All tools".')}
308
311
  </p>
309
312
  )}
310
313
  <div className="relative">
@@ -313,7 +316,7 @@ export function AgentCapabilityFields({
313
316
  type="text"
314
317
  value={toolFilter}
315
318
  onChange={(event) => setToolFilter(event.target.value)}
316
- placeholder="筛选工具"
319
+ placeholder={t('Filter tools')}
317
320
  className="h-7 w-full rounded-agent-md border border-agent-border bg-agent-canvas pl-7 pr-2 text-[11px] text-agent-foreground focus:outline-none focus:ring-2 focus:ring-agent-foreground/30"
318
321
  data-testid="agent-form-tool-filter"
319
322
  />