@steerable/agent-shell-web 0.6.55 → 0.6.57

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 (47) 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/CliInstallSettingsPanel.tsx +67 -0
  19. package/src/components/settings/InsightsSettingsPanel.tsx +15 -15
  20. package/src/components/settings/LlmSettingsPanel.tsx +3 -3
  21. package/src/components/settings/McpSettingsPanel.tsx +12 -12
  22. package/src/components/settings/PythonRunnerSettingsPanel.tsx +4 -4
  23. package/src/components/settings/SecuritySettingsPanel.tsx +3 -3
  24. package/src/components/settings/SkillsSettingsPanel.tsx +7 -7
  25. package/src/components/settings/TelemetrySettingsPanel.tsx +5 -5
  26. package/src/components/settings/UsagePanel.tsx +3 -3
  27. package/src/components/settings/WebSearchSettingsPanel.tsx +5 -5
  28. package/src/hooks/useChatsAndAgents.ts +9 -9
  29. package/src/hooks/useProjects.ts +2 -2
  30. package/src/i18n/index.tsx +1 -1
  31. package/src/layouts/AgentLayout.tsx +14 -6
  32. package/src/lib/app-release.ts +1 -1
  33. package/src/lib/attachments.ts +2 -2
  34. package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +10 -10
  35. package/src/lib/chat-transport.ts +10 -10
  36. package/src/lib/host-bridge.ts +376 -4
  37. package/src/lib/host-paste.ts +1 -1
  38. package/src/lib/http-bridge.ts +10 -8
  39. package/src/lib/insights.ts +2 -2
  40. package/src/lib/local-api.ts +13 -5
  41. package/src/lib/portable.ts +3 -3
  42. package/src/lib/slash-sources.ts +3 -3
  43. package/src/lib/tauri-bridge.ts +1 -1
  44. package/src/main.tsx +3 -3
  45. package/src/pages/AgentPage.tsx +23 -23
  46. package/src/pages/SettingsPage.tsx +4 -4
  47. package/src/lib/electron-bridge.ts +0 -389
@@ -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'
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuShieldCheck } 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
  getSidecarSandboxPosture,
7
7
  type EgressPosture,
@@ -109,7 +109,7 @@ export function SecuritySettingsPanel() {
109
109
  const [unavailable, setUnavailable] = useState<string | null>(null);
110
110
 
111
111
  const fetchPosture = useCallback(async () => {
112
- if (!isElectron()) return;
112
+ if (!hasHostBridge()) return;
113
113
  setLoading(true);
114
114
  setUnavailable(null);
115
115
  try {
@@ -141,7 +141,7 @@ export function SecuritySettingsPanel() {
141
141
  {t('Sidecar process sandbox')}
142
142
  </h4>
143
143
 
144
- {!isElectron() ? (
144
+ {!hasHostBridge() ? (
145
145
  <p className="text-[11px] text-agent-muted-foreground">{t('Open this in the desktop app')}</p>
146
146
  ) : loading ? (
147
147
  <div className="flex items-center gap-2 py-2 text-xs text-agent-muted-foreground">
@@ -6,7 +6,7 @@ import {
6
6
  LuTrash2,
7
7
  } from 'react-icons/lu';
8
8
  import { t } from '@/i18n';
9
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
9
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
10
10
  import { getPackHiddenSlashSkills } from '@/packs/registry';
11
11
 
12
12
  /**
@@ -59,11 +59,11 @@ export function SkillsSettingsPanel() {
59
59
  const [error, setError] = useState<string | null>(null);
60
60
 
61
61
  const fetchSkills = useCallback(async () => {
62
- if (!isElectron()) return;
62
+ if (!hasHostBridge()) return;
63
63
  setSkillsLoading(true);
64
64
  setError(null);
65
65
  try {
66
- const res = await getElectronBridge()!.localBackend.request<{ skills: any[] }>({
66
+ const res = await getHostBridge()!.localBackend.request<{ skills: any[] }>({
67
67
  method: 'GET',
68
68
  path: '/api/v2/chat-agents/skills',
69
69
  });
@@ -81,9 +81,9 @@ export function SkillsSettingsPanel() {
81
81
  }, [fetchSkills]);
82
82
 
83
83
  const handleBrowseFolder = async () => {
84
- if (!isElectron()) return;
84
+ if (!hasHostBridge()) return;
85
85
  try {
86
- const bridge = getElectronBridge();
86
+ const bridge = getHostBridge();
87
87
  if (bridge?.local?.selectDirectory) {
88
88
  setImportStatus(t('Opening folder picker...'));
89
89
  const result = await bridge.local.selectDirectory();
@@ -110,7 +110,7 @@ export function SkillsSettingsPanel() {
110
110
  setError(null);
111
111
  setImportStatus(t('Importing local skill...'));
112
112
  try {
113
- const res = await getElectronBridge()!.localBackend.request<{ success: boolean; name: string }>({
113
+ const res = await getHostBridge()!.localBackend.request<{ success: boolean; name: string }>({
114
114
  method: 'POST',
115
115
  path: '/api/v2/chat-agents/skills/import',
116
116
  body: { path: trimmed },
@@ -137,7 +137,7 @@ export function SkillsSettingsPanel() {
137
137
  setDeletingName(name);
138
138
  setError(null);
139
139
  try {
140
- const res = await getElectronBridge()!.localBackend.request<{ success: boolean }>({
140
+ const res = await getHostBridge()!.localBackend.request<{ success: boolean }>({
141
141
  method: 'DELETE',
142
142
  path: `/api/v2/chat-agents/skills/delete/${encodeURIComponent(name)}`,
143
143
  });
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuActivity } from 'react-icons/lu';
3
3
  import { t } from '@/i18n';
4
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
4
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
5
5
 
6
6
  /**
7
7
  * TelemetrySettingsPanel — W6-6 遥测合规化的桌面 collector 配置面板。
@@ -36,11 +36,11 @@ export function TelemetrySettingsPanel() {
36
36
  const [error, setError] = useState<string | null>(null);
37
37
 
38
38
  const fetchSettings = useCallback(async () => {
39
- if (!isElectron()) return;
39
+ if (!hasHostBridge()) return;
40
40
  setLoading(true);
41
41
  setError(null);
42
42
  try {
43
- const res = await getElectronBridge()!.localBackend.request<TelemetrySettingsWire | null>({
43
+ const res = await getHostBridge()!.localBackend.request<TelemetrySettingsWire | null>({
44
44
  method: 'GET',
45
45
  path: '/api/v2/local-settings/telemetry',
46
46
  });
@@ -58,12 +58,12 @@ export function TelemetrySettingsPanel() {
58
58
  }, [fetchSettings]);
59
59
 
60
60
  const handleSave = async () => {
61
- if (!isElectron()) return;
61
+ if (!hasHostBridge()) return;
62
62
  setSaving(true);
63
63
  setError(null);
64
64
  setStatus(null);
65
65
  try {
66
- const saved = await getElectronBridge()!.localBackend.request<TelemetrySettingsWire>({
66
+ const saved = await getHostBridge()!.localBackend.request<TelemetrySettingsWire>({
67
67
  method: 'POST',
68
68
  path: '/api/v2/local-settings/telemetry',
69
69
  body: { endpoint: endpoint.trim(), privacyMode },
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuChartBar, LuRefreshCw } from 'react-icons/lu';
3
3
  import { t } from '@/i18n';
4
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
4
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
5
5
 
6
6
  /**
7
7
  * UsagePanel — W6-9 用量与成本归因的桌面面板。
@@ -66,11 +66,11 @@ export function UsagePanel() {
66
66
  const [error, setError] = useState<string | null>(null);
67
67
 
68
68
  const fetchSummary = useCallback(async (windowDays: number) => {
69
- if (!isElectron()) return;
69
+ if (!hasHostBridge()) return;
70
70
  setLoading(true);
71
71
  setError(null);
72
72
  try {
73
- const res = await getElectronBridge()!.localBackend.request<UsageSummaryWire>({
73
+ const res = await getHostBridge()!.localBackend.request<UsageSummaryWire>({
74
74
  method: 'GET',
75
75
  path: `/api/v2/usage/summary?days=${windowDays}`,
76
76
  });
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
2
  import { LuLoaderCircle, LuSearch } from 'react-icons/lu';
3
3
  import { t } from '@/i18n';
4
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
4
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
5
5
 
6
6
  /**
7
7
  * Search backend settings. `ddg` registers `web_search` with no key.
@@ -33,11 +33,11 @@ export function WebSearchSettingsPanel() {
33
33
  const [error, setError] = useState<string | null>(null);
34
34
 
35
35
  const fetchSettings = useCallback(async () => {
36
- if (!isElectron()) return;
36
+ if (!hasHostBridge()) return;
37
37
  setLoading(true);
38
38
  setError(null);
39
39
  try {
40
- const res = await getElectronBridge()!.localBackend.request<WebSearchSettingsWire | null>({
40
+ const res = await getHostBridge()!.localBackend.request<WebSearchSettingsWire | null>({
41
41
  method: 'GET',
42
42
  path: '/api/v2/local-settings/web-search',
43
43
  });
@@ -55,13 +55,13 @@ export function WebSearchSettingsPanel() {
55
55
  }, [fetchSettings]);
56
56
 
57
57
  const handleSave = async () => {
58
- if (!isElectron()) return;
58
+ if (!hasHostBridge()) return;
59
59
  setSaving(true);
60
60
  setError(null);
61
61
  setStatus(null);
62
62
  try {
63
63
  const posted = provider;
64
- const saved = await getElectronBridge()!.localBackend.request<WebSearchSettingsWire>({
64
+ const saved = await getHostBridge()!.localBackend.request<WebSearchSettingsWire>({
65
65
  method: 'POST',
66
66
  path: '/api/v2/local-settings/web-search',
67
67
  body: { provider: posted, apiKey: apiKey.trim() },
@@ -8,7 +8,7 @@
8
8
  * adapter:
9
9
  * - It wires the framework hook to the local-backend transport
10
10
  * (`@/lib/local-api`).
11
- * - It gates everything behind `isElectron()` so a renderer running in a
11
+ * - It gates everything behind `hasHostBridge()` so a renderer running in a
12
12
  * plain browser doesn't fire requests.
13
13
  * - It keeps the legacy `UseChatsAndAgentsResult` shape so callers
14
14
  * (`AgentLayout`, `useChatsAndAgents()` consumers) don't change.
@@ -35,7 +35,7 @@ import {
35
35
  type LocalChat,
36
36
  type LocalChatAgent,
37
37
  } from '@/lib/local-api';
38
- import { isElectron } from '@/lib/electron-bridge';
38
+ import { hasHostBridge } from '@/lib/host-bridge';
39
39
  import { pickDefaultAgentId } from '@/brand';
40
40
 
41
41
  const CHAT_PAGE_SIZE = 50;
@@ -66,12 +66,12 @@ export interface UseChatsAndAgentsResult {
66
66
  }
67
67
 
68
68
  export function useChatsAndAgents(): UseChatsAndAgentsResult {
69
- const isElectronRef = useRef(isElectron());
69
+ const hasHostBridgeRef = useRef(hasHostBridge());
70
70
 
71
71
  const transport = useMemo<ChatListTransport<LocalChat, LocalChatAgent>>(
72
72
  () => ({
73
73
  listChats: async ({ page, pageSize }) => {
74
- if (!isElectronRef.current) return { chats: [], hasMore: false };
74
+ if (!hasHostBridgeRef.current) return { chats: [], hasMore: false };
75
75
  const data = await listChats({ page, limit: pageSize });
76
76
  return {
77
77
  chats: data.chats,
@@ -79,13 +79,13 @@ export function useChatsAndAgents(): UseChatsAndAgentsResult {
79
79
  };
80
80
  },
81
81
  listAgents: async () => {
82
- if (!isElectronRef.current) return [];
82
+ if (!hasHostBridgeRef.current) return [];
83
83
  const data = await listChatAgents(false);
84
84
  return data.agents;
85
85
  },
86
86
  createChat: async ({ agentId, projectId }) => {
87
- if (!isElectronRef.current) {
88
- throw new Error('not in electron');
87
+ if (!hasHostBridgeRef.current) {
88
+ throw new Error('not connected to the host');
89
89
  }
90
90
  const res = await apiCreateChat({
91
91
  ...(agentId ? { agentId } : {}),
@@ -94,7 +94,7 @@ export function useChatsAndAgents(): UseChatsAndAgentsResult {
94
94
  return res.chatId;
95
95
  },
96
96
  deleteChat: async (chatId) => {
97
- if (!isElectronRef.current) return false;
97
+ if (!hasHostBridgeRef.current) return false;
98
98
  await apiDeleteChat(chatId);
99
99
  return true;
100
100
  },
@@ -110,7 +110,7 @@ export function useChatsAndAgents(): UseChatsAndAgentsResult {
110
110
  pageSize: CHAT_PAGE_SIZE,
111
111
  // The renderer mounts before electron decides whether the bridge is live;
112
112
  // skip the auto-fetch and trigger it manually after the SSR no-op check.
113
- skipInitialLoad: !isElectronRef.current,
113
+ skipInitialLoad: !hasHostBridgeRef.current,
114
114
  });
115
115
 
116
116
  // Default-select this flavor's builtin expert once the catalog loads.
@@ -3,7 +3,7 @@
3
3
  * 任一侧新建、改名或改目录后调用 refresh,其它订阅者一起更新。
4
4
  */
5
5
  import { useCallback, useEffect, useSyncExternalStore } from 'react';
6
- import { isElectron } from '@/lib/electron-bridge';
6
+ import { hasHostBridge } from '@/lib/host-bridge';
7
7
  import { hostToolChrome } from '@/lib/host-tools';
8
8
  import { listProjects, type LocalProject } from '@/lib/local-api';
9
9
 
@@ -36,7 +36,7 @@ export function getProjectsSnapshot(): ProjectsSnapshot {
36
36
  /** 重新拉取项目列表并通知所有订阅者。非 Electron 或关掉项目入口时清空。 */
37
37
  export async function refreshProjects(): Promise<void> {
38
38
  const seq = ++requestSeq;
39
- if (!isElectron() || !hostToolChrome('projects')) {
39
+ if (!hasHostBridge() || !hostToolChrome('projects')) {
40
40
  if (snapshot.projects.length > 0 || snapshot.error) {
41
41
  snapshot = EMPTY_SNAPSHOT;
42
42
  emit();
@@ -35,7 +35,7 @@ export function I18nProvider({ children }: { children: ReactNode }) {
35
35
  t,
36
36
  setLocale: (next) => {
37
37
  if (!setLocale(next)) return;
38
- window.electron?.setLocale?.(next);
38
+ window.steerableHost?.setLocale?.(next);
39
39
  },
40
40
  }),
41
41
  [locale],
@@ -8,7 +8,7 @@ import { AskUserPromptProvider } from '@/components/chat/AskUserPromptProvider';
8
8
  import { ApprovalPromptProvider } from '@/components/chat/ApprovalPromptProvider';
9
9
  import { InsightsConsentBanner } from '@/components/settings/InsightsSettingsPanel';
10
10
  import { trackBehavior } from '@/lib/insights';
11
- import { getElectronBridge, isElectron } from '@/lib/electron-bridge';
11
+ import { getHostBridge, hasHostBridge } from '@/lib/host-bridge';
12
12
  import { deleteChatIfEmpty, pruneEmptyChats } from '@/lib/local-api';
13
13
  import { PORTABLE_CHATS_CHANGED_EVENT } from '@/lib/portable';
14
14
  import { getPackChatSlots, type PackChatSlotContribution } from '@/packs/registry';
@@ -170,7 +170,7 @@ function AgentLayoutContent() {
170
170
  refreshChatsRef.current = data.refreshChats;
171
171
  }, [data.refreshChats]);
172
172
  useEffect(() => {
173
- const bridge = getElectronBridge();
173
+ const bridge = getHostBridge();
174
174
  if (!bridge?.onChatTitleUpdated) return;
175
175
  const unsubscribe = bridge.onChatTitleUpdated((payload) => {
176
176
  patchChatTitleRef.current(payload.chatId, payload.title);
@@ -181,13 +181,21 @@ function AgentLayoutContent() {
181
181
  // 现场补建(见 router.handleStream),随后广播 `chat-created`。这里刷一次
182
182
  // 侧栏把这条会话拉出来——否则会出现"URL 能聊、列表里却查无此会话"。
183
183
  useEffect(() => {
184
- const bridge = getElectronBridge();
184
+ const bridge = getHostBridge();
185
185
  if (!bridge?.onChatCreated) return;
186
186
  const unsubscribe = bridge.onChatCreated(() => {
187
187
  void refreshChatsRef.current();
188
188
  });
189
189
  return unsubscribe;
190
190
  }, []);
191
+ useEffect(() => {
192
+ const bridge = getHostBridge();
193
+ if (!bridge?.onStoreChanged) return;
194
+ const unsubscribe = bridge.onStoreChanged(() => {
195
+ void refreshChatsRef.current();
196
+ });
197
+ return unsubscribe;
198
+ }, []);
191
199
  useEffect(() => {
192
200
  const onImported = () => {
193
201
  void refreshChatsRef.current();
@@ -199,7 +207,7 @@ function AgentLayoutContent() {
199
207
  // 空会话不进侧栏:启动时清掉从未发过消息的残骸;离开一段空对话时丢掉它。
200
208
  // except / 离开检测都避开当前打开的 chatId,避免和首页首条发送抢跑。
201
209
  useEffect(() => {
202
- if (!isElectron()) return;
210
+ if (!hasHostBridge()) return;
203
211
  let cancelled = false;
204
212
  void pruneEmptyChats(chatId)
205
213
  .then((res) => {
@@ -217,7 +225,7 @@ function AgentLayoutContent() {
217
225
  useEffect(() => {
218
226
  const prev = prevChatIdRef.current;
219
227
  prevChatIdRef.current = chatId;
220
- if (!isElectron() || !prev || prev === chatId) return;
228
+ if (!hasHostBridge() || !prev || prev === chatId) return;
221
229
  void deleteChatIfEmpty(prev)
222
230
  .then((res) => {
223
231
  if (res.deleted) return refreshChatsRef.current();
@@ -416,7 +424,7 @@ function AgentLayoutContent() {
416
424
  // so this flips the layout state owned by AgentLayout.
417
425
  useEffect(() => {
418
426
  if (!hostToolChrome('terminal')) return;
419
- const bridge = getElectronBridge();
427
+ const bridge = getHostBridge();
420
428
  if (!bridge?.onMenuOpenTerminal) return;
421
429
  bridge.onMenuOpenTerminal(() => {
422
430
  toggleRightPanel('terminal');
@@ -1,5 +1,5 @@
1
1
  import { t } from '@/i18n';
2
- import type { AppReleasePhase, AppReleaseSnapshot } from './electron-bridge';
2
+ import type { AppReleasePhase, AppReleaseSnapshot } from './host-bridge';
3
3
 
4
4
  export interface AppReleaseView {
5
5
  version: string | null;
@@ -10,7 +10,7 @@
10
10
  * 返回的落盘路径会重写消息正文里的文件引用与图像附件元数据。
11
11
  */
12
12
  import { t } from '@/i18n';
13
- import { getElectronBridge } from './electron-bridge';
13
+ import { getHostBridge } from './host-bridge';
14
14
 
15
15
  export interface AttachmentFile {
16
16
  name: string;
@@ -83,7 +83,7 @@ export async function saveChatAttachments(
83
83
  files: AttachmentFile[],
84
84
  ): Promise<SaveChatAttachmentsResult> {
85
85
  if (!chatId || files.length === 0) return { files, failures: [] };
86
- const bridge = getElectronBridge();
86
+ const bridge = getHostBridge();
87
87
  if (!bridge?.attachments?.save) return { files, failures: [] };
88
88
  try {
89
89
  // 有真实路径走 path 拷贝(Electron);没有则读字节走 data 上传(BS/browser)。
@@ -1,8 +1,8 @@
1
1
  import type {
2
- ElectronBridge,
2
+ HostBridge,
3
3
  LocalBackendRequestInput,
4
4
  LocalBackendStreamEvent,
5
- } from './electron-bridge';
5
+ } from './host-bridge';
6
6
  import type {
7
7
  ChatAgentWriteInput,
8
8
  LocalChat,
@@ -155,7 +155,7 @@ function createInitialState(): BrowserMockState {
155
155
  chatId: 'chat-browser-demo',
156
156
  role: 'assistant',
157
157
  content:
158
- 'You can debug the chat UI in a normal browser here. This reply comes from the dev mock bridge and does not use Electron IPC.',
158
+ 'You can debug the chat UI in a normal browser here. This reply comes from the dev mock bridge and does not call a live model.',
159
159
  minutesAgo: 7,
160
160
  }),
161
161
  createMessage({
@@ -608,13 +608,13 @@ function cancelStream(streamId: string) {
608
608
  state.activeStreams.delete(streamId);
609
609
  }
610
610
 
611
- export function installBrowserDevElectronMock() {
612
- if (typeof window === 'undefined' || window.electron) return;
611
+ export function installBrowserDevHostMock() {
612
+ if (typeof window === 'undefined' || window.steerableHost) return;
613
613
  // DEV 浏览器预览自动安装;官网静态 demo 构建(app-demo 入口)经 demo flag
614
614
  // 显式安装。普通 prod 构建两条路都不通,本模块根本不会进产物。
615
615
  if (!import.meta.env.DEV && !isDemoMode()) return;
616
616
 
617
- window.electron = {
617
+ window.steerableHost = {
618
618
  runtime: 'local',
619
619
  platform: 'win32',
620
620
  local: {
@@ -622,10 +622,10 @@ export function installBrowserDevElectronMock() {
622
622
  canceled: false,
623
623
  filePaths: ['C:\\browser-preview\\mock-skill'],
624
624
  }),
625
- // 浏览器 dev 模式没有 Electron capturePage/clipboard,如实返回失败。
625
+ // 浏览器 dev 模式没有桌面截图和剪贴板,如实返回失败。
626
626
  captureScreenshot: async () => ({
627
627
  success: false as const,
628
- error: 'Browser dev mode cannot capture a window screenshot (it needs the Electron runtime).',
628
+ error: 'Browser dev mode cannot capture a window screenshot.',
629
629
  }),
630
630
  },
631
631
  localBackend: {
@@ -644,10 +644,10 @@ export function installBrowserDevElectronMock() {
644
644
  suggestedReplyListeners.delete(callback);
645
645
  };
646
646
  },
647
- } satisfies ElectronBridge;
647
+ } satisfies HostBridge;
648
648
 
649
649
  console.info(
650
- '[browser-dev-electron-mock] Installed mock window.electron bridge.',
650
+ '[browser-dev-host-mock] Installed mock window.steerableHost bridge.',
651
651
  state.source === 'fixture'
652
652
  ? { source: state.fixtureMeta?.source, exportedAt: state.fixtureMeta?.exportedAt }
653
653
  : { source: 'built-in fallback' },
@@ -7,7 +7,7 @@
7
7
  * user types in ChatPanel
8
8
  * -> useChatStream.sendUserMessage()
9
9
  * -> transport.stream(input, onEvent)
10
- * -> window.electron.localBackend.startStream({ POST /api/v2/chats/:id/run })
10
+ * -> window.steerableHost.localBackend.startStream({ POST /api/v2/chats/:id/run })
11
11
  * -> main process forwards each raw SSE chunk via webContents.send
12
12
  * -> preload re-emits as `{ type: 'data'; chunk: string }`
13
13
  * -> we parse the SSE frames with the framework's `SSEParser`, normalise
@@ -52,7 +52,7 @@ import {
52
52
  parseSSEData,
53
53
  } from '@steerable/agent-ui/state';
54
54
  import type { SSEEvent } from '@steerable/agent-protocol';
55
- import { getElectronBridge } from './electron-bridge';
55
+ import { getHostBridge } from './host-bridge';
56
56
  import { t } from '@/i18n';
57
57
  import {
58
58
  appendDelta,
@@ -214,7 +214,7 @@ class LocalBackendSseAdapter {
214
214
  // starts a fresh SSE pipe against /api/v2/chats/:id/run.
215
215
  // ---------------------------------------------------------------------------
216
216
 
217
- export interface ElectronChatTransport extends ChatStreamTransport {
217
+ export interface HostChatTransport extends ChatStreamTransport {
218
218
  /**
219
219
  * 立即取消当前进行中的流(如果有)。
220
220
  *
@@ -226,7 +226,7 @@ export interface ElectronChatTransport extends ChatStreamTransport {
226
226
  cancelActive: () => void;
227
227
  }
228
228
 
229
- export function createElectronChatTransport(chatId: string): ElectronChatTransport {
229
+ export function createHostChatTransport(chatId: string): HostChatTransport {
230
230
  // 当前 in-flight 流的 id。startStream resolve 后写入,流结束(end/error)
231
231
  // 或被取消后清空。同一 transport 一次只会有一条流(useChatStream 在
232
232
  // isStreaming 时拒绝重入)。
@@ -234,7 +234,7 @@ export function createElectronChatTransport(chatId: string): ElectronChatTranspo
234
234
 
235
235
  return {
236
236
  cancelActive: () => {
237
- const bridge = getElectronBridge();
237
+ const bridge = getHostBridge();
238
238
  if (active.streamId) {
239
239
  bridge?.localBackend.cancelStream(active.streamId);
240
240
  active.streamId = null;
@@ -255,15 +255,15 @@ export function createElectronChatTransport(chatId: string): ElectronChatTranspo
255
255
  // 轮中转向:仅在 CoreLoop 路径(STEERABLE_USE_CORELOOP=1)且回合仍
256
256
  // 运行时生效;否则主进程软失败,useChatStream 保留草稿。
257
257
  steer: async (content: string) => {
258
- const bridge = getElectronBridge();
258
+ const bridge = getHostBridge();
259
259
  if (!bridge?.localBackend.steerChat) return false;
260
260
  return await bridge.localBackend.steerChat(chatId, content);
261
261
  },
262
262
  stream: async (input: ChatStreamSendInput, onEvent) => {
263
- const bridge = getElectronBridge();
263
+ const bridge = getHostBridge();
264
264
  if (!bridge) {
265
265
  throw new Error(
266
- 'Electron bridge unavailable — chat transport requires the desktop shell.',
266
+ 'Host bridge unavailable — chat transport requires the desktop shell.',
267
267
  );
268
268
  }
269
269
 
@@ -348,10 +348,10 @@ export async function regenerateChatMessage(
348
348
  chatId: string,
349
349
  messageId: string,
350
350
  ): Promise<void> {
351
- const bridge = getElectronBridge();
351
+ const bridge = getHostBridge();
352
352
  if (!bridge) {
353
353
  throw new Error(
354
- 'Electron bridge unavailable — regenerate requires the desktop shell.',
354
+ 'Host bridge unavailable — regenerate requires the desktop shell.',
355
355
  );
356
356
  }
357
357
  await new Promise<void>((resolve, reject) => {