@schlessera/brain-ui-react 0.34.0 → 0.35.0
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 +12 -0
- package/dist/components/activity/activity-page.d.ts.map +1 -1
- package/dist/components/activity/activity-page.js +8 -149
- package/dist/components/activity/activity-page.js.map +1 -1
- package/dist/components/activity/activity-rollups.d.ts +5 -0
- package/dist/components/activity/activity-rollups.d.ts.map +1 -0
- package/dist/components/activity/activity-rollups.js +39 -0
- package/dist/components/activity/activity-rollups.js.map +1 -0
- package/dist/components/activity/activity-run-detail.d.ts +11 -0
- package/dist/components/activity/activity-run-detail.d.ts.map +1 -0
- package/dist/components/activity/activity-run-detail.js +111 -0
- package/dist/components/activity/activity-run-detail.js.map +1 -0
- package/dist/components/activity/activity-run-list.d.ts +15 -0
- package/dist/components/activity/activity-run-list.d.ts.map +1 -0
- package/dist/components/activity/activity-run-list.js +25 -0
- package/dist/components/activity/activity-run-list.js.map +1 -0
- package/dist/components/chat/brain-markdown-code.d.ts +4 -0
- package/dist/components/chat/brain-markdown-code.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-code.js +47 -0
- package/dist/components/chat/brain-markdown-code.js.map +1 -0
- package/dist/components/chat/brain-markdown-entities.d.ts +14 -0
- package/dist/components/chat/brain-markdown-entities.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-entities.js +93 -0
- package/dist/components/chat/brain-markdown-entities.js.map +1 -0
- package/dist/components/chat/brain-markdown-links.d.ts +36 -0
- package/dist/components/chat/brain-markdown-links.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-links.js +176 -0
- package/dist/components/chat/brain-markdown-links.js.map +1 -0
- package/dist/components/chat/brain-markdown-share.d.ts +15 -0
- package/dist/components/chat/brain-markdown-share.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown-share.js +40 -0
- package/dist/components/chat/brain-markdown-share.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +1 -34
- package/dist/components/chat/brain-markdown.d.ts.map +1 -1
- package/dist/components/chat/brain-markdown.js +9 -402
- package/dist/components/chat/brain-markdown.js.map +1 -1
- package/dist/components/chat/composer.d.ts.map +1 -1
- package/dist/components/chat/composer.js +14 -1
- package/dist/components/chat/composer.js.map +1 -1
- package/dist/components/chat/session-drawer.d.ts.map +1 -1
- package/dist/components/chat/session-drawer.js +25 -10
- package/dist/components/chat/session-drawer.js.map +1 -1
- package/dist/components/chat/tool-views.d.ts +2 -2
- package/dist/components/chat/tool-views.d.ts.map +1 -1
- package/dist/components/chat/tool-views.js +2 -16
- package/dist/components/chat/tool-views.js.map +1 -1
- package/dist/components/chat/use-markdown-highlight.d.ts +12 -0
- package/dist/components/chat/use-markdown-highlight.d.ts.map +1 -0
- package/dist/components/chat/use-markdown-highlight.js +56 -0
- package/dist/components/chat/use-markdown-highlight.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -1
- package/dist/components/connectivity/connection-gate.js +94 -8
- package/dist/components/connectivity/connection-gate.js.map +1 -1
- package/dist/components/connectivity/connection-state.d.ts +12 -0
- package/dist/components/connectivity/connection-state.d.ts.map +1 -0
- package/dist/components/connectivity/connection-state.js +23 -0
- package/dist/components/connectivity/connection-state.js.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts +2 -0
- package/dist/components/graph/graph-canvas-lazy.d.ts.map +1 -0
- package/dist/components/graph/graph-canvas-lazy.js +4 -0
- package/dist/components/graph/graph-canvas-lazy.js.map +1 -0
- package/dist/components/graph/graph-controls.js +4 -1
- package/dist/components/graph/graph-controls.js.map +1 -1
- package/dist/components/graph/graph-maintenance.d.ts +8 -0
- package/dist/components/graph/graph-maintenance.d.ts.map +1 -0
- package/dist/components/graph/graph-maintenance.js +115 -0
- package/dist/components/graph/graph-maintenance.js.map +1 -0
- package/dist/components/graph/graph-page.d.ts.map +1 -1
- package/dist/components/graph/graph-page.js +11 -306
- package/dist/components/graph/graph-page.js.map +1 -1
- package/dist/components/graph/graph-scene.d.ts +8 -0
- package/dist/components/graph/graph-scene.d.ts.map +1 -0
- package/dist/components/graph/graph-scene.js +208 -0
- package/dist/components/graph/graph-scene.js.map +1 -0
- package/dist/components/graph/graph-spinner.d.ts +2 -0
- package/dist/components/graph/graph-spinner.d.ts.map +1 -0
- package/dist/components/graph/graph-spinner.js +6 -0
- package/dist/components/graph/graph-spinner.js.map +1 -0
- package/dist/components/layout/app-shell.d.ts.map +1 -1
- package/dist/components/layout/app-shell.js +2 -1
- package/dist/components/layout/app-shell.js.map +1 -1
- package/dist/components/quick-actions/add-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/add-modal.js +47 -4
- package/dist/components/quick-actions/add-modal.js.map +1 -1
- package/dist/components/quick-actions/search-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/search-modal.js +46 -18
- package/dist/components/quick-actions/search-modal.js.map +1 -1
- package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -1
- package/dist/components/quick-actions/streaming-modal.js +4 -1
- package/dist/components/quick-actions/streaming-modal.js.map +1 -1
- package/dist/components/settings/devices-agents-tab.d.ts +5 -0
- package/dist/components/settings/devices-agents-tab.d.ts.map +1 -0
- package/dist/components/settings/devices-agents-tab.js +96 -0
- package/dist/components/settings/devices-agents-tab.js.map +1 -0
- package/dist/components/settings/one-time-agent-credential.d.ts +3 -0
- package/dist/components/settings/one-time-agent-credential.d.ts.map +1 -0
- package/dist/components/settings/one-time-agent-credential.js +55 -0
- package/dist/components/settings/one-time-agent-credential.js.map +1 -0
- package/dist/components/settings/settings-panel.d.ts.map +1 -1
- package/dist/components/settings/settings-panel.js +16 -3
- package/dist/components/settings/settings-panel.js.map +1 -1
- package/dist/hooks/use-vpn-status.d.ts +6 -1
- package/dist/hooks/use-vpn-status.d.ts.map +1 -1
- package/dist/hooks/use-vpn-status.js +67 -30
- package/dist/hooks/use-vpn-status.js.map +1 -1
- package/dist/hooks/use-websocket.d.ts +3 -2
- package/dist/hooks/use-websocket.d.ts.map +1 -1
- package/dist/hooks/use-websocket.js +65 -254
- package/dist/hooks/use-websocket.js.map +1 -1
- package/dist/hooks/websocket-handlers/activity.d.ts +5 -0
- package/dist/hooks/websocket-handlers/activity.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/activity.js +10 -0
- package/dist/hooks/websocket-handlers/activity.js.map +1 -0
- package/dist/hooks/websocket-handlers/chat.d.ts +17 -0
- package/dist/hooks/websocket-handlers/chat.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/chat.js +175 -0
- package/dist/hooks/websocket-handlers/chat.js.map +1 -0
- package/dist/hooks/websocket-handlers/connection.d.ts +6 -0
- package/dist/hooks/websocket-handlers/connection.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/connection.js +27 -0
- package/dist/hooks/websocket-handlers/connection.js.map +1 -0
- package/dist/hooks/websocket-handlers/index.d.ts +4 -0
- package/dist/hooks/websocket-handlers/index.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/index.js +19 -0
- package/dist/hooks/websocket-handlers/index.js.map +1 -0
- package/dist/hooks/websocket-handlers/mask.d.ts +4 -0
- package/dist/hooks/websocket-handlers/mask.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/mask.js +14 -0
- package/dist/hooks/websocket-handlers/mask.js.map +1 -0
- package/dist/hooks/websocket-handlers/provider.d.ts +4 -0
- package/dist/hooks/websocket-handlers/provider.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/provider.js +31 -0
- package/dist/hooks/websocket-handlers/provider.js.map +1 -0
- package/dist/hooks/websocket-handlers/types.d.ts +24 -0
- package/dist/hooks/websocket-handlers/types.d.ts.map +1 -0
- package/dist/hooks/websocket-handlers/types.js +2 -0
- package/dist/hooks/websocket-handlers/types.js.map +1 -0
- package/dist/lib/api-client.d.ts +32 -0
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +11 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/lib/format-time.d.ts +3 -0
- package/dist/lib/format-time.d.ts.map +1 -0
- package/dist/lib/format-time.js +25 -0
- package/dist/lib/format-time.js.map +1 -0
- package/dist/lib/push-registration.d.ts +7 -0
- package/dist/lib/push-registration.d.ts.map +1 -0
- package/dist/lib/push-registration.js +26 -0
- package/dist/lib/push-registration.js.map +1 -0
- package/dist/stores/activity-store-helpers.d.ts +50 -0
- package/dist/stores/activity-store-helpers.d.ts.map +1 -0
- package/dist/stores/activity-store-helpers.js +143 -0
- package/dist/stores/activity-store-helpers.js.map +1 -0
- package/dist/stores/activity-store-types.d.ts +48 -0
- package/dist/stores/activity-store-types.d.ts.map +1 -0
- package/dist/stores/activity-store-types.js +2 -0
- package/dist/stores/activity-store-types.js.map +1 -0
- package/dist/stores/activity-store.d.ts +2 -78
- package/dist/stores/activity-store.d.ts.map +1 -1
- package/dist/stores/activity-store.js +22 -142
- package/dist/stores/activity-store.js.map +1 -1
- package/dist/stores/connection-store.d.ts +14 -2
- package/dist/stores/connection-store.d.ts.map +1 -1
- package/dist/stores/connection-store.js +17 -0
- package/dist/stores/connection-store.js.map +1 -1
- package/dist/stores/file-store.d.ts.map +1 -1
- package/dist/stores/file-store.js +54 -18
- package/dist/stores/file-store.js.map +1 -1
- package/dist/stores/principal-store.d.ts +16 -0
- package/dist/stores/principal-store.d.ts.map +1 -0
- package/dist/stores/principal-store.js +29 -0
- package/dist/stores/principal-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +1 -1
- package/dist/stores/ui-store.d.ts.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/activity/activity-page.tsx +6 -477
- package/src/components/activity/activity-rollups.tsx +94 -0
- package/src/components/activity/activity-run-detail.tsx +301 -0
- package/src/components/activity/activity-run-list.tsx +97 -0
- package/src/components/chat/brain-markdown-code.tsx +60 -0
- package/src/components/chat/brain-markdown-entities.tsx +116 -0
- package/src/components/chat/brain-markdown-links.tsx +247 -0
- package/src/components/chat/brain-markdown-share.ts +47 -0
- package/src/components/chat/brain-markdown.tsx +27 -535
- package/src/components/chat/composer.tsx +14 -1
- package/src/components/chat/session-drawer.tsx +29 -10
- package/src/components/chat/tool-views.tsx +2 -13
- package/src/components/chat/use-markdown-highlight.ts +63 -0
- package/src/components/connectivity/connection-gate.tsx +128 -14
- package/src/components/connectivity/connection-state.ts +37 -0
- package/src/components/graph/graph-canvas-lazy.tsx +4 -0
- package/src/components/graph/graph-controls.tsx +4 -1
- package/src/components/graph/graph-maintenance.tsx +304 -0
- package/src/components/graph/graph-page.tsx +11 -840
- package/src/components/graph/graph-scene.tsx +548 -0
- package/src/components/graph/graph-spinner.tsx +9 -0
- package/src/components/layout/app-shell.tsx +2 -0
- package/src/components/quick-actions/add-modal.tsx +49 -4
- package/src/components/quick-actions/search-modal.tsx +46 -18
- package/src/components/quick-actions/streaming-modal.tsx +3 -1
- package/src/components/settings/devices-agents-tab.tsx +231 -0
- package/src/components/settings/one-time-agent-credential.tsx +131 -0
- package/src/components/settings/settings-panel.tsx +20 -5
- package/src/hooks/use-vpn-status.ts +71 -31
- package/src/hooks/use-websocket.ts +74 -275
- package/src/hooks/websocket-handlers/activity.ts +13 -0
- package/src/hooks/websocket-handlers/chat.ts +210 -0
- package/src/hooks/websocket-handlers/connection.ts +30 -0
- package/src/hooks/websocket-handlers/index.ts +59 -0
- package/src/hooks/websocket-handlers/mask.ts +17 -0
- package/src/hooks/websocket-handlers/provider.ts +35 -0
- package/src/hooks/websocket-handlers/types.ts +27 -0
- package/src/lib/api-client.ts +40 -1
- package/src/lib/format-time.ts +20 -0
- package/src/lib/push-registration.ts +29 -0
- package/src/stores/activity-store-helpers.ts +164 -0
- package/src/stores/activity-store-types.ts +55 -0
- package/src/stores/activity-store.ts +21 -217
- package/src/stores/connection-store.ts +33 -2
- package/src/stores/file-store.ts +42 -12
- package/src/stores/principal-store.ts +38 -0
- package/src/stores/ui-store.ts +1 -1
|
@@ -1,55 +1,91 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
useConnectionStore,
|
|
4
|
+
type VpnStatus,
|
|
5
|
+
} from "../stores/connection-store.js";
|
|
3
6
|
import { getBackendUrl } from "../lib/backend.js";
|
|
4
7
|
|
|
5
8
|
const POLL_INTERVAL = 15_000;
|
|
6
9
|
// Tighter cadence while offline so recovery is near-immediate
|
|
7
10
|
const OFFLINE_POLL_INTERVAL = 3_000;
|
|
8
11
|
const TIMEOUT_MS = 5_000;
|
|
12
|
+
let activeRecheck: (() => void) | null = null;
|
|
13
|
+
|
|
14
|
+
async function fetchVpnStatus(): Promise<VpnStatus> {
|
|
15
|
+
const controller = new AbortController();
|
|
16
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(getBackendUrl("/api/vpn-check"), {
|
|
19
|
+
signal: controller.signal,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (res.ok) return "connected";
|
|
23
|
+
if (res.status === 401) return "unauthorized";
|
|
24
|
+
if (res.status === 403) return "forbidden";
|
|
25
|
+
return "unreachable";
|
|
26
|
+
} catch {
|
|
27
|
+
return "unreachable";
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Ask the mounted connectivity poller for a fresh reading. The poller remains
|
|
35
|
+
* the only writer of vpnStatus and serializes this with its scheduled checks.
|
|
36
|
+
*/
|
|
37
|
+
export function recheckVpnStatus(): void {
|
|
38
|
+
activeRecheck?.();
|
|
39
|
+
}
|
|
9
40
|
|
|
10
41
|
export function useVpnStatus() {
|
|
11
|
-
const
|
|
42
|
+
const [successfulProbeCount, setSuccessfulProbeCount] = useState(0);
|
|
12
43
|
|
|
13
44
|
useEffect(() => {
|
|
14
45
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
15
46
|
let inFlight = false;
|
|
47
|
+
let recheckRequested = false;
|
|
16
48
|
let disposed = false;
|
|
17
49
|
|
|
18
50
|
async function check() {
|
|
19
|
-
if (
|
|
51
|
+
if (disposed) return;
|
|
52
|
+
if (inFlight) {
|
|
53
|
+
recheckRequested = true;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
20
56
|
inFlight = true;
|
|
21
|
-
|
|
57
|
+
const opensAtStart = useConnectionStore.getState().socketOpens;
|
|
22
58
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
32
|
-
setVpnStatus(
|
|
33
|
-
connected
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
setVpnStatus("unauthorized");
|
|
37
|
-
connected = true; // reachable — poll at the relaxed cadence
|
|
38
|
-
} else if (res.status === 403) {
|
|
39
|
-
setVpnStatus("forbidden");
|
|
40
|
-
} else {
|
|
41
|
-
setVpnStatus("unreachable");
|
|
59
|
+
const status = await fetchVpnStatus();
|
|
60
|
+
// A socket that reached `open` while this probe was in flight is newer
|
|
61
|
+
// reachability evidence than a failure the probe was started for.
|
|
62
|
+
// Counting opens rather than sampling wsStatus twice also catches a
|
|
63
|
+
// drop and a replacement open inside one probe, where both samples
|
|
64
|
+
// read "connected".
|
|
65
|
+
const socketOpenedDuringCheck =
|
|
66
|
+
useConnectionStore.getState().socketOpens !== opensAtStart;
|
|
67
|
+
if (!disposed && !(socketOpenedDuringCheck && status !== "connected")) {
|
|
68
|
+
useConnectionStore.getState().setVpnStatus(status);
|
|
69
|
+
if (status === "connected") {
|
|
70
|
+
setSuccessfulProbeCount((count) => count + 1);
|
|
71
|
+
}
|
|
42
72
|
}
|
|
43
|
-
} catch {
|
|
44
|
-
setVpnStatus("unreachable");
|
|
45
73
|
} finally {
|
|
46
74
|
inFlight = false;
|
|
47
75
|
if (!disposed) {
|
|
48
76
|
clearTimeout(timer);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
if (recheckRequested) {
|
|
78
|
+
recheckRequested = false;
|
|
79
|
+
void check();
|
|
80
|
+
} else {
|
|
81
|
+
const status = useConnectionStore.getState().vpnStatus;
|
|
82
|
+
// Auth required but no valid session is still a reachable server.
|
|
83
|
+
const reachable = status === "connected" || status === "unauthorized";
|
|
84
|
+
timer = setTimeout(
|
|
85
|
+
check,
|
|
86
|
+
reachable ? POLL_INTERVAL : OFFLINE_POLL_INTERVAL
|
|
87
|
+
);
|
|
88
|
+
}
|
|
53
89
|
}
|
|
54
90
|
}
|
|
55
91
|
}
|
|
@@ -57,6 +93,7 @@ export function useVpnStatus() {
|
|
|
57
93
|
// Re-check immediately when the network returns or the PWA is foregrounded
|
|
58
94
|
// instead of waiting out the poll interval.
|
|
59
95
|
const recheckNow = () => void check();
|
|
96
|
+
activeRecheck = recheckNow;
|
|
60
97
|
const onVisible = () => {
|
|
61
98
|
if (document.visibilityState === "visible") void check();
|
|
62
99
|
};
|
|
@@ -68,8 +105,11 @@ export function useVpnStatus() {
|
|
|
68
105
|
return () => {
|
|
69
106
|
disposed = true;
|
|
70
107
|
clearTimeout(timer);
|
|
108
|
+
if (activeRecheck === recheckNow) activeRecheck = null;
|
|
71
109
|
window.removeEventListener("online", recheckNow);
|
|
72
110
|
document.removeEventListener("visibilitychange", onVisible);
|
|
73
111
|
};
|
|
74
|
-
}, [
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
return successfulProbeCount;
|
|
75
115
|
}
|
|
@@ -1,116 +1,23 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BrainUiClient,
|
|
4
|
+
type WebSocketClose,
|
|
5
|
+
} from "@schlessera/brain-ui-sdk/client";
|
|
3
6
|
import { useConnectionStore } from "../stores/connection-store.js";
|
|
4
7
|
import { useChatStore, activeChat, type ChatKey } from "../stores/chat-store.js";
|
|
5
|
-
import { useProviderStore } from "../stores/provider-store.js";
|
|
6
|
-
import { useMaskStore } from "../stores/mask-store.js";
|
|
7
8
|
import { useActivityStore, loadSessionActivityHistory } from "../stores/activity-store.js";
|
|
8
9
|
import { getWsUrl } from "../lib/backend.js";
|
|
9
10
|
import type {
|
|
10
11
|
ServerMessage,
|
|
11
12
|
ClientMessage,
|
|
12
13
|
ServerLocationRequest,
|
|
13
|
-
SessionHistoryMessage,
|
|
14
|
-
AskUserQuestion,
|
|
15
14
|
} from "@schlessera/brain-ui-sdk/protocol";
|
|
16
|
-
import
|
|
17
|
-
import {
|
|
15
|
+
import { dispatchServerMessage } from "./websocket-handlers/index.js";
|
|
16
|
+
import { runStateForFrame } from "./websocket-handlers/chat.js";
|
|
17
|
+
import { recheckVpnStatus } from "./use-vpn-status.js";
|
|
18
|
+
import { REFUSAL_ATTEMPTS } from "../components/connectivity/connection-state.js";
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
function historyId() {
|
|
21
|
-
return `hist-${++historyMessageCounter}-${Date.now()}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function convertHistoryMessage(msg: SessionHistoryMessage): ChatMessage {
|
|
25
|
-
// Fallback for history without chronological parts: synthesize the legacy
|
|
26
|
-
// grouped order (thinking, tools, text).
|
|
27
|
-
const parts =
|
|
28
|
-
msg.parts ??
|
|
29
|
-
[
|
|
30
|
-
...(msg.thinking ? [{ kind: "thinking", text: msg.thinking } as const] : []),
|
|
31
|
-
...msg.toolCalls.map((_, i) => ({ kind: "tool", toolIndex: i }) as const),
|
|
32
|
-
...(msg.content ? [{ kind: "text", text: msg.content } as const] : []),
|
|
33
|
-
];
|
|
34
|
-
const askUserExchanges = reconstructAskUserExchanges(msg.toolCalls);
|
|
35
|
-
return {
|
|
36
|
-
id: historyId(),
|
|
37
|
-
role: msg.role,
|
|
38
|
-
content: msg.content,
|
|
39
|
-
thinking: msg.thinking,
|
|
40
|
-
toolCalls: msg.toolCalls.map((tc): ToolCall => ({
|
|
41
|
-
id: tc.id,
|
|
42
|
-
name: tc.name,
|
|
43
|
-
input: tc.input,
|
|
44
|
-
inputJson: JSON.stringify(tc.input, null, 2),
|
|
45
|
-
output: tc.output,
|
|
46
|
-
isError: tc.isError,
|
|
47
|
-
status: "complete",
|
|
48
|
-
})),
|
|
49
|
-
parts,
|
|
50
|
-
isStreaming: false,
|
|
51
|
-
timestamp: Date.now(),
|
|
52
|
-
...(askUserExchanges ? { askUserExchanges } : {}),
|
|
53
|
-
...(msg.attachmentCount ? { attachmentCount: msg.attachmentCount } : {}),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Rebuild ask_user exchanges from a resumed message's tool calls. The ask_user
|
|
59
|
-
* tool's input carries the questions and its output carries the JSON answer
|
|
60
|
-
* payload, so an answered question survives session resume (rendered in its
|
|
61
|
-
* chronological slot, collapsed) instead of vanishing.
|
|
62
|
-
*/
|
|
63
|
-
function isBareAnswersMap(v: unknown): v is Record<string, string> {
|
|
64
|
-
return (
|
|
65
|
-
!!v &&
|
|
66
|
-
typeof v === "object" &&
|
|
67
|
-
!Array.isArray(v) &&
|
|
68
|
-
Object.values(v).every((x) => typeof x === "string")
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function reconstructAskUserExchanges(
|
|
73
|
-
toolCalls: SessionHistoryMessage["toolCalls"]
|
|
74
|
-
): AskUserExchange[] | undefined {
|
|
75
|
-
const exchanges: AskUserExchange[] = [];
|
|
76
|
-
for (const tc of toolCalls) {
|
|
77
|
-
if (!isAskUserTool(tc.name)) continue;
|
|
78
|
-
const questions = (tc.input?.questions as AskUserQuestion[]) ?? [];
|
|
79
|
-
const exchange: AskUserExchange = { requestId: tc.id, questions };
|
|
80
|
-
if (tc.output) {
|
|
81
|
-
try {
|
|
82
|
-
const payload = JSON.parse(tc.output) as {
|
|
83
|
-
answers?: Record<string, string>;
|
|
84
|
-
annotations?: AskUserExchange["annotations"];
|
|
85
|
-
};
|
|
86
|
-
if (payload && typeof payload === "object" && payload.answers) {
|
|
87
|
-
exchange.answers = payload.answers;
|
|
88
|
-
exchange.annotations = payload.annotations;
|
|
89
|
-
} else if (isBareAnswersMap(payload)) {
|
|
90
|
-
// The pi backend persists the bare answers map, without the
|
|
91
|
-
// `{answers}` envelope the Claude tool writes.
|
|
92
|
-
exchange.answers = payload;
|
|
93
|
-
}
|
|
94
|
-
} catch {
|
|
95
|
-
// Non-JSON output means the question was dismissed or errored out.
|
|
96
|
-
exchange.cancelled = true;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (tc.isError && !exchange.answers) exchange.cancelled = true;
|
|
100
|
-
exchanges.push(exchange);
|
|
101
|
-
}
|
|
102
|
-
return exchanges.length ? exchanges : undefined;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Map a frame to the run-state its session should show in the session list. */
|
|
106
|
-
export function runStateForFrame(msg: ServerMessage): "streaming" | "queued" | "idle" {
|
|
107
|
-
if (msg.type === "result" || msg.type === "error") return "idle";
|
|
108
|
-
if (msg.type === "status") {
|
|
109
|
-
if (msg.status === "queued") return "queued";
|
|
110
|
-
if (msg.status === "idle" || msg.status === "cancelled") return "idle";
|
|
111
|
-
}
|
|
112
|
-
return "streaming";
|
|
113
|
-
}
|
|
20
|
+
export { runStateForFrame } from "./websocket-handlers/chat.js";
|
|
114
21
|
|
|
115
22
|
/**
|
|
116
23
|
* Does this frame announce the identity of the conversation THIS client just
|
|
@@ -130,9 +37,11 @@ function isOurDraftAnnouncement(
|
|
|
130
37
|
state: ReturnType<typeof useChatStore.getState>,
|
|
131
38
|
msg: ServerMessage
|
|
132
39
|
): boolean {
|
|
133
|
-
|
|
40
|
+
// A terminal result has no draft correlation. Another client's turn may
|
|
41
|
+
// finish before our session_info arrives, so it can never claim our draft.
|
|
42
|
+
if (msg.type !== "session_info") return false;
|
|
134
43
|
const pending = state.pendingDraftId;
|
|
135
|
-
if (msg.
|
|
44
|
+
if (msg.draftId) return msg.draftId === pending;
|
|
136
45
|
// No echo to compare against.
|
|
137
46
|
return true;
|
|
138
47
|
}
|
|
@@ -226,21 +135,24 @@ export function handleServerMessage(msg: ServerMessage) {
|
|
|
226
135
|
const state = useChatStore.getState();
|
|
227
136
|
|
|
228
137
|
// Activity stream frames feed their own store and never touch chat state.
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
138
|
+
if (
|
|
139
|
+
msg.type === "server_hello" ||
|
|
140
|
+
msg.type === "activity_snapshot" ||
|
|
141
|
+
msg.type === "activity_delta"
|
|
142
|
+
) {
|
|
143
|
+
const key = state.activeSessionId;
|
|
144
|
+
dispatchServerMessage(msg, {
|
|
145
|
+
state,
|
|
146
|
+
frameSessionId: undefined,
|
|
147
|
+
key,
|
|
148
|
+
buffer: () => (key === null ? state.draft : state.buffers[key]),
|
|
149
|
+
enqueueDelta,
|
|
150
|
+
ensureActivitySubscription,
|
|
151
|
+
requestBrowserLocation,
|
|
152
|
+
resyncIfNeeded,
|
|
153
|
+
coldResumeIfNeeded,
|
|
154
|
+
markHistoryReplaced,
|
|
155
|
+
});
|
|
244
156
|
return;
|
|
245
157
|
}
|
|
246
158
|
|
|
@@ -290,162 +202,18 @@ export function handleServerMessage(msg: ServerMessage) {
|
|
|
290
202
|
return key === null ? s.draft : s.buffers[key];
|
|
291
203
|
};
|
|
292
204
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
state.startAssistantMessage(key);
|
|
306
|
-
}
|
|
307
|
-
enqueueDelta(key, "thinking", msg.text);
|
|
308
|
-
break;
|
|
309
|
-
|
|
310
|
-
case "tool_use_start":
|
|
311
|
-
// A tool call INSIDE a subagent nests under its Agent entry, rendered
|
|
312
|
-
// from the activity stream — putting it in the flat transcript would
|
|
313
|
-
// interleave fan-out work with the main turn's steps.
|
|
314
|
-
if (msg.parentToolUseId) break;
|
|
315
|
-
state.startToolCall(key, msg.toolUseId, msg.toolName);
|
|
316
|
-
break;
|
|
317
|
-
|
|
318
|
-
case "tool_input_delta":
|
|
319
|
-
state.appendToolInput(key, msg.partialJson);
|
|
320
|
-
break;
|
|
321
|
-
|
|
322
|
-
case "tool_use_complete":
|
|
323
|
-
// Subagent tool calls render nested (activity stream), and the flat
|
|
324
|
-
// store's name-based fallback match must never see them.
|
|
325
|
-
if (msg.parentToolUseId) break;
|
|
326
|
-
state.completeToolCall(key, msg.toolUseId, msg.toolName, msg.input);
|
|
327
|
-
break;
|
|
328
|
-
|
|
329
|
-
case "tool_approval_request":
|
|
330
|
-
state.requestToolApproval(key, msg.toolUseId, msg.toolName, msg.input, msg.description, msg.kind);
|
|
331
|
-
break;
|
|
332
|
-
|
|
333
|
-
case "tool_result": {
|
|
334
|
-
state.setToolResult(key, msg.toolUseId, msg.output, msg.isError);
|
|
335
|
-
// Once the agent receives the ask_user tool result, the exchange is
|
|
336
|
-
// complete — drop any leftover ask_user UI state.
|
|
337
|
-
const chat = buffer();
|
|
338
|
-
if (chat?.askUser?.answers || chat?.askUser?.cancelled) {
|
|
339
|
-
state.clearAskUser(key);
|
|
340
|
-
}
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
case "ask_user_request":
|
|
345
|
-
state.setAskUserRequest(key, msg.requestId, msg.questions);
|
|
346
|
-
break;
|
|
347
|
-
|
|
348
|
-
case "location_request":
|
|
349
|
-
requestBrowserLocation(msg);
|
|
350
|
-
break;
|
|
351
|
-
|
|
352
|
-
case "mask_request":
|
|
353
|
-
// Opens the editor; the answer travels back from the component, because
|
|
354
|
-
// only the user can say which part of the picture they meant.
|
|
355
|
-
useMaskStore.getState().open({
|
|
356
|
-
requestId: msg.requestId,
|
|
357
|
-
imagePath: msg.imagePath,
|
|
358
|
-
...(msg.instruction ? { instruction: msg.instruction } : {}),
|
|
359
|
-
...(msg.turnId ? { turnId: msg.turnId } : {}),
|
|
360
|
-
});
|
|
361
|
-
break;
|
|
362
|
-
|
|
363
|
-
case "result":
|
|
364
|
-
state.finishAssistantMessage(key);
|
|
365
|
-
resyncIfNeeded(msg.sessionId);
|
|
366
|
-
break;
|
|
367
|
-
|
|
368
|
-
case "session_info": {
|
|
369
|
-
ensureActivitySubscription(msg.sessionId);
|
|
370
|
-
// Record backend ownership for renderer scoping — for ANY session, since
|
|
371
|
-
// background sessions keep their own transcript buffers. Older servers
|
|
372
|
-
// omit backendId; derive it from the pinned profile when still possible
|
|
373
|
-
// (fails only for since-hidden profiles, which then use the default).
|
|
374
|
-
const ownerBackendId =
|
|
375
|
-
msg.backendId ??
|
|
376
|
-
useProviderStore
|
|
377
|
-
.getState()
|
|
378
|
-
.available.find((p) => p.id === msg.providerId)?.backendId;
|
|
379
|
-
if (ownerBackendId) {
|
|
380
|
-
useChatStore.getState().setSessionBackend(msg.sessionId, ownerBackendId);
|
|
381
|
-
}
|
|
382
|
-
// bindDraftSession above handled draft adoption; an info frame may still
|
|
383
|
-
// re-pin the provider picker when it concerns the session in view.
|
|
384
|
-
const current = useChatStore.getState();
|
|
385
|
-
if (frameSessionId && frameSessionId === current.activeSessionId) {
|
|
386
|
-
useProviderStore.getState().setPinned(msg.providerId ?? null);
|
|
387
|
-
// Reattachment on a fresh connection: enter streaming mode so deltas
|
|
388
|
-
// append to a live assistant message instead of vanishing.
|
|
389
|
-
const chat = current.buffers[frameSessionId];
|
|
390
|
-
if (chat && !chat.isStreaming && chat.messages.length === 0) {
|
|
391
|
-
current.startAssistantMessage(frameSessionId);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
case "session_history": {
|
|
398
|
-
const converted = msg.messages.map(convertHistoryMessage);
|
|
399
|
-
if (msg.append) {
|
|
400
|
-
state.appendMessages(key, converted);
|
|
401
|
-
} else {
|
|
402
|
-
// First (replacing) chunk = fresh authoritative transcript. Cancel any
|
|
403
|
-
// pending reconnect resync so we don't redundantly re-fetch it, and
|
|
404
|
-
// mark this session cold-resumed so idle-status handling doesn't loop.
|
|
405
|
-
state.setMessages(key, converted);
|
|
406
|
-
if (key !== null && key === resyncSessionId) resyncSessionId = null;
|
|
407
|
-
coldResumedSessionId = key;
|
|
408
|
-
}
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
case "status": {
|
|
413
|
-
// Only finish streaming if this buffer actually started it
|
|
414
|
-
if (
|
|
415
|
-
(msg.status === "idle" || msg.status === "cancelled") &&
|
|
416
|
-
buffer()?.isStreaming
|
|
417
|
-
) {
|
|
418
|
-
state.finishAssistantMessage(key);
|
|
419
|
-
}
|
|
420
|
-
// A session that finished while we were offline never delivers its
|
|
421
|
-
// `result` — the idle status on reconnect is the cue to resync. Keyed to
|
|
422
|
-
// THIS frame's session: a background session settling must not consume
|
|
423
|
-
// (or trigger) the active session's pending resync.
|
|
424
|
-
if (msg.status === "idle" || msg.status === "cancelled") {
|
|
425
|
-
const current = useChatStore.getState();
|
|
426
|
-
resyncIfNeeded(frameSessionId ?? current.activeSessionId);
|
|
427
|
-
// Cold load: we hold a stored session id but an empty transcript and
|
|
428
|
-
// the server is idle (so it sent no snapshot). Fetch the history.
|
|
429
|
-
// Only the active session's own (or an unscoped legacy) idle counts.
|
|
430
|
-
if (!frameSessionId || frameSessionId === current.activeSessionId) {
|
|
431
|
-
coldResumeIfNeeded(current.activeSessionId, activeChat(current).messages.length);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
break;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
case "error":
|
|
438
|
-
// ALWAYS recorded. Appending to the transcript only works while a
|
|
439
|
-
// message is streaming, and this used to be the whole handler — so an
|
|
440
|
-
// error arriving between turns (a rejected frame, a failed resume) was
|
|
441
|
-
// dropped as silently on the client as it was on the server.
|
|
442
|
-
useConnectionStore.getState().reportError(msg.code, msg.message);
|
|
443
|
-
if (buffer()?.isStreaming) {
|
|
444
|
-
state.appendText(key, `\n\n**Error:** ${msg.message}`);
|
|
445
|
-
state.finishAssistantMessage(key);
|
|
446
|
-
}
|
|
447
|
-
break;
|
|
448
|
-
}
|
|
205
|
+
dispatchServerMessage(msg, {
|
|
206
|
+
state,
|
|
207
|
+
frameSessionId,
|
|
208
|
+
key,
|
|
209
|
+
buffer,
|
|
210
|
+
enqueueDelta,
|
|
211
|
+
ensureActivitySubscription,
|
|
212
|
+
requestBrowserLocation,
|
|
213
|
+
resyncIfNeeded,
|
|
214
|
+
coldResumeIfNeeded,
|
|
215
|
+
markHistoryReplaced,
|
|
216
|
+
});
|
|
449
217
|
}
|
|
450
218
|
|
|
451
219
|
/**
|
|
@@ -509,6 +277,17 @@ function requestBrowserLocation(msg: ServerLocationRequest) {
|
|
|
509
277
|
let wasDisconnected = false;
|
|
510
278
|
let resyncSessionId: string | null = null;
|
|
511
279
|
|
|
280
|
+
function handleSocketClose(close: WebSocketClose): void {
|
|
281
|
+
const connection = useConnectionStore.getState();
|
|
282
|
+
connection.recordWsClose(close.opened, close.code);
|
|
283
|
+
if (
|
|
284
|
+
!close.opened &&
|
|
285
|
+
useConnectionStore.getState().handshakeFailures >= REFUSAL_ATTEMPTS
|
|
286
|
+
) {
|
|
287
|
+
recheckVpnStatus();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
512
291
|
function resyncIfNeeded(sessionId: string | null) {
|
|
513
292
|
if (!resyncSessionId || !sessionId || sessionId !== resyncSessionId) return;
|
|
514
293
|
resyncSessionId = null;
|
|
@@ -524,6 +303,11 @@ function resyncIfNeeded(sessionId: string | null) {
|
|
|
524
303
|
// status and loop.
|
|
525
304
|
let coldResumedSessionId: string | null = null;
|
|
526
305
|
|
|
306
|
+
function markHistoryReplaced(key: ChatKey): void {
|
|
307
|
+
if (key !== null && key === resyncSessionId) resyncSessionId = null;
|
|
308
|
+
coldResumedSessionId = key;
|
|
309
|
+
}
|
|
310
|
+
|
|
527
311
|
function coldResumeIfNeeded(sessionId: string | null, messageCount: number) {
|
|
528
312
|
if (!sessionId || messageCount > 0) return;
|
|
529
313
|
if (coldResumedSessionId === sessionId) return;
|
|
@@ -534,6 +318,10 @@ function coldResumeIfNeeded(sessionId: string | null, messageCount: number) {
|
|
|
534
318
|
function handleStatusChange(status: "connecting" | "connected" | "disconnected") {
|
|
535
319
|
useConnectionStore.getState().setWsStatus(status);
|
|
536
320
|
|
|
321
|
+
if (status === "connected") {
|
|
322
|
+
useConnectionStore.getState().noteSocketOpen();
|
|
323
|
+
}
|
|
324
|
+
|
|
537
325
|
// A status change can be followed by a history replay that rewrites the
|
|
538
326
|
// buffer, so land whatever is still buffered before anything reads it. In a
|
|
539
327
|
// hidden tab requestAnimationFrame does not run at all, which is exactly the
|
|
@@ -581,12 +369,19 @@ export function sendClientMessage(msg: ClientMessage): boolean {
|
|
|
581
369
|
return true;
|
|
582
370
|
}
|
|
583
371
|
|
|
372
|
+
/** Retry the singleton socket immediately, skipping any remaining backoff. */
|
|
373
|
+
export function reconnectWebSocketNow(): void {
|
|
374
|
+
wsClient?.reconnectNow();
|
|
375
|
+
}
|
|
376
|
+
|
|
584
377
|
export function useWebSocket() {
|
|
585
378
|
const initialized = useRef(false);
|
|
586
379
|
|
|
587
380
|
useEffect(() => {
|
|
588
381
|
if (initialized.current) return;
|
|
589
382
|
initialized.current = true;
|
|
383
|
+
let disposed = false;
|
|
384
|
+
useConnectionStore.getState().noteSocketOpen();
|
|
590
385
|
|
|
591
386
|
wsClient = new BrainUiClient({
|
|
592
387
|
url: getWsUrl(),
|
|
@@ -594,6 +389,9 @@ export function useWebSocket() {
|
|
|
594
389
|
// session-buffer demux — see handleServerMessage.
|
|
595
390
|
handlers: { onAny: handleServerMessage },
|
|
596
391
|
onStatusChange: handleStatusChange,
|
|
392
|
+
onClose: (close) => {
|
|
393
|
+
if (!disposed) handleSocketClose(close);
|
|
394
|
+
},
|
|
597
395
|
// A frame the SDK refused. Surfaced rather than logged into a console
|
|
598
396
|
// nobody is attached to; the server-side counterpart is the
|
|
599
397
|
// ws.frames.dropped counter.
|
|
@@ -619,6 +417,7 @@ export function useWebSocket() {
|
|
|
619
417
|
document.addEventListener("visibilitychange", onVisible);
|
|
620
418
|
|
|
621
419
|
return () => {
|
|
420
|
+
disposed = true;
|
|
622
421
|
window.removeEventListener("online", reconnectNow);
|
|
623
422
|
document.removeEventListener("visibilitychange", onVisible);
|
|
624
423
|
wsClient?.close();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useActivityStore } from "../../stores/activity-store.js";
|
|
2
|
+
import type { ServerMessageHandlerMap } from "./types.js";
|
|
3
|
+
|
|
4
|
+
type ActivityFrame = "activity_snapshot" | "activity_delta";
|
|
5
|
+
|
|
6
|
+
export const activityFrameHandlers = {
|
|
7
|
+
activity_snapshot: (msg) => {
|
|
8
|
+
useActivityStore.getState().applySnapshot(msg);
|
|
9
|
+
},
|
|
10
|
+
activity_delta: (msg) => {
|
|
11
|
+
useActivityStore.getState().applyDelta(msg);
|
|
12
|
+
},
|
|
13
|
+
} satisfies ServerMessageHandlerMap<ActivityFrame>;
|