@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,7 +1,374 @@
1
1
  /**
2
- * Canonical renderer host contract.
2
+ * Host-neutral bridge used by the renderer in desktop and browser modes.
3
3
  *
4
- * The legacy `electron-bridge` module remains available so existing product
5
- * sources can migrate without a synchronized breaking release.
4
+ * Implementations are `tauri-bridge.ts` (desktop) and `http-bridge.ts`
5
+ * (browser server); both must satisfy {@link HostBridge}.
6
+ * Callers use `window.steerableHost` and `getHostBridge()`.
6
7
  */
7
- export * from './electron-bridge';
8
+
9
+ import type { SSEEvent } from '@steerable/agent-protocol';
10
+ import { getHttpBridge } from './http-bridge';
11
+ import { getTauriBridge } from './tauri-bridge';
12
+
13
+ export type LocalBackendRequestInput = {
14
+ method: string;
15
+ path: string;
16
+ body?: unknown;
17
+ };
18
+
19
+ export type LocalBackendStreamEvent =
20
+ | { type: 'data'; chunk: string; parsed?: SSEEvent }
21
+ | { type: 'end'; status: number }
22
+ | { type: 'error'; error: string };
23
+
24
+ /**
25
+ * W4-1: mirror of the approval algebra's decision variants
26
+ * (`steerable_agent_runtime.approval.APPROVAL_KINDS` minus `timed_out`,
27
+ * which the sidecar synthesizes itself on timeout).
28
+ */
29
+ export type ApprovalDecisionKind =
30
+ | 'allow_once'
31
+ | 'allow_for_session'
32
+ | 'allow_always'
33
+ | 'deny_once'
34
+ | 'deny_for_session'
35
+ | 'deny_always'
36
+ | 'abort';
37
+
38
+ export interface ApprovalPromptRequest {
39
+ requestId: string;
40
+ toolName: string;
41
+ arguments: Record<string, unknown>;
42
+ mode: string;
43
+ category: string;
44
+ round: number;
45
+ /** 发起审批的对话。缺省时卡片不绑定会话(旧请求)。 */
46
+ chatId?: string;
47
+ }
48
+
49
+ /**
50
+ * W8: mirror of the sidecar's `ask_user.request` reverse-call payload
51
+ * (`HostAskUserHandler` in steerable_sidecar.host_tools). `questions`
52
+ * entries match `AskUserQuestionsPayload['questions'][number]` from
53
+ * `@steerable/agent-protocol`; kept structurally typed here so the
54
+ * renderer's compile graph stays free of main-process imports.
55
+ */
56
+ export interface AskUserPromptRequest {
57
+ requestId: string;
58
+ intro: string;
59
+ questions: Array<Record<string, unknown>>;
60
+ /** 发起提问的对话。缺省时卡片不绑定会话(旧请求)。 */
61
+ chatId?: string;
62
+ }
63
+
64
+ /**
65
+ * Mirror of `TerminalSession` / `TerminalSpawnOptions` in
66
+ * `src/terminal-manager.ts`. Kept inline (rather than cross-imported)
67
+ * for the same reason as the rest of this file — we don't want the
68
+ * renderer's compile graph to pull in main-process modules.
69
+ */
70
+ export interface TerminalSession {
71
+ id: string;
72
+ shell: string;
73
+ pid: number;
74
+ cwd: string;
75
+ cols: number;
76
+ rows: number;
77
+ }
78
+
79
+ export interface TerminalSpawnOptions {
80
+ shell?: string;
81
+ cwd?: string;
82
+ env?: Record<string, string>;
83
+ cols?: number;
84
+ rows?: number;
85
+ }
86
+
87
+ export type AppReleasePhase =
88
+ | 'disabled'
89
+ | 'idle'
90
+ | 'checking'
91
+ | 'downloading'
92
+ | 'ready'
93
+ | 'installing'
94
+ | 'error';
95
+
96
+ /**
97
+ * 桌面版本与更新。`version` 是当前安装版本,`availableVersion` 是待安装版本。
98
+ * 与主进程 `AppReleaseSnapshot` 对齐。
99
+ */
100
+ export interface AppReleaseSnapshot {
101
+ version: string;
102
+ enabled: boolean;
103
+ phase: AppReleasePhase;
104
+ availableVersion?: string;
105
+ percent?: number;
106
+ message?: string;
107
+ }
108
+
109
+ export type PythonRunnerPhase =
110
+ | 'idle'
111
+ | 'downloading'
112
+ | 'verifying'
113
+ | 'extracting'
114
+ | 'ready'
115
+ | 'error';
116
+
117
+ export interface PythonRunnerSnapshot {
118
+ supported: boolean;
119
+ source: 'default' | 'url' | 'local';
120
+ phase: PythonRunnerPhase;
121
+ percent?: number;
122
+ downloadedBytes?: number;
123
+ totalBytes?: number;
124
+ message?: string;
125
+ defaultUrl?: string;
126
+ configuredUrl?: string;
127
+ activeRunner?: string;
128
+ configuredRunner?: string;
129
+ restartRequired: boolean;
130
+ }
131
+
132
+ export interface HostBridge {
133
+ runtime: 'local';
134
+ platform: NodeJS.Platform;
135
+ local?: {
136
+ selectDirectory: (options?: {
137
+ title?: string;
138
+ }) => Promise<{ canceled: boolean; filePaths: string[] }>;
139
+ saveTextFile?: (options: {
140
+ title?: string;
141
+ defaultPath?: string;
142
+ content: string;
143
+ }) => Promise<{ canceled: boolean; filePath?: string }>;
144
+ /** 截取窗口(或指定 DIP 区域)并写入系统剪贴板。 */
145
+ captureScreenshot: (rect?: {
146
+ x: number;
147
+ y: number;
148
+ width: number;
149
+ height: number;
150
+ }) => Promise<
151
+ | { success: true; width: number; height: number }
152
+ | { success: false; error: string }
153
+ >;
154
+ };
155
+ localBackend: {
156
+ request: <T>(input: LocalBackendRequestInput) => Promise<T>;
157
+ startStream: (
158
+ input: LocalBackendRequestInput,
159
+ onEvent: (payload: LocalBackendStreamEvent) => void,
160
+ ) => Promise<string | null>;
161
+ cancelStream: (streamId: string) => void;
162
+ /** 轮中转向:注入一条用户消息到运行中的 CoreLoop 回合;false = 无可转向回合。 */
163
+ steerChat?: (chatId: string, content: string) => Promise<boolean>;
164
+ };
165
+ /** 会话附件:把源文件路径 / base64 字节拷贝进会话空间(`attachments:save`)。 */
166
+ attachments?: {
167
+ save: (input: {
168
+ chatId: string;
169
+ files: Array<{ path?: string; name?: string; data?: string }>;
170
+ }) => Promise<{
171
+ files: Array<{ name: string; path: string; size: number; error?: string }>;
172
+ }>;
173
+ };
174
+ /**
175
+ * Desktop menu — `Cmd+N` fires `menu:new-chat`. Renderer subscribes via
176
+ * `onMenuNewChat(cb)` and must call `offMenuNewChat()` on unmount
177
+ * so a remounted AgentSidebar does not keep the previous callback.
178
+ */
179
+ /** 把当前界面语言告诉桌面壳,以便重建应用菜单。浏览器预览里没有。 */
180
+ setLocale?: (locale: string) => void;
181
+ onMenuNewChat?: (callback: () => void) => void;
182
+ offMenuNewChat?: () => void;
183
+ /**
184
+ * App menu IPC — `Cmd+T` / 视图 → 打开终端 fires `menu:open-terminal`.
185
+ * The terminal is a toggleable panel inside AgentLayout (not a separate
186
+ * window, not a route), so the renderer just flips layout state. Same
187
+ * subscribe/off pairing as `onMenuNewChat`.
188
+ */
189
+ onMenuOpenTerminal?: (callback: () => void) => void;
190
+ offMenuOpenTerminal?: () => void;
191
+ /**
192
+ * App menu IPC — `Cmd+V` / 编辑 → 粘贴 fires `menu:paste`.
193
+ * WKWebView does not expose that pasteboard to the page, so the renderer
194
+ * reads it through `readClipboard`.
195
+ */
196
+ onMenuPaste?: (callback: () => void) => void;
197
+ offMenuPaste?: () => void;
198
+ /**
199
+ * OS pasteboard. `files` are copied paths or a screenshot PNG.
200
+ * Text is empty when the pasteboard holds files, so the path is not
201
+ * inserted into the composer as well.
202
+ */
203
+ readClipboard?: () => Promise<{
204
+ text: string;
205
+ files: Array<{
206
+ name: string;
207
+ path?: string;
208
+ dataBase64?: string;
209
+ mime?: string;
210
+ }>;
211
+ }>;
212
+ /** Plain text from the OS pasteboard. Empty when the clipboard has no text. */
213
+ readClipboardText?: () => Promise<string>;
214
+ /**
215
+ * 异步 AI 标题就绪通知。Backend 在每条 chat 首条助手回复完成后 fire-and-forget
216
+ * 跑 LLM 生成标题;完成时通过这个通道把 `{chatId, title}` 推过来。返回的函数
217
+ * 解除订阅(preload 用 add/remove 配对,不是 removeAllListeners——可以多处订阅)。
218
+ */
219
+ onChatTitleUpdated?: (
220
+ callback: (payload: { chatId: string; title: string }) => void,
221
+ ) => () => void;
222
+ /**
223
+ * 回合追问建议就绪。Backend 在助手回复完成后推 `{chatId, messageId, suggestions}`
224
+ * (先启发式、后 LLM 替换)。返回解除订阅函数。
225
+ */
226
+ onSuggestedReplies?: (
227
+ callback: (payload: {
228
+ chatId: string;
229
+ messageId: string;
230
+ suggestions: string[];
231
+ }) => void,
232
+ ) => () => void;
233
+ /**
234
+ * 会话补建通知。Backend 在「向一个本地不存在的 chatId 首次发送」时按 URL
235
+ * 里的 id 现场补建会话(见 router.handleStream),随后广播 `chat-created`
236
+ * ({chatId, agentId});渲染端据此 refresh 一次侧栏,避免"URL 能聊、列表
237
+ * 里却找不到这条会话"。返回解除订阅函数。
238
+ */
239
+ onChatCreated?: (
240
+ callback: (payload: { chatId: string; agentId?: string | null }) => void,
241
+ ) => () => void;
242
+ /**
243
+ * 4.6a 后台任务状态推送。任务到达终态或 worktree 合并/丢弃完成时主进程
244
+ * 广播;载荷只有 chatId/taskId,面板收到后重新拉列表。
245
+ */
246
+ onTaskUpdated?: (
247
+ callback: (payload: { chatId: string; taskId: string }) => void,
248
+ ) => () => void;
249
+ /**
250
+ * 场景包广播事件订阅(3.1 通用化):按通道名订阅主进程/BS 后端的
251
+ * 广播事件,载荷由订阅方(包)自行收窄校验。shell 不知道包的事件
252
+ * 语义——包的通道名约定带包前缀(如 `<pack>:updated`)。返回解除
253
+ * 订阅函数。
254
+ */
255
+ onPackEvent?: (
256
+ channel: string,
257
+ callback: (payload: unknown) => void,
258
+ ) => () => void;
259
+ /**
260
+ * 后台任务推理时间线推送。任务流的 reasoning / 工具 / 文本增量经主进程
261
+ * 广播;右侧过程栏订阅后不用轮询。
262
+ */
263
+ onTaskProcess?: (
264
+ callback: (payload: {
265
+ chatId: string;
266
+ taskId: string;
267
+ timeline: unknown;
268
+ live: boolean;
269
+ }) => void,
270
+ ) => () => void;
271
+ /**
272
+ * W4-1 审批代数:sidecar ApprovalExecutor 的请示经主进程广播到 renderer。
273
+ * onRequest 订阅返回 unsubscribe;decide 把用户的 7 变体决定送回主进程。
274
+ */
275
+ approval?: {
276
+ onRequest: (callback: (request: ApprovalPromptRequest) => void) => () => void;
277
+ decide: (decision: {
278
+ requestId: string;
279
+ kind: ApprovalDecisionKind;
280
+ reason?: string;
281
+ }) => Promise<void>;
282
+ pending: () => Promise<ApprovalPromptRequest[]>;
283
+ };
284
+ /**
285
+ * W8 结构化提问:sidecar ask_user 工具的请示经主进程/BS 服务器广播到
286
+ * renderer。onRequest 订阅返回 unsubscribe;answer 把答案映射送回
287
+ * (空映射 = 用户未作答,模型自行推进)。
288
+ */
289
+ askUser?: {
290
+ onRequest: (callback: (request: AskUserPromptRequest) => void) => () => void;
291
+ answer: (reply: {
292
+ requestId: string;
293
+ answers: Record<string, string | string[]>;
294
+ }) => Promise<void>;
295
+ pending: () => Promise<AskUserPromptRequest[]>;
296
+ };
297
+ terminal?: {
298
+ /** Ensures a session exists (reuses last one if alive). */
299
+ ensure: (options?: TerminalSpawnOptions) => Promise<TerminalSession>;
300
+ write: (id: string, data: string) => Promise<boolean>;
301
+ resize: (id: string, cols: number, rows: number) => Promise<boolean>;
302
+ onData: (
303
+ callback: (payload: { sessionId: string; chunk: string }) => void,
304
+ ) => () => void;
305
+ onExit: (
306
+ callback: (payload: {
307
+ sessionId: string;
308
+ code: number;
309
+ signal: string | null;
310
+ }) => void,
311
+ ) => () => void;
312
+ };
313
+ /**
314
+ * 桌面版本与更新。Tauri 提供;浏览器预览没有这一段。
315
+ */
316
+ app?: {
317
+ snapshot: () => Promise<AppReleaseSnapshot>;
318
+ check: () => Promise<AppReleaseSnapshot>;
319
+ install: () => Promise<AppReleaseSnapshot>;
320
+ onState: (callback: (snapshot: AppReleaseSnapshot) => void) => () => void;
321
+ };
322
+ /** Desktop-only Python child interpreter management for run_code. */
323
+ pythonRunner?: {
324
+ snapshot: () => Promise<PythonRunnerSnapshot>;
325
+ download: (url?: string) => Promise<PythonRunnerSnapshot>;
326
+ cancel: () => Promise<PythonRunnerSnapshot>;
327
+ pickLocal: () => Promise<string | null>;
328
+ useLocal: (path: string) => Promise<PythonRunnerSnapshot>;
329
+ useDefault: () => Promise<PythonRunnerSnapshot>;
330
+ restart: () => Promise<void>;
331
+ onState: (callback: (snapshot: PythonRunnerSnapshot) => void) => () => void;
332
+ };
333
+ // 场景包的 invoke 命名空间(如包 preload 贡献的 `<pack>` / `<pack>Mock`)
334
+ // 不进本接口——包用自己的结构化收窄访问器(见各包 web/bridge.ts),
335
+ // shell 桥接口保持产品中立(3.1)。
336
+ }
337
+
338
+ declare global {
339
+ interface Window {
340
+ steerableHost?: HostBridge;
341
+ __TAURI_INTERNALS__?: object;
342
+ /**
343
+ * BS server 托管 index.html 时注入的引导信息。存在且没有 Tauri
344
+ * 内部对象时,`getHostBridge()` 返回 HTTP 实现。
345
+ */
346
+ __DEEPPATH_BS__?: {
347
+ platform: NodeJS.Platform;
348
+ flavor: string;
349
+ brandName: string;
350
+ };
351
+ }
352
+ }
353
+
354
+ export function getHostBridge(): HostBridge | null {
355
+ if (typeof window === 'undefined') return null;
356
+ if (window.steerableHost) return window.steerableHost;
357
+ if (window.__TAURI_INTERNALS__ && window.__DEEPPATH_BS__) {
358
+ return getTauriBridge();
359
+ }
360
+ if (window.__DEEPPATH_BS__) return getHttpBridge();
361
+ return null;
362
+ }
363
+
364
+ /** True when the page can call the Node host, including browser mode. */
365
+ export function hasHostBridge(): boolean {
366
+ return getHostBridge() !== null;
367
+ }
368
+
369
+ export function isDesktopHost(): boolean {
370
+ if (typeof window === 'undefined') return false;
371
+ return Boolean(
372
+ window.steerableHost || (window.__TAURI_INTERNALS__ && window.__DEEPPATH_BS__),
373
+ );
374
+ }
@@ -15,7 +15,7 @@ export interface HostClipboard {
15
15
  text: string;
16
16
  files: HostClipboardFile[];
17
17
  }
18
- import { getHostBridge } from './electron-bridge';
18
+ import { getHostBridge } from './host-bridge';
19
19
 
20
20
  let pending = false;
21
21
  let lastEditable: HTMLElement | null = null;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * BS 模式的宿主 bridge:与 Electron preload(src/preload.ts)暴露的
3
- * `window.electron` 同一接口(ElectronBridge),但传输换成 HTTP + SSE:
3
+ * `window.steerableHost` 同一接口(HostBridge),但传输换成 HTTP + SSE:
4
4
  *
5
5
  * localBackend.request → fetch /api/v2/*
6
6
  * localBackend.startStream → fetch(ReadableStream 逐 chunk 喂 onEvent)
@@ -12,17 +12,18 @@
12
12
  * onPackEvent(= subscribeChannel)订阅。
13
13
  *
14
14
  * server 在 index.html 里注入 `window.__DEEPPATH_BS__`(platform/brand),
15
- * getElectronBridge() 据此在浏览器里选中本实现。
15
+ * getHostBridge() 据此在浏览器里选中本实现。
16
16
  */
17
17
  import type {
18
18
  ApprovalPromptRequest,
19
19
  AskUserPromptRequest,
20
- ElectronBridge,
20
+ HostBridge,
21
21
  LocalBackendRequestInput,
22
22
  LocalBackendStreamEvent,
23
23
  TerminalSession,
24
24
  TerminalSpawnOptions,
25
- } from './electron-bridge';
25
+ } from './host-bridge';
26
+ import { t } from '@/i18n';
26
27
 
27
28
  interface BsBootstrap {
28
29
  platform: NodeJS.Platform;
@@ -197,9 +198,9 @@ async function terminalEnsure(options?: TerminalSpawnOptions): Promise<TerminalS
197
198
  return session;
198
199
  }
199
200
 
200
- export function createHttpBridge(): ElectronBridge {
201
+ export function createHttpBridge(): HostBridge {
201
202
  const boot = bootstrap();
202
- const bridge: ElectronBridge = {
203
+ const bridge: HostBridge = {
203
204
  runtime: 'local',
204
205
  platform: boot.platform,
205
206
 
@@ -225,7 +226,9 @@ export function createHttpBridge(): ElectronBridge {
225
226
  },
226
227
  captureScreenshot: async () => ({
227
228
  success: false as const,
228
- error: '浏览器模式不支持窗口截图,请用系统截图工具。',
229
+ error: t(
230
+ 'Browser mode cannot capture window screenshots. Use your system screenshot tool.',
231
+ ),
229
232
  }),
230
233
  },
231
234
 
@@ -318,9 +321,9 @@ export function createHttpBridge(): ElectronBridge {
318
321
  return bridge;
319
322
  }
320
323
 
321
- let singleton: ElectronBridge | null = null;
324
+ let singleton: HostBridge | null = null;
322
325
 
323
- export function getHttpBridge(): ElectronBridge {
326
+ export function getHttpBridge(): HostBridge {
324
327
  if (!singleton) singleton = createHttpBridge();
325
328
  return singleton;
326
329
  }
@@ -1,11 +1,11 @@
1
- import { getElectronBridge } from './electron-bridge';
1
+ import { getHostBridge } from './host-bridge';
2
2
 
3
3
  /** Fire-and-forget local enqueue. Never throws into UI. */
4
4
  export function trackBehavior(
5
5
  eventName: string,
6
6
  properties: Record<string, unknown> = {},
7
7
  ): void {
8
- const bridge = getElectronBridge();
8
+ const bridge = getHostBridge();
9
9
  if (!bridge) return;
10
10
  void bridge.localBackend
11
11
  .request({
@@ -1,18 +1,18 @@
1
1
  /**
2
- * Typed wrappers around `window.electron.localBackend.request` for the
2
+ * Typed wrappers around `window.steerableHost.localBackend.request` for the
3
3
  * endpoints that apps/web actually consumes today. Shapes follow what
4
4
  * `src/local-backend/router.ts` returns — see the corresponding route
5
5
  * handlers there if you need to add fields.
6
6
  *
7
7
  * Conventions:
8
- * - Every helper throws if the bridge is missing (caller should `isElectron()`
8
+ * - Every helper throws if the bridge is missing (caller should `hasHostBridge()`
9
9
  * check first if a graceful degradation is needed).
10
10
  * - We deliberately keep the local return shapes (not Pydantic / protocol
11
11
  * types) — they're the contract between local-backend and renderer, and
12
12
  * may drift from any public cloud API. Aligning the two is out of scope.
13
13
  */
14
14
 
15
- import { getElectronBridge } from './electron-bridge';
15
+ import { getHostBridge } from './host-bridge';
16
16
 
17
17
  export interface LocalChat {
18
18
  id: string;
@@ -100,10 +100,10 @@ interface CreateChatResponse {
100
100
  }
101
101
 
102
102
  function bridge() {
103
- const b = getElectronBridge();
103
+ const b = getHostBridge();
104
104
  if (!b) {
105
105
  throw new Error(
106
- 'Electron bridge unavailable — local API can only be used inside the desktop shell.',
106
+ 'Host bridge unavailable — local API can only be used inside the desktop shell.',
107
107
  );
108
108
  }
109
109
  return b;
@@ -3,7 +3,8 @@
3
3
  * 界面档位和命令权限存在 localStorage,由这里并进配置包;其余段走本地后端。
4
4
  */
5
5
 
6
- import { getElectronBridge } from '@/lib/electron-bridge';
6
+ import { t } from '@/i18n';
7
+ import { getHostBridge } from '@/lib/host-bridge';
7
8
  import { listChats } from '@/lib/local-api';
8
9
  import { persistExecPolicy, readStoredExecPolicy, type ExecPolicy } from '@/lib/exec-policy';
9
10
  import {
@@ -69,9 +70,9 @@ export interface PortableChatImportResult {
69
70
  const CLIENT_SECTION_IDS = ['appearance', 'execPolicy'] as const;
70
71
 
71
72
  export function portableErrorMessage(err: unknown): string {
72
- if (!(err instanceof Error)) return '操作失败';
73
+ if (!(err instanceof Error)) return t('Operation failed');
73
74
  const message = err.message.trim();
74
- if (!message) return '操作失败';
75
+ if (!message) return t('Operation failed');
75
76
  try {
76
77
  const parsed = JSON.parse(message) as { detail?: unknown; error?: unknown };
77
78
  if (typeof parsed.detail === 'string' && parsed.detail.trim()) return parsed.detail;
@@ -86,13 +87,13 @@ export function parsePortableText(text: string): { ok: true; value: unknown } |
86
87
  try {
87
88
  return { ok: true, value: JSON.parse(text) as unknown };
88
89
  } catch {
89
- return { ok: false, error: '文件不是 JSON' };
90
+ return { ok: false, error: t('The file is not JSON') };
90
91
  }
91
92
  }
92
93
 
93
94
  export function safeDownloadName(stem: string, suffix: string): string {
94
95
  const cleaned = stem.replace(/[\\/:*?"<>|]/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 40);
95
- return `${cleaned || '导出'}-${suffix}.json`;
96
+ return `${cleaned || t('Export')}-${suffix}.json`;
96
97
  }
97
98
 
98
99
  export function readLocalSections(): { appearance: { thinkingDisplay: ThinkingDisplayMode }; execPolicy: { policy: ExecPolicy } } {
@@ -138,8 +139,8 @@ export function isClientSection(id: string): boolean {
138
139
  }
139
140
 
140
141
  async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
141
- const bridge = getElectronBridge();
142
- if (!bridge) throw new Error('当前环境没有本地后端');
142
+ const bridge = getHostBridge();
143
+ if (!bridge) throw new Error(t('No local backend in this environment'));
143
144
  return bridge.localBackend.request<T>(body === undefined ? { method, path } : { method, path, body });
144
145
  }
145
146
 
@@ -159,7 +160,7 @@ export async function listPortableChats(): Promise<Array<{ id: string; title: st
159
160
  for (;;) {
160
161
  const res = await listChats({ page, limit: 100 });
161
162
  for (const chat of res.chats) {
162
- rows.push({ id: chat.id, title: chat.title.trim() || '未命名对话' });
163
+ rows.push({ id: chat.id, title: chat.title.trim() || t('Untitled chat') });
163
164
  }
164
165
  if (!res.pagination.hasMore || page >= 50) break;
165
166
  page += 1;
@@ -188,9 +189,9 @@ export async function importChatDocument(document: unknown): Promise<PortableCha
188
189
 
189
190
  export async function saveJsonFile(filename: string, data: unknown): Promise<string | null> {
190
191
  const content = `${JSON.stringify(data, null, 2)}\n`;
191
- const save = getElectronBridge()?.local?.saveTextFile;
192
+ const save = getHostBridge()?.local?.saveTextFile;
192
193
  if (save) {
193
- const saved = await save({ title: '导出', defaultPath: filename, content });
194
+ const saved = await save({ title: t('Export'), defaultPath: filename, content });
194
195
  if (saved.canceled || !saved.filePath) return null;
195
196
  return saved.filePath;
196
197
  }
@@ -19,9 +19,21 @@ export const THINKING_DISPLAY_OPTIONS: ReadonlyArray<{
19
19
  label: string;
20
20
  hint: string;
21
21
  }> = [
22
- { mode: 'hidden', label: '隐藏', hint: '不展示思考正文,只保留工作状态行。' },
23
- { mode: 'peek', label: '显示5行', hint: '当前轮思考最多露出 5 行,不足不撑开;该轮结束后自动折叠。完整正文请用「完整显示」。' },
24
- { mode: 'full', label: '完整显示', hint: '当前轮思考完整展开,该轮结束后自动折叠;工作行结束后仍自动折叠。' },
22
+ {
23
+ mode: 'hidden',
24
+ label: 'Hide',
25
+ hint: 'Hide the thinking text and keep only the work status line.',
26
+ },
27
+ {
28
+ mode: 'peek',
29
+ label: 'Show 5 lines',
30
+ hint: 'Show at most 5 lines of the current turn\'s thinking. Do not stretch the row when there is less, and collapse it when the turn ends. Use "Show all" for the full text.',
31
+ },
32
+ {
33
+ mode: 'full',
34
+ label: 'Show all',
35
+ hint: 'Expand the current turn\'s thinking, then collapse it when the turn ends. The work row still collapses when it finishes.',
36
+ },
25
37
  ];
26
38
 
27
39
  export function isThinkingDisplayMode(value: unknown): value is ThinkingDisplayMode {
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { useCallback, useEffect, useState } from 'react';
16
16
  import { getPackHiddenSlashSkills } from '../packs/registry';
17
- import { getElectronBridge, isElectron } from './electron-bridge';
17
+ import { getHostBridge, hasHostBridge } from './host-bridge';
18
18
 
19
19
  export type SkillItem = {
20
20
  /** Skill directory name (e.g. `70-plan-mode`); absent on hand-built items. */
@@ -125,7 +125,7 @@ let inflight: Promise<SlashSources> | null = null;
125
125
  const listeners = new Set<(sources: SlashSources) => void>();
126
126
 
127
127
  async function load(): Promise<SlashSources> {
128
- const bridge = getElectronBridge();
128
+ const bridge = getHostBridge();
129
129
  if (!bridge) return cached;
130
130
 
131
131
  let skills = cached.skills;
@@ -159,7 +159,7 @@ async function load(): Promise<SlashSources> {
159
159
 
160
160
  /** Re-read the catalog. Concurrent callers share one in-flight request. */
161
161
  export function refreshSlashSources(): Promise<SlashSources> {
162
- if (!isElectron()) return Promise.resolve(cached);
162
+ if (!hasHostBridge()) return Promise.resolve(cached);
163
163
  if (!inflight) {
164
164
  inflight = load().finally(() => {
165
165
  inflight = null;
@@ -12,7 +12,7 @@ import type {
12
12
  AppReleaseSnapshot,
13
13
  HostBridge,
14
14
  PythonRunnerSnapshot,
15
- } from './electron-bridge';
15
+ } from './host-bridge';
16
16
  import { createHttpBridge } from './http-bridge';
17
17
 
18
18
  type VoidCallback = () => void;