@steerable/agent-shell-web 0.6.55 → 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 (46) hide show
  1. package/README.md +3 -4
  2. package/package.json +4 -4
  3. package/src/AppShell.tsx +3 -3
  4. package/src/components/AgentSidebar.tsx +4 -4
  5. package/src/components/CreateProjectModal.tsx +2 -2
  6. package/src/components/SidebarRelease.tsx +4 -4
  7. package/src/components/TerminalPanel.tsx +3 -3
  8. package/src/components/TerminalView.tsx +4 -4
  9. package/src/components/chat/ApprovalModal.tsx +1 -1
  10. package/src/components/chat/ApprovalPromptProvider.tsx +4 -4
  11. package/src/components/chat/AskUserPromptProvider.tsx +3 -3
  12. package/src/components/chat/ChatProjectBadge.tsx +3 -3
  13. package/src/components/chat/TaskPanelModal.tsx +2 -2
  14. package/src/components/chat/TaskProcessPanel.tsx +2 -2
  15. package/src/components/chat/useChatTasks.ts +3 -3
  16. package/src/components/settings/AgentCapabilityFields.tsx +2 -2
  17. package/src/components/settings/AgentsSettingsPanel.tsx +3 -3
  18. package/src/components/settings/InsightsSettingsPanel.tsx +15 -15
  19. package/src/components/settings/LlmSettingsPanel.tsx +3 -3
  20. package/src/components/settings/McpSettingsPanel.tsx +12 -12
  21. package/src/components/settings/PythonRunnerSettingsPanel.tsx +4 -4
  22. package/src/components/settings/SecuritySettingsPanel.tsx +3 -3
  23. package/src/components/settings/SkillsSettingsPanel.tsx +7 -7
  24. package/src/components/settings/TelemetrySettingsPanel.tsx +5 -5
  25. package/src/components/settings/UsagePanel.tsx +3 -3
  26. package/src/components/settings/WebSearchSettingsPanel.tsx +5 -5
  27. package/src/hooks/useChatsAndAgents.ts +9 -9
  28. package/src/hooks/useProjects.ts +2 -2
  29. package/src/i18n/index.tsx +1 -1
  30. package/src/layouts/AgentLayout.tsx +6 -6
  31. package/src/lib/app-release.ts +1 -1
  32. package/src/lib/attachments.ts +2 -2
  33. package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +10 -10
  34. package/src/lib/chat-transport.ts +10 -10
  35. package/src/lib/host-bridge.ts +371 -4
  36. package/src/lib/host-paste.ts +1 -1
  37. package/src/lib/http-bridge.ts +8 -8
  38. package/src/lib/insights.ts +2 -2
  39. package/src/lib/local-api.ts +5 -5
  40. package/src/lib/portable.ts +3 -3
  41. package/src/lib/slash-sources.ts +3 -3
  42. package/src/lib/tauri-bridge.ts +1 -1
  43. package/src/main.tsx +3 -3
  44. package/src/pages/AgentPage.tsx +23 -23
  45. package/src/pages/SettingsPage.tsx +4 -4
  46. package/src/lib/electron-bridge.ts +0 -389
package/README.md CHANGED
@@ -17,10 +17,9 @@ bundle; there is no prebuilt `dist/` here. See
17
17
  [`docs/spec/architecture.md`](https://github.com/pathlyapp/steerable/blob/develop/docs/spec/architecture.md)
18
18
  for the tier model.
19
19
 
20
- Renderer code reads native and backend capabilities through `HostBridge`.
21
- Browser mode uses the HTTP/SSE adapter; Tauri mode layers native commands and
22
- menu events over that same adapter. `ElectronBridge` and
23
- `getElectronBridge()` remain deprecated compatibility aliases.
20
+ Renderer code reads native and backend capabilities through `HostBridge`
21
+ (`getHostBridge()`). Browser mode uses the HTTP/SSE adapter; Tauri mode
22
+ layers native commands and menu events over that same adapter.
24
23
 
25
24
  Products that require authentication can register an `AppShellGate` from
26
25
  `@steerable/agent-shell-web/auth/gate` before calling `bootstrap()`. An
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.55",
3
+ "version": "0.6.56",
4
4
  "type": "module",
5
5
  "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
6
6
  "license": "Apache-2.0",
@@ -45,8 +45,8 @@
45
45
  "react-router-dom": "^6.30.0",
46
46
  "rehype-raw": "^7.0.0",
47
47
  "remark-gfm": "^4.0.1",
48
- "@steerable/agent-protocol": "0.6.55",
49
- "@steerable/agent-ui": "0.6.55"
48
+ "@steerable/agent-ui": "0.6.56",
49
+ "@steerable/agent-protocol": "0.6.56"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tailwindcss/vite": "^4.3.0",
@@ -59,7 +59,7 @@
59
59
  "typescript": "^5.8.3",
60
60
  "vite": "^5.4.21",
61
61
  "vitest": "^3.2.4",
62
- "@steerable/pack-sdk": "0.6.55"
62
+ "@steerable/pack-sdk": "0.6.56"
63
63
  },
64
64
  "scripts": {
65
65
  "gen": "echo agent-shell-web",
package/src/AppShell.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Outlet } from 'react-router-dom';
2
- import { isElectron } from './lib/electron-bridge';
2
+ import { hasHostBridge } from './lib/host-bridge';
3
3
  import { isDemoMode } from './lib/demo-flag';
4
4
 
5
5
  export function AppShell() {
@@ -13,9 +13,9 @@ export function AppShell() {
13
13
  Demo · simulated data, no live model
14
14
  </div>
15
15
  ) : (
16
- !isElectron() && (
16
+ !hasHostBridge() && (
17
17
  <div className="border-t border-agent-border bg-agent-muted px-3 py-1 text-xs text-agent-muted-foreground">
18
- Browser preview mode &middot; running outside Electron, IPC bridge unavailable
18
+ Browser preview mode &middot; no host bridge
19
19
  </div>
20
20
  )
21
21
  )}
@@ -92,7 +92,7 @@ import {
92
92
  import { RiPushpin2Fill, RiPushpin2Line } from "react-icons/ri";
93
93
  import { parseChatTitle } from "@/lib/chat-title";
94
94
  import { getDateGroupLabel, getDateGroupPriority } from "@/lib/date-groups";
95
- import { getElectronBridge, isElectron } from "@/lib/electron-bridge";
95
+ import { getHostBridge, hasHostBridge } from "@/lib/host-bridge";
96
96
  import { hasGeneralSettingsChrome, hostToolChrome, settingsChrome } from "@/lib/host-tools";
97
97
  import {
98
98
  createProject,
@@ -290,7 +290,7 @@ export function AgentSidebar({
290
290
  const navigate = useNavigate();
291
291
  const location = useLocation();
292
292
  const { chatId: currentChatId } = useParams<{ chatId?: string }>();
293
- const bridge = getElectronBridge();
293
+ const bridge = getHostBridge();
294
294
  const release = useAppRelease();
295
295
  const onSettingsPage = location.pathname === "/settings";
296
296
  // 插件页(及旧的 agents/skills/mcp 深链)高亮「插件」;其余 /settings 高亮底部综合设置。
@@ -385,7 +385,7 @@ export function AgentSidebar({
385
385
  const backgroundIds = [...streamingChatIds].filter(
386
386
  (id) => id !== currentChatId,
387
387
  );
388
- if (backgroundIds.length === 0 || !isElectron()) return;
388
+ if (backgroundIds.length === 0 || !hasHostBridge()) return;
389
389
 
390
390
  let cancelled = false;
391
391
  let ticking = false;
@@ -908,7 +908,7 @@ export function AgentSidebar({
908
908
  normalizedChats.length,
909
909
  ]);
910
910
 
911
- const hasElectron = isElectron();
911
+ const hasElectron = hasHostBridge();
912
912
 
913
913
  return (
914
914
  <div className="flex h-full w-full flex-col border-r border-agent-border/60 bg-agent-muted/70 backdrop-blur-md">
@@ -7,7 +7,7 @@ import { createPortal } from 'react-dom';
7
7
  import { LuFolder, LuFolderPlus, LuX } from 'react-icons/lu';
8
8
  import { BRAND_NAME } from '@/brand';
9
9
  import { ConfirmDialog } from '@/components/ConfirmDialog';
10
- import { getElectronBridge } from '@/lib/electron-bridge';
10
+ import { getHostBridge } from '@/lib/host-bridge';
11
11
  import { t } from '@/i18n';
12
12
 
13
13
  export interface ProjectFormValues {
@@ -83,7 +83,7 @@ export function CreateProjectModal({
83
83
  addSourceFolder(typed);
84
84
  return;
85
85
  }
86
- const picker = getElectronBridge()?.local?.selectDirectory;
86
+ const picker = getHostBridge()?.local?.selectDirectory;
87
87
  if (!picker) {
88
88
  setError(t('Enter a folder path'));
89
89
  return;
@@ -4,9 +4,9 @@ import {
4
4
  type AppReleaseView,
5
5
  } from '@/lib/app-release';
6
6
  import {
7
- getElectronBridge,
7
+ getHostBridge,
8
8
  type AppReleaseSnapshot,
9
- } from '@/lib/electron-bridge';
9
+ } from '@/lib/host-bridge';
10
10
  import { t } from '@/i18n';
11
11
 
12
12
  export function useAppRelease(): AppReleaseView & { run: () => void } {
@@ -14,7 +14,7 @@ export function useAppRelease(): AppReleaseView & { run: () => void } {
14
14
  const [confirmedCurrent, setConfirmedCurrent] = useState(false);
15
15
 
16
16
  useEffect(() => {
17
- const app = getElectronBridge()?.app;
17
+ const app = getHostBridge()?.app;
18
18
  if (!app) return;
19
19
  let alive = true;
20
20
  void app.snapshot().then((next) => {
@@ -32,7 +32,7 @@ export function useAppRelease(): AppReleaseView & { run: () => void } {
32
32
  }, []);
33
33
 
34
34
  const run = useCallback(() => {
35
- const app = getElectronBridge()?.app;
35
+ const app = getHostBridge()?.app;
36
36
  if (!app || !snap || !snap.enabled) return;
37
37
  if (snap.phase !== 'idle' && snap.phase !== 'error' && snap.phase !== 'ready') return;
38
38
  const task = snap.phase === 'ready' ? app.install() : app.check();
@@ -1,5 +1,5 @@
1
1
  import { Suspense, lazy, useEffect, useState } from 'react';
2
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
2
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
3
3
  import { t } from '@/i18n';
4
4
 
5
5
  // 把 xterm 那一坨 (~300KB) 拆到独立 chunk —— 用户没打开终端就不下载。
@@ -17,7 +17,7 @@ const TerminalView = lazy(() =>
17
17
  * xterm 视图,不杀 shell;重新打开时通过 `terminal:ensure` 的 replay
18
18
  * buffer 补回面板关闭期间的输出。
19
19
  *
20
- * Electron 检测保留给浏览器预览 / dev 模式(没有 `window.electron` 就
20
+ * Electron 检测保留给浏览器预览 / dev 模式(没有 `window.steerableHost` 就
21
21
  * 连不到本地 PTY)。延迟到首个 effect 再渲染,避免检测闪烁。
22
22
  */
23
23
  export function TerminalPanel({
@@ -33,7 +33,7 @@ export function TerminalPanel({
33
33
  const [hasElectron, setHasElectron] = useState<boolean | null>(null);
34
34
 
35
35
  useEffect(() => {
36
- setHasElectron(isElectron() && !!getElectronBridge()?.terminal);
36
+ setHasElectron(hasHostBridge() && !!getHostBridge()?.terminal);
37
37
  }, []);
38
38
 
39
39
  if (hasElectron === null) {
@@ -5,7 +5,7 @@ import { WebLinksAddon } from '@xterm/addon-web-links';
5
5
  import { LuListTodo } from 'react-icons/lu';
6
6
  import '@xterm/xterm/css/xterm.css';
7
7
  import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
8
- import { getElectronBridge } from '@/lib/electron-bridge';
8
+ import { getHostBridge } from '@/lib/host-bridge';
9
9
  import { BRAND_NAME } from '@/brand';
10
10
  import { t } from '@/i18n';
11
11
 
@@ -14,7 +14,7 @@ import { t } from '@/i18n';
14
14
  * `terminal.*` bridge. Ported from `deeppath/apps/web/src/app/agent/terminal/
15
15
  * TerminalView.tsx`, with these adaptations:
16
16
  *
17
- * - `window.electron` direct access → `getElectronBridge()` helper so the
17
+ * - `window.steerableHost` direct access → `getHostBridge()` helper so the
18
18
  * "no bridge" path is type-safe.
19
19
  * - Removed `'use client'` directive (not a Next.js project).
20
20
  * - Container is `h-full` instead of `h-screen` so the component is
@@ -45,11 +45,11 @@ export function TerminalView({
45
45
 
46
46
  useEffect(() => {
47
47
  if (!containerRef.current) return;
48
- const bridge = getElectronBridge();
48
+ const bridge = getHostBridge();
49
49
  const terminal = bridge?.terminal;
50
50
  if (!terminal) {
51
51
  setStatus('error');
52
- setStatusMsg(t('window.electron.terminal is unavailable'));
52
+ setStatusMsg(t('The host terminal is unavailable'));
53
53
  return;
54
54
  }
55
55
 
@@ -2,7 +2,7 @@ import { LuShieldAlert, LuTriangleAlert } from 'react-icons/lu';
2
2
  import {
3
3
  type ApprovalDecisionKind,
4
4
  type ApprovalPromptRequest,
5
- } from '@/lib/electron-bridge';
5
+ } from '@/lib/host-bridge';
6
6
  import { t } from '@/i18n';
7
7
 
8
8
  /**
@@ -9,10 +9,10 @@ import {
9
9
  type ReactNode,
10
10
  } from 'react';
11
11
  import {
12
- getElectronBridge,
12
+ getHostBridge,
13
13
  type ApprovalDecisionKind,
14
14
  type ApprovalPromptRequest,
15
- } from '@/lib/electron-bridge';
15
+ } from '@/lib/host-bridge';
16
16
  import { bindPromptToActiveChat, promptVisibleInChat } from './prompt-chat-scope';
17
17
 
18
18
  interface ApprovalPromptContextValue {
@@ -37,7 +37,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
37
37
  const decidedRequestIds = useRef(new Set<string>());
38
38
 
39
39
  useEffect(() => {
40
- const bridge = getElectronBridge();
40
+ const bridge = getHostBridge();
41
41
  if (!bridge?.approval) return;
42
42
  let active = true;
43
43
  const addRequests = (
@@ -76,7 +76,7 @@ export function ApprovalPromptProvider({ children }: { children: ReactNode }) {
76
76
  }, []);
77
77
 
78
78
  const decide = useCallback((requestId: string, kind: ApprovalDecisionKind) => {
79
- const bridge = getElectronBridge();
79
+ const bridge = getHostBridge();
80
80
  decidedRequestIds.current.add(requestId);
81
81
  setQueue((previous) => previous.filter((request) => request.requestId !== requestId));
82
82
  void bridge?.approval?.decide({ requestId, kind });
@@ -8,7 +8,7 @@ import {
8
8
  useState,
9
9
  type ReactNode,
10
10
  } from 'react';
11
- import { getElectronBridge, type AskUserPromptRequest } from '@/lib/electron-bridge';
11
+ import { getHostBridge, type AskUserPromptRequest } from '@/lib/host-bridge';
12
12
  import { bindPromptToActiveChat, promptVisibleInChat } from './prompt-chat-scope';
13
13
 
14
14
  type AnswerValue = string | string[];
@@ -35,7 +35,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
35
35
  const answeredRequestIds = useRef(new Set<string>());
36
36
 
37
37
  useEffect(() => {
38
- const bridge = getElectronBridge();
38
+ const bridge = getHostBridge();
39
39
  if (!bridge?.askUser) return;
40
40
  let active = true;
41
41
  const addRequests = (
@@ -75,7 +75,7 @@ export function AskUserPromptProvider({ children }: { children: ReactNode }) {
75
75
 
76
76
  const answer = useCallback(
77
77
  (requestId: string, answers: Record<string, AnswerValue>) => {
78
- const bridge = getElectronBridge();
78
+ const bridge = getHostBridge();
79
79
  answeredRequestIds.current.add(requestId);
80
80
  setQueue((previous) => previous.filter((request) => request.requestId !== requestId));
81
81
  void bridge?.askUser?.answer({ requestId, answers });
@@ -10,7 +10,7 @@ import {
10
10
  LuPlus,
11
11
  } from 'react-icons/lu';
12
12
  import { CreateProjectModal } from '@/components/CreateProjectModal';
13
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
13
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
14
14
  import {
15
15
  createProject,
16
16
  updateChatProject,
@@ -219,8 +219,8 @@ export function ChatProjectBadge({
219
219
 
220
220
  const changeFolder = () =>
221
221
  run(async () => {
222
- if (!isElectron() || !project) return;
223
- const result = await getElectronBridge()!.local?.selectDirectory({
222
+ if (!hasHostBridge() || !project) return;
223
+ const result = await getHostBridge()!.local?.selectDirectory({
224
224
  title: t('Choose a new folder for "{name}"', { name: project.name }),
225
225
  });
226
226
  if (!result || result.canceled || result.filePaths.length === 0) return;
@@ -18,7 +18,7 @@ import {
18
18
  mergeTaskWorktree,
19
19
  type LocalTask,
20
20
  } from '@/lib/local-api';
21
- import { getElectronBridge } from '@/lib/electron-bridge';
21
+ import { getHostBridge } from '@/lib/host-bridge';
22
22
  import { t } from '@/i18n';
23
23
 
24
24
  /**
@@ -81,7 +81,7 @@ export function TaskPanelModal({
81
81
  // 任务终态推送 → 重拉列表。preload 只透传 chatId/taskId/status,
82
82
  // 记录本体以 SQLite 为准。
83
83
  useEffect(() => {
84
- const bridge = getElectronBridge();
84
+ const bridge = getHostBridge();
85
85
  if (!bridge?.onTaskUpdated) return;
86
86
  return bridge.onTaskUpdated((payload) => {
87
87
  if (payload.chatId === chatId) void refresh();
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react
2
2
  import { LuArrowDown, LuLoaderCircle, LuTerminal } from 'react-icons/lu';
3
3
  import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
4
4
  import { getChildProcess, getTaskProcess } from '@/lib/local-api';
5
- import { getElectronBridge } from '@/lib/electron-bridge';
5
+ import { getHostBridge } from '@/lib/host-bridge';
6
6
  import { t } from '@/i18n';
7
7
  import { Markdown } from '@/components/chat/Markdown';
8
8
  import { TurnProcessGroup } from '@/components/chat/TurnProcessGroup';
@@ -154,7 +154,7 @@ export function TaskProcessPanel({
154
154
 
155
155
  useEffect(() => {
156
156
  if (childRecordId) return;
157
- const bridge = getElectronBridge();
157
+ const bridge = getHostBridge();
158
158
  if (!bridge?.onTaskProcess) return;
159
159
  return bridge.onTaskProcess((payload) => {
160
160
  if (payload.taskId !== inspected.id) return;
@@ -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,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
3
3
  import { t } from '@/i18n';
4
- import { isElectron } from '@/lib/electron-bridge';
4
+ import { hasHostBridge } from '@/lib/host-bridge';
5
5
  import {
6
6
  listChatAgentSkills,
7
7
  listChatAgentTools,
@@ -154,7 +154,7 @@ export function AgentCapabilityFields({
154
154
  const [toolFilter, setToolFilter] = useState('');
155
155
 
156
156
  const fetchCatalogs = useCallback(async () => {
157
- if (!isElectron()) return;
157
+ if (!hasHostBridge()) return;
158
158
  setLoading(true);
159
159
  try {
160
160
  const [skillRes, toolRes] = await Promise.all([
@@ -7,7 +7,7 @@ import {
7
7
  } from 'react-icons/lu';
8
8
  import { ConfirmDialog } from '@/components/ConfirmDialog';
9
9
  import { t } from '@/i18n';
10
- import { isElectron } from '@/lib/electron-bridge';
10
+ import { hasHostBridge } from '@/lib/host-bridge';
11
11
  import {
12
12
  archiveChatAgent,
13
13
  createChatAgent,
@@ -101,7 +101,7 @@ export function AgentsSettingsPanel({
101
101
  const [error, setError] = useState<string | null>(null);
102
102
 
103
103
  const fetchAgents = useCallback(async () => {
104
- if (!isElectron()) return;
104
+ if (!hasHostBridge()) return;
105
105
  setLoading(true);
106
106
  setError(null);
107
107
  try {
@@ -151,7 +151,7 @@ export function AgentsSettingsPanel({
151
151
 
152
152
  const handleSave = async () => {
153
153
  const trimmedName = name.trim();
154
- if (!trimmedName || !isElectron()) return;
154
+ if (!trimmedName || !hasHostBridge()) return;
155
155
  setSaving(true);
156
156
  setError(null);
157
157
  try {
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuChartBar } from 'react-icons/lu';
3
3
  import { BRAND_NAME } from '@/brand';
4
4
  import { t } from '@/i18n';
5
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
5
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
6
6
 
7
7
  type InsightsWire = {
8
8
  installId: string;
@@ -34,8 +34,8 @@ export function InsightsConsentBanner() {
34
34
  const [saving, setSaving] = useState(false);
35
35
 
36
36
  useEffect(() => {
37
- if (!isElectron()) return;
38
- void getElectronBridge()!
37
+ if (!hasHostBridge()) return;
38
+ void getHostBridge()!
39
39
  .localBackend.request<InsightsWire>({ method: 'GET', path: '/api/v2/local-settings/insights' })
40
40
  .then((data) => {
41
41
  if (!data.promptedAt) setOpen(true);
@@ -44,10 +44,10 @@ export function InsightsConsentBanner() {
44
44
  }, []);
45
45
 
46
46
  const save = useCallback(async (upload: boolean) => {
47
- if (!isElectron()) return;
47
+ if (!hasHostBridge()) return;
48
48
  setSaving(true);
49
49
  try {
50
- await getElectronBridge()!.localBackend.request({
50
+ await getHostBridge()!.localBackend.request({
51
51
  method: 'POST',
52
52
  path: '/api/v2/local-settings/insights',
53
53
  body: {
@@ -112,11 +112,11 @@ export function InsightsSettingsPanel() {
112
112
  const [error, setError] = useState<string | null>(null);
113
113
 
114
114
  const load = useCallback(async () => {
115
- if (!isElectron()) return;
115
+ if (!hasHostBridge()) return;
116
116
  setLoading(true);
117
117
  setError(null);
118
118
  try {
119
- const res = await getElectronBridge()!.localBackend.request<InsightsWire>({
119
+ const res = await getHostBridge()!.localBackend.request<InsightsWire>({
120
120
  method: 'GET',
121
121
  path: '/api/v2/local-settings/insights',
122
122
  });
@@ -133,11 +133,11 @@ export function InsightsSettingsPanel() {
133
133
  }, [load]);
134
134
 
135
135
  const save = async () => {
136
- if (!isElectron()) return;
136
+ if (!hasHostBridge()) return;
137
137
  setSaving(true);
138
138
  setError(null);
139
139
  try {
140
- const res = await getElectronBridge()!.localBackend.request<InsightsWire>({
140
+ const res = await getHostBridge()!.localBackend.request<InsightsWire>({
141
141
  method: 'POST',
142
142
  path: '/api/v2/local-settings/insights',
143
143
  body: {
@@ -162,15 +162,15 @@ export function InsightsSettingsPanel() {
162
162
  };
163
163
 
164
164
  const exportFile = async () => {
165
- if (!isElectron()) return;
165
+ if (!hasHostBridge()) return;
166
166
  setError(null);
167
167
  try {
168
- const bundle = await getElectronBridge()!.localBackend.request<unknown>({
168
+ const bundle = await getHostBridge()!.localBackend.request<unknown>({
169
169
  method: 'GET',
170
170
  path: '/api/v2/insights/export',
171
171
  });
172
172
  const text = `${JSON.stringify(bundle, null, 2)}\n`;
173
- const saved = await getElectronBridge()!.local?.saveTextFile?.({
173
+ const saved = await getHostBridge()!.local?.saveTextFile?.({
174
174
  title: t('Export local insight records'),
175
175
  defaultPath: `deeppath-insights-${new Date().toISOString().slice(0, 10)}.json`,
176
176
  content: text,
@@ -182,10 +182,10 @@ export function InsightsSettingsPanel() {
182
182
  };
183
183
 
184
184
  const uploadNow = async () => {
185
- if (!isElectron()) return;
185
+ if (!hasHostBridge()) return;
186
186
  setError(null);
187
187
  try {
188
- const res = await getElectronBridge()!.localBackend.request<{ ok: boolean; detail: string }>({
188
+ const res = await getHostBridge()!.localBackend.request<{ ok: boolean; detail: string }>({
189
189
  method: 'POST',
190
190
  path: '/api/v2/insights/upload-local',
191
191
  });
@@ -214,7 +214,7 @@ export function InsightsSettingsPanel() {
214
214
  'Even if the user does not agree to upload, records stay on this device. You can export JSON and send it to the product team, or click "Upload now" once.',
215
215
  )}
216
216
  </p>
217
- {!isElectron() ? (
217
+ {!hasHostBridge() ? (
218
218
  <p className="text-[11px] text-agent-muted-foreground">{t('Open this in the desktop app')}</p>
219
219
  ) : loading ? (
220
220
  <p className="text-[11px] text-agent-muted-foreground">{t('Loading…')}</p>
@@ -33,7 +33,7 @@ import { modelCapabilityChips } from '@/components/settings/model-capabilities';
33
33
  import { ModelIdCombobox } from '@/components/settings/ModelIdCombobox';
34
34
  import { SettingsSaveButton } from '@/components/settings/SettingsSaveButton';
35
35
  import { t } from '@/i18n';
36
- import { isElectron } from '@/lib/electron-bridge';
36
+ import { hasHostBridge } from '@/lib/host-bridge';
37
37
  import {
38
38
  COMPAT_AUTO,
39
39
  formStateFromOverrides,
@@ -124,7 +124,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
124
124
  const [resolvedPreset, setResolvedPreset] = useState<ProviderPresetOverride | null>(null);
125
125
 
126
126
  const reload = useCallback(async () => {
127
- if (!isElectron()) {
127
+ if (!hasHostBridge()) {
128
128
  setError(t('Open this in the desktop app'));
129
129
  return;
130
130
  }
@@ -344,7 +344,7 @@ export const LlmSettingsPanel = forwardRef<LlmSettingsPanelHandle, LlmSettingsPa
344
344
  };
345
345
 
346
346
  const handleSave = async () => {
347
- if (!isElectron()) {
347
+ if (!hasHostBridge()) {
348
348
  setError(t('Open this in the desktop app'));
349
349
  return;
350
350
  }
@@ -10,7 +10,7 @@ import {
10
10
  LuTrash2,
11
11
  } from 'react-icons/lu';
12
12
  import { t } from '@/i18n';
13
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
13
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
14
14
 
15
15
  /**
16
16
  * McpSettingsPanel — MCP 服务管理面板(JSON 导入 / 手动增改 / 启停 / 测试 /
@@ -19,8 +19,8 @@ import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
19
19
  * 渲染在 `/settings?section=plugins` 的 MCP 分类(AgentLayout 右侧内容区)。
20
20
  *
21
21
  * 数据与状态完全自管理:挂载时拉一次列表,操作后刷新。后端走
22
- * local-backend REST(`/api/v2/mcp/servers*`),注册表持久化在 main 进程的
23
- * electron-store(`agent-mcp-servers.json`),无专用 IPC channel。
22
+ * local-backend REST(`/api/v2/mcp/servers*`),注册表持久化在用户数据目录的
23
+ * `agent-mcp-servers.json`。
24
24
  */
25
25
 
26
26
  /** GET /api/v2/mcp/servers 返回的服务条目(含工具缓存状态)。 */
@@ -68,11 +68,11 @@ export function McpSettingsPanel() {
68
68
  const [error, setError] = useState<string | null>(null);
69
69
 
70
70
  const fetchMcpServers = useCallback(async () => {
71
- if (!isElectron()) return;
71
+ if (!hasHostBridge()) return;
72
72
  setMcpLoading(true);
73
73
  setError(null);
74
74
  try {
75
- const res = await getElectronBridge()!.localBackend.request<{ servers: McpServerInfo[] }>({
75
+ const res = await getHostBridge()!.localBackend.request<{ servers: McpServerInfo[] }>({
76
76
  method: 'GET',
77
77
  path: '/api/v2/mcp/servers',
78
78
  });
@@ -116,7 +116,7 @@ export function McpSettingsPanel() {
116
116
  };
117
117
 
118
118
  const handleSaveMcpServer = async () => {
119
- if (!isElectron()) return;
119
+ if (!hasHostBridge()) return;
120
120
  setMcpSaving(true);
121
121
  setError(null);
122
122
  try {
@@ -142,13 +142,13 @@ export function McpSettingsPanel() {
142
142
  bearerTokenEnvVar: mcpFormBearerEnv.trim() || undefined,
143
143
  };
144
144
  if (mcpEditingId) {
145
- await getElectronBridge()!.localBackend.request({
145
+ await getHostBridge()!.localBackend.request({
146
146
  method: 'PUT',
147
147
  path: `/api/v2/mcp/servers/${encodeURIComponent(mcpEditingId)}`,
148
148
  body,
149
149
  });
150
150
  } else {
151
- await getElectronBridge()!.localBackend.request({
151
+ await getHostBridge()!.localBackend.request({
152
152
  method: 'POST',
153
153
  path: '/api/v2/mcp/servers',
154
154
  body,
@@ -188,7 +188,7 @@ export function McpSettingsPanel() {
188
188
  const handleToggleMcpServer = async (server: McpServerInfo) => {
189
189
  setError(null);
190
190
  try {
191
- await getElectronBridge()!.localBackend.request({
191
+ await getHostBridge()!.localBackend.request({
192
192
  method: 'PUT',
193
193
  path: `/api/v2/mcp/servers/${encodeURIComponent(server.id)}`,
194
194
  body: { enabled: !server.enabled },
@@ -203,7 +203,7 @@ export function McpSettingsPanel() {
203
203
  setMcpDeletingId(server.id);
204
204
  setError(null);
205
205
  try {
206
- await getElectronBridge()!.localBackend.request({
206
+ await getHostBridge()!.localBackend.request({
207
207
  method: 'DELETE',
208
208
  path: `/api/v2/mcp/servers/${encodeURIComponent(server.id)}`,
209
209
  });
@@ -219,7 +219,7 @@ export function McpSettingsPanel() {
219
219
  setMcpTestingId(server.id);
220
220
  setError(null);
221
221
  try {
222
- const res = await getElectronBridge()!.localBackend.request<{
222
+ const res = await getHostBridge()!.localBackend.request<{
223
223
  success: boolean;
224
224
  toolCount: number;
225
225
  tools: { name: string }[];
@@ -245,7 +245,7 @@ export function McpSettingsPanel() {
245
245
  setMcpImporting(true);
246
246
  setError(null);
247
247
  try {
248
- const res = await getElectronBridge()!.localBackend.request<{
248
+ const res = await getHostBridge()!.localBackend.request<{
249
249
  added: { name: string }[];
250
250
  skipped: string[];
251
251
  }>({
@@ -2,9 +2,9 @@ import { useEffect, useState } from 'react';
2
2
  import { LuCodeXml } from 'react-icons/lu';
3
3
  import { t } from '@/i18n';
4
4
  import {
5
- getElectronBridge,
5
+ getHostBridge,
6
6
  type PythonRunnerSnapshot,
7
- } from '@/lib/electron-bridge';
7
+ } from '@/lib/host-bridge';
8
8
 
9
9
  type Source = PythonRunnerSnapshot['source'];
10
10
 
@@ -20,7 +20,7 @@ export function PythonRunnerSettingsPanel() {
20
20
  const [localPath, setLocalPath] = useState('');
21
21
 
22
22
  useEffect(() => {
23
- const runner = getElectronBridge()?.pythonRunner;
23
+ const runner = getHostBridge()?.pythonRunner;
24
24
  if (!runner) return;
25
25
  let alive = true;
26
26
  void runner.snapshot().then((next) => {
@@ -39,7 +39,7 @@ export function PythonRunnerSettingsPanel() {
39
39
  }, []);
40
40
 
41
41
  if (!snapshot?.supported) return null;
42
- const runner = getElectronBridge()?.pythonRunner;
42
+ const runner = getHostBridge()?.pythonRunner;
43
43
  const busy =
44
44
  snapshot.phase === 'downloading'
45
45
  || snapshot.phase === 'verifying'