@blackbelt-technology/pi-agent-dashboard 0.5.0 → 0.5.2

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 (201) hide show
  1. package/AGENTS.md +26 -5
  2. package/README.md +49 -7
  3. package/docs/architecture.md +129 -1
  4. package/package.json +15 -15
  5. package/packages/extension/package.json +11 -3
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +1 -1
  7. package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +362 -0
  8. package/packages/extension/src/__tests__/command-handler.test.ts +78 -8
  9. package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +1 -1
  10. package/packages/extension/src/__tests__/extension-slash-command-detection.test.ts +107 -0
  11. package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +1 -1
  12. package/packages/extension/src/__tests__/prompt-expander.test.ts +110 -1
  13. package/packages/extension/src/__tests__/provider-register-reload.test.ts +74 -0
  14. package/packages/extension/src/__tests__/retry-tracker.test.ts +147 -0
  15. package/packages/extension/src/__tests__/server-launcher-launch.test.ts +78 -0
  16. package/packages/extension/src/__tests__/session-sync.test.ts +72 -0
  17. package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +105 -0
  18. package/packages/extension/src/ask-user-tool.ts +1 -1
  19. package/packages/extension/src/bridge-context.ts +68 -4
  20. package/packages/extension/src/bridge.ts +79 -11
  21. package/packages/extension/src/command-handler.ts +95 -15
  22. package/packages/extension/src/flow-event-wiring.ts +1 -1
  23. package/packages/extension/src/multiselect-list.ts +1 -1
  24. package/packages/extension/src/pi-env.d.ts +16 -9
  25. package/packages/extension/src/prompt-expander.ts +74 -63
  26. package/packages/extension/src/provider-register.ts +16 -9
  27. package/packages/extension/src/retry-tracker.ts +123 -0
  28. package/packages/extension/src/server-launcher.ts +31 -70
  29. package/packages/extension/src/session-sync.ts +10 -1
  30. package/packages/extension/src/slash-dispatch.ts +123 -0
  31. package/packages/extension/src/usage-limit-orderer.ts +76 -0
  32. package/packages/server/bin/pi-dashboard.mjs +84 -0
  33. package/packages/server/package.json +8 -7
  34. package/packages/server/scripts/fix-pty-permissions.cjs +52 -0
  35. package/packages/server/src/__tests__/changelog-fs.test.ts +171 -0
  36. package/packages/server/src/__tests__/changelog-parser.test.ts +220 -0
  37. package/packages/server/src/__tests__/changelog-remote.test.ts +193 -0
  38. package/packages/server/src/__tests__/cli-parse.test.ts +16 -4
  39. package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +187 -0
  40. package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +1 -1
  41. package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +1 -1
  42. package/packages/server/src/__tests__/directory-service-toctou.test.ts +1 -1
  43. package/packages/server/src/__tests__/directory-service.test.ts +2 -2
  44. package/packages/server/src/__tests__/dispatch-extension-command-router.test.ts +178 -0
  45. package/packages/server/src/__tests__/e2e/model-proxy-google-flash.test.ts +184 -0
  46. package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +68 -1
  47. package/packages/server/src/__tests__/fixtures/pi-changelog-slice.md +180 -0
  48. package/packages/server/src/__tests__/fork-empty-session-preflight.test.ts +268 -0
  49. package/packages/server/src/__tests__/headless-pid-registry.test.ts +316 -0
  50. package/packages/server/src/__tests__/is-pi-process.test.ts +1 -1
  51. package/packages/server/src/__tests__/keeper-manager.test.ts +298 -0
  52. package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +149 -0
  53. package/packages/server/src/__tests__/model-proxy-api-key-routes.test.ts +277 -0
  54. package/packages/server/src/__tests__/model-proxy-auth-gate.test.ts +263 -0
  55. package/packages/server/src/__tests__/model-proxy-multi-user.test.ts +169 -0
  56. package/packages/server/src/__tests__/model-proxy-routes.test.ts +286 -0
  57. package/packages/server/src/__tests__/model-proxy-second-port.test.ts +116 -0
  58. package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +64 -8
  59. package/packages/server/src/__tests__/openspec-group-broadcast.test.ts +97 -0
  60. package/packages/server/src/__tests__/openspec-group-join.test.ts +80 -0
  61. package/packages/server/src/__tests__/openspec-group-routes.test.ts +370 -0
  62. package/packages/server/src/__tests__/openspec-group-store.test.ts +496 -0
  63. package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +4 -4
  64. package/packages/server/src/__tests__/package-routes.test.ts +1 -1
  65. package/packages/server/src/__tests__/pending-fork-registry.test.ts +48 -24
  66. package/packages/server/src/__tests__/pi-ai-shape.test.ts +147 -0
  67. package/packages/server/src/__tests__/pi-changelog-integration.test.ts +165 -0
  68. package/packages/server/src/__tests__/pi-changelog-routes.test.ts +409 -0
  69. package/packages/server/src/__tests__/pi-core-checker.test.ts +155 -13
  70. package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +62 -3
  71. package/packages/server/src/__tests__/pi-core-updater.test.ts +1 -1
  72. package/packages/server/src/__tests__/pi-dashboard-bin-wrapper.test.ts +84 -0
  73. package/packages/server/src/__tests__/pi-dev-version-check.test.ts +184 -0
  74. package/packages/server/src/__tests__/pi-version-skew.test.ts +4 -4
  75. package/packages/server/src/__tests__/process-manager-keeper-spawn.test.ts +206 -0
  76. package/packages/server/src/__tests__/provider-auth-routes.test.ts +12 -4
  77. package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +13 -23
  78. package/packages/server/src/__tests__/provider-routes-recursion-guard.test.ts +131 -0
  79. package/packages/server/src/__tests__/recommended-routes.test.ts +3 -3
  80. package/packages/server/src/__tests__/spawn-correlation-token-integration.test.ts +91 -0
  81. package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +84 -0
  82. package/packages/server/src/__tests__/spawn-token.test.ts +57 -0
  83. package/packages/server/src/__tests__/tunnel-watchdog.test.ts +139 -0
  84. package/packages/server/src/auth-plugin.ts +3 -0
  85. package/packages/server/src/bootstrap-state.ts +10 -0
  86. package/packages/server/src/browser-gateway.ts +27 -10
  87. package/packages/server/src/browser-handlers/handler-context.ts +9 -0
  88. package/packages/server/src/browser-handlers/session-action-handler.ts +128 -19
  89. package/packages/server/src/changelog-fs.ts +167 -0
  90. package/packages/server/src/changelog-parser.ts +321 -0
  91. package/packages/server/src/changelog-remote.ts +134 -0
  92. package/packages/server/src/cli.ts +62 -82
  93. package/packages/server/src/config-api.ts +14 -2
  94. package/packages/server/src/directory-service.ts +106 -4
  95. package/packages/server/src/event-wiring.ts +90 -6
  96. package/packages/server/src/headless-pid-registry.ts +344 -37
  97. package/packages/server/src/legacy-pi-cleanup.ts +151 -0
  98. package/packages/server/src/model-proxy/__tests__/api-key-store.test.ts +142 -0
  99. package/packages/server/src/model-proxy/__tests__/auth-json-contention.test.ts +98 -0
  100. package/packages/server/src/model-proxy/__tests__/concurrency.test.ts +107 -0
  101. package/packages/server/src/model-proxy/__tests__/failed-auth-backoff.test.ts +46 -0
  102. package/packages/server/src/model-proxy/__tests__/recursion-guard.test.ts +61 -0
  103. package/packages/server/src/model-proxy/__tests__/streamer.test.ts +139 -0
  104. package/packages/server/src/model-proxy/api-key-store.ts +87 -0
  105. package/packages/server/src/model-proxy/auth-gate.ts +116 -0
  106. package/packages/server/src/model-proxy/concurrency.ts +76 -0
  107. package/packages/server/src/model-proxy/convert/UPSTREAM.md +13 -0
  108. package/packages/server/src/model-proxy/convert/__tests__/anthropic-in.test.ts +137 -0
  109. package/packages/server/src/model-proxy/convert/__tests__/anthropic-out.test.ts +183 -0
  110. package/packages/server/src/model-proxy/convert/__tests__/openai-in.test.ts +134 -0
  111. package/packages/server/src/model-proxy/convert/__tests__/openai-out.test.ts +166 -0
  112. package/packages/server/src/model-proxy/convert/anthropic-in.ts +129 -0
  113. package/packages/server/src/model-proxy/convert/anthropic-out.ts +173 -0
  114. package/packages/server/src/model-proxy/convert/index.ts +8 -0
  115. package/packages/server/src/model-proxy/convert/openai-in.ts +119 -0
  116. package/packages/server/src/model-proxy/convert/openai-out.ts +151 -0
  117. package/packages/server/src/model-proxy/convert/types.ts +70 -0
  118. package/packages/server/src/model-proxy/failed-auth-backoff.ts +45 -0
  119. package/packages/server/src/model-proxy/internal-auth-storage.ts +146 -0
  120. package/packages/server/src/model-proxy/internal-registry.ts +157 -0
  121. package/packages/server/src/model-proxy/recursion-guard.ts +72 -0
  122. package/packages/server/src/model-proxy/registry-singleton.ts +109 -0
  123. package/packages/server/src/model-proxy/request-log.ts +53 -0
  124. package/packages/server/src/model-proxy/streamer.ts +59 -0
  125. package/packages/server/src/openspec-group-store.ts +490 -0
  126. package/packages/server/src/pending-client-correlations.ts +73 -0
  127. package/packages/server/src/pending-fork-registry.ts +24 -12
  128. package/packages/server/src/pi-core-checker.ts +77 -17
  129. package/packages/server/src/pi-core-updater.ts +16 -6
  130. package/packages/server/src/pi-dev-version-check.ts +145 -0
  131. package/packages/server/src/pi-gateway.ts +4 -0
  132. package/packages/server/src/pi-version-skew.ts +12 -4
  133. package/packages/server/src/process-manager.ts +182 -11
  134. package/packages/server/src/provider-auth-storage.ts +29 -47
  135. package/packages/server/src/provider-catalogue-cache.ts +24 -18
  136. package/packages/server/src/restart-helper.ts +17 -16
  137. package/packages/server/src/routes/bootstrap-routes.ts +37 -0
  138. package/packages/server/src/routes/jj-routes.ts +3 -0
  139. package/packages/server/src/routes/model-proxy-api-key-routes.ts +168 -0
  140. package/packages/server/src/routes/model-proxy-refresh-routes.ts +24 -0
  141. package/packages/server/src/routes/model-proxy-routes.ts +330 -0
  142. package/packages/server/src/routes/openspec-group-routes.ts +231 -0
  143. package/packages/server/src/routes/pi-changelog-routes.ts +194 -0
  144. package/packages/server/src/routes/pi-core-routes.ts +1 -1
  145. package/packages/server/src/routes/provider-auth-routes.ts +8 -1
  146. package/packages/server/src/routes/provider-routes.ts +28 -5
  147. package/packages/server/src/routes/system-routes.ts +44 -2
  148. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi-shim.sh +9 -0
  149. package/packages/server/src/rpc-keeper/__tests__/fixtures/mock-pi.cjs +50 -0
  150. package/packages/server/src/rpc-keeper/__tests__/keeper.test.ts +371 -0
  151. package/packages/server/src/rpc-keeper/dispatch-router.ts +85 -0
  152. package/packages/server/src/rpc-keeper/keeper-manager.ts +364 -0
  153. package/packages/server/src/rpc-keeper/keeper.cjs +313 -0
  154. package/packages/server/src/server.ts +254 -60
  155. package/packages/server/src/session-api.ts +63 -4
  156. package/packages/server/src/session-discovery.ts +1 -1
  157. package/packages/server/src/session-file-reader.ts +1 -1
  158. package/packages/server/src/spawn-register-watchdog.ts +62 -7
  159. package/packages/server/src/spawn-token.ts +20 -0
  160. package/packages/server/src/tunnel-watchdog.ts +230 -0
  161. package/packages/server/src/tunnel.ts +5 -1
  162. package/packages/shared/package.json +1 -1
  163. package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +228 -0
  164. package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +24 -17
  165. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +5 -4
  166. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +6 -5
  167. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +1 -0
  168. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +5 -4
  169. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +2 -1
  170. package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +5 -3
  171. package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +1 -1
  172. package/packages/shared/src/__tests__/changelog-types.test.ts +78 -0
  173. package/packages/shared/src/__tests__/config-openspec.test.ts +74 -0
  174. package/packages/shared/src/__tests__/model-proxy-config.test.ts +146 -0
  175. package/packages/shared/src/__tests__/no-raw-node-import.test.ts +7 -5
  176. package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +56 -20
  177. package/packages/shared/src/__tests__/node-spawn.test.ts +51 -0
  178. package/packages/shared/src/__tests__/openspec-groups-types.test.ts +135 -0
  179. package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +96 -0
  180. package/packages/shared/src/__tests__/recommended-extensions.test.ts +11 -3
  181. package/packages/shared/src/__tests__/server-launcher.test.ts +227 -0
  182. package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +1 -1
  183. package/packages/shared/src/bootstrap-install.ts +1 -1
  184. package/packages/shared/src/browser-protocol.ts +70 -0
  185. package/packages/shared/src/changelog-types.ts +111 -0
  186. package/packages/shared/src/config.ts +172 -2
  187. package/packages/shared/src/dashboard-plugin/manifest-types.ts +16 -1
  188. package/packages/shared/src/dashboard-plugin/slot-props.ts +8 -0
  189. package/packages/shared/src/dashboard-plugin/slot-types.ts +57 -0
  190. package/packages/shared/src/platform/binary-lookup.ts +204 -0
  191. package/packages/shared/src/platform/node-spawn.ts +71 -26
  192. package/packages/shared/src/protocol.ts +27 -1
  193. package/packages/shared/src/recommended-extensions.ts +18 -0
  194. package/packages/shared/src/rest-api.ts +219 -1
  195. package/packages/shared/src/server-launcher.ts +277 -0
  196. package/packages/shared/src/skill-block-parser.ts +1 -1
  197. package/packages/shared/src/tool-registry/__tests__/pi-ai-registration.test.ts +124 -0
  198. package/packages/shared/src/tool-registry/definitions.ts +15 -3
  199. package/packages/shared/src/types.ts +62 -0
  200. package/packages/shared/src/__tests__/resolve-jiti.test.ts +0 -53
  201. package/packages/shared/src/resolve-jiti.ts +0 -102
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Unit tests for `launchDashboardServer`.
3
+ *
4
+ * Mocks the test seams (`_resolveJiti`, `_spawnNodeScript`,
5
+ * `_isDashboardRunning`, `_fs`, `_sleep`, `_now`) so the launcher's
6
+ * orchestration logic is exercised without spawning a real child or
7
+ * touching the filesystem.
8
+ */
9
+ import { describe, it, expect, vi } from "vitest";
10
+ import { EventEmitter } from "node:events";
11
+ import {
12
+ launchDashboardServer,
13
+ JitiNotFoundError,
14
+ PortConflictError,
15
+ EarlyExitError,
16
+ } from "../server-launcher.js";
17
+ import type { ChildProcess } from "node:child_process";
18
+ import type { spawnNodeScript } from "../platform/node-spawn.js";
19
+ import type { isDashboardRunning } from "../server-identity.js";
20
+
21
+ const spawnSpy = (impl: () => ChildProcess) =>
22
+ vi.fn<typeof spawnNodeScript>(impl as unknown as typeof spawnNodeScript);
23
+ const probeSpy = <T>(impl: () => Promise<T>) =>
24
+ vi.fn<typeof isDashboardRunning>(impl as unknown as typeof isDashboardRunning);
25
+
26
+ interface FakeChildOpts {
27
+ pid?: number | null;
28
+ exitCode?: number | null;
29
+ }
30
+
31
+ function makeFakeChild(opts: FakeChildOpts = {}): ChildProcess {
32
+ const ee = new EventEmitter() as unknown as ChildProcess & {
33
+ pid: number | undefined;
34
+ exitCode: number | null;
35
+ signalCode: NodeJS.Signals | null;
36
+ unref: () => void;
37
+ };
38
+ ee.pid = (opts.pid ?? 12345) as number | undefined;
39
+ ee.exitCode = opts.exitCode ?? null;
40
+ ee.signalCode = null;
41
+ ee.unref = vi.fn();
42
+ return ee;
43
+ }
44
+
45
+ function baseOpts(overrides: Partial<Parameters<typeof launchDashboardServer>[0]> = {}) {
46
+ return {
47
+ cliPath: "/srv/cli.ts",
48
+ stdio: "ignore" as const,
49
+ healthTimeoutMs: 5000,
50
+ port: 8000,
51
+ _resolveJiti: () => "file:///loader/jiti-register.mjs",
52
+ _spawnNodeScript: spawnSpy(() => makeFakeChild()),
53
+ _isDashboardRunning: probeSpy(async () => ({ running: true, pid: 99 })),
54
+ _sleep: () => Promise.resolve(),
55
+ _pollIntervalMs: 1,
56
+ ...overrides,
57
+ };
58
+ }
59
+
60
+ describe("launchDashboardServer — happy path", () => {
61
+ it("returns childPid + reportedPid + healthOk on first health-ok poll", async () => {
62
+ const result = await launchDashboardServer(baseOpts());
63
+ expect(result.childPid).toBe(12345);
64
+ expect(result.reportedPid).toBe(99);
65
+ expect(result.healthOk).toBe(true);
66
+ });
67
+
68
+ it("delegates argv to spawnNodeScript with loader + entry + args", async () => {
69
+ const spy = spawnSpy(() => makeFakeChild());
70
+ await launchDashboardServer(baseOpts({
71
+ _spawnNodeScript: spy,
72
+ extraArgs: ["--port", "8000", "--pi-port", "9999"],
73
+ }));
74
+ expect(spy).toHaveBeenCalledOnce();
75
+ const call = spy.mock.calls[0]![0]!;
76
+ expect(call.loader).toBe("file:///loader/jiti-register.mjs");
77
+ expect(call.entry).toBe("/srv/cli.ts");
78
+ expect(call.args).toEqual(["--port", "8000", "--pi-port", "9999"]);
79
+ expect(call.spawnOptions?.detached).toBe(true);
80
+ expect(call.spawnOptions?.windowsHide).toBe(true);
81
+ expect(call.spawnOptions?.stdio).toBe("ignore");
82
+ });
83
+ });
84
+
85
+ describe("launchDashboardServer — jiti resolution", () => {
86
+ it("throws JitiNotFoundError when resolveJiti returns null (no spawn)", async () => {
87
+ const spawn = spawnSpy(() => makeFakeChild());
88
+ await expect(launchDashboardServer(baseOpts({
89
+ _resolveJiti: () => null,
90
+ _spawnNodeScript: spawn,
91
+ }))).rejects.toBeInstanceOf(JitiNotFoundError);
92
+ expect(spawn).not.toHaveBeenCalled();
93
+ });
94
+ });
95
+
96
+ describe("launchDashboardServer — readiness termination", () => {
97
+ it("throws PortConflictError when probe reports portConflict", async () => {
98
+ await expect(launchDashboardServer(baseOpts({
99
+ _isDashboardRunning: async () => ({ running: false, portConflict: true }),
100
+ }))).rejects.toBeInstanceOf(PortConflictError);
101
+ });
102
+
103
+ it("throws EarlyExitError when child exits during poll", async () => {
104
+ const child = makeFakeChild();
105
+ let calls = 0;
106
+ const spawnFn = spawnSpy(() => child);
107
+ const probe = probeSpy(async () => {
108
+ calls++;
109
+ if (calls === 1) {
110
+ // Mid-poll, child crashes.
111
+ (child as unknown as { exitCode: number }).exitCode = 7;
112
+ }
113
+ return { running: false };
114
+ });
115
+ await expect(launchDashboardServer(baseOpts({
116
+ _spawnNodeScript: spawnFn,
117
+ _isDashboardRunning: probe,
118
+ }))).rejects.toBeInstanceOf(EarlyExitError);
119
+ });
120
+
121
+ it("throws readiness-timeout Error after healthTimeoutMs elapses", async () => {
122
+ let now = 1000;
123
+ await expect(launchDashboardServer(baseOpts({
124
+ healthTimeoutMs: 100,
125
+ _now: () => { now += 60; return now; }, // each poll advances 60ms — 2 polls past deadline
126
+ _isDashboardRunning: async () => ({ running: false }),
127
+ }))).rejects.toThrow(/readiness timeout/);
128
+ });
129
+
130
+ it("port-conflict beats timeout (probe order respected)", async () => {
131
+ let now = 1000;
132
+ await expect(launchDashboardServer(baseOpts({
133
+ healthTimeoutMs: 100,
134
+ _now: () => { now += 200; return now; },
135
+ _isDashboardRunning: async () => ({ running: false, portConflict: true }),
136
+ }))).rejects.toBeInstanceOf(PortConflictError);
137
+ });
138
+ });
139
+
140
+ describe("launchDashboardServer — log-file stdio", () => {
141
+ it("mkdirs parent, opens append fd, writes header, passes fd, closes parent's copy", async () => {
142
+ const calls: string[] = [];
143
+ const fsStub = {
144
+ mkdirSync: vi.fn((p: any) => { calls.push(`mkdir:${p}`); }),
145
+ openSync: vi.fn((p: any, mode: any) => { calls.push(`open:${p}:${mode}`); return 42; }),
146
+ writeSync: vi.fn((fd: number, s: any) => { calls.push(`write:${fd}:${String(s).slice(0, 20)}…`); return s.length; }),
147
+ closeSync: vi.fn((fd: number) => { calls.push(`close:${fd}`); }),
148
+ };
149
+ const spawn = spawnSpy(() => makeFakeChild());
150
+ await launchDashboardServer(baseOpts({
151
+ stdio: { logFile: "/var/log/dashboard/server.log" },
152
+ starter: "Standalone",
153
+ _fs: fsStub as any,
154
+ _spawnNodeScript: spawn,
155
+ }));
156
+ expect(fsStub.mkdirSync).toHaveBeenCalledWith("/var/log/dashboard", { recursive: true });
157
+ expect(fsStub.openSync).toHaveBeenCalledWith("/var/log/dashboard/server.log", "a");
158
+ expect(fsStub.writeSync).toHaveBeenCalledOnce();
159
+ expect(String(fsStub.writeSync.mock.calls[0]![1])).toContain("Standalone launch");
160
+ // Spawn received [ignore, fd, fd]:
161
+ const stdio = spawn.mock.calls[0]![0]!.spawnOptions!.stdio as Array<unknown>;
162
+ expect(stdio).toEqual(["ignore", 42, 42]);
163
+ // Parent fd closed AFTER spawn:
164
+ expect(fsStub.closeSync).toHaveBeenCalledWith(42);
165
+ const closeIdx = calls.findIndex((c) => c.startsWith("close:42"));
166
+ const writeIdx = calls.findIndex((c) => c.startsWith("write:42"));
167
+ expect(closeIdx).toBeGreaterThan(writeIdx);
168
+ });
169
+ });
170
+
171
+ describe("launchDashboardServer — env merge", () => {
172
+ it("caller env keys override buildSpawnEnv defaults", async () => {
173
+ const spawn = spawnSpy(() => makeFakeChild());
174
+ await launchDashboardServer(baseOpts({
175
+ _spawnNodeScript: spawn,
176
+ env: { DASHBOARD_STARTER: "Bridge", CUSTOM_KEY: "x" },
177
+ }));
178
+ const env = spawn.mock.calls[0]![0]!.spawnOptions!.env as Record<string, string>;
179
+ expect(env.DASHBOARD_STARTER).toBe("Bridge");
180
+ expect(env.CUSTOM_KEY).toBe("x");
181
+ });
182
+
183
+ it("starter option becomes DASHBOARD_STARTER when env does not supply it", async () => {
184
+ const spawn = spawnSpy(() => makeFakeChild());
185
+ await launchDashboardServer(baseOpts({
186
+ _spawnNodeScript: spawn,
187
+ starter: "Electron",
188
+ }));
189
+ const env = spawn.mock.calls[0]![0]!.spawnOptions!.env as Record<string, string>;
190
+ expect(env.DASHBOARD_STARTER).toBe("Electron");
191
+ });
192
+
193
+ it("explicit env.DASHBOARD_STARTER wins over starter option", async () => {
194
+ const spawn = spawnSpy(() => makeFakeChild());
195
+ await launchDashboardServer(baseOpts({
196
+ _spawnNodeScript: spawn,
197
+ starter: "Electron",
198
+ env: { DASHBOARD_STARTER: "Bridge" },
199
+ }));
200
+ const env = spawn.mock.calls[0]![0]!.spawnOptions!.env as Record<string, string>;
201
+ expect(env.DASHBOARD_STARTER).toBe("Bridge");
202
+ });
203
+ });
204
+
205
+ describe("launchDashboardServer — entry URL-wrapping", () => {
206
+ // The launcher delegates to spawnNodeScript, which uses
207
+ // `shouldUrlWrapEntry(loader, platform)`. We verify the launcher
208
+ // simply forwards the raw entry; the URL-wrap behaviour itself is
209
+ // pinned by node-spawn-jiti-contract.test.ts.
210
+ it("forwards `cliPath` verbatim to spawnNodeScript (URL-wrapping owned downstream)", async () => {
211
+ const spawn = spawnSpy(() => makeFakeChild());
212
+ await launchDashboardServer(baseOpts({
213
+ _spawnNodeScript: spawn,
214
+ cliPath: "/posix/cli.ts",
215
+ }));
216
+ expect(spawn.mock.calls[0]![0]!.entry).toBe("/posix/cli.ts");
217
+ });
218
+
219
+ it("forwards Windows-style `cliPath` verbatim too", async () => {
220
+ const spawn = spawnSpy(() => makeFakeChild());
221
+ await launchDashboardServer(baseOpts({
222
+ _spawnNodeScript: spawn,
223
+ cliPath: "C:\\srv\\cli.ts",
224
+ }));
225
+ expect(spawn.mock.calls[0]![0]!.entry).toBe("C:\\srv\\cli.ts");
226
+ });
227
+ });
@@ -104,7 +104,7 @@ describe("pi binary definition", () => {
104
104
  });
105
105
 
106
106
  describe("pi-coding-agent module definition", () => {
107
- it("probes both @mariozechner and @oh-my-pi alias names", () => {
107
+ it("probes both @earendil-works (preferred) and @mariozechner (legacy fallback) alias names", () => {
108
108
  const r = freshRegistry({ exists: () => false });
109
109
  const res = r.resolve("pi-coding-agent");
110
110
  const names = res.tried.map((t) => t.strategy);
@@ -213,7 +213,7 @@ export async function bootstrapInstallDefaults(
213
213
  progress?: ProgressCallback,
214
214
  ): Promise<BootstrapInstallResult> {
215
215
  return bootstrapInstall({
216
- packages: ["@mariozechner/pi-coding-agent", "@fission-ai/openspec", "tsx"],
216
+ packages: ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"],
217
217
  progress,
218
218
  });
219
219
  }
@@ -9,6 +9,7 @@ import type {
9
9
  ImageContent,
10
10
  FileEntry,
11
11
  OpenSpecData,
12
+ OpenSpecGroup,
12
13
  ModelInfo,
13
14
  PiSessionInfo,
14
15
  ExtensionUiModule,
@@ -22,6 +23,14 @@ import type { EditorInstanceStatus } from "./editor-types.js";
22
23
  export interface SessionAddedMessage {
23
24
  type: "session_added";
24
25
  session: DashboardSession;
26
+ /**
27
+ * Echoed `requestId` from the originating browser `spawn_session` /
28
+ * `resume_session` (when known). Lets the client auto-select / dismiss
29
+ * placeholder by exact correlation, replacing the cwd-FIFO heuristic.
30
+ * Absent for server-initiated spawns (auto-resume, headless reload).
31
+ * See change: spawn-correlation-token.
32
+ */
33
+ spawnRequestId?: string;
25
34
  }
26
35
 
27
36
  export interface SessionUpdatedMessage {
@@ -88,6 +97,19 @@ export interface BrowserOpenSpecUpdateMessage {
88
97
  data: OpenSpecData;
89
98
  }
90
99
 
100
+ /**
101
+ * Per-repo OpenSpec change-grouping update. Broadcast after every successful
102
+ * write to `<cwd>/openspec/groups/groups.json`, debounced 100 ms per cwd.
103
+ * Full payload (no incremental delta) so client logic stays simple.
104
+ * See change: add-openspec-change-grouping.
105
+ */
106
+ export interface BrowserOpenSpecGroupsUpdateMessage {
107
+ type: "openspec_groups_update";
108
+ cwd: string;
109
+ groups: OpenSpecGroup[];
110
+ assignments: Record<string, string>;
111
+ }
112
+
91
113
  export interface BrowserModelsListMessage {
92
114
  type: "models_list";
93
115
  sessionId: string;
@@ -118,6 +140,24 @@ export interface ResumeResultBrowserMessage {
118
140
  sessionId: string;
119
141
  success: boolean;
120
142
  message: string;
143
+ /** Echoed from input `resume_session.requestId` when provided. */
144
+ requestId?: string;
145
+ /**
146
+ * For `mode: "fork"` only — populated once the new fork's bridge has
147
+ * registered and been correlated. Absent for `mode: "continue"` (the
148
+ * sessionId is unchanged across the respawn).
149
+ * See change: spawn-correlation-token.
150
+ */
151
+ newSessionId?: string;
152
+ /**
153
+ * Optional structured failure classifier. Known values:
154
+ * - `"FORK_EMPTY_SESSION"`: fork attempted on a session whose
155
+ * `sessionFile` does not exist on disk yet (e.g., freshly spawned,
156
+ * no messages persisted).
157
+ * Old clients that don't read this field still get the human-readable
158
+ * `message`. See change: fix-fork-empty-session-silent-timeout.
159
+ */
160
+ code?: string;
121
161
  }
122
162
 
123
163
  export interface SpawnResultBrowserMessage {
@@ -125,6 +165,10 @@ export interface SpawnResultBrowserMessage {
125
165
  cwd: string;
126
166
  success: boolean;
127
167
  message: string;
168
+ /** Echoed from input `spawn_session.requestId` when provided. */
169
+ requestId?: string;
170
+ /** Spawned process PID when known (headless strategies); informational. */
171
+ pid?: number;
128
172
  }
129
173
 
130
174
  /**
@@ -356,6 +400,18 @@ export interface BootstrapStateSnapshot {
356
400
  upgradeDashboard?: boolean;
357
401
  };
358
402
  bridgeRegistrationError?: string;
403
+ /**
404
+ * Legacy `@mariozechner/pi-coding-agent` installs detected on disk.
405
+ * Surfaced by the client as a one-click cleanup banner. Empty array
406
+ * means no legacy installs found. Pi was renamed to
407
+ * `@earendil-works/pi-coding-agent` at v0.74 — the legacy scope can
408
+ * collide with the new scope's `bin/pi` symlink.
409
+ */
410
+ legacyPiInstalls?: Array<{
411
+ scope: "npm-global" | "npx-cache" | "managed";
412
+ path: string;
413
+ version: string | null;
414
+ }>;
359
415
  }
360
416
 
361
417
  /**
@@ -484,6 +540,7 @@ export type ServerToBrowserMessage =
484
540
  | BrowserUiDismissMessage
485
541
  | BrowserFilesListMessage
486
542
  | BrowserOpenSpecUpdateMessage
543
+ | BrowserOpenSpecGroupsUpdateMessage
487
544
  | BrowserModelsListMessage
488
545
  | SessionsListBrowserMessage
489
546
  | ResumeResultBrowserMessage
@@ -665,6 +722,12 @@ export interface ResumeSessionBrowserMessage {
665
722
  mode: "continue" | "fork";
666
723
  /** When forking, optionally fork from a specific session entry instead of the latest */
667
724
  entryId?: string;
725
+ /**
726
+ * Client-minted UUIDv4 used to correlate `resume_result` and (for fork mode)
727
+ * the eventual `session_added` for the new session. Optional for back-compat.
728
+ * See change: spawn-correlation-token.
729
+ */
730
+ requestId?: string;
668
731
  /**
669
732
  * Placement intent for the resumed session in the cwd's sessionOrder:
670
733
  * - "front" (default): move to top of alive tier (Resume button, REST,
@@ -699,6 +762,13 @@ export interface SpawnSessionBrowserMessage {
699
762
  * add-folder-task-checker-and-spawn-attach.
700
763
  */
701
764
  attachProposal?: string;
765
+ /**
766
+ * Client-minted UUIDv4 used to correlate `spawn_result` and the eventual
767
+ * `session_added` (which echoes it as `spawnRequestId`). Optional for
768
+ * back-compat with older clients.
769
+ * See change: spawn-correlation-token.
770
+ */
771
+ requestId?: string;
702
772
  }
703
773
 
704
774
  export interface AttachProposalBrowserMessage {
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Shared types for the pi changelog display feature.
3
+ *
4
+ * Server parses `CHANGELOG.md` files installed alongside core packages
5
+ * and the client renders the result via `WhatsNewDialog`. These types
6
+ * are the wire contract between the two halves.
7
+ *
8
+ * See change: pi-update-whats-new-panel.
9
+ */
10
+
11
+ /**
12
+ * One bullet point under a release section. Preserves the original
13
+ * markdown prose verbatim so issue/PR links and inline code formatting
14
+ * survive intact for the client renderer.
15
+ */
16
+ export interface ChangelogBullet {
17
+ /**
18
+ * Original markdown text of the bullet, with the leading `- `
19
+ * removed and any continuation lines joined with `\n`. Inline
20
+ * markdown (links, code spans, emphasis) is preserved exactly as
21
+ * written so the client can hand it to a markdown renderer.
22
+ */
23
+ text: string;
24
+ /**
25
+ * Issue / PR references mined from the prose via the canonical
26
+ * `([#NNN](URL))` pattern pi uses at end-of-bullet. May be empty
27
+ * when no such pattern is found. Order matches occurrence order.
28
+ * The `text` field still contains the link in its original form.
29
+ */
30
+ issues: { num: number; url: string }[];
31
+ }
32
+
33
+ /**
34
+ * One release entry parsed from a Keep-a-Changelog-style markdown
35
+ * document. Typed sub-section arrays are populated only when the
36
+ * corresponding H3 heading is present in the source.
37
+ */
38
+ export interface ChangelogRelease {
39
+ /**
40
+ * Version string lifted from the `## [<version>] - <date>` H2
41
+ * heading. The bracket contents are taken verbatim so versions
42
+ * like `0.4.3-rc.1` survive.
43
+ */
44
+ version: string;
45
+ /**
46
+ * Date string lifted from the H2 heading. Set to `null` when the
47
+ * date token is missing or fails to parse as a YYYY-MM-DD-ish form.
48
+ * Parser tolerance is intentional — pi has shipped rare entries
49
+ * with date ranges or missing dates.
50
+ */
51
+ date: string | null;
52
+ /** Bullets under `### Breaking Changes`, in source order. */
53
+ breaking: ChangelogBullet[];
54
+ /**
55
+ * Union of bullets under `### New Features` and `### Added`. Pi
56
+ * uses both labels at different times for the same concept, so
57
+ * we merge them. Source order is preserved within each sub-section
58
+ * and the two sub-sections are concatenated in source order.
59
+ */
60
+ features: ChangelogBullet[];
61
+ /** Bullets under `### Changed`. */
62
+ changed: ChangelogBullet[];
63
+ /** Bullets under `### Fixed`. */
64
+ fixed: ChangelogBullet[];
65
+ /**
66
+ * Full markdown text from the release's H2 line up to (but not
67
+ * including) the next H2 line. Retained as a fallback render path
68
+ * when the typed arrays don't capture the content (e.g. an
69
+ * unrecognized H3 heading).
70
+ */
71
+ raw: string;
72
+ }
73
+
74
+ /**
75
+ * Response shape for `GET /api/pi-core/changelog`. Always wraps the
76
+ * filtered release list in this envelope so the client gets the
77
+ * derived `hasBreaking` flag and the GitHub link in one round-trip.
78
+ */
79
+ export interface ChangelogResponse {
80
+ /** Package the changelog was parsed for. */
81
+ pkg: string;
82
+ /**
83
+ * Lower bound of the version range, EXCLUSIVE. Echoes the request
84
+ * query param. Releases at or below this version are filtered out.
85
+ */
86
+ from: string;
87
+ /**
88
+ * Upper bound of the version range, INCLUSIVE. Echoes the request
89
+ * query param. Releases above this version are filtered out.
90
+ */
91
+ to: string;
92
+ /**
93
+ * Filtered release list, ordered with the latest version FIRST.
94
+ * Empty when no releases exist in `(from, to]` or when the
95
+ * CHANGELOG could not be located.
96
+ */
97
+ releases: ChangelogRelease[];
98
+ /**
99
+ * Convenience flag derived from `releases.some(r => r.breaking.length > 0)`.
100
+ * Lets the client render the warning icon without re-walking the array.
101
+ */
102
+ hasBreaking: boolean;
103
+ /**
104
+ * Public URL to the full CHANGELOG on GitHub, derived from the
105
+ * package's `repository` field. `null` when the repository is not
106
+ * GitHub-hosted or is unparseable.
107
+ */
108
+ changelogUrl: string | null;
109
+ /** ISO timestamp at which the parser produced this response. */
110
+ parsedAt: string;
111
+ }