@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,243 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type {
3
+ ExtensionToServerMessage,
4
+ ServerToExtensionMessage,
5
+ } from "../protocol.js";
6
+ import type {
7
+ ServerToBrowserMessage,
8
+ BrowserToServerMessage,
9
+ } from "../browser-protocol.js";
10
+ import type {
11
+ DashboardSession,
12
+ DashboardEvent,
13
+ SessionSource,
14
+ SessionStatus,
15
+ CommandInfo,
16
+ ApiResponse,
17
+ } from "../types.js";
18
+
19
+ describe("Protocol message serialization round-trip", () => {
20
+ it("should serialize/deserialize extension→server messages", () => {
21
+ const messages: ExtensionToServerMessage[] = [
22
+ {
23
+ type: "session_register",
24
+ sessionId: "s1",
25
+ cwd: "/home/user/project",
26
+ source: "tui",
27
+ model: "claude-sonnet-4-20250514",
28
+ thinkingLevel: "medium",
29
+ },
30
+ {
31
+ type: "session_unregister",
32
+ sessionId: "s1",
33
+ },
34
+ {
35
+ type: "session_heartbeat",
36
+ sessionId: "s1",
37
+ },
38
+ {
39
+ type: "event_forward",
40
+ sessionId: "s1",
41
+ event: {
42
+ eventType: "message_update",
43
+ timestamp: Date.now(),
44
+ data: { text: "Hello" },
45
+ },
46
+ },
47
+ {
48
+ type: "commands_list",
49
+ sessionId: "s1",
50
+ commands: [
51
+ { name: "test", description: "Run tests", source: "extension" },
52
+ ],
53
+ },
54
+ {
55
+ type: "extension_ui_request",
56
+ sessionId: "s1",
57
+ requestId: "req-1",
58
+ method: "confirm",
59
+ params: { title: "Allow?", message: "Delete files?" },
60
+ },
61
+
62
+ ];
63
+
64
+ for (const msg of messages) {
65
+ const json = JSON.stringify(msg);
66
+ const parsed = JSON.parse(json) as ExtensionToServerMessage;
67
+ expect(parsed).toEqual(msg);
68
+ expect(parsed.type).toBe(msg.type);
69
+ }
70
+ });
71
+
72
+ it("should serialize/deserialize server→extension messages", () => {
73
+ const messages: ServerToExtensionMessage[] = [
74
+ {
75
+ type: "send_prompt",
76
+ sessionId: "s1",
77
+ text: "Hello agent",
78
+ },
79
+ {
80
+ type: "abort",
81
+ sessionId: "s1",
82
+ },
83
+ {
84
+ type: "request_commands",
85
+ sessionId: "s1",
86
+ },
87
+ {
88
+ type: "request_state_sync",
89
+ sessionId: "s1",
90
+ },
91
+ {
92
+ type: "extension_ui_response",
93
+ sessionId: "s1",
94
+ requestId: "req-1",
95
+ result: { confirmed: true },
96
+ },
97
+ {
98
+ type: "extension_ui_response",
99
+ sessionId: "s1",
100
+ requestId: "req-2",
101
+ cancelled: true,
102
+ },
103
+ ];
104
+
105
+ for (const msg of messages) {
106
+ const json = JSON.stringify(msg);
107
+ const parsed = JSON.parse(json) as ServerToExtensionMessage;
108
+ expect(parsed).toEqual(msg);
109
+ expect(parsed.type).toBe(msg.type);
110
+ }
111
+ });
112
+
113
+ it("should serialize/deserialize server→browser messages", () => {
114
+ const messages: ServerToBrowserMessage[] = [
115
+ {
116
+ type: "session_added",
117
+ session: {
118
+ id: "s1",
119
+ cwd: "/project",
120
+ source: "tui",
121
+ status: "active",
122
+ model: "claude-sonnet-4-20250514",
123
+ thinkingLevel: "medium",
124
+ startedAt: Date.now(),
125
+ },
126
+ },
127
+ {
128
+ type: "session_updated",
129
+ sessionId: "s1",
130
+ updates: { status: "streaming" },
131
+ },
132
+ {
133
+ type: "session_removed",
134
+ sessionId: "s1",
135
+ },
136
+ {
137
+ type: "event",
138
+ sessionId: "s1",
139
+ seq: 42,
140
+ event: {
141
+ eventType: "message_update",
142
+ timestamp: Date.now(),
143
+ data: { text: "hi" },
144
+ },
145
+ },
146
+ {
147
+ type: "event_replay",
148
+ sessionId: "s1",
149
+ events: [],
150
+ isLast: true,
151
+ },
152
+ {
153
+ type: "commands_list",
154
+ sessionId: "s1",
155
+ commands: [],
156
+ },
157
+ {
158
+ type: "extension_ui_request",
159
+ sessionId: "s1",
160
+ requestId: "req-2",
161
+ method: "notify",
162
+ params: { message: "done", level: "info" },
163
+ },
164
+ ];
165
+
166
+ for (const msg of messages) {
167
+ const json = JSON.stringify(msg);
168
+ const parsed = JSON.parse(json) as ServerToBrowserMessage;
169
+ expect(parsed).toEqual(msg);
170
+ expect(parsed.type).toBe(msg.type);
171
+ }
172
+ });
173
+
174
+ it("should serialize/deserialize browser→server messages", () => {
175
+ const messages: BrowserToServerMessage[] = [
176
+ {
177
+ type: "subscribe",
178
+ sessionId: "s1",
179
+ lastSeq: 0,
180
+ },
181
+ {
182
+ type: "unsubscribe",
183
+ sessionId: "s1",
184
+ },
185
+ {
186
+ type: "send_prompt",
187
+ sessionId: "s1",
188
+ text: "Hello",
189
+ },
190
+ {
191
+ type: "abort",
192
+ sessionId: "s1",
193
+ },
194
+ {
195
+ type: "request_commands",
196
+ sessionId: "s1",
197
+ },
198
+ {
199
+ type: "fetch_content",
200
+ sessionId: "s1",
201
+ seq: 42,
202
+ },
203
+ {
204
+ type: "extension_ui_response",
205
+ sessionId: "s1",
206
+ requestId: "req-1",
207
+ result: { value: "Option A" },
208
+ },
209
+ ];
210
+
211
+ for (const msg of messages) {
212
+ const json = JSON.stringify(msg);
213
+ const parsed = JSON.parse(json) as BrowserToServerMessage;
214
+ expect(parsed).toEqual(msg);
215
+ expect(parsed.type).toBe(msg.type);
216
+ }
217
+ });
218
+ });
219
+
220
+ describe("Shared data model types", () => {
221
+ it("should have correct SessionSource values", () => {
222
+ const sources: SessionSource[] = ["tui", "zed", "tmux", "dashboard", "unknown"];
223
+ expect(sources).toHaveLength(5);
224
+ });
225
+
226
+ it("should have correct SessionStatus values", () => {
227
+ const statuses: SessionStatus[] = ["active", "streaming", "ended"];
228
+ expect(statuses).toHaveLength(3);
229
+ });
230
+
231
+ it("should construct valid ApiResponse", () => {
232
+ const success: ApiResponse<{ id: string }> = {
233
+ success: true,
234
+ data: { id: "123" },
235
+ };
236
+ const error: ApiResponse = {
237
+ success: false,
238
+ error: "Not found",
239
+ };
240
+ expect(success.success).toBe(true);
241
+ expect(error.success).toBe(false);
242
+ });
243
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("resolveJitiImport", () => {
4
+ it("throws with clear error when pi-coding-agent is not resolvable", async () => {
5
+ // In test context (vitest, not inside pi's jiti loader),
6
+ // peer deps are not resolvable — should throw
7
+ const { resolveJitiImport } = await import("../resolve-jiti.js");
8
+
9
+ expect(() => resolveJitiImport()).toThrow("Cannot find pi's TypeScript loader");
10
+ });
11
+
12
+ it("error message mentions pi-coding-agent", async () => {
13
+ const { resolveJitiImport } = await import("../resolve-jiti.js");
14
+
15
+ expect(() => resolveJitiImport()).toThrow("pi-coding-agent");
16
+ });
17
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { isDashboardRunning } from "../server-identity.js";
3
+
4
+ describe("isDashboardRunning", () => {
5
+ const originalFetch = globalThis.fetch;
6
+
7
+ afterEach(() => {
8
+ globalThis.fetch = originalFetch;
9
+ });
10
+
11
+ it("returns running: true with pid when health endpoint responds correctly", async () => {
12
+ globalThis.fetch = vi.fn().mockResolvedValue({
13
+ ok: true,
14
+ json: () => Promise.resolve({ ok: true, pid: 12345, uptime: 60 }),
15
+ });
16
+
17
+ const result = await isDashboardRunning(8000);
18
+ expect(result).toEqual({ running: true, pid: 12345 });
19
+ expect(globalThis.fetch).toHaveBeenCalledWith(
20
+ "http://localhost:8000/api/health",
21
+ expect.objectContaining({ signal: expect.any(AbortSignal) }),
22
+ );
23
+ });
24
+
25
+ it("returns portConflict when port returns non-ok HTTP status", async () => {
26
+ globalThis.fetch = vi.fn().mockResolvedValue({
27
+ ok: false,
28
+ status: 404,
29
+ });
30
+
31
+ const result = await isDashboardRunning(8000);
32
+ expect(result).toEqual({ running: false, portConflict: true });
33
+ });
34
+
35
+ it("returns portConflict when response is HTTP 200 but not dashboard format", async () => {
36
+ globalThis.fetch = vi.fn().mockResolvedValue({
37
+ ok: true,
38
+ json: () => Promise.resolve({ status: "ok", service: "nginx" }),
39
+ });
40
+
41
+ const result = await isDashboardRunning(8000);
42
+ expect(result).toEqual({ running: false, portConflict: true });
43
+ });
44
+
45
+ it("returns running: false when connection refused (nothing running)", async () => {
46
+ globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
47
+
48
+ const result = await isDashboardRunning(8000);
49
+ expect(result).toEqual({ running: false });
50
+ });
51
+
52
+ it("returns running: false when request times out", async () => {
53
+ const abortError = new Error("aborted");
54
+ abortError.name = "AbortError";
55
+ globalThis.fetch = vi.fn().mockRejectedValue(abortError);
56
+
57
+ const result = await isDashboardRunning(8000);
58
+ expect(result).toEqual({ running: false });
59
+ });
60
+
61
+ it("uses custom host when provided", async () => {
62
+ globalThis.fetch = vi.fn().mockResolvedValue({
63
+ ok: true,
64
+ json: () => Promise.resolve({ ok: true, pid: 999 }),
65
+ });
66
+
67
+ await isDashboardRunning(8000, "192.168.1.10");
68
+ expect(globalThis.fetch).toHaveBeenCalledWith(
69
+ "http://192.168.1.10:8000/api/health",
70
+ expect.any(Object),
71
+ );
72
+ });
73
+ });
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { metaPath, readSessionMeta, writeSessionMeta, mergeSessionMeta } from "../session-meta.js";
6
+
7
+ describe("session-meta", () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "session-meta-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ describe("metaPath", () => {
19
+ it("should derive .meta.json from .jsonl path", () => {
20
+ const sessionFile = "/home/user/.pi/sessions/cwd/2026-01-01T00-00-00-000Z_abc123.jsonl";
21
+ expect(metaPath(sessionFile)).toBe(
22
+ "/home/user/.pi/sessions/cwd/2026-01-01T00-00-00-000Z_abc123.meta.json"
23
+ );
24
+ });
25
+ });
26
+
27
+ describe("writeSessionMeta / readSessionMeta", () => {
28
+ it("should write and read meta", () => {
29
+ const sessionFile = path.join(tmpDir, "test-session.jsonl");
30
+ writeSessionMeta(sessionFile, { source: "dashboard" });
31
+ const meta = readSessionMeta(sessionFile);
32
+ expect(meta).toEqual({ source: "dashboard" });
33
+ });
34
+
35
+ it("should return undefined for missing meta file", () => {
36
+ const sessionFile = path.join(tmpDir, "nonexistent.jsonl");
37
+ expect(readSessionMeta(sessionFile)).toBeUndefined();
38
+ });
39
+
40
+ it("should return undefined for invalid JSON", () => {
41
+ const sessionFile = path.join(tmpDir, "bad.jsonl");
42
+ fs.writeFileSync(path.join(tmpDir, "bad.meta.json"), "not json");
43
+ expect(readSessionMeta(sessionFile)).toBeUndefined();
44
+ });
45
+
46
+ it("should write and read expanded fields", () => {
47
+ const sessionFile = path.join(tmpDir, "expanded.jsonl");
48
+ writeSessionMeta(sessionFile, {
49
+ source: "dashboard",
50
+ name: "General",
51
+ attachedProposal: "my-change",
52
+ hidden: false,
53
+ cwd: "/Users/test/project",
54
+ status: "ended",
55
+ startedAt: 1000,
56
+ endedAt: 2000,
57
+ model: "anthropic/claude-sonnet-4-20250514",
58
+ thinkingLevel: "medium",
59
+ tokensIn: 100,
60
+ tokensOut: 200,
61
+ cacheRead: 300,
62
+ cacheWrite: 400,
63
+ cost: 1.5,
64
+ contextTokens: 5000,
65
+ contextWindow: 200000,
66
+ firstMessage: "Hello",
67
+ cachedAt: 3000,
68
+ });
69
+ const meta = readSessionMeta(sessionFile);
70
+ expect(meta?.name).toBe("General");
71
+ expect(meta?.cost).toBe(1.5);
72
+ expect(meta?.cachedAt).toBe(3000);
73
+ expect(meta?.hidden).toBe(false);
74
+ });
75
+
76
+ it("should read minimal meta (backward compat)", () => {
77
+ const sessionFile = path.join(tmpDir, "minimal.jsonl");
78
+ writeSessionMeta(sessionFile, { source: "dashboard" });
79
+ const meta = readSessionMeta(sessionFile);
80
+ expect(meta).toEqual({ source: "dashboard" });
81
+ expect(meta?.name).toBeUndefined();
82
+ expect(meta?.cost).toBeUndefined();
83
+ });
84
+
85
+ it("should use atomic write (tmp + rename)", () => {
86
+ const sessionFile = path.join(tmpDir, "atomic.jsonl");
87
+ writeSessionMeta(sessionFile, { source: "dashboard" });
88
+ // tmp file should not remain
89
+ const tmpFile = metaPath(sessionFile) + ".tmp";
90
+ expect(fs.existsSync(tmpFile)).toBe(false);
91
+ expect(fs.existsSync(metaPath(sessionFile))).toBe(true);
92
+ });
93
+ });
94
+
95
+ describe("mergeSessionMeta", () => {
96
+ it("should merge new fields into existing meta", () => {
97
+ const sessionFile = path.join(tmpDir, "merge.jsonl");
98
+ writeSessionMeta(sessionFile, { source: "dashboard", name: "Old" });
99
+ mergeSessionMeta(sessionFile, { name: "New", cost: 5.0 });
100
+ const meta = readSessionMeta(sessionFile);
101
+ expect(meta?.source).toBe("dashboard");
102
+ expect(meta?.name).toBe("New");
103
+ expect(meta?.cost).toBe(5.0);
104
+ });
105
+
106
+ it("should create file if it does not exist", () => {
107
+ const sessionFile = path.join(tmpDir, "new-merge.jsonl");
108
+ mergeSessionMeta(sessionFile, { hidden: true, cost: 1.0 });
109
+ const meta = readSessionMeta(sessionFile);
110
+ expect(meta?.hidden).toBe(true);
111
+ expect(meta?.cost).toBe(1.0);
112
+ });
113
+
114
+ it("should preserve unknown fields", () => {
115
+ const sessionFile = path.join(tmpDir, "unknown.jsonl");
116
+ // Write a file with an unknown field
117
+ const p = metaPath(sessionFile);
118
+ fs.writeFileSync(p, JSON.stringify({ source: "dashboard", customField: 42 }) + "\n");
119
+ mergeSessionMeta(sessionFile, { name: "Test" });
120
+ const raw = JSON.parse(fs.readFileSync(p, "utf-8"));
121
+ expect(raw.customField).toBe(42);
122
+ expect(raw.name).toBe("Test");
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Type for OpenSpec archive entries. Shared between server and client.
3
+ */
4
+ export interface ArchiveEntry {
5
+ /** Full directory name, e.g. "2026-03-27-openspec-artifact-reader" */
6
+ name: string;
7
+ /** Date extracted from the prefix, e.g. "2026-03-27" */
8
+ date: string;
9
+ /** Detected artifacts with status "done" */
10
+ artifacts: { id: string; status: "done" }[];
11
+ }