@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,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session action handlers: send_prompt, abort, resume, spawn, shutdown, flow_control.
|
|
3
|
+
*/
|
|
4
|
+
import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
5
|
+
import type { BrowserHandlerContext } from "./handler-context.js";
|
|
6
|
+
import { spawnPiSession } from "../process-manager.js";
|
|
7
|
+
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
8
|
+
import { createBranchedSessionFile } from "../session-file-reader.js";
|
|
9
|
+
import { execSync } from "node:child_process";
|
|
10
|
+
|
|
11
|
+
function killHeadlessBySessionId(sessionId: string): boolean {
|
|
12
|
+
if (process.platform === "win32") return false;
|
|
13
|
+
try {
|
|
14
|
+
const output = execSync(
|
|
15
|
+
`ps -eo pid,command | grep "${sessionId}" | grep "sleep 2147483647" | grep -v grep`,
|
|
16
|
+
{ encoding: "utf8", timeout: 3000 },
|
|
17
|
+
).trim();
|
|
18
|
+
if (!output) return false;
|
|
19
|
+
for (const line of output.split("\n")) {
|
|
20
|
+
const pid = parseInt(line.trim(), 10);
|
|
21
|
+
if (pid > 0) {
|
|
22
|
+
try { process.kill(-pid, "SIGTERM"); } catch {
|
|
23
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* ignore */ }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function handleSendPrompt(
|
|
34
|
+
msg: Extract<BrowserToServerMessage, { type: "send_prompt" }>,
|
|
35
|
+
ctx: BrowserHandlerContext,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
const { sessionManager, piGateway, headlessPidRegistry, pendingResumeRegistry, pendingDashboardSpawns, broadcast } = ctx;
|
|
38
|
+
const promptSession = sessionManager.get(msg.sessionId);
|
|
39
|
+
|
|
40
|
+
if (promptSession?.status === "ended") {
|
|
41
|
+
if (!promptSession.sessionFile) {
|
|
42
|
+
console.error(`[dashboard] auto-resume failed: no session file for session ${msg.sessionId}`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const alreadyResuming = promptSession.resuming;
|
|
46
|
+
pendingResumeRegistry.record(promptSession.cwd, {
|
|
47
|
+
text: msg.text,
|
|
48
|
+
images: msg.images,
|
|
49
|
+
oldSessionId: msg.sessionId,
|
|
50
|
+
sessionFile: promptSession.sessionFile,
|
|
51
|
+
});
|
|
52
|
+
if (alreadyResuming) return;
|
|
53
|
+
sessionManager.update(msg.sessionId, { resuming: true });
|
|
54
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { resuming: true } });
|
|
55
|
+
const autoResumeConfig = loadConfig();
|
|
56
|
+
const spawnResult = await spawnPiSession(promptSession.cwd, {
|
|
57
|
+
sessionFile: promptSession.sessionFile,
|
|
58
|
+
mode: "continue",
|
|
59
|
+
strategy: autoResumeConfig.spawnStrategy,
|
|
60
|
+
});
|
|
61
|
+
if (!spawnResult.success) {
|
|
62
|
+
console.error(`[dashboard] auto-resume spawn failed: ${spawnResult.message}`);
|
|
63
|
+
pendingResumeRegistry.consume(promptSession.cwd);
|
|
64
|
+
sessionManager.update(msg.sessionId, { resuming: false });
|
|
65
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { resuming: false } });
|
|
66
|
+
}
|
|
67
|
+
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
68
|
+
pendingDashboardSpawns?.set(promptSession.cwd, (pendingDashboardSpawns?.get(promptSession.cwd) ?? 0) + 1);
|
|
69
|
+
}
|
|
70
|
+
if (spawnResult.process && spawnResult.pid) {
|
|
71
|
+
headlessPidRegistry.register(spawnResult.pid, promptSession.cwd, spawnResult.process);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const sent = piGateway.sendToSession(msg.sessionId, {
|
|
75
|
+
type: "send_prompt",
|
|
76
|
+
sessionId: msg.sessionId,
|
|
77
|
+
text: msg.text,
|
|
78
|
+
images: msg.images,
|
|
79
|
+
});
|
|
80
|
+
if (!sent) {
|
|
81
|
+
console.error(`[dashboard] send_prompt failed: no bridge connection for session ${msg.sessionId}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function handleResumeSession(
|
|
87
|
+
msg: Extract<BrowserToServerMessage, { type: "resume_session" }>,
|
|
88
|
+
ctx: BrowserHandlerContext,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
const { ws, sessionManager, pendingForkRegistry, headlessPidRegistry, pendingDashboardSpawns, sendTo } = ctx;
|
|
91
|
+
const session = sessionManager.get(msg.sessionId);
|
|
92
|
+
if (!session) {
|
|
93
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session not found" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!session.sessionFile) {
|
|
97
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session file is unknown (pre-migration session)" });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (msg.mode === "continue" && session.status !== "ended") {
|
|
101
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session is already active" });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (session.resuming) {
|
|
105
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: "Session is already being resumed" });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (msg.mode === "fork" && pendingForkRegistry) {
|
|
109
|
+
pendingForkRegistry.recordFork(session.cwd, msg.sessionId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// For fork-from-message: create a pruned session file first
|
|
113
|
+
let forkSessionFile = session.sessionFile;
|
|
114
|
+
if (msg.mode === "fork" && msg.entryId) {
|
|
115
|
+
try {
|
|
116
|
+
forkSessionFile = createBranchedSessionFile(session.sessionFile, msg.entryId);
|
|
117
|
+
} catch (err: any) {
|
|
118
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: false, message: `Fork from entry failed: ${err.message}` });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const resumeConfig = loadConfig();
|
|
124
|
+
const result = await spawnPiSession(session.cwd, {
|
|
125
|
+
sessionFile: forkSessionFile,
|
|
126
|
+
mode: msg.mode,
|
|
127
|
+
strategy: resumeConfig.spawnStrategy,
|
|
128
|
+
});
|
|
129
|
+
if (result.dashboardSpawned && result.success) {
|
|
130
|
+
pendingDashboardSpawns?.set(session.cwd, (pendingDashboardSpawns?.get(session.cwd) ?? 0) + 1);
|
|
131
|
+
}
|
|
132
|
+
if (result.process && result.pid) {
|
|
133
|
+
headlessPidRegistry.register(result.pid, session.cwd, result.process);
|
|
134
|
+
}
|
|
135
|
+
sendTo(ws, { type: "resume_result", sessionId: msg.sessionId, success: result.success, message: result.message });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function handleSpawnSession(
|
|
139
|
+
msg: Extract<BrowserToServerMessage, { type: "spawn_session" }>,
|
|
140
|
+
ctx: BrowserHandlerContext,
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const { ws, headlessPidRegistry, pendingDashboardSpawns, sendTo } = ctx;
|
|
143
|
+
const config = loadConfig();
|
|
144
|
+
const spawnResult = await spawnPiSession(msg.cwd, { strategy: config.spawnStrategy });
|
|
145
|
+
if (spawnResult.process && spawnResult.pid) {
|
|
146
|
+
headlessPidRegistry.register(spawnResult.pid, msg.cwd, spawnResult.process);
|
|
147
|
+
}
|
|
148
|
+
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
149
|
+
pendingDashboardSpawns?.set(msg.cwd, (pendingDashboardSpawns?.get(msg.cwd) ?? 0) + 1);
|
|
150
|
+
}
|
|
151
|
+
sendTo(ws, { type: "spawn_result", cwd: msg.cwd, success: spawnResult.success, message: spawnResult.message });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function handleShutdown(
|
|
155
|
+
msg: Extract<BrowserToServerMessage, { type: "shutdown" }>,
|
|
156
|
+
ctx: BrowserHandlerContext,
|
|
157
|
+
): void {
|
|
158
|
+
const { sessionManager, piGateway, headlessPidRegistry, broadcast } = ctx;
|
|
159
|
+
piGateway.sendToSession(msg.sessionId, { type: "shutdown", sessionId: msg.sessionId });
|
|
160
|
+
headlessPidRegistry.killBySessionId(msg.sessionId);
|
|
161
|
+
killHeadlessBySessionId(msg.sessionId);
|
|
162
|
+
sessionManager.unregister(msg.sessionId);
|
|
163
|
+
broadcast({ type: "session_removed", sessionId: msg.sessionId });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function handleAbort(
|
|
167
|
+
msg: Extract<BrowserToServerMessage, { type: "abort" }>,
|
|
168
|
+
ctx: BrowserHandlerContext,
|
|
169
|
+
): void {
|
|
170
|
+
ctx.piGateway.sendToSession(msg.sessionId, { type: "abort", sessionId: msg.sessionId });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function handleFlowControl(
|
|
174
|
+
msg: Extract<BrowserToServerMessage, { type: "flow_control" }>,
|
|
175
|
+
ctx: BrowserHandlerContext,
|
|
176
|
+
): void {
|
|
177
|
+
ctx.piGateway.sendToSession(msg.sessionId, { type: "flow_control", sessionId: msg.sessionId, action: msg.action });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function handleKillProcess(
|
|
181
|
+
msg: Extract<BrowserToServerMessage, { type: "kill_process" }>,
|
|
182
|
+
ctx: BrowserHandlerContext,
|
|
183
|
+
): void {
|
|
184
|
+
ctx.piGateway.sendToSession(msg.sessionId, { type: "kill_process", sessionId: msg.sessionId, pgid: msg.pgid });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if a PID belongs to a pi/node process (safety check before SIGKILL).
|
|
189
|
+
* Returns true if the process looks like a pi-related process, false otherwise.
|
|
190
|
+
*/
|
|
191
|
+
function isPiProcess(pid: number): boolean {
|
|
192
|
+
try {
|
|
193
|
+
const cmd = process.platform === "darwin"
|
|
194
|
+
? `ps -p ${pid} -o command=`
|
|
195
|
+
: `cat /proc/${pid}/cmdline 2>/dev/null || ps -p ${pid} -o command=`;
|
|
196
|
+
const output = execSync(cmd, { encoding: "utf8", timeout: 2000 }).trim();
|
|
197
|
+
return /\bpi\b|\bnode\b/.test(output);
|
|
198
|
+
} catch {
|
|
199
|
+
// Process already exited — treat as dead
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if a process is still alive.
|
|
206
|
+
*/
|
|
207
|
+
function isProcessAlive(pid: number): boolean {
|
|
208
|
+
try {
|
|
209
|
+
process.kill(pid, 0);
|
|
210
|
+
return true;
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function handleForceKill(
|
|
217
|
+
msg: Extract<BrowserToServerMessage, { type: "force_kill" }>,
|
|
218
|
+
ctx: BrowserHandlerContext,
|
|
219
|
+
): Promise<void> {
|
|
220
|
+
const { sessionManager, piGateway, headlessPidRegistry, broadcast, sendTo, ws } = ctx;
|
|
221
|
+
const session = sessionManager.get(msg.sessionId);
|
|
222
|
+
if (!session) {
|
|
223
|
+
sendTo(ws, { type: "force_kill_result", sessionId: msg.sessionId, success: false, message: "Session not found" });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Force-close the bridge WebSocket regardless of PID availability
|
|
228
|
+
piGateway.closeSession(msg.sessionId);
|
|
229
|
+
|
|
230
|
+
const pid = session?.pid;
|
|
231
|
+
if (!pid) {
|
|
232
|
+
// No PID — we can only close the WebSocket
|
|
233
|
+
sessionManager.update(msg.sessionId, { status: "ended", endedAt: Date.now() });
|
|
234
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { status: "ended", endedAt: Date.now() } });
|
|
235
|
+
sendTo(ws, { type: "force_kill_result", sessionId: msg.sessionId, success: true, message: "WebSocket closed (no PID available)" });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Step 1: SIGTERM
|
|
240
|
+
try {
|
|
241
|
+
process.kill(pid, "SIGTERM");
|
|
242
|
+
} catch {
|
|
243
|
+
// Process already dead
|
|
244
|
+
sessionManager.update(msg.sessionId, { status: "ended", endedAt: Date.now() });
|
|
245
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { status: "ended", endedAt: Date.now() } });
|
|
246
|
+
sendTo(ws, { type: "force_kill_result", sessionId: msg.sessionId, success: true, message: "Process already exited" });
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Also kill via headless registry if applicable
|
|
251
|
+
headlessPidRegistry.killBySessionId(msg.sessionId);
|
|
252
|
+
|
|
253
|
+
// Step 2: Wait 2s, then SIGKILL if still alive
|
|
254
|
+
await new Promise<void>((resolve) => {
|
|
255
|
+
setTimeout(() => {
|
|
256
|
+
if (isProcessAlive(pid)) {
|
|
257
|
+
// Safety check: verify PID still belongs to a pi process
|
|
258
|
+
if (isPiProcess(pid)) {
|
|
259
|
+
try {
|
|
260
|
+
process.kill(pid, "SIGKILL");
|
|
261
|
+
} catch { /* already dead */ }
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
resolve();
|
|
265
|
+
}, 2000);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
sessionManager.update(msg.sessionId, { status: "ended", endedAt: Date.now() });
|
|
269
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { status: "ended", endedAt: Date.now() } });
|
|
270
|
+
sendTo(ws, { type: "force_kill_result", sessionId: msg.sessionId, success: true });
|
|
271
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session metadata handlers: rename, hide, unhide, attach/detach proposal, fetch_content, list_sessions.
|
|
3
|
+
*/
|
|
4
|
+
import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
5
|
+
import type { BrowserHandlerContext } from "./handler-context.js";
|
|
6
|
+
|
|
7
|
+
export function handleRenameSession(
|
|
8
|
+
msg: Extract<BrowserToServerMessage, { type: "rename_session" }>,
|
|
9
|
+
ctx: BrowserHandlerContext,
|
|
10
|
+
): void {
|
|
11
|
+
const { sessionManager, piGateway, broadcast } = ctx;
|
|
12
|
+
const nameUpdates = { name: msg.name || undefined };
|
|
13
|
+
sessionManager.update(msg.sessionId, nameUpdates);
|
|
14
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: nameUpdates });
|
|
15
|
+
piGateway.sendToSession(msg.sessionId, { type: "rename_session", sessionId: msg.sessionId, name: msg.name });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function handleHideSession(
|
|
19
|
+
msg: Extract<BrowserToServerMessage, { type: "hide_session" }>,
|
|
20
|
+
ctx: BrowserHandlerContext,
|
|
21
|
+
): void {
|
|
22
|
+
const updates = { hidden: true };
|
|
23
|
+
ctx.sessionManager.update(msg.sessionId, updates);
|
|
24
|
+
ctx.broadcast({ type: "session_updated", sessionId: msg.sessionId, updates });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function handleUnhideSession(
|
|
28
|
+
msg: Extract<BrowserToServerMessage, { type: "unhide_session" }>,
|
|
29
|
+
ctx: BrowserHandlerContext,
|
|
30
|
+
): void {
|
|
31
|
+
const updates = { hidden: false };
|
|
32
|
+
ctx.sessionManager.update(msg.sessionId, updates);
|
|
33
|
+
ctx.broadcast({ type: "session_updated", sessionId: msg.sessionId, updates });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function handleAttachProposal(
|
|
37
|
+
msg: Extract<BrowserToServerMessage, { type: "attach_proposal" }>,
|
|
38
|
+
ctx: BrowserHandlerContext,
|
|
39
|
+
): void {
|
|
40
|
+
const { sessionManager, piGateway, broadcast } = ctx;
|
|
41
|
+
const updates: Record<string, unknown> = { attachedProposal: msg.changeName };
|
|
42
|
+
const session = sessionManager.get(msg.sessionId);
|
|
43
|
+
if (session && !session.name?.trim()) {
|
|
44
|
+
updates.name = msg.changeName;
|
|
45
|
+
piGateway.sendToSession(msg.sessionId, { type: "rename_session", sessionId: msg.sessionId, name: msg.changeName });
|
|
46
|
+
}
|
|
47
|
+
sessionManager.update(msg.sessionId, updates);
|
|
48
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function handleDetachProposal(
|
|
52
|
+
msg: Extract<BrowserToServerMessage, { type: "detach_proposal" }>,
|
|
53
|
+
ctx: BrowserHandlerContext,
|
|
54
|
+
): void {
|
|
55
|
+
const updates = { attachedProposal: null, openspecPhase: null, openspecChange: null };
|
|
56
|
+
ctx.sessionManager.update(msg.sessionId, updates);
|
|
57
|
+
ctx.broadcast({ type: "session_updated", sessionId: msg.sessionId, updates });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function handleFetchContent(
|
|
61
|
+
msg: Extract<BrowserToServerMessage, { type: "fetch_content" }>,
|
|
62
|
+
ctx: BrowserHandlerContext,
|
|
63
|
+
): void {
|
|
64
|
+
const event = ctx.eventStore.getEvent(msg.sessionId, msg.seq);
|
|
65
|
+
if (event) {
|
|
66
|
+
ctx.sendTo(ctx.ws, { type: "event", sessionId: msg.sessionId, seq: msg.seq, event });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function handleListSessions(
|
|
71
|
+
msg: Extract<BrowserToServerMessage, { type: "list_sessions" }>,
|
|
72
|
+
ctx: BrowserHandlerContext,
|
|
73
|
+
): void {
|
|
74
|
+
const { ws, sessionManager, piGateway, sendTo } = ctx;
|
|
75
|
+
const cwd = msg.cwd;
|
|
76
|
+
const bridgeSessionId = piGateway.findSessionByCwd(cwd);
|
|
77
|
+
if (bridgeSessionId) {
|
|
78
|
+
piGateway.sendToSession(bridgeSessionId, { type: "list_sessions", sessionId: bridgeSessionId, cwd });
|
|
79
|
+
} else {
|
|
80
|
+
const allSessions = sessionManager.listAll();
|
|
81
|
+
const filtered = allSessions
|
|
82
|
+
.filter((s) => s.cwd === cwd || s.cwd.startsWith(cwd + "/") || cwd.startsWith(s.cwd + "/"))
|
|
83
|
+
.map((s) => ({
|
|
84
|
+
id: s.id,
|
|
85
|
+
path: s.sessionFile || "",
|
|
86
|
+
cwd: s.cwd,
|
|
87
|
+
name: s.name,
|
|
88
|
+
created: new Date(s.startedAt).toISOString(),
|
|
89
|
+
modified: new Date(s.endedAt || s.startedAt).toISOString(),
|
|
90
|
+
messageCount: 0,
|
|
91
|
+
firstMessage: s.firstMessage,
|
|
92
|
+
}));
|
|
93
|
+
sendTo(ws, { type: "sessions_list", sessionId: "", cwd, sessions: filtered });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription message handlers: subscribe, unsubscribe.
|
|
3
|
+
*/
|
|
4
|
+
import type { WebSocket } from "ws";
|
|
5
|
+
import type { ServerToBrowserMessage, BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
6
|
+
import type { BrowserHandlerContext } from "./handler-context.js";
|
|
7
|
+
import { extractStatsFromEvents } from "../event-status-extraction.js";
|
|
8
|
+
import type { StoredEvent } from "../memory-event-store.js";
|
|
9
|
+
|
|
10
|
+
const REPLAY_BATCH_SIZE = 50;
|
|
11
|
+
/** Max events to replay per session subscription (0 = unlimited) */
|
|
12
|
+
const MAX_REPLAY_EVENTS = 0;
|
|
13
|
+
/** Max buffered bytes before pausing replay sends (1MB) */
|
|
14
|
+
const BACKPRESSURE_THRESHOLD = 1_024 * 1_024;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Send stored events to a WebSocket in batches with backpressure handling.
|
|
18
|
+
* Yields between batches to let the event loop flush data and avoid OOM.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Send stored events to a WebSocket in batches with backpressure handling.
|
|
22
|
+
* Returns the highest seq sent, or 0 if no events were sent.
|
|
23
|
+
*/
|
|
24
|
+
async function sendEventBatches(
|
|
25
|
+
ws: WebSocket,
|
|
26
|
+
sessionId: string,
|
|
27
|
+
stored: StoredEvent[],
|
|
28
|
+
sendTo: (ws: WebSocket, msg: ServerToBrowserMessage) => void,
|
|
29
|
+
): Promise<number> {
|
|
30
|
+
for (let i = 0; i < stored.length; i += REPLAY_BATCH_SIZE) {
|
|
31
|
+
if (ws.readyState !== ws.OPEN) return 0;
|
|
32
|
+
const batch = stored.slice(i, i + REPLAY_BATCH_SIZE);
|
|
33
|
+
sendTo(ws, {
|
|
34
|
+
type: "event_replay",
|
|
35
|
+
sessionId,
|
|
36
|
+
events: batch.map((e) => ({ seq: e.seq, event: e.event })),
|
|
37
|
+
isLast: i + REPLAY_BATCH_SIZE >= stored.length,
|
|
38
|
+
});
|
|
39
|
+
// Yield to event loop between batches to allow GC and buffer flushing
|
|
40
|
+
if (ws.bufferedAmount > BACKPRESSURE_THRESHOLD) {
|
|
41
|
+
await new Promise<void>((resolve) => {
|
|
42
|
+
const check = () => {
|
|
43
|
+
if (ws.readyState !== ws.OPEN || ws.bufferedAmount < BACKPRESSURE_THRESHOLD) {
|
|
44
|
+
resolve();
|
|
45
|
+
} else {
|
|
46
|
+
setTimeout(check, 50);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
setTimeout(check, 10);
|
|
50
|
+
});
|
|
51
|
+
} else {
|
|
52
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return stored.length > 0 ? stored[stored.length - 1].seq : 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function handleSubscribe(
|
|
59
|
+
msg: Extract<BrowserToServerMessage, { type: "subscribe" }>,
|
|
60
|
+
subs: Set<string>,
|
|
61
|
+
ctx: BrowserHandlerContext,
|
|
62
|
+
): void {
|
|
63
|
+
const { ws, sessionManager, eventStore, directoryService, piGateway, sendTo, broadcast, getSubscribers, replayPendingUiRequests, markReplaying, clearReplaying } = ctx;
|
|
64
|
+
subs.add(msg.sessionId);
|
|
65
|
+
|
|
66
|
+
// Request metadata from the extension so commands/flows/models/roles arrive
|
|
67
|
+
// while the browser is actually subscribed (responses use sendToSubscribers).
|
|
68
|
+
piGateway.sendToSession(msg.sessionId, { type: "request_commands", sessionId: msg.sessionId });
|
|
69
|
+
piGateway.sendToSession(msg.sessionId, { type: "request_models", sessionId: msg.sessionId });
|
|
70
|
+
piGateway.sendToSession(msg.sessionId, { type: "request_roles", sessionId: msg.sessionId });
|
|
71
|
+
|
|
72
|
+
if (eventStore.hasEvents(msg.sessionId)) {
|
|
73
|
+
const lastSeq = msg.lastSeq ?? 0;
|
|
74
|
+
const maxSeq = eventStore.getMaxSeq(msg.sessionId);
|
|
75
|
+
|
|
76
|
+
// Stale lastSeq: client has higher seq than server (e.g. server restarted)
|
|
77
|
+
if (lastSeq > 0 && lastSeq > maxSeq) {
|
|
78
|
+
sendTo(ws, { type: "session_state_reset", sessionId: msg.sessionId });
|
|
79
|
+
// Full replay from seq 1
|
|
80
|
+
let events = eventStore.getEvents(msg.sessionId, 1);
|
|
81
|
+
if (MAX_REPLAY_EVENTS > 0 && events.length > MAX_REPLAY_EVENTS) {
|
|
82
|
+
events = events.slice(events.length - MAX_REPLAY_EVENTS);
|
|
83
|
+
}
|
|
84
|
+
markReplaying(ws, msg.sessionId);
|
|
85
|
+
sendEventBatches(ws, msg.sessionId, events, sendTo).then((lastSent) => {
|
|
86
|
+
clearReplaying(ws, msg.sessionId, lastSent);
|
|
87
|
+
replayPendingUiRequests(ws, msg.sessionId);
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
let events = eventStore.getEvents(msg.sessionId, lastSeq + 1);
|
|
91
|
+
if (MAX_REPLAY_EVENTS > 0 && events.length > MAX_REPLAY_EVENTS) {
|
|
92
|
+
events = events.slice(events.length - MAX_REPLAY_EVENTS);
|
|
93
|
+
}
|
|
94
|
+
// Suppress live events during delta replay to prevent out-of-order delivery
|
|
95
|
+
if (lastSeq > 0 && events.length > 0) {
|
|
96
|
+
markReplaying(ws, msg.sessionId);
|
|
97
|
+
sendEventBatches(ws, msg.sessionId, events, sendTo).then((lastSent) => {
|
|
98
|
+
clearReplaying(ws, msg.sessionId, lastSent);
|
|
99
|
+
replayPendingUiRequests(ws, msg.sessionId);
|
|
100
|
+
});
|
|
101
|
+
} else {
|
|
102
|
+
sendEventBatches(ws, msg.sessionId, events, sendTo).then(() => {
|
|
103
|
+
replayPendingUiRequests(ws, msg.sessionId);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} else if (directoryService) {
|
|
108
|
+
const session = sessionManager.get(msg.sessionId);
|
|
109
|
+
if (session?.sessionFile) {
|
|
110
|
+
sendTo(ws, {
|
|
111
|
+
type: "event_replay",
|
|
112
|
+
sessionId: msg.sessionId,
|
|
113
|
+
events: [],
|
|
114
|
+
isLast: false,
|
|
115
|
+
});
|
|
116
|
+
directoryService.loadSessionEvents(msg.sessionId, session.sessionFile).then(async (result) => {
|
|
117
|
+
if (result.success) {
|
|
118
|
+
for (const evt of result.events) {
|
|
119
|
+
eventStore.insertEvent(msg.sessionId, evt);
|
|
120
|
+
}
|
|
121
|
+
const statsUpdates = extractStatsFromEvents(result.events);
|
|
122
|
+
const metaUpdates: Record<string, unknown> = { dataUnavailable: false, ...statsUpdates };
|
|
123
|
+
sessionManager.update(msg.sessionId, metaUpdates);
|
|
124
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: metaUpdates });
|
|
125
|
+
let stored = eventStore.getEvents(msg.sessionId, 1);
|
|
126
|
+
if (MAX_REPLAY_EVENTS > 0 && stored.length > MAX_REPLAY_EVENTS) {
|
|
127
|
+
stored = stored.slice(stored.length - MAX_REPLAY_EVENTS);
|
|
128
|
+
}
|
|
129
|
+
const subscribers = getSubscribers(msg.sessionId);
|
|
130
|
+
for (const sub of subscribers) {
|
|
131
|
+
await sendEventBatches(sub, msg.sessionId, stored, sendTo);
|
|
132
|
+
replayPendingUiRequests(sub, msg.sessionId);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
sendTo(ws, { type: "event_replay", sessionId: msg.sessionId, events: [], isLast: true });
|
|
136
|
+
sessionManager.update(msg.sessionId, { dataUnavailable: true });
|
|
137
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { dataUnavailable: true } });
|
|
138
|
+
}
|
|
139
|
+
}).catch(() => {
|
|
140
|
+
sendTo(ws, { type: "event_replay", sessionId: msg.sessionId, events: [], isLast: true });
|
|
141
|
+
sessionManager.update(msg.sessionId, { dataUnavailable: true });
|
|
142
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { dataUnavailable: true } });
|
|
143
|
+
});
|
|
144
|
+
} else {
|
|
145
|
+
sendTo(ws, { type: "event_replay", sessionId: msg.sessionId, events: [], isLast: true });
|
|
146
|
+
if (session) {
|
|
147
|
+
sessionManager.update(msg.sessionId, { dataUnavailable: true });
|
|
148
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { dataUnavailable: true } });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
sendTo(ws, { type: "event_replay", sessionId: msg.sessionId, events: [], isLast: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal message handlers: create, kill, rename.
|
|
3
|
+
*/
|
|
4
|
+
import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
5
|
+
import type { BrowserHandlerContext } from "./handler-context.js";
|
|
6
|
+
|
|
7
|
+
export function handleCreateTerminal(
|
|
8
|
+
msg: Extract<BrowserToServerMessage, { type: "create_terminal" }>,
|
|
9
|
+
ctx: BrowserHandlerContext,
|
|
10
|
+
): void {
|
|
11
|
+
const { terminalManager, sessionOrderManager, broadcast } = ctx;
|
|
12
|
+
if (terminalManager && sessionOrderManager) {
|
|
13
|
+
const terminal = terminalManager.spawn(msg.cwd);
|
|
14
|
+
sessionOrderManager.insert(msg.cwd, terminal.id);
|
|
15
|
+
broadcast({ type: "terminal_added", terminal });
|
|
16
|
+
broadcast({ type: "sessions_reordered", cwd: msg.cwd, sessionIds: sessionOrderManager.getOrder(msg.cwd) });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function handleKillTerminal(
|
|
21
|
+
msg: Extract<BrowserToServerMessage, { type: "kill_terminal" }>,
|
|
22
|
+
ctx: BrowserHandlerContext,
|
|
23
|
+
): void {
|
|
24
|
+
if (ctx.terminalManager) {
|
|
25
|
+
try { ctx.terminalManager.kill(msg.terminalId); } catch { /* ignore */ }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function handleRenameTerminal(
|
|
30
|
+
msg: Extract<BrowserToServerMessage, { type: "rename_terminal" }>,
|
|
31
|
+
ctx: BrowserHandlerContext,
|
|
32
|
+
): void {
|
|
33
|
+
if (ctx.terminalManager) {
|
|
34
|
+
ctx.terminalManager.updateTitle(msg.terminalId, msg.title);
|
|
35
|
+
ctx.broadcast({ type: "terminal_updated", terminalId: msg.terminalId, updates: { title: msg.title } });
|
|
36
|
+
}
|
|
37
|
+
}
|