@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.
- package/README.md +3 -4
- package/package.json +4 -4
- package/src/AppShell.tsx +3 -3
- package/src/components/AgentSidebar.tsx +4 -4
- package/src/components/CreateProjectModal.tsx +2 -2
- package/src/components/SidebarRelease.tsx +4 -4
- package/src/components/TerminalPanel.tsx +3 -3
- package/src/components/TerminalView.tsx +4 -4
- package/src/components/chat/ApprovalModal.tsx +1 -1
- package/src/components/chat/ApprovalPromptProvider.tsx +4 -4
- package/src/components/chat/AskUserPromptProvider.tsx +3 -3
- package/src/components/chat/ChatProjectBadge.tsx +3 -3
- package/src/components/chat/TaskPanelModal.tsx +2 -2
- package/src/components/chat/TaskProcessPanel.tsx +2 -2
- package/src/components/chat/useChatTasks.ts +3 -3
- package/src/components/settings/AgentCapabilityFields.tsx +2 -2
- package/src/components/settings/AgentsSettingsPanel.tsx +3 -3
- package/src/components/settings/CliInstallSettingsPanel.tsx +67 -0
- package/src/components/settings/InsightsSettingsPanel.tsx +15 -15
- package/src/components/settings/LlmSettingsPanel.tsx +3 -3
- package/src/components/settings/McpSettingsPanel.tsx +12 -12
- package/src/components/settings/PythonRunnerSettingsPanel.tsx +4 -4
- package/src/components/settings/SecuritySettingsPanel.tsx +3 -3
- package/src/components/settings/SkillsSettingsPanel.tsx +7 -7
- package/src/components/settings/TelemetrySettingsPanel.tsx +5 -5
- package/src/components/settings/UsagePanel.tsx +3 -3
- package/src/components/settings/WebSearchSettingsPanel.tsx +5 -5
- package/src/hooks/useChatsAndAgents.ts +9 -9
- package/src/hooks/useProjects.ts +2 -2
- package/src/i18n/index.tsx +1 -1
- package/src/layouts/AgentLayout.tsx +14 -6
- package/src/lib/app-release.ts +1 -1
- package/src/lib/attachments.ts +2 -2
- package/src/lib/{browser-dev-electron-mock.ts → browser-dev-host-mock.ts} +10 -10
- package/src/lib/chat-transport.ts +10 -10
- package/src/lib/host-bridge.ts +376 -4
- package/src/lib/host-paste.ts +1 -1
- package/src/lib/http-bridge.ts +10 -8
- package/src/lib/insights.ts +2 -2
- package/src/lib/local-api.ts +13 -5
- package/src/lib/portable.ts +3 -3
- package/src/lib/slash-sources.ts +3 -3
- package/src/lib/tauri-bridge.ts +1 -1
- package/src/main.tsx +3 -3
- package/src/pages/AgentPage.tsx +23 -23
- package/src/pages/SettingsPage.tsx +4 -4
- 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 {
|
|
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
|
|
23
|
-
*
|
|
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 (!
|
|
71
|
+
if (!hasHostBridge()) return;
|
|
72
72
|
setMcpLoading(true);
|
|
73
73
|
setError(null);
|
|
74
74
|
try {
|
|
75
|
-
const res = await
|
|
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 (!
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
5
|
+
getHostBridge,
|
|
6
6
|
type PythonRunnerSnapshot,
|
|
7
|
-
} from '@/lib/
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 (!
|
|
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
|
-
{!
|
|
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 {
|
|
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 (!
|
|
62
|
+
if (!hasHostBridge()) return;
|
|
63
63
|
setSkillsLoading(true);
|
|
64
64
|
setError(null);
|
|
65
65
|
try {
|
|
66
|
-
const res = await
|
|
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 (!
|
|
84
|
+
if (!hasHostBridge()) return;
|
|
85
85
|
try {
|
|
86
|
-
const bridge =
|
|
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
|
|
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
|
|
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 {
|
|
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 (!
|
|
39
|
+
if (!hasHostBridge()) return;
|
|
40
40
|
setLoading(true);
|
|
41
41
|
setError(null);
|
|
42
42
|
try {
|
|
43
|
-
const res = await
|
|
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 (!
|
|
61
|
+
if (!hasHostBridge()) return;
|
|
62
62
|
setSaving(true);
|
|
63
63
|
setError(null);
|
|
64
64
|
setStatus(null);
|
|
65
65
|
try {
|
|
66
|
-
const saved = await
|
|
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 {
|
|
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 (!
|
|
69
|
+
if (!hasHostBridge()) return;
|
|
70
70
|
setLoading(true);
|
|
71
71
|
setError(null);
|
|
72
72
|
try {
|
|
73
|
-
const res = await
|
|
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 {
|
|
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 (!
|
|
36
|
+
if (!hasHostBridge()) return;
|
|
37
37
|
setLoading(true);
|
|
38
38
|
setError(null);
|
|
39
39
|
try {
|
|
40
|
-
const res = await
|
|
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 (!
|
|
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
|
|
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 `
|
|
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 {
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
88
|
-
throw new Error('not
|
|
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 (!
|
|
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: !
|
|
113
|
+
skipInitialLoad: !hasHostBridgeRef.current,
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
// Default-select this flavor's builtin expert once the catalog loads.
|
package/src/hooks/useProjects.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 任一侧新建、改名或改目录后调用 refresh,其它订阅者一起更新。
|
|
4
4
|
*/
|
|
5
5
|
import { useCallback, useEffect, useSyncExternalStore } from 'react';
|
|
6
|
-
import {
|
|
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 (!
|
|
39
|
+
if (!hasHostBridge() || !hostToolChrome('projects')) {
|
|
40
40
|
if (snapshot.projects.length > 0 || snapshot.error) {
|
|
41
41
|
snapshot = EMPTY_SNAPSHOT;
|
|
42
42
|
emit();
|
package/src/i18n/index.tsx
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
427
|
+
const bridge = getHostBridge();
|
|
420
428
|
if (!bridge?.onMenuOpenTerminal) return;
|
|
421
429
|
bridge.onMenuOpenTerminal(() => {
|
|
422
430
|
toggleRightPanel('terminal');
|
package/src/lib/app-release.ts
CHANGED
package/src/lib/attachments.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 返回的落盘路径会重写消息正文里的文件引用与图像附件元数据。
|
|
11
11
|
*/
|
|
12
12
|
import { t } from '@/i18n';
|
|
13
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
2
|
+
HostBridge,
|
|
3
3
|
LocalBackendRequestInput,
|
|
4
4
|
LocalBackendStreamEvent,
|
|
5
|
-
} from './
|
|
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
|
|
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
|
|
612
|
-
if (typeof window === 'undefined' || window.
|
|
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.
|
|
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
|
|
625
|
+
// 浏览器 dev 模式没有桌面截图和剪贴板,如实返回失败。
|
|
626
626
|
captureScreenshot: async () => ({
|
|
627
627
|
success: false as const,
|
|
628
|
-
error: 'Browser dev mode cannot capture a window screenshot
|
|
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
|
|
647
|
+
} satisfies HostBridge;
|
|
648
648
|
|
|
649
649
|
console.info(
|
|
650
|
-
'[browser-dev-
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
263
|
+
const bridge = getHostBridge();
|
|
264
264
|
if (!bridge) {
|
|
265
265
|
throw new Error(
|
|
266
|
-
'
|
|
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 =
|
|
351
|
+
const bridge = getHostBridge();
|
|
352
352
|
if (!bridge) {
|
|
353
353
|
throw new Error(
|
|
354
|
-
'
|
|
354
|
+
'Host bridge unavailable — regenerate requires the desktop shell.',
|
|
355
355
|
);
|
|
356
356
|
}
|
|
357
357
|
await new Promise<void>((resolve, reject) => {
|