@blackbelt-technology/pi-agent-dashboard 0.2.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/AGENTS.md +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- package/packages/shared/tsconfig.json +8 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser Gateway - WebSocket handler for browser client connections.
|
|
3
|
+
* Runs on the HTTP server port via upgrade handling.
|
|
4
|
+
*/
|
|
5
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
6
|
+
import type {
|
|
7
|
+
ServerToBrowserMessage,
|
|
8
|
+
BrowserToServerMessage,
|
|
9
|
+
} from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
10
|
+
import type { SessionManager } from "./memory-session-manager.js";
|
|
11
|
+
import type { EventStore } from "./memory-event-store.js";
|
|
12
|
+
import type { PiGateway } from "./pi-gateway.js";
|
|
13
|
+
// PendingLoadManager removed — server loads sessions directly via DirectoryService
|
|
14
|
+
import { createHeadlessPidRegistry, type HeadlessPidRegistry } from "./headless-pid-registry.js";
|
|
15
|
+
import type { PendingForkRegistry } from "./pending-fork-registry.js";
|
|
16
|
+
import type { SessionOrderManager } from "./session-order-manager.js";
|
|
17
|
+
import type { PreferencesStore } from "./preferences-store.js";
|
|
18
|
+
import type { DirectoryService } from "./directory-service.js";
|
|
19
|
+
import { createPendingResumeRegistry, type PendingResumeRegistry } from "./pending-resume-registry.js";
|
|
20
|
+
import type { TerminalManager } from "./terminal-manager.js";
|
|
21
|
+
import type { BrowserHandlerContext } from "./browser-handlers/handler-context.js";
|
|
22
|
+
import { handleSubscribe } from "./browser-handlers/subscription-handler.js";
|
|
23
|
+
import { handleSendPrompt, handleResumeSession, handleSpawnSession, handleShutdown, handleAbort, handleFlowControl, handleForceKill, handleKillProcess } from "./browser-handlers/session-action-handler.js";
|
|
24
|
+
import { handleRenameSession, handleHideSession, handleUnhideSession, handleAttachProposal, handleDetachProposal, handleFetchContent, handleListSessions } from "./browser-handlers/session-meta-handler.js";
|
|
25
|
+
import { handleCreateTerminal, handleKillTerminal, handleRenameTerminal } from "./browser-handlers/terminal-handler.js";
|
|
26
|
+
import { handlePinDirectory, handleUnpinDirectory, handleReorderPinnedDirs, handleReorderSessions, handleOpenSpecRefresh, handleOpenSpecBulkArchive, handleExtensionUiResponse, handlePiGatewayForward } from "./browser-handlers/directory-handler.js";
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
export interface BrowserGateway {
|
|
31
|
+
wss: WebSocketServer;
|
|
32
|
+
broadcastEvent(sessionId: string, seq: number, event: any): void;
|
|
33
|
+
broadcastSessionAdded(session: any): void;
|
|
34
|
+
broadcastSessionUpdated(sessionId: string, updates: any): void;
|
|
35
|
+
broadcastSessionRemoved(sessionId: string): void;
|
|
36
|
+
sendToSubscribers(sessionId: string, msg: ServerToBrowserMessage): void;
|
|
37
|
+
broadcastToAll(msg: ServerToBrowserMessage): void;
|
|
38
|
+
/** Get number of browser subscribers for a session */
|
|
39
|
+
getSubscriberCount(sessionId: string): number;
|
|
40
|
+
/** Track a pending interactive UI request for replay on reconnect */
|
|
41
|
+
trackUiRequest(sessionId: string, requestId: string, method: string, params: Record<string, unknown>): boolean | void;
|
|
42
|
+
/** Clear a pending interactive UI request (resolved or cancelled) */
|
|
43
|
+
clearUiRequest(sessionId: string, requestId: string): void;
|
|
44
|
+
/** Tell browser subscribers to reset accumulated state for a session (bridge reconnected) */
|
|
45
|
+
broadcastSessionStateReset(sessionId: string): void;
|
|
46
|
+
/** Shut down all tracked headless child processes */
|
|
47
|
+
shutdownHeadlessProcesses(): void;
|
|
48
|
+
/** Registry for linking headless PIDs to session IDs */
|
|
49
|
+
headlessPidRegistry: HeadlessPidRegistry;
|
|
50
|
+
/** Registry for pending auto-resume prompts */
|
|
51
|
+
pendingResumeRegistry: PendingResumeRegistry;
|
|
52
|
+
/** Send a message to a specific WebSocket client */
|
|
53
|
+
sendToClient(ws: WebSocket, msg: ServerToBrowserMessage): void;
|
|
54
|
+
/** Callback invoked when a new browser client connects */
|
|
55
|
+
onConnect?: (ws: WebSocket) => void;
|
|
56
|
+
/** Broadcast a message to all connected clients */
|
|
57
|
+
broadcast(msg: ServerToBrowserMessage): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createBrowserGateway(
|
|
61
|
+
sessionManager: SessionManager,
|
|
62
|
+
eventStore: EventStore,
|
|
63
|
+
piGateway: PiGateway,
|
|
64
|
+
_pendingLoadManager?: unknown,
|
|
65
|
+
pendingForkRegistry?: PendingForkRegistry,
|
|
66
|
+
sessionOrderManager?: SessionOrderManager,
|
|
67
|
+
preferencesStore?: PreferencesStore,
|
|
68
|
+
directoryService?: DirectoryService,
|
|
69
|
+
terminalManager?: TerminalManager,
|
|
70
|
+
pendingDashboardSpawns?: Map<string, number>,
|
|
71
|
+
maxWsBufferBytes?: number,
|
|
72
|
+
): BrowserGateway {
|
|
73
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
74
|
+
|
|
75
|
+
// Track subscriptions: ws → Set<sessionId>
|
|
76
|
+
const subscriptions = new Map<WebSocket, Set<string>>();
|
|
77
|
+
// Track which sessions are mid-replay per WebSocket (suppress live events)
|
|
78
|
+
const replayingSessions = new Map<WebSocket, Set<string>>();
|
|
79
|
+
|
|
80
|
+
// Track headless child processes with sessionId linkage
|
|
81
|
+
const headlessPidRegistry = createHeadlessPidRegistry();
|
|
82
|
+
|
|
83
|
+
// Track pending interactive UI requests per session for replay on reconnect
|
|
84
|
+
const pendingUiRequests = new Map<string, Map<string, { requestId: string; method: string; params: Record<string, unknown> }>>();
|
|
85
|
+
|
|
86
|
+
// Track pending auto-resume prompts for ended sessions
|
|
87
|
+
const pendingResumeRegistry = createPendingResumeRegistry({
|
|
88
|
+
onTimeout(oldSessionId) {
|
|
89
|
+
// Clear resuming flag when resume times out
|
|
90
|
+
sessionManager.update(oldSessionId, { resuming: false });
|
|
91
|
+
broadcast({ type: "session_updated", sessionId: oldSessionId, updates: { resuming: false } });
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/** Send any pending interactive UI requests to a specific browser socket */
|
|
96
|
+
function replayPendingUiRequests(ws: WebSocket, sessionId: string) {
|
|
97
|
+
const sessionPending = pendingUiRequests.get(sessionId);
|
|
98
|
+
if (!sessionPending) return;
|
|
99
|
+
for (const req of sessionPending.values()) {
|
|
100
|
+
sendTo(ws, {
|
|
101
|
+
type: "extension_ui_request",
|
|
102
|
+
sessionId,
|
|
103
|
+
requestId: req.requestId,
|
|
104
|
+
method: req.method,
|
|
105
|
+
params: req.params,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function trackUiRequest(sessionId: string, requestId: string, method: string, params: Record<string, unknown>): boolean | void {
|
|
111
|
+
let sessionMap = pendingUiRequests.get(sessionId);
|
|
112
|
+
if (!sessionMap) {
|
|
113
|
+
sessionMap = new Map();
|
|
114
|
+
pendingUiRequests.set(sessionId, sessionMap);
|
|
115
|
+
}
|
|
116
|
+
const title = params.title;
|
|
117
|
+
if (title !== undefined) {
|
|
118
|
+
for (const existing of sessionMap.values()) {
|
|
119
|
+
if (existing.method === method && existing.params.title === title) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
sessionMap.set(requestId, { requestId, method, params });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getSubscribers(sessionId: string): WebSocket[] {
|
|
129
|
+
const result: WebSocket[] = [];
|
|
130
|
+
for (const [ws, subs] of subscriptions) {
|
|
131
|
+
if (subs.has(sessionId) && ws.readyState === WebSocket.OPEN) {
|
|
132
|
+
result.push(ws);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Max buffered bytes per browser WebSocket before dropping messages (0 = no limit) */
|
|
139
|
+
const MAX_WS_BUFFER = maxWsBufferBytes ?? 4 * 1024 * 1024; // 4MB default
|
|
140
|
+
|
|
141
|
+
function sendTo(ws: WebSocket, msg: ServerToBrowserMessage) {
|
|
142
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
143
|
+
// Drop messages if the send buffer is full (browser not consuming)
|
|
144
|
+
if (MAX_WS_BUFFER > 0 && ws.bufferedAmount > MAX_WS_BUFFER) return;
|
|
145
|
+
ws.send(JSON.stringify(msg));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function broadcast(msg: ServerToBrowserMessage) {
|
|
150
|
+
for (const [ws] of subscriptions) {
|
|
151
|
+
sendTo(ws, msg);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
wss.on("connection", (ws, req) => {
|
|
156
|
+
const remoteAddr = req?.socket?.remoteAddress ?? 'unknown';
|
|
157
|
+
const origin = req?.headers?.origin ?? 'no-origin';
|
|
158
|
+
const ua = req?.headers?.['user-agent'] ?? 'no-ua';
|
|
159
|
+
console.error(`[browser-gw] browser client connected from ${remoteAddr} origin=${origin} ua=${ua.slice(0, 80)} (total: ${subscriptions.size + 1})`);
|
|
160
|
+
const subs = new Set<string>();
|
|
161
|
+
subscriptions.set(ws, subs);
|
|
162
|
+
|
|
163
|
+
// Send all sessions on connect (client filters by hidden flag)
|
|
164
|
+
const allSessions = sessionManager.listAll();
|
|
165
|
+
for (const session of allSessions) {
|
|
166
|
+
sendTo(ws, { type: "session_added", session });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Send pinned directories on connect
|
|
170
|
+
if (preferencesStore) {
|
|
171
|
+
sendTo(ws, { type: "pinned_dirs_updated", paths: preferencesStore.getPinnedDirectories() });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Send session orders for all cwds
|
|
175
|
+
if (sessionOrderManager) {
|
|
176
|
+
const allOrders = sessionOrderManager.getAllOrders();
|
|
177
|
+
for (const [cwd, sessionIds] of Object.entries(allOrders)) {
|
|
178
|
+
if (sessionIds.length > 0) {
|
|
179
|
+
sendTo(ws, { type: "sessions_reordered", cwd, sessionIds });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Send cached OpenSpec data for all known directories
|
|
185
|
+
if (directoryService) {
|
|
186
|
+
for (const cwd of directoryService.knownDirectories()) {
|
|
187
|
+
const data = directoryService.getOpenSpecData(cwd);
|
|
188
|
+
if (data && data.initialized) {
|
|
189
|
+
sendTo(ws, { type: "openspec_update", cwd, data });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Send active terminals on connect
|
|
195
|
+
if (terminalManager) {
|
|
196
|
+
for (const terminal of terminalManager.list()) {
|
|
197
|
+
sendTo(ws, { type: "terminal_added", terminal });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Notify server of new connection (for mDNS peer list etc.)
|
|
202
|
+
if (gateway.onConnect) {
|
|
203
|
+
gateway.onConnect(ws);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
ws.on("message", async (raw) => {
|
|
208
|
+
try {
|
|
209
|
+
const msg = JSON.parse(raw.toString()) as BrowserToServerMessage;
|
|
210
|
+
const ctx: BrowserHandlerContext = {
|
|
211
|
+
ws, sessionManager, eventStore, piGateway,
|
|
212
|
+
pendingForkRegistry, sessionOrderManager, preferencesStore,
|
|
213
|
+
directoryService, terminalManager,
|
|
214
|
+
headlessPidRegistry, pendingResumeRegistry, pendingDashboardSpawns,
|
|
215
|
+
sendTo, broadcast, getSubscribers, replayPendingUiRequests,
|
|
216
|
+
trackUiRequest: trackUiRequest,
|
|
217
|
+
markReplaying(targetWs, sessionId) {
|
|
218
|
+
let set = replayingSessions.get(targetWs);
|
|
219
|
+
if (!set) { set = new Set(); replayingSessions.set(targetWs, set); }
|
|
220
|
+
set.add(sessionId);
|
|
221
|
+
},
|
|
222
|
+
clearReplaying(targetWs, sessionId, lastReplayedSeq) {
|
|
223
|
+
const set = replayingSessions.get(targetWs);
|
|
224
|
+
if (set) {
|
|
225
|
+
set.delete(sessionId);
|
|
226
|
+
if (set.size === 0) replayingSessions.delete(targetWs);
|
|
227
|
+
}
|
|
228
|
+
// Send catch-up: any events after lastReplayedSeq
|
|
229
|
+
if (lastReplayedSeq > 0) {
|
|
230
|
+
const catchUp = eventStore.getEvents(sessionId, lastReplayedSeq + 1);
|
|
231
|
+
if (catchUp.length > 0) {
|
|
232
|
+
sendTo(targetWs, {
|
|
233
|
+
type: "event_replay",
|
|
234
|
+
sessionId,
|
|
235
|
+
events: catchUp.map((e) => ({ seq: e.seq, event: e.event })),
|
|
236
|
+
isLast: true,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
switch (msg.type) {
|
|
244
|
+
case "subscribe":
|
|
245
|
+
handleSubscribe(msg, subs, ctx);
|
|
246
|
+
break;
|
|
247
|
+
case "unsubscribe":
|
|
248
|
+
subs.delete(msg.sessionId);
|
|
249
|
+
break;
|
|
250
|
+
case "send_prompt":
|
|
251
|
+
await handleSendPrompt(msg, ctx);
|
|
252
|
+
break;
|
|
253
|
+
case "abort":
|
|
254
|
+
handleAbort(msg, ctx);
|
|
255
|
+
break;
|
|
256
|
+
case "force_kill":
|
|
257
|
+
await handleForceKill(msg, ctx);
|
|
258
|
+
break;
|
|
259
|
+
case "flow_control":
|
|
260
|
+
handleFlowControl(msg, ctx);
|
|
261
|
+
break;
|
|
262
|
+
case "kill_process":
|
|
263
|
+
handleKillProcess(msg, ctx);
|
|
264
|
+
break;
|
|
265
|
+
case "shutdown":
|
|
266
|
+
handleShutdown(msg, ctx);
|
|
267
|
+
break;
|
|
268
|
+
case "rename_session":
|
|
269
|
+
handleRenameSession(msg, ctx);
|
|
270
|
+
break;
|
|
271
|
+
case "hide_session":
|
|
272
|
+
handleHideSession(msg, ctx);
|
|
273
|
+
break;
|
|
274
|
+
case "unhide_session":
|
|
275
|
+
handleUnhideSession(msg, ctx);
|
|
276
|
+
break;
|
|
277
|
+
case "attach_proposal":
|
|
278
|
+
handleAttachProposal(msg, ctx);
|
|
279
|
+
break;
|
|
280
|
+
case "detach_proposal":
|
|
281
|
+
handleDetachProposal(msg, ctx);
|
|
282
|
+
break;
|
|
283
|
+
case "fetch_content":
|
|
284
|
+
handleFetchContent(msg, ctx);
|
|
285
|
+
break;
|
|
286
|
+
case "list_sessions":
|
|
287
|
+
handleListSessions(msg, ctx);
|
|
288
|
+
break;
|
|
289
|
+
case "resume_session":
|
|
290
|
+
await handleResumeSession(msg, ctx);
|
|
291
|
+
break;
|
|
292
|
+
case "spawn_session":
|
|
293
|
+
await handleSpawnSession(msg, ctx);
|
|
294
|
+
break;
|
|
295
|
+
case "reorder_sessions":
|
|
296
|
+
handleReorderSessions(msg, ctx);
|
|
297
|
+
break;
|
|
298
|
+
case "pin_directory":
|
|
299
|
+
handlePinDirectory(msg, ctx);
|
|
300
|
+
break;
|
|
301
|
+
case "unpin_directory":
|
|
302
|
+
handleUnpinDirectory(msg, ctx);
|
|
303
|
+
break;
|
|
304
|
+
case "reorder_pinned_dirs":
|
|
305
|
+
handleReorderPinnedDirs(msg, ctx);
|
|
306
|
+
break;
|
|
307
|
+
case "openspec_refresh":
|
|
308
|
+
handleOpenSpecRefresh(msg, ctx);
|
|
309
|
+
break;
|
|
310
|
+
case "openspec_bulk_archive":
|
|
311
|
+
handleOpenSpecBulkArchive(msg, ctx);
|
|
312
|
+
break;
|
|
313
|
+
case "extension_ui_response": {
|
|
314
|
+
// Clear pending UI request tracking
|
|
315
|
+
const sessionMap = pendingUiRequests.get(msg.sessionId);
|
|
316
|
+
if (sessionMap) {
|
|
317
|
+
sessionMap.delete(msg.requestId);
|
|
318
|
+
if (sessionMap.size === 0) pendingUiRequests.delete(msg.sessionId);
|
|
319
|
+
}
|
|
320
|
+
handleExtensionUiResponse(msg, ctx);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case "flow_management": {
|
|
325
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
326
|
+
type: "flow_management",
|
|
327
|
+
sessionId: msg.sessionId,
|
|
328
|
+
action: msg.action,
|
|
329
|
+
flowName: msg.flowName,
|
|
330
|
+
task: msg.task,
|
|
331
|
+
description: msg.description,
|
|
332
|
+
});
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
case "architect_prompt_response": {
|
|
336
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
337
|
+
type: "architect_prompt_response",
|
|
338
|
+
sessionId: msg.sessionId,
|
|
339
|
+
promptId: msg.promptId,
|
|
340
|
+
answer: msg.answer,
|
|
341
|
+
cancelled: msg.cancelled,
|
|
342
|
+
});
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "role_set": {
|
|
346
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
347
|
+
type: "role_set",
|
|
348
|
+
sessionId: msg.sessionId,
|
|
349
|
+
role: (msg as any).role,
|
|
350
|
+
modelId: (msg as any).modelId,
|
|
351
|
+
});
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
case "role_preset_load": {
|
|
355
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
356
|
+
type: "role_preset_load",
|
|
357
|
+
sessionId: msg.sessionId,
|
|
358
|
+
presetName: (msg as any).presetName,
|
|
359
|
+
});
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
case "role_preset_save": {
|
|
363
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
364
|
+
type: "role_preset_save",
|
|
365
|
+
sessionId: msg.sessionId,
|
|
366
|
+
presetName: (msg as any).presetName,
|
|
367
|
+
});
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case "role_preset_delete": {
|
|
371
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
372
|
+
type: "role_preset_delete",
|
|
373
|
+
sessionId: msg.sessionId,
|
|
374
|
+
presetName: (msg as any).presetName,
|
|
375
|
+
});
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "request_roles": {
|
|
379
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
380
|
+
type: "request_roles",
|
|
381
|
+
sessionId: msg.sessionId,
|
|
382
|
+
});
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case "create_terminal":
|
|
386
|
+
handleCreateTerminal(msg, ctx);
|
|
387
|
+
break;
|
|
388
|
+
case "kill_terminal":
|
|
389
|
+
handleKillTerminal(msg, ctx);
|
|
390
|
+
break;
|
|
391
|
+
case "rename_terminal":
|
|
392
|
+
handleRenameTerminal(msg, ctx);
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
// Forward simple pi-gateway commands
|
|
396
|
+
handlePiGatewayForward(msg, ctx);
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
} catch {
|
|
400
|
+
// Ignore malformed messages
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
ws.on("close", () => {
|
|
405
|
+
console.error(`[browser-gw] browser client disconnected (remaining: ${subscriptions.size - 1})`);
|
|
406
|
+
subscriptions.delete(ws);
|
|
407
|
+
replayingSessions.delete(ws);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
const gateway: BrowserGateway = {
|
|
412
|
+
wss,
|
|
413
|
+
|
|
414
|
+
sendToClient(ws: WebSocket, msg: ServerToBrowserMessage) {
|
|
415
|
+
sendTo(ws, msg);
|
|
416
|
+
},
|
|
417
|
+
|
|
418
|
+
broadcast(msg: ServerToBrowserMessage) {
|
|
419
|
+
broadcast(msg);
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
broadcastEvent(sessionId: string, seq: number, event: any) {
|
|
423
|
+
const subscribers = getSubscribers(sessionId);
|
|
424
|
+
const msg: ServerToBrowserMessage = {
|
|
425
|
+
type: "event",
|
|
426
|
+
sessionId,
|
|
427
|
+
seq,
|
|
428
|
+
event,
|
|
429
|
+
};
|
|
430
|
+
for (const ws of subscribers) {
|
|
431
|
+
// Skip WebSockets that are mid-replay for this session
|
|
432
|
+
const replaying = replayingSessions.get(ws);
|
|
433
|
+
if (replaying?.has(sessionId)) continue;
|
|
434
|
+
sendTo(ws, msg);
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
broadcastSessionAdded(session: any) {
|
|
439
|
+
broadcast({ type: "session_added", session });
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
broadcastSessionUpdated(sessionId: string, updates: any) {
|
|
443
|
+
broadcast({ type: "session_updated", sessionId, updates });
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
broadcastSessionRemoved(sessionId: string) {
|
|
447
|
+
broadcast({ type: "session_removed", sessionId });
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
broadcastSessionStateReset(sessionId: string) {
|
|
451
|
+
const subscribers = getSubscribers(sessionId);
|
|
452
|
+
const msg: ServerToBrowserMessage = { type: "session_state_reset", sessionId };
|
|
453
|
+
for (const ws of subscribers) {
|
|
454
|
+
sendTo(ws, msg);
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
sendToSubscribers(sessionId: string, msg: ServerToBrowserMessage) {
|
|
459
|
+
const subscribers = getSubscribers(sessionId);
|
|
460
|
+
for (const ws of subscribers) {
|
|
461
|
+
sendTo(ws, msg);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
broadcastToAll(msg: ServerToBrowserMessage) {
|
|
466
|
+
broadcast(msg);
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
getSubscriberCount(sessionId: string): number {
|
|
470
|
+
return getSubscribers(sessionId).length;
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
trackUiRequest,
|
|
474
|
+
|
|
475
|
+
clearUiRequest(sessionId: string, requestId: string) {
|
|
476
|
+
const sessionMap = pendingUiRequests.get(sessionId);
|
|
477
|
+
if (sessionMap) {
|
|
478
|
+
sessionMap.delete(requestId);
|
|
479
|
+
if (sessionMap.size === 0) {
|
|
480
|
+
pendingUiRequests.delete(sessionId);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
|
|
485
|
+
shutdownHeadlessProcesses() {
|
|
486
|
+
headlessPidRegistry.killAll();
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
headlessPidRegistry,
|
|
490
|
+
|
|
491
|
+
pendingResumeRegistry,
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
return gateway;
|
|
495
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Directory and preference handlers: pin, unpin, reorder, openspec, pi-gateway forwards.
|
|
3
|
+
*/
|
|
4
|
+
import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
5
|
+
import type { BrowserHandlerContext } from "./handler-context.js";
|
|
6
|
+
import { safeRealpathSync } from "../resolve-path.js";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
export function handlePinDirectory(
|
|
13
|
+
msg: Extract<BrowserToServerMessage, { type: "pin_directory" }>,
|
|
14
|
+
ctx: BrowserHandlerContext,
|
|
15
|
+
): void {
|
|
16
|
+
const { preferencesStore, directoryService, sessionManager, broadcast } = ctx;
|
|
17
|
+
if (!preferencesStore) return;
|
|
18
|
+
const resolved = safeRealpathSync(msg.path);
|
|
19
|
+
preferencesStore.pinDirectory(resolved);
|
|
20
|
+
broadcast({ type: "pinned_dirs_updated", paths: preferencesStore.getPinnedDirectories() });
|
|
21
|
+
if (directoryService) {
|
|
22
|
+
directoryService.onDirectoryAdded(resolved).then(({ sessions, openspecData }) => {
|
|
23
|
+
for (const hist of sessions) {
|
|
24
|
+
if (!sessionManager.get(hist.id)) {
|
|
25
|
+
sessionManager.register({
|
|
26
|
+
id: hist.id,
|
|
27
|
+
cwd: hist.cwd,
|
|
28
|
+
name: hist.name,
|
|
29
|
+
source: "tui",
|
|
30
|
+
sessionFile: hist.sessionFile,
|
|
31
|
+
sessionDir: hist.sessionDir,
|
|
32
|
+
firstMessage: hist.firstMessage,
|
|
33
|
+
startedAt: hist.startedAt,
|
|
34
|
+
});
|
|
35
|
+
sessionManager.unregister(hist.id);
|
|
36
|
+
sessionManager.update(hist.id, { hidden: true });
|
|
37
|
+
const s = sessionManager.get(hist.id);
|
|
38
|
+
if (s) broadcast({ type: "session_added", session: s });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
broadcast({ type: "openspec_update", cwd: resolved, data: openspecData } as any);
|
|
42
|
+
}).catch(() => {});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function handleUnpinDirectory(
|
|
47
|
+
msg: Extract<BrowserToServerMessage, { type: "unpin_directory" }>,
|
|
48
|
+
ctx: BrowserHandlerContext,
|
|
49
|
+
): void {
|
|
50
|
+
if (ctx.preferencesStore) {
|
|
51
|
+
ctx.preferencesStore.unpinDirectory(safeRealpathSync(msg.path));
|
|
52
|
+
ctx.broadcast({ type: "pinned_dirs_updated", paths: ctx.preferencesStore.getPinnedDirectories() });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function handleReorderPinnedDirs(
|
|
57
|
+
msg: Extract<BrowserToServerMessage, { type: "reorder_pinned_dirs" }>,
|
|
58
|
+
ctx: BrowserHandlerContext,
|
|
59
|
+
): void {
|
|
60
|
+
if (ctx.preferencesStore) {
|
|
61
|
+
ctx.preferencesStore.reorderPinnedDirs(msg.paths.map(safeRealpathSync));
|
|
62
|
+
ctx.broadcast({ type: "pinned_dirs_updated", paths: ctx.preferencesStore.getPinnedDirectories() });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function handleReorderSessions(
|
|
67
|
+
msg: Extract<BrowserToServerMessage, { type: "reorder_sessions" }>,
|
|
68
|
+
ctx: BrowserHandlerContext,
|
|
69
|
+
): void {
|
|
70
|
+
if (ctx.sessionOrderManager) {
|
|
71
|
+
ctx.sessionOrderManager.reorder(msg.cwd, msg.sessionIds);
|
|
72
|
+
ctx.broadcast({ type: "sessions_reordered", cwd: msg.cwd, sessionIds: msg.sessionIds });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function handleOpenSpecRefresh(
|
|
77
|
+
msg: Extract<BrowserToServerMessage, { type: "openspec_refresh" }>,
|
|
78
|
+
ctx: BrowserHandlerContext,
|
|
79
|
+
): void {
|
|
80
|
+
if (ctx.directoryService) {
|
|
81
|
+
ctx.directoryService.refreshOpenSpec(msg.cwd).then((data) => {
|
|
82
|
+
ctx.broadcast({ type: "openspec_update", cwd: msg.cwd, data });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function handleOpenSpecBulkArchive(
|
|
88
|
+
msg: Extract<BrowserToServerMessage, { type: "openspec_bulk_archive" }>,
|
|
89
|
+
ctx: BrowserHandlerContext,
|
|
90
|
+
): void {
|
|
91
|
+
if (ctx.directoryService) {
|
|
92
|
+
execFileAsync("openspec", ["archive", "--completed"], { cwd: msg.cwd, timeout: 30000 })
|
|
93
|
+
.catch(() => {})
|
|
94
|
+
.then(() => ctx.directoryService!.refreshOpenSpec(msg.cwd))
|
|
95
|
+
.then((data) => {
|
|
96
|
+
if (data) ctx.broadcast({ type: "openspec_update", cwd: msg.cwd, data });
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function handleExtensionUiResponse(
|
|
102
|
+
msg: Extract<BrowserToServerMessage, { type: "extension_ui_response" }>,
|
|
103
|
+
ctx: BrowserHandlerContext,
|
|
104
|
+
): void {
|
|
105
|
+
ctx.piGateway.sendToSession(msg.sessionId, {
|
|
106
|
+
type: "extension_ui_response",
|
|
107
|
+
sessionId: msg.sessionId,
|
|
108
|
+
requestId: msg.requestId,
|
|
109
|
+
result: msg.result,
|
|
110
|
+
cancelled: msg.cancelled,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Forward simple pi-gateway commands (request_commands, list_files, request_models, set_model, set_thinking_level) */
|
|
115
|
+
export function handlePiGatewayForward(
|
|
116
|
+
msg: BrowserToServerMessage,
|
|
117
|
+
ctx: BrowserHandlerContext,
|
|
118
|
+
): void {
|
|
119
|
+
const { piGateway } = ctx;
|
|
120
|
+
switch (msg.type) {
|
|
121
|
+
case "request_commands":
|
|
122
|
+
piGateway.sendToSession(msg.sessionId, { type: "request_commands", sessionId: msg.sessionId });
|
|
123
|
+
break;
|
|
124
|
+
case "list_files":
|
|
125
|
+
piGateway.sendToSession(msg.sessionId, { type: "list_files", sessionId: msg.sessionId, query: msg.query });
|
|
126
|
+
break;
|
|
127
|
+
case "request_models":
|
|
128
|
+
piGateway.sendToSession(msg.sessionId, { type: "request_models", sessionId: msg.sessionId });
|
|
129
|
+
break;
|
|
130
|
+
case "set_thinking_level":
|
|
131
|
+
piGateway.sendToSession(msg.sessionId, { type: "set_thinking_level", sessionId: msg.sessionId, level: msg.level });
|
|
132
|
+
break;
|
|
133
|
+
case "set_model":
|
|
134
|
+
piGateway.sendToSession(msg.sessionId, { type: "set_model", sessionId: msg.sessionId, provider: msg.provider, modelId: msg.modelId });
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared context for browser message handlers.
|
|
3
|
+
* Each handler receives only what it needs via this context.
|
|
4
|
+
*/
|
|
5
|
+
import type { WebSocket } from "ws";
|
|
6
|
+
import type { ServerToBrowserMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
7
|
+
import type { SessionManager } from "../memory-session-manager.js";
|
|
8
|
+
import type { EventStore } from "../memory-event-store.js";
|
|
9
|
+
import type { PiGateway } from "../pi-gateway.js";
|
|
10
|
+
import type { PendingForkRegistry } from "../pending-fork-registry.js";
|
|
11
|
+
import type { SessionOrderManager } from "../session-order-manager.js";
|
|
12
|
+
import type { PreferencesStore } from "../preferences-store.js";
|
|
13
|
+
import type { DirectoryService } from "../directory-service.js";
|
|
14
|
+
import type { TerminalManager } from "../terminal-manager.js";
|
|
15
|
+
import type { HeadlessPidRegistry } from "../headless-pid-registry.js";
|
|
16
|
+
import type { PendingResumeRegistry } from "../pending-resume-registry.js";
|
|
17
|
+
|
|
18
|
+
export interface BrowserHandlerContext {
|
|
19
|
+
ws: WebSocket;
|
|
20
|
+
sessionManager: SessionManager;
|
|
21
|
+
eventStore: EventStore;
|
|
22
|
+
piGateway: PiGateway;
|
|
23
|
+
pendingForkRegistry?: PendingForkRegistry;
|
|
24
|
+
sessionOrderManager?: SessionOrderManager;
|
|
25
|
+
preferencesStore?: PreferencesStore;
|
|
26
|
+
directoryService?: DirectoryService;
|
|
27
|
+
terminalManager?: TerminalManager;
|
|
28
|
+
headlessPidRegistry: HeadlessPidRegistry;
|
|
29
|
+
pendingResumeRegistry: PendingResumeRegistry;
|
|
30
|
+
pendingDashboardSpawns?: Map<string, number>;
|
|
31
|
+
/** Send message to a specific WebSocket */
|
|
32
|
+
sendTo(ws: WebSocket, msg: ServerToBrowserMessage): void;
|
|
33
|
+
/** Broadcast to all connected browsers */
|
|
34
|
+
broadcast(msg: ServerToBrowserMessage): void;
|
|
35
|
+
/** Get subscribers for a session */
|
|
36
|
+
getSubscribers(sessionId: string): WebSocket[];
|
|
37
|
+
/** Track UI request */
|
|
38
|
+
trackUiRequest(sessionId: string, requestId: string, method: string, params: Record<string, unknown>): boolean | void;
|
|
39
|
+
/** Replay pending UI requests to a browser */
|
|
40
|
+
replayPendingUiRequests(ws: WebSocket, sessionId: string): void;
|
|
41
|
+
/** Mark a session as mid-replay for a specific WebSocket (suppresses live events) */
|
|
42
|
+
markReplaying(ws: WebSocket, sessionId: string): void;
|
|
43
|
+
/** Clear replay flag and send catch-up events */
|
|
44
|
+
clearReplaying(ws: WebSocket, sessionId: string, lastReplayedSeq: number): void;
|
|
45
|
+
}
|