@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,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REST API wrappers for session control operations.
|
|
3
|
+
* These expose WebSocket-only operations as HTTP endpoints
|
|
4
|
+
* for use by skills, scripts, and external tooling.
|
|
5
|
+
*/
|
|
6
|
+
import type { FastifyInstance } from "fastify";
|
|
7
|
+
import type { SessionManager } from "./memory-session-manager.js";
|
|
8
|
+
import type { PiGateway } from "./pi-gateway.js";
|
|
9
|
+
import type { BrowserGateway } from "./browser-gateway.js";
|
|
10
|
+
import type { ApiResponse } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
11
|
+
import { spawnPiSession } from "./process-manager.js";
|
|
12
|
+
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
13
|
+
import type { PendingForkRegistry } from "./pending-fork-registry.js";
|
|
14
|
+
|
|
15
|
+
export interface SessionApiDeps {
|
|
16
|
+
sessionManager: SessionManager;
|
|
17
|
+
piGateway: PiGateway;
|
|
18
|
+
browserGateway: BrowserGateway;
|
|
19
|
+
pendingForkRegistry?: PendingForkRegistry;
|
|
20
|
+
pendingDashboardSpawns?: Map<string, number>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type IdParams = { Params: { id: string } };
|
|
24
|
+
|
|
25
|
+
/** Helper: validate session exists, return it or send error response */
|
|
26
|
+
function getSessionOrFail(sessionManager: SessionManager, id: string): { session: any } | { error: ApiResponse } {
|
|
27
|
+
const session = sessionManager.get(id);
|
|
28
|
+
if (!session) return { error: { success: false, error: "session not found" } };
|
|
29
|
+
return { session };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function registerSessionApi(fastify: FastifyInstance, deps: SessionApiDeps) {
|
|
33
|
+
const { sessionManager, piGateway, browserGateway, pendingForkRegistry, pendingDashboardSpawns } = deps;
|
|
34
|
+
|
|
35
|
+
// POST /api/session/:id/prompt
|
|
36
|
+
fastify.post<IdParams & { Body: { text?: string; images?: any[] } }>(
|
|
37
|
+
"/api/session/:id/prompt",
|
|
38
|
+
async (request, reply) => {
|
|
39
|
+
const { id } = request.params;
|
|
40
|
+
const { text, images } = request.body ?? {};
|
|
41
|
+
if (!text) {
|
|
42
|
+
reply.code(400);
|
|
43
|
+
return { success: false, error: "text is required" } satisfies ApiResponse;
|
|
44
|
+
}
|
|
45
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
46
|
+
if ("error" in result) {
|
|
47
|
+
reply.code(404);
|
|
48
|
+
return result.error;
|
|
49
|
+
}
|
|
50
|
+
const sent = piGateway.sendToSession(id, {
|
|
51
|
+
type: "send_prompt",
|
|
52
|
+
sessionId: id,
|
|
53
|
+
text,
|
|
54
|
+
images,
|
|
55
|
+
});
|
|
56
|
+
if (!sent) {
|
|
57
|
+
reply.code(502);
|
|
58
|
+
return { success: false, error: "no bridge connection for session" } satisfies ApiResponse;
|
|
59
|
+
}
|
|
60
|
+
return { success: true } satisfies ApiResponse;
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// POST /api/session/:id/abort
|
|
65
|
+
fastify.post<IdParams>(
|
|
66
|
+
"/api/session/:id/abort",
|
|
67
|
+
async (request, reply) => {
|
|
68
|
+
const { id } = request.params;
|
|
69
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
70
|
+
if ("error" in result) {
|
|
71
|
+
reply.code(404);
|
|
72
|
+
return result.error;
|
|
73
|
+
}
|
|
74
|
+
piGateway.sendToSession(id, { type: "abort", sessionId: id });
|
|
75
|
+
return { success: true } satisfies ApiResponse;
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// POST /api/session/:id/shutdown
|
|
80
|
+
fastify.post<IdParams>(
|
|
81
|
+
"/api/session/:id/shutdown",
|
|
82
|
+
async (request, reply) => {
|
|
83
|
+
const { id } = request.params;
|
|
84
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
85
|
+
if ("error" in result) {
|
|
86
|
+
reply.code(404);
|
|
87
|
+
return result.error;
|
|
88
|
+
}
|
|
89
|
+
piGateway.sendToSession(id, { type: "shutdown", sessionId: id });
|
|
90
|
+
browserGateway.headlessPidRegistry.killBySessionId(id);
|
|
91
|
+
sessionManager.unregister(id);
|
|
92
|
+
browserGateway.broadcastSessionRemoved(id);
|
|
93
|
+
return { success: true } satisfies ApiResponse;
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// POST /api/session/:id/rename
|
|
98
|
+
fastify.post<IdParams & { Body: { name?: string } }>(
|
|
99
|
+
"/api/session/:id/rename",
|
|
100
|
+
async (request, reply) => {
|
|
101
|
+
const { id } = request.params;
|
|
102
|
+
const { name } = request.body ?? {};
|
|
103
|
+
if (name === undefined) {
|
|
104
|
+
reply.code(400);
|
|
105
|
+
return { success: false, error: "name is required" } satisfies ApiResponse;
|
|
106
|
+
}
|
|
107
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
108
|
+
if ("error" in result) {
|
|
109
|
+
reply.code(404);
|
|
110
|
+
return result.error;
|
|
111
|
+
}
|
|
112
|
+
const updates = { name: name || undefined };
|
|
113
|
+
sessionManager.update(id, updates);
|
|
114
|
+
browserGateway.broadcastSessionUpdated(id, updates);
|
|
115
|
+
piGateway.sendToSession(id, { type: "rename_session", sessionId: id, name });
|
|
116
|
+
return { success: true } satisfies ApiResponse;
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// POST /api/session/:id/hide
|
|
121
|
+
fastify.post<IdParams>(
|
|
122
|
+
"/api/session/:id/hide",
|
|
123
|
+
async (request, reply) => {
|
|
124
|
+
const { id } = request.params;
|
|
125
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
126
|
+
if ("error" in result) {
|
|
127
|
+
reply.code(404);
|
|
128
|
+
return result.error;
|
|
129
|
+
}
|
|
130
|
+
const updates = { hidden: true };
|
|
131
|
+
sessionManager.update(id, updates);
|
|
132
|
+
browserGateway.broadcastSessionUpdated(id, updates);
|
|
133
|
+
return { success: true } satisfies ApiResponse;
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// POST /api/session/:id/unhide
|
|
138
|
+
fastify.post<IdParams>(
|
|
139
|
+
"/api/session/:id/unhide",
|
|
140
|
+
async (request, reply) => {
|
|
141
|
+
const { id } = request.params;
|
|
142
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
143
|
+
if ("error" in result) {
|
|
144
|
+
reply.code(404);
|
|
145
|
+
return result.error;
|
|
146
|
+
}
|
|
147
|
+
const updates = { hidden: false };
|
|
148
|
+
sessionManager.update(id, updates);
|
|
149
|
+
browserGateway.broadcastSessionUpdated(id, updates);
|
|
150
|
+
return { success: true } satisfies ApiResponse;
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// POST /api/session/spawn
|
|
155
|
+
fastify.post<{ Body: { cwd?: string } }>(
|
|
156
|
+
"/api/session/spawn",
|
|
157
|
+
async (request, reply) => {
|
|
158
|
+
const { cwd } = request.body ?? {};
|
|
159
|
+
if (!cwd) {
|
|
160
|
+
reply.code(400);
|
|
161
|
+
return { success: false, error: "cwd is required" } satisfies ApiResponse;
|
|
162
|
+
}
|
|
163
|
+
const config = loadConfig();
|
|
164
|
+
const spawnResult = await spawnPiSession(cwd, { strategy: config.spawnStrategy });
|
|
165
|
+
if (spawnResult.process && spawnResult.pid) {
|
|
166
|
+
browserGateway.headlessPidRegistry.register(spawnResult.pid, cwd, spawnResult.process);
|
|
167
|
+
}
|
|
168
|
+
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
169
|
+
pendingDashboardSpawns?.set(cwd, (pendingDashboardSpawns?.get(cwd) ?? 0) + 1);
|
|
170
|
+
}
|
|
171
|
+
if (!spawnResult.success) {
|
|
172
|
+
reply.code(500);
|
|
173
|
+
return { success: false, error: spawnResult.message } satisfies ApiResponse;
|
|
174
|
+
}
|
|
175
|
+
return { success: true, data: { message: spawnResult.message } } satisfies ApiResponse;
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// POST /api/session/:id/resume
|
|
180
|
+
fastify.post<IdParams & { Body: { mode?: string } }>(
|
|
181
|
+
"/api/session/:id/resume",
|
|
182
|
+
async (request, reply) => {
|
|
183
|
+
const { id } = request.params;
|
|
184
|
+
const { mode } = request.body ?? {};
|
|
185
|
+
if (mode !== "continue" && mode !== "fork") {
|
|
186
|
+
reply.code(400);
|
|
187
|
+
return { success: false, error: "mode must be 'continue' or 'fork'" } satisfies ApiResponse;
|
|
188
|
+
}
|
|
189
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
190
|
+
if ("error" in result) {
|
|
191
|
+
reply.code(404);
|
|
192
|
+
return result.error;
|
|
193
|
+
}
|
|
194
|
+
const session = result.session;
|
|
195
|
+
if (!session.sessionFile) {
|
|
196
|
+
reply.code(400);
|
|
197
|
+
return { success: false, error: "session file is unknown" } satisfies ApiResponse;
|
|
198
|
+
}
|
|
199
|
+
if (mode === "continue" && session.status !== "ended") {
|
|
200
|
+
reply.code(409);
|
|
201
|
+
return { success: false, error: "session is already active" } satisfies ApiResponse;
|
|
202
|
+
}
|
|
203
|
+
if (session.resuming) {
|
|
204
|
+
reply.code(409);
|
|
205
|
+
return { success: false, error: "session is already being resumed" } satisfies ApiResponse;
|
|
206
|
+
}
|
|
207
|
+
if (mode === "fork" && pendingForkRegistry) {
|
|
208
|
+
pendingForkRegistry.recordFork(session.cwd, id);
|
|
209
|
+
}
|
|
210
|
+
const config = loadConfig();
|
|
211
|
+
const spawnResult = await spawnPiSession(session.cwd, {
|
|
212
|
+
sessionFile: session.sessionFile,
|
|
213
|
+
mode,
|
|
214
|
+
strategy: config.spawnStrategy,
|
|
215
|
+
});
|
|
216
|
+
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
217
|
+
pendingDashboardSpawns?.set(session.cwd, (pendingDashboardSpawns?.get(session.cwd) ?? 0) + 1);
|
|
218
|
+
}
|
|
219
|
+
if (!spawnResult.success) {
|
|
220
|
+
reply.code(500);
|
|
221
|
+
return { success: false, error: spawnResult.message } satisfies ApiResponse;
|
|
222
|
+
}
|
|
223
|
+
return { success: true, data: { message: spawnResult.message } } satisfies ApiResponse;
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// POST /api/session/:id/flow-control
|
|
228
|
+
fastify.post<IdParams & { Body: { action?: string } }>(
|
|
229
|
+
"/api/session/:id/flow-control",
|
|
230
|
+
async (request, reply) => {
|
|
231
|
+
const { id } = request.params;
|
|
232
|
+
const { action } = request.body ?? {};
|
|
233
|
+
if (action !== "abort" && action !== "toggle_autonomous") {
|
|
234
|
+
reply.code(400);
|
|
235
|
+
return { success: false, error: "action must be 'abort' or 'toggle_autonomous'" } satisfies ApiResponse;
|
|
236
|
+
}
|
|
237
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
238
|
+
if ("error" in result) {
|
|
239
|
+
reply.code(404);
|
|
240
|
+
return result.error;
|
|
241
|
+
}
|
|
242
|
+
piGateway.sendToSession(id, { type: "flow_control", sessionId: id, action });
|
|
243
|
+
return { success: true } satisfies ApiResponse;
|
|
244
|
+
},
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// POST /api/session/:id/model
|
|
248
|
+
fastify.post<IdParams & { Body: { provider?: string; modelId?: string } }>(
|
|
249
|
+
"/api/session/:id/model",
|
|
250
|
+
async (request, reply) => {
|
|
251
|
+
const { id } = request.params;
|
|
252
|
+
const { provider, modelId } = request.body ?? {};
|
|
253
|
+
if (!provider || !modelId) {
|
|
254
|
+
reply.code(400);
|
|
255
|
+
return { success: false, error: "provider and modelId are required" } satisfies ApiResponse;
|
|
256
|
+
}
|
|
257
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
258
|
+
if ("error" in result) {
|
|
259
|
+
reply.code(404);
|
|
260
|
+
return result.error;
|
|
261
|
+
}
|
|
262
|
+
piGateway.sendToSession(id, { type: "set_model", sessionId: id, provider, modelId });
|
|
263
|
+
return { success: true } satisfies ApiResponse;
|
|
264
|
+
},
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// POST /api/session/:id/thinking-level
|
|
268
|
+
fastify.post<IdParams & { Body: { level?: string } }>(
|
|
269
|
+
"/api/session/:id/thinking-level",
|
|
270
|
+
async (request, reply) => {
|
|
271
|
+
const { id } = request.params;
|
|
272
|
+
const { level } = request.body ?? {};
|
|
273
|
+
if (!level) {
|
|
274
|
+
reply.code(400);
|
|
275
|
+
return { success: false, error: "level is required" } satisfies ApiResponse;
|
|
276
|
+
}
|
|
277
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
278
|
+
if ("error" in result) {
|
|
279
|
+
reply.code(404);
|
|
280
|
+
return result.error;
|
|
281
|
+
}
|
|
282
|
+
piGateway.sendToSession(id, { type: "set_thinking_level", sessionId: id, level });
|
|
283
|
+
return { success: true } satisfies ApiResponse;
|
|
284
|
+
},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// POST /api/session/:id/attach-proposal
|
|
288
|
+
fastify.post<IdParams & { Body: { changeName?: string } }>(
|
|
289
|
+
"/api/session/:id/attach-proposal",
|
|
290
|
+
async (request, reply) => {
|
|
291
|
+
const { id } = request.params;
|
|
292
|
+
const { changeName } = request.body ?? {};
|
|
293
|
+
if (!changeName) {
|
|
294
|
+
reply.code(400);
|
|
295
|
+
return { success: false, error: "changeName is required" } satisfies ApiResponse;
|
|
296
|
+
}
|
|
297
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
298
|
+
if ("error" in result) {
|
|
299
|
+
reply.code(404);
|
|
300
|
+
return result.error;
|
|
301
|
+
}
|
|
302
|
+
const updates: Record<string, unknown> = { attachedProposal: changeName };
|
|
303
|
+
const session = result.session;
|
|
304
|
+
if (!session.name?.trim()) {
|
|
305
|
+
updates.name = changeName;
|
|
306
|
+
piGateway.sendToSession(id, { type: "rename_session", sessionId: id, name: changeName });
|
|
307
|
+
}
|
|
308
|
+
sessionManager.update(id, updates);
|
|
309
|
+
browserGateway.broadcastSessionUpdated(id, updates);
|
|
310
|
+
return { success: true } satisfies ApiResponse;
|
|
311
|
+
},
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// POST /api/session/:id/detach-proposal
|
|
315
|
+
fastify.post<IdParams>(
|
|
316
|
+
"/api/session/:id/detach-proposal",
|
|
317
|
+
async (request, reply) => {
|
|
318
|
+
const { id } = request.params;
|
|
319
|
+
const result = getSessionOrFail(sessionManager, id);
|
|
320
|
+
if ("error" in result) {
|
|
321
|
+
reply.code(404);
|
|
322
|
+
return result.error;
|
|
323
|
+
}
|
|
324
|
+
const updates = { attachedProposal: null, openspecPhase: null, openspecChange: null };
|
|
325
|
+
sessionManager.update(id, updates);
|
|
326
|
+
browserGateway.broadcastSessionUpdated(id, updates);
|
|
327
|
+
return { success: true } satisfies ApiResponse;
|
|
328
|
+
},
|
|
329
|
+
);
|
|
330
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session bootstrap: discovers sessions from known directories and starts OpenSpec polling.
|
|
3
|
+
* Called during server startup (async, non-blocking).
|
|
4
|
+
*/
|
|
5
|
+
import type { SessionManager } from "./memory-session-manager.js";
|
|
6
|
+
import type { BrowserGateway } from "./browser-gateway.js";
|
|
7
|
+
import type { DirectoryService } from "./directory-service.js";
|
|
8
|
+
import { extractSessionStats } from "./session-stats-reader.js";
|
|
9
|
+
|
|
10
|
+
export interface SessionBootstrapDeps {
|
|
11
|
+
sessionManager: SessionManager;
|
|
12
|
+
browserGateway: BrowserGateway;
|
|
13
|
+
directoryService: DirectoryService;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Discover sessions from all known directories and broadcast them.
|
|
18
|
+
* Runs async and does not block server startup.
|
|
19
|
+
*/
|
|
20
|
+
export async function discoverAndBroadcastSessions(deps: SessionBootstrapDeps): Promise<void> {
|
|
21
|
+
const { sessionManager, browserGateway, directoryService } = deps;
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const dirs = directoryService.knownDirectories();
|
|
25
|
+
for (const cwd of dirs) {
|
|
26
|
+
const discovered = directoryService.discoverSessions(cwd);
|
|
27
|
+
for (const hist of discovered) {
|
|
28
|
+
if (!sessionManager.get(hist.id)) {
|
|
29
|
+
let contextTokens: number | undefined;
|
|
30
|
+
let contextWindow: number | undefined;
|
|
31
|
+
let model: string | undefined;
|
|
32
|
+
if (hist.sessionFile) {
|
|
33
|
+
try {
|
|
34
|
+
const stats = extractSessionStats(hist.sessionFile);
|
|
35
|
+
if (stats) {
|
|
36
|
+
contextTokens = stats.lastTotalTokens;
|
|
37
|
+
contextWindow = stats.contextWindow;
|
|
38
|
+
model = stats.model;
|
|
39
|
+
}
|
|
40
|
+
} catch { /* ignore */ }
|
|
41
|
+
}
|
|
42
|
+
sessionManager.restore({
|
|
43
|
+
id: hist.id,
|
|
44
|
+
cwd: hist.cwd,
|
|
45
|
+
name: hist.name,
|
|
46
|
+
source: "tui",
|
|
47
|
+
status: "ended",
|
|
48
|
+
startedAt: hist.startedAt,
|
|
49
|
+
sessionFile: hist.sessionFile,
|
|
50
|
+
sessionDir: hist.sessionDir,
|
|
51
|
+
firstMessage: hist.firstMessage,
|
|
52
|
+
hidden: true,
|
|
53
|
+
dataUnavailable: true,
|
|
54
|
+
model,
|
|
55
|
+
contextTokens,
|
|
56
|
+
contextWindow,
|
|
57
|
+
});
|
|
58
|
+
const session = sessionManager.get(hist.id);
|
|
59
|
+
if (session) browserGateway.broadcastSessionAdded(session);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error("[dashboard] Session discovery failed:", err);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Start OpenSpec polling, broadcast changes to browsers
|
|
68
|
+
directoryService.startPolling((cwd, data) => {
|
|
69
|
+
browserGateway.broadcastToAll({
|
|
70
|
+
type: "openspec_update",
|
|
71
|
+
cwd,
|
|
72
|
+
data,
|
|
73
|
+
} as any);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Initial OpenSpec poll for all known directories
|
|
77
|
+
await Promise.all(
|
|
78
|
+
directoryService.knownDirectories().map((cwd) => directoryService.refreshOpenSpec(cwd)),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session diff extraction — scans session events for file changes
|
|
3
|
+
* and optionally enriches with git diffs.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { resolve, relative, isAbsolute } from "node:path";
|
|
7
|
+
import type { DashboardEvent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
8
|
+
import type { FileChangeEvent, FileDiffEntry, EditOperation } from "@blackbelt-technology/pi-dashboard-shared/diff-types.js";
|
|
9
|
+
import { isGitRepo } from "./git-operations.js";
|
|
10
|
+
|
|
11
|
+
const GIT_TIMEOUT = 15_000;
|
|
12
|
+
const MAX_MESSAGE_LENGTH = 120;
|
|
13
|
+
|
|
14
|
+
const WRITE_EDIT_TOOLS = new Set(["write", "edit"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract file change events from session events.
|
|
18
|
+
* Scans tool_execution_start events for Write/Edit tools,
|
|
19
|
+
* groups by file path, and includes preceding assistant message as context.
|
|
20
|
+
*/
|
|
21
|
+
export function extractFileChanges(events: DashboardEvent[], cwd: string): FileDiffEntry[] {
|
|
22
|
+
const fileMap = new Map<string, FileChangeEvent[]>();
|
|
23
|
+
let lastAssistantMessage: string | undefined;
|
|
24
|
+
|
|
25
|
+
for (const event of events) {
|
|
26
|
+
// Track most recent assistant message for context
|
|
27
|
+
if (event.eventType === "message_end") {
|
|
28
|
+
const msg = event.data.message as any;
|
|
29
|
+
if (msg?.role === "assistant") {
|
|
30
|
+
const content = Array.isArray(msg.content)
|
|
31
|
+
? msg.content
|
|
32
|
+
.filter((c: any) => c?.type === "text")
|
|
33
|
+
.map((c: any) => c.text)
|
|
34
|
+
.join("")
|
|
35
|
+
: typeof msg.content === "string" ? msg.content : "";
|
|
36
|
+
if (content) {
|
|
37
|
+
lastAssistantMessage = content.length > MAX_MESSAGE_LENGTH
|
|
38
|
+
? content.slice(0, MAX_MESSAGE_LENGTH) + "..."
|
|
39
|
+
: content;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (event.eventType !== "tool_execution_start") continue;
|
|
45
|
+
|
|
46
|
+
const toolName = (event.data.toolName as string || "").toLowerCase();
|
|
47
|
+
if (!WRITE_EDIT_TOOLS.has(toolName)) continue;
|
|
48
|
+
|
|
49
|
+
const args = event.data.args as Record<string, unknown> | undefined;
|
|
50
|
+
if (!args) continue;
|
|
51
|
+
|
|
52
|
+
const rawPath = (args.path || args.file_path) as string | undefined;
|
|
53
|
+
if (!rawPath) continue;
|
|
54
|
+
|
|
55
|
+
// Resolve and filter paths outside cwd
|
|
56
|
+
const filePath = normalizePath(rawPath, cwd);
|
|
57
|
+
if (!filePath) continue;
|
|
58
|
+
|
|
59
|
+
const changeEvent: FileChangeEvent = {
|
|
60
|
+
type: toolName === "write" ? "write" : "edit",
|
|
61
|
+
timestamp: event.timestamp,
|
|
62
|
+
message: lastAssistantMessage,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (toolName === "write") {
|
|
66
|
+
changeEvent.content = args.content as string | undefined;
|
|
67
|
+
} else {
|
|
68
|
+
changeEvent.edits = args.edits as EditOperation[] | undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const existing = fileMap.get(filePath);
|
|
72
|
+
if (existing) {
|
|
73
|
+
existing.push(changeEvent);
|
|
74
|
+
} else {
|
|
75
|
+
fileMap.set(filePath, [changeEvent]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Build result, sorted by path, changes sorted by timestamp
|
|
80
|
+
const result: FileDiffEntry[] = [];
|
|
81
|
+
for (const [path, changes] of fileMap) {
|
|
82
|
+
changes.sort((a, b) => a.timestamp - b.timestamp);
|
|
83
|
+
result.push({ path, changes });
|
|
84
|
+
}
|
|
85
|
+
result.sort((a, b) => a.path.localeCompare(b.path));
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Normalize a file path relative to cwd.
|
|
92
|
+
* Returns null if the path is outside cwd.
|
|
93
|
+
*/
|
|
94
|
+
function normalizePath(rawPath: string, cwd: string): string | null {
|
|
95
|
+
let absPath: string;
|
|
96
|
+
if (isAbsolute(rawPath)) {
|
|
97
|
+
absPath = rawPath;
|
|
98
|
+
} else {
|
|
99
|
+
absPath = resolve(cwd, rawPath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if the resolved path is inside cwd
|
|
103
|
+
const rel = relative(cwd, absPath);
|
|
104
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return rel;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Enrich file entries with git diff output.
|
|
113
|
+
* Runs `git diff HEAD -- <path>` for each file when in a git repo.
|
|
114
|
+
* Returns gracefully on any git errors.
|
|
115
|
+
*/
|
|
116
|
+
export function enrichWithGitDiff(
|
|
117
|
+
cwd: string,
|
|
118
|
+
files: FileDiffEntry[],
|
|
119
|
+
): { enrichedFiles: FileDiffEntry[]; isGitRepo: boolean } {
|
|
120
|
+
let gitAvailable = false;
|
|
121
|
+
try {
|
|
122
|
+
gitAvailable = isGitRepo(cwd);
|
|
123
|
+
} catch {
|
|
124
|
+
return { enrichedFiles: files, isGitRepo: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!gitAvailable) {
|
|
128
|
+
return { enrichedFiles: files, isGitRepo: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const enriched = files.map((file) => {
|
|
132
|
+
try {
|
|
133
|
+
const diff = execSync(`git diff HEAD -- ${JSON.stringify(file.path)}`, {
|
|
134
|
+
cwd,
|
|
135
|
+
encoding: "utf-8",
|
|
136
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
137
|
+
timeout: GIT_TIMEOUT,
|
|
138
|
+
}).trim();
|
|
139
|
+
|
|
140
|
+
if (diff) {
|
|
141
|
+
return { ...file, gitDiff: diff };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// No diff from HEAD — try untracked (new file)
|
|
145
|
+
const status = execSync(`git status --porcelain -- ${JSON.stringify(file.path)}`, {
|
|
146
|
+
cwd,
|
|
147
|
+
encoding: "utf-8",
|
|
148
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
149
|
+
timeout: GIT_TIMEOUT,
|
|
150
|
+
}).trim();
|
|
151
|
+
|
|
152
|
+
if (status.startsWith("??") || status.startsWith("A")) {
|
|
153
|
+
// Untracked or newly added — generate synthetic diff
|
|
154
|
+
const content = execSync(`cat ${JSON.stringify(resolve(cwd, file.path))}`, {
|
|
155
|
+
encoding: "utf-8",
|
|
156
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
157
|
+
timeout: GIT_TIMEOUT,
|
|
158
|
+
});
|
|
159
|
+
const lines = content.split("\n");
|
|
160
|
+
const diffLines = [
|
|
161
|
+
`diff --git a/${file.path} b/${file.path}`,
|
|
162
|
+
"new file mode 100644",
|
|
163
|
+
`--- /dev/null`,
|
|
164
|
+
`+++ b/${file.path}`,
|
|
165
|
+
`@@ -0,0 +1,${lines.length} @@`,
|
|
166
|
+
...lines.map((l) => `+${l}`),
|
|
167
|
+
];
|
|
168
|
+
return { ...file, gitDiff: diffLines.join("\n") };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return file;
|
|
172
|
+
} catch {
|
|
173
|
+
return file;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return { enrichedFiles: enriched, isGitRepo: true };
|
|
178
|
+
}
|