@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.
Files changed (212) hide show
  1. package/AGENTS.md +342 -0
  2. package/README.md +619 -0
  3. package/docs/architecture.md +646 -0
  4. package/package.json +92 -0
  5. package/packages/extension/package.json +33 -0
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  8. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  9. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  10. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  11. package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
  12. package/packages/extension/src/__tests__/git-info.test.ts +112 -0
  13. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  14. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
  15. package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
  16. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  17. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  18. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  19. package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
  20. package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
  21. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  22. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  23. package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
  24. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  25. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  26. package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
  27. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  28. package/packages/extension/src/ask-user-tool.ts +63 -0
  29. package/packages/extension/src/bridge-context.ts +64 -0
  30. package/packages/extension/src/bridge.ts +926 -0
  31. package/packages/extension/src/command-handler.ts +538 -0
  32. package/packages/extension/src/connection.ts +204 -0
  33. package/packages/extension/src/dev-build.ts +39 -0
  34. package/packages/extension/src/event-forwarder.ts +40 -0
  35. package/packages/extension/src/flow-event-wiring.ts +102 -0
  36. package/packages/extension/src/git-info.ts +65 -0
  37. package/packages/extension/src/git-link-builder.ts +112 -0
  38. package/packages/extension/src/model-tracker.ts +56 -0
  39. package/packages/extension/src/pi-env.d.ts +23 -0
  40. package/packages/extension/src/process-metrics.ts +70 -0
  41. package/packages/extension/src/process-scanner.ts +396 -0
  42. package/packages/extension/src/prompt-expander.ts +87 -0
  43. package/packages/extension/src/provider-register.ts +276 -0
  44. package/packages/extension/src/server-auto-start.ts +87 -0
  45. package/packages/extension/src/server-launcher.ts +82 -0
  46. package/packages/extension/src/server-probe.ts +33 -0
  47. package/packages/extension/src/session-sync.ts +154 -0
  48. package/packages/extension/src/source-detector.ts +26 -0
  49. package/packages/extension/src/ui-proxy.ts +269 -0
  50. package/packages/extension/tsconfig.json +11 -0
  51. package/packages/server/package.json +37 -0
  52. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  53. package/packages/server/src/__tests__/auth.test.ts +224 -0
  54. package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
  55. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  56. package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
  57. package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
  58. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  59. package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
  60. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  61. package/packages/server/src/__tests__/config-api.test.ts +104 -0
  62. package/packages/server/src/__tests__/cors.test.ts +48 -0
  63. package/packages/server/src/__tests__/directory-service.test.ts +240 -0
  64. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  65. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  66. package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
  67. package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
  68. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  69. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  70. package/packages/server/src/__tests__/extension-register.test.ts +61 -0
  71. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  72. package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
  73. package/packages/server/src/__tests__/git-operations.test.ts +251 -0
  74. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  75. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  76. package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
  77. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  78. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  79. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  80. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  81. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  82. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  83. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  84. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  85. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  86. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  87. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
  88. package/packages/server/src/__tests__/package-routes.test.ts +172 -0
  89. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  90. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  91. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  92. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  93. package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
  94. package/packages/server/src/__tests__/process-manager.test.ts +184 -0
  95. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  96. package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
  97. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  98. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  99. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  100. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  101. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  102. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  103. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  104. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  105. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
  106. package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
  107. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  108. package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
  109. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  110. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  111. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
  112. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  113. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  114. package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
  115. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  116. package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
  117. package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
  118. package/packages/server/src/__tests__/tunnel.test.ts +206 -0
  119. package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
  120. package/packages/server/src/auth-plugin.ts +302 -0
  121. package/packages/server/src/auth.ts +323 -0
  122. package/packages/server/src/browse.ts +55 -0
  123. package/packages/server/src/browser-gateway.ts +495 -0
  124. package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
  125. package/packages/server/src/browser-handlers/handler-context.ts +45 -0
  126. package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
  127. package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
  128. package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
  129. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  130. package/packages/server/src/cli.ts +347 -0
  131. package/packages/server/src/config-api.ts +130 -0
  132. package/packages/server/src/directory-service.ts +162 -0
  133. package/packages/server/src/editor-detection.ts +60 -0
  134. package/packages/server/src/editor-manager.ts +352 -0
  135. package/packages/server/src/editor-proxy.ts +134 -0
  136. package/packages/server/src/editor-registry.ts +108 -0
  137. package/packages/server/src/event-status-extraction.ts +131 -0
  138. package/packages/server/src/event-wiring.ts +589 -0
  139. package/packages/server/src/extension-register.ts +92 -0
  140. package/packages/server/src/git-operations.ts +200 -0
  141. package/packages/server/src/headless-pid-registry.ts +207 -0
  142. package/packages/server/src/idle-timer.ts +61 -0
  143. package/packages/server/src/json-store.ts +32 -0
  144. package/packages/server/src/localhost-guard.ts +117 -0
  145. package/packages/server/src/memory-event-store.ts +193 -0
  146. package/packages/server/src/memory-session-manager.ts +123 -0
  147. package/packages/server/src/meta-persistence.ts +64 -0
  148. package/packages/server/src/migrate-persistence.ts +195 -0
  149. package/packages/server/src/npm-search-proxy.ts +143 -0
  150. package/packages/server/src/oauth-callback-server.ts +177 -0
  151. package/packages/server/src/openspec-archive.ts +60 -0
  152. package/packages/server/src/package-manager-wrapper.ts +200 -0
  153. package/packages/server/src/pending-fork-registry.ts +53 -0
  154. package/packages/server/src/pending-load-manager.ts +110 -0
  155. package/packages/server/src/pending-resume-registry.ts +69 -0
  156. package/packages/server/src/pi-gateway.ts +419 -0
  157. package/packages/server/src/pi-resource-scanner.ts +369 -0
  158. package/packages/server/src/preferences-store.ts +116 -0
  159. package/packages/server/src/process-manager.ts +311 -0
  160. package/packages/server/src/provider-auth-handlers.ts +438 -0
  161. package/packages/server/src/provider-auth-storage.ts +200 -0
  162. package/packages/server/src/resolve-path.ts +12 -0
  163. package/packages/server/src/routes/editor-routes.ts +86 -0
  164. package/packages/server/src/routes/file-routes.ts +116 -0
  165. package/packages/server/src/routes/git-routes.ts +89 -0
  166. package/packages/server/src/routes/openspec-routes.ts +99 -0
  167. package/packages/server/src/routes/package-routes.ts +172 -0
  168. package/packages/server/src/routes/provider-auth-routes.ts +244 -0
  169. package/packages/server/src/routes/provider-routes.ts +101 -0
  170. package/packages/server/src/routes/route-deps.ts +23 -0
  171. package/packages/server/src/routes/session-routes.ts +91 -0
  172. package/packages/server/src/routes/system-routes.ts +271 -0
  173. package/packages/server/src/server-pid.ts +84 -0
  174. package/packages/server/src/server.ts +554 -0
  175. package/packages/server/src/session-api.ts +330 -0
  176. package/packages/server/src/session-bootstrap.ts +80 -0
  177. package/packages/server/src/session-diff.ts +178 -0
  178. package/packages/server/src/session-discovery.ts +134 -0
  179. package/packages/server/src/session-file-reader.ts +135 -0
  180. package/packages/server/src/session-order-manager.ts +73 -0
  181. package/packages/server/src/session-scanner.ts +233 -0
  182. package/packages/server/src/session-stats-reader.ts +99 -0
  183. package/packages/server/src/terminal-gateway.ts +51 -0
  184. package/packages/server/src/terminal-manager.ts +241 -0
  185. package/packages/server/src/tunnel.ts +329 -0
  186. package/packages/server/tsconfig.json +11 -0
  187. package/packages/shared/package.json +15 -0
  188. package/packages/shared/src/__tests__/config.test.ts +358 -0
  189. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  190. package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
  191. package/packages/shared/src/__tests__/protocol.test.ts +243 -0
  192. package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
  193. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  194. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  195. package/packages/shared/src/archive-types.ts +11 -0
  196. package/packages/shared/src/browser-protocol.ts +534 -0
  197. package/packages/shared/src/config.ts +245 -0
  198. package/packages/shared/src/diff-types.ts +41 -0
  199. package/packages/shared/src/editor-types.ts +18 -0
  200. package/packages/shared/src/mdns-discovery.ts +248 -0
  201. package/packages/shared/src/openspec-activity-detector.ts +109 -0
  202. package/packages/shared/src/openspec-poller.ts +96 -0
  203. package/packages/shared/src/protocol.ts +369 -0
  204. package/packages/shared/src/resolve-jiti.ts +43 -0
  205. package/packages/shared/src/rest-api.ts +255 -0
  206. package/packages/shared/src/server-identity.ts +51 -0
  207. package/packages/shared/src/session-meta.ts +86 -0
  208. package/packages/shared/src/state-replay.ts +174 -0
  209. package/packages/shared/src/stats-extractor.ts +54 -0
  210. package/packages/shared/src/terminal-types.ts +18 -0
  211. package/packages/shared/src/types.ts +351 -0
  212. package/packages/shared/tsconfig.json +8 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * In-memory event store with LRU eviction.
3
+ * Replaces SQLite-backed event-store.ts.
4
+ */
5
+ import type { DashboardEvent } from "@blackbelt-technology/pi-dashboard-shared/types.js";
6
+
7
+ export interface StoredEvent {
8
+ seq: number;
9
+ event: DashboardEvent;
10
+ }
11
+
12
+ export interface EventStore {
13
+ /** Insert an event, returns assigned sequence number */
14
+ insertEvent(sessionId: string, event: DashboardEvent): number;
15
+ /** Get events for a session starting from minSeq (inclusive) */
16
+ getEvents(sessionId: string, minSeq: number): StoredEvent[];
17
+ /** Get a single event by sessionId and seq */
18
+ getEvent(sessionId: string, seq: number): DashboardEvent | undefined;
19
+ /** Delete all events for a specific session */
20
+ deleteEventsForSession(sessionId: string): number;
21
+ /** Check if session has events in memory */
22
+ hasEvents(sessionId: string): boolean;
23
+ /** Return the highest seq for a session, or 0 if no events */
24
+ getMaxSeq(sessionId: string): number;
25
+ /** Number of cached sessions */
26
+ sessionCount(): number;
27
+ }
28
+
29
+ interface SessionBuffer {
30
+ events: StoredEvent[];
31
+ nextSeq: number;
32
+ lastAccess: number;
33
+ }
34
+
35
+ export const DEFAULT_MAX_CACHED_SESSIONS = 100;
36
+ export const DEFAULT_MAX_EVENTS_PER_SESSION = 5000;
37
+
38
+ /** Default max size for any string field within event data */
39
+ const DEFAULT_MAX_STRING_SIZE = 4_000;
40
+ /** Max total serialized size for an individual event's data */
41
+ const MAX_EVENT_DATA_SIZE = 20_000;
42
+
43
+ /**
44
+ * Recursively truncate large string fields in an object.
45
+ * Returns a new object if any truncation occurred, otherwise the original.
46
+ */
47
+ function truncateStrings(obj: unknown, maxSize: number, depth = 0): unknown {
48
+ if (depth > 4) return obj;
49
+ if (typeof obj === "string") {
50
+ return obj.length > maxSize ? obj.slice(0, maxSize) + "\n…[truncated]" : obj;
51
+ }
52
+ if (Array.isArray(obj)) {
53
+ // Skip large arrays (e.g., edits arrays)
54
+ if (obj.length > 20) return "[array truncated]";
55
+ let changed = false;
56
+ const result = obj.map((item) => {
57
+ const t = truncateStrings(item, maxSize, depth + 1);
58
+ if (t !== item) changed = true;
59
+ return t;
60
+ });
61
+ return changed ? result : obj;
62
+ }
63
+ if (obj && typeof obj === "object") {
64
+ let changed = false;
65
+ const result: Record<string, unknown> = {};
66
+ for (const [key, val] of Object.entries(obj)) {
67
+ // Preserve base64 image data — skip truncation when sibling mimeType exists
68
+ if (key === "data" && typeof val === "string" && "mimeType" in obj) {
69
+ result[key] = val;
70
+ continue;
71
+ }
72
+ // Skip 'thinking' blocks entirely — large and not shown in chat
73
+ if (key === "thinking" && typeof val === "string" && val.length > maxSize) {
74
+ result[key] = (val as string).slice(0, 500) + "\n…[truncated]";
75
+ changed = true;
76
+ continue;
77
+ }
78
+ const t = truncateStrings(val, maxSize, depth + 1);
79
+ if (t !== val) changed = true;
80
+ result[key] = t;
81
+ }
82
+ return changed ? result : obj;
83
+ }
84
+ return obj;
85
+ }
86
+
87
+ /**
88
+ * Truncate large event data to bound memory usage per event.
89
+ */
90
+ function createTruncator(maxStringSize: number) {
91
+ if (maxStringSize <= 0) return (event: DashboardEvent) => event; // disabled
92
+ return (event: DashboardEvent): DashboardEvent => {
93
+ const data = event.data;
94
+ if (!data || typeof data !== "object") return event;
95
+ const truncated = truncateStrings(data, maxStringSize) as Record<string, unknown>;
96
+ return truncated !== data ? { ...event, data: truncated } : event;
97
+ };
98
+ }
99
+
100
+ export function createMemoryEventStore(
101
+ isSessionPinned: (sessionId: string) => boolean,
102
+ maxCachedSessions: number = DEFAULT_MAX_CACHED_SESSIONS,
103
+ maxEventsPerSession: number = DEFAULT_MAX_EVENTS_PER_SESSION,
104
+ maxStringFieldSize: number = DEFAULT_MAX_STRING_SIZE,
105
+ ): EventStore {
106
+ const truncateEventData = createTruncator(maxStringFieldSize);
107
+ const buffers = new Map<string, SessionBuffer>();
108
+
109
+ function getOrCreate(sessionId: string): SessionBuffer {
110
+ let buf = buffers.get(sessionId);
111
+ if (!buf) {
112
+ buf = { events: [], nextSeq: 1, lastAccess: Date.now() };
113
+ buffers.set(sessionId, buf);
114
+ }
115
+ buf.lastAccess = Date.now();
116
+ return buf;
117
+ }
118
+
119
+ function evictIfNeeded(): void {
120
+ if (buffers.size <= maxCachedSessions) return;
121
+
122
+ // Collect evictable sessions sorted by lastAccess ascending
123
+ const evictable: Array<[string, number]> = [];
124
+ for (const [id, buf] of buffers) {
125
+ if (!isSessionPinned(id)) {
126
+ evictable.push([id, buf.lastAccess]);
127
+ }
128
+ }
129
+ evictable.sort((a, b) => a[1] - b[1]);
130
+
131
+ // Evict until we're at or below the limit
132
+ let toEvict = buffers.size - maxCachedSessions;
133
+ for (const [id] of evictable) {
134
+ if (toEvict <= 0) break;
135
+ buffers.delete(id);
136
+ toEvict--;
137
+ }
138
+ }
139
+
140
+ return {
141
+ insertEvent(sessionId: string, event: DashboardEvent): number {
142
+ const buf = getOrCreate(sessionId);
143
+ const seq = buf.nextSeq++;
144
+ buf.events.push({ seq, event: truncateEventData(event) });
145
+ // Trim oldest events when over the per-session limit (0 = unlimited)
146
+ if (maxEventsPerSession > 0 && buf.events.length > maxEventsPerSession) {
147
+ const excess = buf.events.length - maxEventsPerSession;
148
+ buf.events.splice(0, excess);
149
+ }
150
+ evictIfNeeded();
151
+ return seq;
152
+ },
153
+
154
+ getEvents(sessionId: string, minSeq: number): StoredEvent[] {
155
+ const buf = buffers.get(sessionId);
156
+ if (!buf) return [];
157
+ buf.lastAccess = Date.now();
158
+ const effectiveMin = minSeq > 0 ? minSeq : 1;
159
+ return buf.events.filter((e) => e.seq >= effectiveMin);
160
+ },
161
+
162
+ getEvent(sessionId: string, seq: number): DashboardEvent | undefined {
163
+ const buf = buffers.get(sessionId);
164
+ if (!buf) return undefined;
165
+ buf.lastAccess = Date.now();
166
+ const entry = buf.events.find((e) => e.seq === seq);
167
+ return entry?.event;
168
+ },
169
+
170
+ deleteEventsForSession(sessionId: string): number {
171
+ const buf = buffers.get(sessionId);
172
+ if (!buf) return 0;
173
+ const count = buf.events.length;
174
+ buffers.delete(sessionId);
175
+ return count;
176
+ },
177
+
178
+ hasEvents(sessionId: string): boolean {
179
+ const buf = buffers.get(sessionId);
180
+ return buf !== undefined && buf.events.length > 0;
181
+ },
182
+
183
+ getMaxSeq(sessionId: string): number {
184
+ const buf = buffers.get(sessionId);
185
+ if (!buf || buf.events.length === 0) return 0;
186
+ return buf.events[buf.events.length - 1].seq;
187
+ },
188
+
189
+ sessionCount(): number {
190
+ return buffers.size;
191
+ },
192
+ };
193
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Pure in-memory session registry.
3
+ * Replaces SQLite-backed session-manager.ts.
4
+ */
5
+ import type { DashboardSession, SessionSource, SessionStatus } from "@blackbelt-technology/pi-dashboard-shared/types.js";
6
+
7
+ export interface RegisterSessionParams {
8
+ id: string;
9
+ cwd: string;
10
+ name?: string;
11
+ source: SessionSource;
12
+ model?: string;
13
+ thinkingLevel?: string;
14
+ sessionFile?: string;
15
+ sessionDir?: string;
16
+ firstMessage?: string;
17
+ startedAt?: number;
18
+ pid?: number;
19
+ }
20
+
21
+ export interface SessionManager {
22
+ register(params: RegisterSessionParams): DashboardSession;
23
+ /** Restore a previously persisted session (e.g. on startup). Does not trigger onChange. */
24
+ restore(session: DashboardSession): void;
25
+ unregister(sessionId: string): void;
26
+ update(sessionId: string, updates: Partial<DashboardSession>): void;
27
+ get(sessionId: string): DashboardSession | undefined;
28
+ listActive(): DashboardSession[];
29
+ listAll(): DashboardSession[];
30
+ /** Called after any mutation (register, unregister, update). Receives the affected session ID. */
31
+ onChange?: (sessionId: string) => void;
32
+ /** Called after a session is unregistered (status set to ended). */
33
+ onUnregister?: (sessionId: string) => void;
34
+ }
35
+
36
+ export function createMemorySessionManager(): SessionManager {
37
+ const sessions = new Map<string, DashboardSession>();
38
+
39
+ const mgr: SessionManager = {
40
+ register(params: RegisterSessionParams): DashboardSession {
41
+ // Preserve accumulated data (tokens, cost) from a prior session with the
42
+ // same ID (e.g. restored after server restart). Git and openspec data are
43
+ // polled by the bridge extension shortly after reconnect, so they don't
44
+ // need to be carried over.
45
+ const existing = sessions.get(params.id);
46
+
47
+ const session: DashboardSession = {
48
+ // Carry over accumulated data from the existing session (e.g. restored after restart)
49
+ ...(existing ? {
50
+ tokensIn: existing.tokensIn,
51
+ tokensOut: existing.tokensOut,
52
+ cacheRead: existing.cacheRead,
53
+ cacheWrite: existing.cacheWrite,
54
+ cost: existing.cost,
55
+ // Preserve user-set openspec assignment (not polled, set via dashboard UI)
56
+ attachedProposal: existing.attachedProposal,
57
+ // Preserve context usage until bridge sends fresh data
58
+ contextTokens: existing.contextTokens,
59
+ contextWindow: existing.contextWindow,
60
+ } : {
61
+ tokensIn: 0,
62
+ tokensOut: 0,
63
+ cost: 0,
64
+ }),
65
+ // Apply registration params (always override)
66
+ id: params.id,
67
+ cwd: params.cwd,
68
+ name: params.name ?? existing?.name,
69
+ source: params.source,
70
+ status: "active",
71
+ model: params.model,
72
+ thinkingLevel: params.thinkingLevel,
73
+ startedAt: params.startedAt ?? existing?.startedAt ?? Date.now(),
74
+ endedAt: undefined,
75
+ sessionFile: params.sessionFile,
76
+ sessionDir: params.sessionDir,
77
+ hidden: false,
78
+ firstMessage: params.firstMessage ?? existing?.firstMessage,
79
+ dataUnavailable: false,
80
+ pid: params.pid,
81
+ };
82
+ sessions.set(params.id, session);
83
+ mgr.onChange?.(params.id);
84
+ return session;
85
+ },
86
+
87
+ restore(session: DashboardSession): void {
88
+ sessions.set(session.id, session);
89
+ },
90
+
91
+ unregister(sessionId: string): void {
92
+ const session = sessions.get(sessionId);
93
+ if (session) {
94
+ session.status = "ended";
95
+ session.endedAt = Date.now();
96
+ mgr.onChange?.(sessionId);
97
+ mgr.onUnregister?.(sessionId);
98
+ }
99
+ },
100
+
101
+ update(sessionId: string, updates: Partial<DashboardSession>): void {
102
+ const session = sessions.get(sessionId);
103
+ if (session) {
104
+ Object.assign(session, updates);
105
+ mgr.onChange?.(sessionId);
106
+ }
107
+ },
108
+
109
+ get(sessionId: string): DashboardSession | undefined {
110
+ return sessions.get(sessionId);
111
+ },
112
+
113
+ listActive(): DashboardSession[] {
114
+ return Array.from(sessions.values()).filter((s) => s.status !== "ended");
115
+ },
116
+
117
+ listAll(): DashboardSession[] {
118
+ return Array.from(sessions.values());
119
+ },
120
+ };
121
+
122
+ return mgr;
123
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Per-session debounced `.meta.json` writer.
3
+ * Each session gets its own debounce timer — updating session A
4
+ * does not trigger a write for session B.
5
+ */
6
+ import { type SessionMeta, metaPath, readSessionMeta, writeSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/session-meta.js";
7
+
8
+ const DEBOUNCE_MS = 1000;
9
+
10
+ export interface MetaPersistence {
11
+ /** Schedule a debounced write of the session's `.meta.json`. */
12
+ save(sessionFile: string, meta: SessionMeta): void;
13
+ /** Flush all pending writes immediately. */
14
+ flushAll(): void;
15
+ /** Stop all debounce timers. */
16
+ dispose(): void;
17
+ }
18
+
19
+ interface PendingWrite {
20
+ sessionFile: string;
21
+ meta: SessionMeta;
22
+ timer: ReturnType<typeof setTimeout>;
23
+ }
24
+
25
+ export function createMetaPersistence(): MetaPersistence {
26
+ // Keyed by sessionFile path
27
+ const pending = new Map<string, PendingWrite>();
28
+
29
+ function writeNow(sessionFile: string): void {
30
+ const entry = pending.get(sessionFile);
31
+ if (!entry) return;
32
+ clearTimeout(entry.timer);
33
+ pending.delete(sessionFile);
34
+ writeSessionMeta(sessionFile, entry.meta);
35
+ }
36
+
37
+ return {
38
+ save(sessionFile: string, meta: SessionMeta): void {
39
+ const existing = pending.get(sessionFile);
40
+ if (existing) {
41
+ // Update meta and reset timer
42
+ clearTimeout(existing.timer);
43
+ existing.meta = meta;
44
+ existing.timer = setTimeout(() => writeNow(sessionFile), DEBOUNCE_MS);
45
+ } else {
46
+ const timer = setTimeout(() => writeNow(sessionFile), DEBOUNCE_MS);
47
+ pending.set(sessionFile, { sessionFile, meta, timer });
48
+ }
49
+ },
50
+
51
+ flushAll(): void {
52
+ for (const sessionFile of [...pending.keys()]) {
53
+ writeNow(sessionFile);
54
+ }
55
+ },
56
+
57
+ dispose(): void {
58
+ for (const entry of pending.values()) {
59
+ clearTimeout(entry.timer);
60
+ }
61
+ pending.clear();
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Migration utility: converts sessions.json + state.json → per-session .meta.json + preferences.json.
3
+ * Runs automatically on first startup when old files are detected.
4
+ * Idempotent — safe to run multiple times.
5
+ */
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ import os from "node:os";
9
+ import { CONFIG_DIR } from "@blackbelt-technology/pi-dashboard-shared/config.js";
10
+ import { type SessionMeta, readSessionMeta, writeSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/session-meta.js";
11
+ import { readJsonFile, writeJsonFile } from "./json-store.js";
12
+ import { PREFERENCES_FILE } from "./preferences-store.js";
13
+
14
+ const DEFAULT_SESSIONS_FILE = path.join(CONFIG_DIR, "sessions.json");
15
+ const DEFAULT_STATE_FILE = path.join(CONFIG_DIR, "state.json");
16
+
17
+ interface OldSession {
18
+ id: string;
19
+ sessionFile?: string;
20
+ cwd?: string;
21
+ name?: string;
22
+ source?: string;
23
+ status?: string;
24
+ model?: string;
25
+ thinkingLevel?: string;
26
+ startedAt?: number;
27
+ endedAt?: number;
28
+ tokensIn?: number;
29
+ tokensOut?: number;
30
+ cacheRead?: number;
31
+ cacheWrite?: number;
32
+ cost?: number;
33
+ contextTokens?: number;
34
+ contextWindow?: number;
35
+ firstMessage?: string;
36
+ hidden?: boolean;
37
+ attachedProposal?: string | null;
38
+ }
39
+
40
+ interface OldState {
41
+ hiddenSessions?: string[];
42
+ sessionOrder?: Record<string, string[]>;
43
+ pinnedDirectories?: string[];
44
+ }
45
+
46
+ export interface MigrationResult {
47
+ sessionsWritten: number;
48
+ hiddenApplied: number;
49
+ hiddenOrphaned: number;
50
+ preferencesWritten: boolean;
51
+ oldFilesRenamed: string[];
52
+ }
53
+
54
+ export interface MigrationPaths {
55
+ sessionsFile?: string;
56
+ stateFile?: string;
57
+ preferencesFile?: string;
58
+ sessionsDir?: string;
59
+ }
60
+
61
+ /** Check if migration is needed (old files exist without .bak) */
62
+ export function needsMigration(paths?: MigrationPaths): boolean {
63
+ const sessionsFile = paths?.sessionsFile ?? DEFAULT_SESSIONS_FILE;
64
+ const stateFile = paths?.stateFile ?? DEFAULT_STATE_FILE;
65
+ return fs.existsSync(sessionsFile) || fs.existsSync(stateFile);
66
+ }
67
+
68
+ /**
69
+ * Run the full migration:
70
+ * 1. Read sessions.json → write .meta.json for each session
71
+ * 2. Read state.json → apply hiddenSessions to .meta.json, write preferences.json
72
+ * 3. Rename old files to .bak
73
+ */
74
+ export function runMigration(paths?: MigrationPaths): MigrationResult {
75
+ const sessionsFile = paths?.sessionsFile ?? DEFAULT_SESSIONS_FILE;
76
+ const stateFile = paths?.stateFile ?? DEFAULT_STATE_FILE;
77
+ const preferencesFile = paths?.preferencesFile ?? PREFERENCES_FILE;
78
+ const sessionsScanDir = paths?.sessionsDir ?? path.join(os.homedir(), ".pi", "agent", "sessions");
79
+
80
+ const result: MigrationResult = {
81
+ sessionsWritten: 0,
82
+ hiddenApplied: 0,
83
+ hiddenOrphaned: 0,
84
+ preferencesWritten: false,
85
+ oldFilesRenamed: [],
86
+ };
87
+
88
+ // --- Step 1: Migrate sessions.json → per-session .meta.json ---
89
+ const sessions = readJsonFile<OldSession[]>(sessionsFile, []);
90
+ const sessionFileById = new Map<string, string>();
91
+
92
+ for (const session of sessions) {
93
+ if (!session.sessionFile || !fs.existsSync(session.sessionFile)) continue;
94
+
95
+ sessionFileById.set(session.id, session.sessionFile);
96
+
97
+ // Build meta from session data
98
+ const newMeta: SessionMeta = {
99
+ source: session.source,
100
+ name: session.name,
101
+ attachedProposal: session.attachedProposal,
102
+ hidden: session.hidden ?? false,
103
+ cwd: session.cwd,
104
+ status: session.status,
105
+ startedAt: session.startedAt,
106
+ endedAt: session.endedAt,
107
+ model: session.model,
108
+ thinkingLevel: session.thinkingLevel,
109
+ tokensIn: session.tokensIn,
110
+ tokensOut: session.tokensOut,
111
+ cacheRead: session.cacheRead,
112
+ cacheWrite: session.cacheWrite,
113
+ cost: session.cost,
114
+ contextTokens: session.contextTokens,
115
+ contextWindow: session.contextWindow,
116
+ firstMessage: session.firstMessage,
117
+ cachedAt: Date.now(),
118
+ };
119
+
120
+ // Merge with existing .meta.json — strip undefined values so they don't overwrite
121
+ const existing = readSessionMeta(session.sessionFile) ?? {};
122
+ const cleaned = Object.fromEntries(Object.entries(newMeta).filter(([, v]) => v !== undefined));
123
+ const merged = { ...existing, ...cleaned };
124
+ writeSessionMeta(session.sessionFile, merged);
125
+ result.sessionsWritten++;
126
+ }
127
+
128
+ // --- Step 2: Migrate state.json ---
129
+ const state = readJsonFile<OldState>(stateFile, {});
130
+
131
+ // Apply hidden IDs to .meta.json files
132
+ if (state.hiddenSessions) {
133
+ for (const hiddenId of state.hiddenSessions) {
134
+ // Try to find the session file
135
+ const sessionFile = sessionFileById.get(hiddenId);
136
+ if (sessionFile) {
137
+ // Known session — merge hidden flag
138
+ const existing = readSessionMeta(sessionFile) ?? {};
139
+ writeSessionMeta(sessionFile, { ...existing, hidden: true });
140
+ result.hiddenApplied++;
141
+ continue;
142
+ }
143
+
144
+ // Try scanning session directories for this UUID
145
+ const found = findSessionFileByUuid(hiddenId, sessionsScanDir);
146
+ if (found) {
147
+ const existing = readSessionMeta(found) ?? {};
148
+ writeSessionMeta(found, { ...existing, hidden: true });
149
+ result.hiddenApplied++;
150
+ } else {
151
+ result.hiddenOrphaned++;
152
+ }
153
+ }
154
+ }
155
+
156
+ // Write preferences.json
157
+ const preferences = {
158
+ pinnedDirectories: state.pinnedDirectories ?? [],
159
+ sessionOrder: state.sessionOrder ?? {},
160
+ };
161
+ writeJsonFile(preferencesFile, preferences);
162
+ result.preferencesWritten = true;
163
+
164
+ // --- Step 3: Rename old files to .bak ---
165
+ for (const file of [sessionsFile, stateFile]) {
166
+ if (fs.existsSync(file)) {
167
+ const bakFile = file + ".bak";
168
+ fs.renameSync(file, bakFile);
169
+ result.oldFilesRenamed.push(path.basename(file));
170
+ }
171
+ }
172
+
173
+ return result;
174
+ }
175
+
176
+ /** Scan session directories for a .jsonl file containing the given UUID */
177
+ function findSessionFileByUuid(uuid: string, scanDir: string): string | null {
178
+ if (!fs.existsSync(scanDir)) return null;
179
+
180
+ try {
181
+ for (const cwdDir of fs.readdirSync(scanDir)) {
182
+ const cwdPath = path.join(scanDir, cwdDir);
183
+ try {
184
+ if (!fs.statSync(cwdPath).isDirectory()) continue;
185
+ for (const file of fs.readdirSync(cwdPath)) {
186
+ if (file.endsWith(".jsonl") && file.includes(uuid)) {
187
+ return path.join(cwdPath, file);
188
+ }
189
+ }
190
+ } catch { continue; }
191
+ }
192
+ } catch { /* ignore */ }
193
+
194
+ return null;
195
+ }