@steerable/agent-shell-web 0.6.53 → 0.6.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +3 -4
  2. package/package.json +4 -4
  3. package/src/AppShell.tsx +3 -3
  4. package/src/brand.ts +1 -1
  5. package/src/components/AgentSidebar.tsx +64 -56
  6. package/src/components/ChatHeader.tsx +41 -25
  7. package/src/components/ConfirmDialog.tsx +4 -3
  8. package/src/components/CreateProjectModal.tsx +31 -21
  9. package/src/components/LocalLlmSettingsModal.tsx +4 -3
  10. package/src/components/SidebarRelease.tsx +6 -5
  11. package/src/components/TerminalPanel.tsx +6 -5
  12. package/src/components/TerminalView.tsx +15 -10
  13. package/src/components/chat/ApprovalModal.tsx +40 -26
  14. package/src/components/chat/ApprovalPromptProvider.tsx +5 -5
  15. package/src/components/chat/AskUserPromptProvider.tsx +4 -4
  16. package/src/components/chat/AskUserQuestionMenu.tsx +19 -14
  17. package/src/components/chat/AssistantMessage.tsx +18 -13
  18. package/src/components/chat/ChatInput.tsx +56 -40
  19. package/src/components/chat/ChatProjectBadge.tsx +27 -22
  20. package/src/components/chat/EmptyChatHero.tsx +2 -1
  21. package/src/components/chat/ExecPolicyPicker.tsx +12 -11
  22. package/src/components/chat/ExecutedActionsCard.tsx +45 -33
  23. package/src/components/chat/FilePathCode.tsx +8 -3
  24. package/src/components/chat/InterruptedTurnCard.tsx +4 -3
  25. package/src/components/chat/LocalChatPanel.tsx +2 -1
  26. package/src/components/chat/Markdown.tsx +2 -1
  27. package/src/components/chat/MessageList.tsx +3 -2
  28. package/src/components/chat/ModelPicker.tsx +25 -16
  29. package/src/components/chat/OrchestrationChildrenCard.tsx +2 -1
  30. package/src/components/chat/OrchestrationFlowCard.tsx +23 -22
  31. package/src/components/chat/PlanTodoList.tsx +2 -1
  32. package/src/components/chat/ProjectTrustBanner.tsx +19 -5
  33. package/src/components/chat/SessionOrchestrationFlow.tsx +27 -26
  34. package/src/components/chat/SessionTodoList.tsx +8 -7
  35. package/src/components/chat/SessionTreeModal.tsx +10 -9
  36. package/src/components/chat/StreamingStatus.tsx +4 -3
  37. package/src/components/chat/SuggestedReplies.tsx +3 -2
  38. package/src/components/chat/TaskOutcomeCards.tsx +8 -5
  39. package/src/components/chat/TaskPanelModal.tsx +33 -23
  40. package/src/components/chat/TaskProcessPanel.tsx +16 -15
  41. package/src/components/chat/TurnFilesCard.tsx +14 -13
  42. package/src/components/chat/UserMessage.tsx +12 -11
  43. package/src/components/chat/executed-actions-model.ts +47 -35
  44. package/src/components/chat/orchestration-flow-model.ts +16 -5
  45. package/src/components/chat/process-status.ts +8 -7
  46. package/src/components/chat/todo-list-model.ts +5 -4
  47. package/src/components/chat/turn-files.ts +8 -8
  48. package/src/components/chat/useChatTasks.ts +3 -3
  49. package/src/components/settings/AgentCapabilityFields.tsx +26 -23
  50. package/src/components/settings/AgentsSettingsPanel.tsx +33 -28
  51. package/src/components/settings/AppUpdateSettingsPanel.tsx +3 -2
  52. package/src/components/settings/AppearanceSettingsPanel.tsx +81 -37
  53. package/src/components/settings/DiagnoseSettingsPanel.tsx +11 -8
  54. package/src/components/settings/InsightsSettingsPanel.tsx +58 -43
  55. package/src/components/settings/LlmSettingsPanel.tsx +107 -86
  56. package/src/components/settings/McpSettingsPanel.tsx +72 -46
  57. package/src/components/settings/ModelIdCombobox.tsx +2 -1
  58. package/src/components/settings/OrchestrationSettingsPanel.tsx +8 -5
  59. package/src/components/settings/PortableSettingsPanel.tsx +83 -55
  60. package/src/components/settings/PythonRunnerSettingsPanel.tsx +26 -23
  61. package/src/components/settings/SecuritySettingsPanel.tsx +58 -34
  62. package/src/components/settings/SettingsNavMenu.tsx +3 -2
  63. package/src/components/settings/SettingsSaveButton.tsx +2 -1
  64. package/src/components/settings/SkillsSettingsPanel.tsx +43 -38
  65. package/src/components/settings/TelemetrySettingsPanel.tsx +20 -17
  66. package/src/components/settings/UsagePanel.tsx +22 -23
  67. package/src/components/settings/WebSearchSettingsPanel.tsx +28 -19
  68. package/src/components/settings/llm-vendors.ts +12 -7
  69. package/src/components/settings/model-capabilities.ts +16 -15
  70. package/src/components/settings/preset-choice-model.ts +6 -3
  71. package/src/fixtures/browser-dev-data.json +24 -24
  72. package/src/hooks/useChatsAndAgents.ts +9 -9
  73. package/src/hooks/useProjects.ts +3 -3
  74. package/src/i18n/agent-label.ts +15 -0
  75. package/src/i18n/index.tsx +68 -0
  76. package/src/i18n/runtime.ts +117 -0
  77. package/src/i18n/translate.ts +65 -0
  78. package/src/layouts/AgentLayout.tsx +13 -12
  79. package/src/lib/app-release.ts +16 -11
  80. package/src/lib/attachments.ts +18 -9
  81. package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +42 -42
  82. package/src/lib/chat-title.ts +3 -2
  83. package/src/lib/chat-transport.ts +16 -12
  84. package/src/lib/date-groups.ts +23 -15
  85. package/src/lib/host-bridge.ts +371 -4
  86. package/src/lib/host-paste.ts +1 -1
  87. package/src/lib/http-bridge.ts +12 -9
  88. package/src/lib/insights.ts +2 -2
  89. package/src/lib/local-api.ts +5 -5
  90. package/src/lib/portable.ts +11 -10
  91. package/src/lib/show-thinking-content.ts +15 -3
  92. package/src/lib/slash-sources.ts +3 -3
  93. package/src/lib/tauri-bridge.ts +1 -1
  94. package/src/main.tsx +27 -4
  95. package/src/pages/AgentPage.tsx +39 -34
  96. package/src/pages/ChatPanelPreviewPage.tsx +46 -44
  97. package/src/pages/SettingsPage.tsx +51 -45
  98. package/src/vite-env.d.ts +3 -0
  99. package/vite.base.js +6 -2
  100. package/src/lib/electron-bridge.ts +0 -390
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuShieldCheck } 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
  getSidecarSandboxPosture,
6
7
  type EgressPosture,
@@ -23,44 +24,52 @@ type PostureReason = SidecarSandboxPosture['reason'];
23
24
 
24
25
  /** 收容失败、已拒绝启动(告警行首句)。 */
25
26
  const REFUSED_REASON_TEXT: Partial<Record<PostureReason, string>> = {
26
- platform_unsupported: '当前平台没有可用的进程沙箱后端',
27
- seatbelt_missing: '未找到 /usr/bin/sandbox-exec',
28
- profile_failed: '沙箱配置(Seatbelt profile)生成失败',
29
- wrap_failed: 'Linux 进程沙箱(bwrap / Landlock)未能包住 sidecar',
30
- helper_missing: '未找到 win-spawn-helper.exe',
27
+ platform_unsupported: 'No process sandbox backend is available on this platform',
28
+ seatbelt_missing: '/usr/bin/sandbox-exec not found',
29
+ profile_failed: 'Failed to generate the sandbox configuration (Seatbelt profile)',
30
+ wrap_failed: 'The Linux process sandbox (bwrap / Landlock) could not wrap the sidecar',
31
+ helper_missing: 'win-spawn-helper.exe not found',
31
32
  };
32
33
 
33
34
  /** 手动关闭原因的中文说明(中性行,不告警)。 */
34
35
  const OPT_OUT_REASON_TEXT: Partial<Record<PostureReason, string>> = {
35
- disabled_by_option: '已通过 sandbox: false 关闭',
36
- disabled_by_env: '已通过 STEERABLE_SIDECAR_SANDBOX=0 关闭',
36
+ disabled_by_option: 'Turned off with sandbox: false',
37
+ disabled_by_env: 'Turned off with STEERABLE_SIDECAR_SANDBOX=0',
37
38
  };
38
39
 
39
40
  function activeHeadline(backend: SidecarSandboxPosture['backend']): string {
40
41
  switch (backend) {
41
42
  case 'seatbelt':
42
- return 'Seatbelt · 部分强制';
43
+ return t('Seatbelt · partial enforcement');
43
44
  case 'bwrap':
44
- return 'bwrap · 部分强制';
45
+ return t('bwrap · partial enforcement');
45
46
  case 'landlock':
46
- return 'Landlock · 部分强制';
47
+ return t('Landlock · partial enforcement');
47
48
  case 'windows-restricted-token':
48
- return 'Windows 受限令牌 · 部分强制';
49
+ return t('Windows restricted token · partial enforcement');
49
50
  case 'none':
50
- return '未沙箱';
51
+ return t('Not sandboxed');
51
52
  }
52
53
  }
53
54
 
54
55
  function activeBody(backend: SidecarSandboxPosture['backend']): string {
55
56
  switch (backend) {
56
57
  case 'seatbelt':
57
- return 'sidecar 进程在 macOS Seatbelt 下运行:写入限制在 ~/.steerable 与临时目录。远程主机的出站网络只能按端口限制(Seatbelt 不识别主机名),故记为「部分」而非「完全」。';
58
+ return t(
59
+ 'The sidecar process runs under macOS Seatbelt: writes are limited to ~/.steerable and the temp directory. Outbound network to remote hosts can only be limited by port (Seatbelt does not recognize host names), so this is recorded as "partial" rather than "full".',
60
+ );
58
61
  case 'bwrap':
59
- return 'sidecar 进程在 bubblewrap 下运行:写入限制在 ~/.steerable 与临时目录。出站网络保持开启(sidecar 要访问 LLM),故记为「部分」。';
62
+ return t(
63
+ 'The sidecar process runs under bubblewrap: writes are limited to ~/.steerable and the temp directory. Outbound network stays open (the sidecar needs to reach the LLM), so this is recorded as "partial".',
64
+ );
60
65
  case 'landlock':
61
- return 'sidecar 进程在 Linux Landlock 下运行:写入限制在 ~/.steerable 与临时目录。出站网络保持开启(sidecar 要访问 LLM),故记为「部分」。';
66
+ return t(
67
+ 'The sidecar process runs under Linux Landlock: writes are limited to ~/.steerable and the temp directory. Outbound network stays open (the sidecar needs to reach the LLM), so this is recorded as "partial".',
68
+ );
62
69
  case 'windows-restricted-token':
63
- return 'sidecar 进程在 Windows 受限令牌 + Job Object 下运行:写入限制在 ~/.steerable。网络策略未由 helper 强制(无 WFP),故记为「部分」。';
70
+ return t(
71
+ 'The sidecar process runs under a Windows restricted token + Job Object: writes are limited to ~/.steerable. The helper does not enforce network policy (no WFP), so this is recorded as "partial".',
72
+ );
64
73
  case 'none':
65
74
  return '';
66
75
  }
@@ -71,20 +80,24 @@ function EgressPostureRow({ egress }: { egress: EgressPosture }) {
71
80
  if (egress.mode === 'per-host-proxy') {
72
81
  return (
73
82
  <p className="text-[11px] text-agent-muted-foreground">
74
- 出网管控:按主机白名单代理生效中;白名单外的请求会请你逐项放行(仅本次会话有效)。
83
+ {t(
84
+ 'Egress control: the per-host allowlist proxy is active. Requests outside the allowlist ask you to allow them one by one (valid for this session only).',
85
+ )}
75
86
  </p>
76
87
  );
77
88
  }
78
89
  if (egress.mode === 'disabled') {
79
90
  return (
80
91
  <p className="text-[11px] text-agent-muted-foreground">
81
- 出网管控:已关闭{egress.reason ? ` — ${egress.reason}` : ''}。
92
+ {t('Egress control: off{reason}.', { reason: egress.reason ? ` — ${egress.reason}` : '' })}
82
93
  </p>
83
94
  );
84
95
  }
85
96
  return (
86
97
  <p className="text-[11px] text-amber-600 dark:text-amber-400">
87
- 出网管控:已退回端口级(较弱的管控粒度){egress.reason ? ` — ${egress.reason}` : ''}。
98
+ {t('Egress control: fell back to port level (coarser control){reason}.', {
99
+ reason: egress.reason ? ` — ${egress.reason}` : '',
100
+ })}
88
101
  </p>
89
102
  );
90
103
  }
@@ -96,19 +109,19 @@ export function SecuritySettingsPanel() {
96
109
  const [unavailable, setUnavailable] = useState<string | null>(null);
97
110
 
98
111
  const fetchPosture = useCallback(async () => {
99
- if (!isElectron()) return;
112
+ if (!hasHostBridge()) return;
100
113
  setLoading(true);
101
114
  setUnavailable(null);
102
115
  try {
103
116
  const res = await getSidecarSandboxPosture();
104
117
  setPosture(res.posture ?? null);
105
118
  setEgress(res.egress ?? null);
106
- if (!res.posture) setUnavailable('无法读取沙箱状态');
119
+ if (!res.posture) setUnavailable(t('Could not read sandbox status'));
107
120
  } catch {
108
121
  // 503 = sidecar 未就绪且没有收容失败记录。
109
122
  setPosture(null);
110
123
  setEgress(null);
111
- setUnavailable('sidecar 未就绪 — 无法读取沙箱状态');
124
+ setUnavailable(t('Sidecar not ready — could not read sandbox status'));
112
125
  } finally {
113
126
  setLoading(false);
114
127
  }
@@ -125,35 +138,46 @@ export function SecuritySettingsPanel() {
125
138
  <div className="bg-agent-muted/30 border border-agent-border/60 rounded-agent-md p-2.5 space-y-2">
126
139
  <h4 className="text-xs font-semibold text-agent-foreground flex items-center gap-1.5">
127
140
  <LuShieldCheck className="h-3.5 w-3.5 text-agent-muted-foreground" />
128
- Sidecar 进程沙箱
141
+ {t('Sidecar process sandbox')}
129
142
  </h4>
130
143
 
131
- {!isElectron() ? (
132
- <p className="text-[11px] text-agent-muted-foreground">需要在桌面客户端中打开</p>
144
+ {!hasHostBridge() ? (
145
+ <p className="text-[11px] text-agent-muted-foreground">{t('Open this in the desktop app')}</p>
133
146
  ) : loading ? (
134
147
  <div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
135
148
  <LuLoaderCircle className="h-3.5 w-3.5 animate-spin" />
136
- 读取沙箱状态...
149
+ {t('Loading sandbox status...')}
137
150
  </div>
138
151
  ) : unavailable ? (
139
152
  <p className="text-[11px] text-agent-muted-foreground">{unavailable}</p>
140
153
  ) : posture && refusedReason ? (
141
154
  <div className="rounded-agent-md border border-agent-destructive/20 bg-agent-destructive/10 p-2.5 text-xs text-agent-destructive space-y-1.5">
142
- <p className="font-medium">无法收容、已拒绝启动 — {refusedReason}。</p>
155
+ <p className="font-medium">
156
+ {t('Cannot contain the process, so startup was refused — {reason}.', {
157
+ reason: t(refusedReason),
158
+ })}
159
+ </p>
143
160
  <p>
144
- 持有服务商 API 密钥的 sidecar 进程没有启动,因此也没有在无隔离状态下运行。
145
- 唯一裸跑入口是显式设置 STEERABLE_SIDECAR_SANDBOX=0。
161
+ {t(
162
+ 'The sidecar process that holds the provider API keys did not start, so it is not running without isolation either.',
163
+ )}{' '}
164
+ {t('The only way to run it unsandboxed is to set STEERABLE_SIDECAR_SANDBOX=0 explicitly.')}
146
165
  </p>
147
166
  <p>
148
- 如需隔离,请安装对应平台的收容后端(macOS Seatbelt、Linux bwrap/Landlock、Windows
149
- win-spawn-helper),或将本应用运行在容器内。
167
+ {t(
168
+ 'For isolation, install the containment backend for your platform (macOS Seatbelt, Linux bwrap/Landlock, Windows win-spawn-helper), or run this app in a container.',
169
+ )}
150
170
  </p>
151
171
  </div>
152
172
  ) : posture && optOutReason ? (
153
173
  <div className="rounded-agent-md border border-agent-border/60 bg-agent-canvas p-2.5 text-xs text-agent-muted-foreground space-y-1.5">
154
- <p className="font-medium text-agent-foreground">已手动关闭 — {optOutReason}。</p>
174
+ <p className="font-medium text-agent-foreground">
175
+ {t('Turned off manually — {reason}.', { reason: t(optOutReason) })}
176
+ </p>
155
177
  <p>
156
- sidecar 进程(持有服务商 API 密钥)无操作系统隔离运行。如需恢复,移除该设置并重启应用。
178
+ {t(
179
+ 'The sidecar process (which holds the provider API keys) runs without OS isolation. To restore it, remove the setting and restart the app.',
180
+ )}
157
181
  </p>
158
182
  </div>
159
183
  ) : posture ? (
@@ -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,7 +5,8 @@ import {
5
5
  LuLoaderCircle,
6
6
  LuTrash2,
7
7
  } from 'react-icons/lu';
8
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
8
+ import { t } from '@/i18n';
9
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
9
10
  import { getPackHiddenSlashSkills } from '@/packs/registry';
10
11
 
11
12
  /**
@@ -58,17 +59,17 @@ export function SkillsSettingsPanel() {
58
59
  const [error, setError] = useState<string | null>(null);
59
60
 
60
61
  const fetchSkills = useCallback(async () => {
61
- if (!isElectron()) return;
62
+ if (!hasHostBridge()) return;
62
63
  setSkillsLoading(true);
63
64
  setError(null);
64
65
  try {
65
- const res = await getElectronBridge()!.localBackend.request<{ skills: any[] }>({
66
+ const res = await getHostBridge()!.localBackend.request<{ skills: any[] }>({
66
67
  method: 'GET',
67
68
  path: '/api/v2/chat-agents/skills',
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);
@@ -80,24 +81,24 @@ export function SkillsSettingsPanel() {
80
81
  }, [fetchSkills]);
81
82
 
82
83
  const handleBrowseFolder = async () => {
83
- if (!isElectron()) return;
84
+ if (!hasHostBridge()) return;
84
85
  try {
85
- const bridge = getElectronBridge();
86
+ const bridge = getHostBridge();
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,25 +108,25 @@ 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
- const res = await getElectronBridge()!.localBackend.request<{ success: boolean; name: string }>({
113
+ const res = await getHostBridge()!.localBackend.request<{ success: boolean; name: string }>({
113
114
  method: 'POST',
114
115
  path: '/api/v2/chat-agents/skills/import',
115
116
  body: { path: trimmed },
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
  }
@@ -136,14 +137,14 @@ export function SkillsSettingsPanel() {
136
137
  setDeletingName(name);
137
138
  setError(null);
138
139
  try {
139
- const res = await getElectronBridge()!.localBackend.request<{ success: boolean }>({
140
+ const res = await getHostBridge()!.localBackend.request<{ success: boolean }>({
140
141
  method: 'DELETE',
141
142
  path: `/api/v2/chat-agents/skills/delete/${encodeURIComponent(name)}`,
142
143
  });
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,6 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuActivity } from 'react-icons/lu';
3
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
3
+ import { t } from '@/i18n';
4
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
4
5
 
5
6
  /**
6
7
  * TelemetrySettingsPanel — W6-6 遥测合规化的桌面 collector 配置面板。
@@ -35,11 +36,11 @@ export function TelemetrySettingsPanel() {
35
36
  const [error, setError] = useState<string | null>(null);
36
37
 
37
38
  const fetchSettings = useCallback(async () => {
38
- if (!isElectron()) return;
39
+ if (!hasHostBridge()) return;
39
40
  setLoading(true);
40
41
  setError(null);
41
42
  try {
42
- const res = await getElectronBridge()!.localBackend.request<TelemetrySettingsWire | null>({
43
+ const res = await getHostBridge()!.localBackend.request<TelemetrySettingsWire | null>({
43
44
  method: 'GET',
44
45
  path: '/api/v2/local-settings/telemetry',
45
46
  });
@@ -57,12 +58,12 @@ export function TelemetrySettingsPanel() {
57
58
  }, [fetchSettings]);
58
59
 
59
60
  const handleSave = async () => {
60
- if (!isElectron()) return;
61
+ if (!hasHostBridge()) return;
61
62
  setSaving(true);
62
63
  setError(null);
63
64
  setStatus(null);
64
65
  try {
65
- const saved = await getElectronBridge()!.localBackend.request<TelemetrySettingsWire>({
66
+ const saved = await getHostBridge()!.localBackend.request<TelemetrySettingsWire>({
66
67
  method: 'POST',
67
68
  path: '/api/v2/local-settings/telemetry',
68
69
  body: { endpoint: endpoint.trim(), privacyMode },
@@ -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