@blackbelt-technology/pi-agent-dashboard 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +67 -116
- package/README.md +93 -7
- package/docs/architecture.md +408 -9
- package/package.json +6 -4
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/git-info.test.ts +67 -55
- package/packages/extension/src/__tests__/openspec-poller.test.ts +101 -96
- package/packages/extension/src/__tests__/process-scanner-kill.test.ts +61 -0
- package/packages/extension/src/__tests__/provider-register-reload.test.ts +394 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +95 -4
- package/packages/extension/src/__tests__/server-launcher.test.ts +16 -0
- package/packages/extension/src/bridge.ts +69 -2
- package/packages/extension/src/dev-build.ts +1 -1
- package/packages/extension/src/git-info.ts +9 -19
- package/packages/extension/src/pi-env.d.ts +1 -0
- package/packages/extension/src/process-scanner.ts +72 -38
- package/packages/extension/src/provider-register.ts +304 -16
- package/packages/extension/src/server-auto-start.ts +27 -1
- package/packages/extension/src/server-launcher.ts +71 -27
- package/packages/server/package.json +16 -2
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +120 -0
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +125 -0
- package/packages/server/src/__tests__/bootstrap-state.test.ts +119 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +17 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +11 -0
- package/packages/server/src/__tests__/concurrent-launch.test.ts +110 -0
- package/packages/server/src/__tests__/config-api.test.ts +68 -0
- package/packages/server/src/__tests__/crash-recovery.test.ts +88 -0
- package/packages/server/src/__tests__/directory-service.test.ts +234 -8
- package/packages/server/src/__tests__/editor-registry.test.ts +28 -15
- package/packages/server/src/__tests__/extension-register-appimage.test.ts +5 -1
- package/packages/server/src/__tests__/extension-register.test.ts +3 -1
- package/packages/server/src/__tests__/find-port-holders.test.ts +94 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/home-lock-escape-hatch.test.ts +60 -0
- package/packages/server/src/__tests__/home-lock-release.test.ts +85 -0
- package/packages/server/src/__tests__/home-lock.test.ts +308 -0
- package/packages/server/src/__tests__/is-pi-process.test.ts +36 -0
- package/packages/server/src/__tests__/node-guard.test.ts +85 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +5 -1
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-version-skew.test.ts +165 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +73 -4
- package/packages/server/src/__tests__/process-manager.test.ts +45 -18
- package/packages/server/src/__tests__/provider-probe.test.ts +287 -0
- package/packages/server/src/__tests__/provider-test-route.test.ts +149 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +83 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +467 -0
- package/packages/server/src/__tests__/session-action-handler-reload-predicate.test.ts +73 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +74 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/tool-routes.test.ts +277 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +19 -0
- package/packages/server/src/__tests__/trusted-networks-no-oauth-roundtrip.test.ts +126 -0
- package/packages/server/src/__tests__/tunnel-cleanup.test.ts +90 -0
- package/packages/server/src/__tests__/tunnel.test.ts +13 -7
- package/packages/server/src/__tests__/wsl-tmux-probe-cache.test.ts +44 -0
- package/packages/server/src/bootstrap-queue.ts +130 -0
- package/packages/server/src/bootstrap-state.ts +131 -0
- package/packages/server/src/browse.ts +8 -3
- package/packages/server/src/browser-handlers/directory-handler.ts +23 -8
- package/packages/server/src/browser-handlers/session-action-handler.ts +213 -79
- package/packages/server/src/browser-handlers/session-action-helpers.ts +36 -0
- package/packages/server/src/cli.ts +256 -32
- package/packages/server/src/config-api.ts +16 -0
- package/packages/server/src/directory-service.ts +270 -39
- package/packages/server/src/editor-detection.ts +12 -9
- package/packages/server/src/editor-manager.ts +19 -4
- package/packages/server/src/editor-pid-registry.ts +9 -8
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +7 -20
- package/packages/server/src/home-lock-release.ts +72 -0
- package/packages/server/src/home-lock.ts +389 -0
- package/packages/server/src/node-guard.ts +52 -0
- package/packages/server/src/package-manager-wrapper.ts +207 -47
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-core-updater.ts +7 -1
- package/packages/server/src/pi-resource-scanner.ts +5 -8
- package/packages/server/src/pi-version-skew.ts +196 -0
- package/packages/server/src/preferences-store.ts +17 -3
- package/packages/server/src/process-manager.ts +403 -222
- package/packages/server/src/provider-probe.ts +234 -0
- package/packages/server/src/restart-helper.ts +130 -0
- package/packages/server/src/routes/bootstrap-routes.ts +88 -0
- package/packages/server/src/routes/openspec-routes.ts +25 -1
- package/packages/server/src/routes/pi-core-routes.ts +24 -1
- package/packages/server/src/routes/provider-auth-routes.ts +8 -8
- package/packages/server/src/routes/provider-routes.ts +43 -0
- package/packages/server/src/routes/recommended-routes.ts +10 -12
- package/packages/server/src/routes/system-routes.ts +20 -33
- package/packages/server/src/routes/tool-routes.ts +153 -0
- package/packages/server/src/server-pid.ts +5 -9
- package/packages/server/src/server.ts +211 -10
- package/packages/server/src/session-api.ts +77 -8
- package/packages/server/src/session-bootstrap.ts +17 -3
- package/packages/server/src/session-diff.ts +21 -21
- package/packages/server/src/terminal-manager.ts +61 -20
- package/packages/server/src/tunnel.ts +42 -28
- package/packages/shared/package.json +10 -3
- package/packages/shared/src/__tests__/{tool-resolver.test.ts → binary-lookup.test.ts} +32 -12
- package/packages/shared/src/__tests__/bootstrap/README.md +133 -0
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +370 -0
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +136 -0
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +47 -0
- package/packages/shared/src/__tests__/bootstrap/cube.ts +66 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +83 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +89 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +20 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +33 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +46 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +12 -0
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +156 -0
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +157 -0
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +102 -0
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +76 -0
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +94 -0
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +87 -0
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +143 -0
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +64 -0
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +77 -0
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +19 -0
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +61 -0
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +50 -0
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +272 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +58 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +84 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +9 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +85 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +122 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +36 -0
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +39 -0
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +220 -0
- package/packages/shared/src/__tests__/bootstrap/harness.ts +413 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +125 -0
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +132 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +29 -6
- package/packages/shared/src/__tests__/config-openspec.test.ts +106 -0
- package/packages/shared/src/__tests__/config.test.ts +56 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +243 -0
- package/packages/shared/src/__tests__/managed-paths.test.ts +60 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +112 -0
- package/packages/shared/src/__tests__/no-direct-platform-branch.test.ts +174 -0
- package/packages/shared/src/__tests__/no-direct-process-kill.test.ts +105 -0
- package/packages/shared/src/__tests__/platform-commands.test.ts +108 -0
- package/packages/shared/src/__tests__/platform-exec.test.ts +103 -0
- package/packages/shared/src/__tests__/platform-git.test.ts +194 -0
- package/packages/shared/src/__tests__/platform-npm.test.ts +137 -0
- package/packages/shared/src/__tests__/platform-openspec.test.ts +92 -0
- package/packages/shared/src/__tests__/platform-paths.test.ts +284 -0
- package/packages/shared/src/__tests__/platform-process-scan.test.ts +55 -0
- package/packages/shared/src/__tests__/platform-process.test.ts +160 -0
- package/packages/shared/src/__tests__/platform-runner.test.ts +173 -0
- package/packages/shared/src/__tests__/platform-shell.test.ts +74 -0
- package/packages/shared/src/__tests__/process-identify.test.ts +113 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +40 -7
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +43 -7
- package/packages/shared/src/__tests__/semaphore.test.ts +119 -0
- package/packages/shared/src/__tests__/spawn-mechanism.test.ts +131 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +239 -0
- package/packages/shared/src/__tests__/tool-registry-overrides.test.ts +137 -0
- package/packages/shared/src/__tests__/tool-registry-registry.test.ts +343 -0
- package/packages/shared/src/bootstrap-install.ts +212 -0
- package/packages/shared/src/bridge-register.ts +87 -20
- package/packages/shared/src/browser-protocol.ts +71 -1
- package/packages/shared/src/config.ts +87 -15
- package/packages/shared/src/managed-paths.ts +31 -4
- package/packages/shared/src/openspec-poller.ts +63 -46
- package/packages/shared/src/{tool-resolver.ts → platform/binary-lookup.ts} +125 -25
- package/packages/shared/src/platform/commands.ts +100 -0
- package/packages/shared/src/platform/detached-spawn.ts +305 -0
- package/packages/shared/src/platform/exec.ts +220 -0
- package/packages/shared/src/platform/git.ts +155 -0
- package/packages/shared/src/platform/index.ts +15 -0
- package/packages/shared/src/platform/npm.ts +162 -0
- package/packages/shared/src/platform/openspec.ts +91 -0
- package/packages/shared/src/platform/paths.ts +276 -0
- package/packages/shared/src/platform/process-identify.ts +126 -0
- package/packages/shared/src/platform/process-scan.ts +94 -0
- package/packages/shared/src/platform/process.ts +168 -0
- package/packages/shared/src/platform/runner.ts +369 -0
- package/packages/shared/src/platform/shell.ts +44 -0
- package/packages/shared/src/platform/spawn-mechanism.ts +124 -0
- package/packages/shared/src/platform/subprocess-adapter.ts +124 -0
- package/packages/shared/src/recommended-extensions.ts +18 -2
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +26 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/tool-registry/definitions.ts +342 -0
- package/packages/shared/src/tool-registry/index.ts +56 -0
- package/packages/shared/src/tool-registry/overrides.ts +118 -0
- package/packages/shared/src/tool-registry/registry.ts +262 -0
- package/packages/shared/src/tool-registry/strategies.ts +198 -0
- package/packages/shared/src/tool-registry/types.ts +180 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the provider-register hot-reload path.
|
|
3
|
+
*
|
|
4
|
+
* `reloadProviders(pi)` diffs the current providers.json against a
|
|
5
|
+
* last-registered snapshot and calls pi.registerProvider / pi.unregisterProvider
|
|
6
|
+
* as needed. Async model discovery is stubbed via a fetch mock.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
// We re-import the module fresh in each test so module-level `lastRegistered`
|
|
14
|
+
// state starts empty.
|
|
15
|
+
async function importFresh() {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
return (await import("../provider-register.js")) as typeof import("../provider-register.js");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeMockPi() {
|
|
21
|
+
const registerProvider = vi.fn();
|
|
22
|
+
const unregisterProvider = vi.fn();
|
|
23
|
+
const pi = {
|
|
24
|
+
registerProvider,
|
|
25
|
+
unregisterProvider,
|
|
26
|
+
events: { on: vi.fn(), emit: vi.fn() },
|
|
27
|
+
on: vi.fn(),
|
|
28
|
+
} as any;
|
|
29
|
+
return { pi, registerProvider, unregisterProvider };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeProvidersJson(home: string, providers: Record<string, any>) {
|
|
33
|
+
const dir = join(home, ".pi", "agent");
|
|
34
|
+
mkdirSync(dir, { recursive: true });
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(dir, "providers.json"),
|
|
37
|
+
JSON.stringify({ providers }, null, 2),
|
|
38
|
+
"utf-8",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("reloadProviders", () => {
|
|
43
|
+
let tmpHome: string;
|
|
44
|
+
const originalHome = process.env.HOME;
|
|
45
|
+
const originalFetch = globalThis.fetch;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
tmpHome = mkdtempSync(join(tmpdir(), "provider-reload-"));
|
|
49
|
+
process.env.HOME = tmpHome;
|
|
50
|
+
// Stub fetch to return 2 models so discovery succeeds cheaply
|
|
51
|
+
globalThis.fetch = vi.fn(async () =>
|
|
52
|
+
new Response(JSON.stringify({ data: [{ id: "m1" }, { id: "m2" }] }), { status: 200 }),
|
|
53
|
+
) as any;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
globalThis.fetch = originalFetch;
|
|
58
|
+
process.env.HOME = originalHome;
|
|
59
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("adds a new provider when providers.json gains an entry (snapshot was empty)", async () => {
|
|
63
|
+
const mod = await importFresh();
|
|
64
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
65
|
+
|
|
66
|
+
writeProvidersJson(tmpHome, {
|
|
67
|
+
"my-llm": {
|
|
68
|
+
baseUrl: "https://api.example.com/v1",
|
|
69
|
+
apiKey: "sk-abc",
|
|
70
|
+
api: "openai-completions",
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const diff = await mod.reloadProviders(pi);
|
|
75
|
+
expect(diff.added).toEqual(["my-llm"]);
|
|
76
|
+
expect(diff.removed).toEqual([]);
|
|
77
|
+
expect(diff.changed).toEqual([]);
|
|
78
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(registerProvider).toHaveBeenCalledWith("my-llm", expect.objectContaining({
|
|
80
|
+
baseUrl: "https://api.example.com/v1",
|
|
81
|
+
api: "openai-completions",
|
|
82
|
+
}));
|
|
83
|
+
expect(unregisterProvider).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("removes a provider when its entry disappears from providers.json", async () => {
|
|
87
|
+
const mod = await importFresh();
|
|
88
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
89
|
+
|
|
90
|
+
writeProvidersJson(tmpHome, {
|
|
91
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-completions" },
|
|
92
|
+
});
|
|
93
|
+
await mod.reloadProviders(pi);
|
|
94
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
95
|
+
|
|
96
|
+
writeProvidersJson(tmpHome, {});
|
|
97
|
+
const diff = await mod.reloadProviders(pi);
|
|
98
|
+
|
|
99
|
+
expect(diff.removed).toEqual(["my-llm"]);
|
|
100
|
+
expect(diff.added).toEqual([]);
|
|
101
|
+
expect(unregisterProvider).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(unregisterProvider).toHaveBeenCalledWith("my-llm");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("re-registers (unregister then register) when baseUrl changes", async () => {
|
|
106
|
+
const mod = await importFresh();
|
|
107
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
108
|
+
|
|
109
|
+
writeProvidersJson(tmpHome, {
|
|
110
|
+
"my-llm": { baseUrl: "https://old.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
111
|
+
});
|
|
112
|
+
await mod.reloadProviders(pi);
|
|
113
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
114
|
+
|
|
115
|
+
writeProvidersJson(tmpHome, {
|
|
116
|
+
"my-llm": { baseUrl: "https://new.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
117
|
+
});
|
|
118
|
+
const diff = await mod.reloadProviders(pi);
|
|
119
|
+
|
|
120
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
121
|
+
// unregister must be called before the second register
|
|
122
|
+
const unregOrder = unregisterProvider.mock.invocationCallOrder[0];
|
|
123
|
+
const reg2Order = registerProvider.mock.invocationCallOrder[1];
|
|
124
|
+
expect(unregOrder).toBeLessThan(reg2Order);
|
|
125
|
+
expect(registerProvider).toHaveBeenLastCalledWith(
|
|
126
|
+
"my-llm",
|
|
127
|
+
expect.objectContaining({ baseUrl: "https://new.example.com/v1" }),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("unchanged providers.json produces no register/unregister calls on second reload", async () => {
|
|
132
|
+
const mod = await importFresh();
|
|
133
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
134
|
+
|
|
135
|
+
writeProvidersJson(tmpHome, {
|
|
136
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-completions" },
|
|
137
|
+
});
|
|
138
|
+
await mod.reloadProviders(pi);
|
|
139
|
+
registerProvider.mockClear();
|
|
140
|
+
unregisterProvider.mockClear();
|
|
141
|
+
|
|
142
|
+
const diff = await mod.reloadProviders(pi);
|
|
143
|
+
expect(diff.added).toEqual([]);
|
|
144
|
+
expect(diff.removed).toEqual([]);
|
|
145
|
+
expect(diff.changed).toEqual([]);
|
|
146
|
+
expect(registerProvider).not.toHaveBeenCalled();
|
|
147
|
+
expect(unregisterProvider).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("malformed providers.json returns empty diff and does not throw", async () => {
|
|
151
|
+
const mod = await importFresh();
|
|
152
|
+
const { pi } = makeMockPi();
|
|
153
|
+
|
|
154
|
+
const dir = join(tmpHome, ".pi", "agent");
|
|
155
|
+
mkdirSync(dir, { recursive: true });
|
|
156
|
+
writeFileSync(join(dir, "providers.json"), "not valid json {", "utf-8");
|
|
157
|
+
|
|
158
|
+
await expect(mod.reloadProviders(pi)).resolves.toEqual({
|
|
159
|
+
added: [],
|
|
160
|
+
removed: [],
|
|
161
|
+
changed: [],
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("treats apiKey change as 'changed'", async () => {
|
|
166
|
+
const mod = await importFresh();
|
|
167
|
+
const { pi, registerProvider, unregisterProvider } = makeMockPi();
|
|
168
|
+
|
|
169
|
+
writeProvidersJson(tmpHome, {
|
|
170
|
+
"my-llm": { baseUrl: "https://x", apiKey: "old", api: "openai-completions" },
|
|
171
|
+
});
|
|
172
|
+
await mod.reloadProviders(pi);
|
|
173
|
+
|
|
174
|
+
writeProvidersJson(tmpHome, {
|
|
175
|
+
"my-llm": { baseUrl: "https://x", apiKey: "new", api: "openai-completions" },
|
|
176
|
+
});
|
|
177
|
+
const diff = await mod.reloadProviders(pi);
|
|
178
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
179
|
+
expect(unregisterProvider).toHaveBeenCalledWith("my-llm");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("treats api type change as 'changed'", async () => {
|
|
183
|
+
const mod = await importFresh();
|
|
184
|
+
const { pi } = makeMockPi();
|
|
185
|
+
|
|
186
|
+
writeProvidersJson(tmpHome, {
|
|
187
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-completions" },
|
|
188
|
+
});
|
|
189
|
+
await mod.reloadProviders(pi);
|
|
190
|
+
|
|
191
|
+
writeProvidersJson(tmpHome, {
|
|
192
|
+
"my-llm": { baseUrl: "https://x", apiKey: "k", api: "openai-responses" },
|
|
193
|
+
});
|
|
194
|
+
const diff = await mod.reloadProviders(pi);
|
|
195
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ── input capability default (see change: enable-image-input-custom-providers) ─────
|
|
199
|
+
// Every discovered model must advertise `input: ["text", "image"]` so pi-ai does
|
|
200
|
+
// not strip pasted images via `downgradeUnsupportedImages` before the request
|
|
201
|
+
// leaves the bridge. This guards both the initial-registration path and the
|
|
202
|
+
// reloadProviders() re-register path.
|
|
203
|
+
|
|
204
|
+
it("discovered models default to input: [\"text\", \"image\"] on fresh register", async () => {
|
|
205
|
+
const mod = await importFresh();
|
|
206
|
+
const { pi, registerProvider } = makeMockPi();
|
|
207
|
+
|
|
208
|
+
writeProvidersJson(tmpHome, {
|
|
209
|
+
"my-llm": {
|
|
210
|
+
baseUrl: "https://api.example.com/v1",
|
|
211
|
+
apiKey: "sk-abc",
|
|
212
|
+
api: "openai-completions",
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await mod.reloadProviders(pi);
|
|
217
|
+
|
|
218
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
219
|
+
const [, config] = registerProvider.mock.calls[0];
|
|
220
|
+
expect(Array.isArray(config.models)).toBe(true);
|
|
221
|
+
expect(config.models.length).toBe(2); // two models from the fetch stub
|
|
222
|
+
for (const m of config.models) {
|
|
223
|
+
expect(m.input).toEqual(["text", "image"]);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("re-registered models (after reload diff) retain input: [\"text\", \"image\"]", async () => {
|
|
228
|
+
const mod = await importFresh();
|
|
229
|
+
const { pi, registerProvider } = makeMockPi();
|
|
230
|
+
|
|
231
|
+
// Initial register
|
|
232
|
+
writeProvidersJson(tmpHome, {
|
|
233
|
+
"my-llm": { baseUrl: "https://old.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
234
|
+
});
|
|
235
|
+
await mod.reloadProviders(pi);
|
|
236
|
+
|
|
237
|
+
// Change baseUrl → triggers unregister + re-register
|
|
238
|
+
writeProvidersJson(tmpHome, {
|
|
239
|
+
"my-llm": { baseUrl: "https://new.example.com/v1", apiKey: "k", api: "openai-completions" },
|
|
240
|
+
});
|
|
241
|
+
const diff = await mod.reloadProviders(pi);
|
|
242
|
+
expect(diff.changed).toEqual(["my-llm"]);
|
|
243
|
+
|
|
244
|
+
// Second registerProvider call is the re-register; must carry same input default
|
|
245
|
+
expect(registerProvider).toHaveBeenCalledTimes(2);
|
|
246
|
+
const [, secondConfig] = registerProvider.mock.calls[1];
|
|
247
|
+
for (const m of secondConfig.models) {
|
|
248
|
+
expect(m.input).toEqual(["text", "image"]);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── model metadata enrichment (see change: enrich-custom-provider-model-metadata) ──
|
|
253
|
+
// registerEntry() resolves per-model metadata via enrichModelMetadata() which
|
|
254
|
+
// consults pi-ai's bundled catalog. These tests verify the end-to-end path:
|
|
255
|
+
// discovered id → catalog match → registerProvider receives accurate fields.
|
|
256
|
+
|
|
257
|
+
it("captures ctx.modelRegistry from session_start and re-registers providers with enriched metadata", async () => {
|
|
258
|
+
const mod = await importFresh();
|
|
259
|
+
const { pi, registerProvider } = makeMockPi();
|
|
260
|
+
|
|
261
|
+
// Capture the session_start handler registered by activate().
|
|
262
|
+
const handlers = new Map<string, (event: any, ctx: any) => Promise<void> | void>();
|
|
263
|
+
pi.on = vi.fn((event: string, handler: any) => {
|
|
264
|
+
handlers.set(event, handler);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Fake pi's ModelRegistry. `find(provider, id)` returns Opus 4.7's real
|
|
268
|
+
// metadata for the anthropic entry, simulating what pi passes via
|
|
269
|
+
// ctx.modelRegistry at session_start.
|
|
270
|
+
const fakeRegistry = {
|
|
271
|
+
find: vi.fn((provider: string, id: string) => {
|
|
272
|
+
if (provider === "anthropic" && id === "claude-opus-4-7") {
|
|
273
|
+
return {
|
|
274
|
+
contextWindow: 1_000_000,
|
|
275
|
+
maxTokens: 128_000,
|
|
276
|
+
reasoning: true,
|
|
277
|
+
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
|
|
278
|
+
input: ["text", "image"],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// Fetch mock advertises the real model id.
|
|
286
|
+
globalThis.fetch = vi.fn(async () =>
|
|
287
|
+
new Response(
|
|
288
|
+
JSON.stringify({ data: [{ id: "cc/claude-opus-4-7" }] }),
|
|
289
|
+
{ status: 200 },
|
|
290
|
+
),
|
|
291
|
+
) as any;
|
|
292
|
+
|
|
293
|
+
writeProvidersJson(tmpHome, {
|
|
294
|
+
proxy: {
|
|
295
|
+
baseUrl: "https://llmproxy.example.com/v1",
|
|
296
|
+
apiKey: "sk-test",
|
|
297
|
+
api: "anthropic-messages",
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// 1) activate() registers the provider BEFORE ctx.modelRegistry is
|
|
302
|
+
// available — first registration hits the fallback path.
|
|
303
|
+
mod.activate(pi);
|
|
304
|
+
// Wait a microtask for the fire-and-forget registerEntry inside activate.
|
|
305
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
306
|
+
|
|
307
|
+
expect(registerProvider).toHaveBeenCalledTimes(1);
|
|
308
|
+
const firstCall = registerProvider.mock.calls[0];
|
|
309
|
+
expect(firstCall[1].models[0].contextWindow).toBe(200_000); // fallback
|
|
310
|
+
|
|
311
|
+
// 2) Fire session_start with ctx.modelRegistry — the handler captures it
|
|
312
|
+
// and re-registers all known providers with the enriched metadata.
|
|
313
|
+
const sessionStartHandler = handlers.get("session_start");
|
|
314
|
+
expect(sessionStartHandler).toBeDefined();
|
|
315
|
+
await sessionStartHandler!({ type: "session_start" }, {
|
|
316
|
+
ui: { notify: vi.fn() },
|
|
317
|
+
modelRegistry: fakeRegistry,
|
|
318
|
+
model: undefined,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// 3) The re-registration should have issued a second registerProvider
|
|
322
|
+
// call, this time with the catalog-enriched metadata.
|
|
323
|
+
expect(registerProvider).toHaveBeenCalledTimes(2);
|
|
324
|
+
const secondCall = registerProvider.mock.calls[1];
|
|
325
|
+
const [name, config] = secondCall;
|
|
326
|
+
expect(name).toBe("proxy");
|
|
327
|
+
const [opus] = config.models;
|
|
328
|
+
expect(opus.id).toBe("cc/claude-opus-4-7");
|
|
329
|
+
expect(opus.contextWindow).toBe(1_000_000);
|
|
330
|
+
expect(opus.maxTokens).toBe(128_000);
|
|
331
|
+
expect(opus.reasoning).toBe(true);
|
|
332
|
+
expect(opus.cost.input).toBe(5);
|
|
333
|
+
expect(opus.cost.output).toBe(25);
|
|
334
|
+
// Probed with prefix-stripped bare id under `anthropic`.
|
|
335
|
+
expect(fakeRegistry.find).toHaveBeenCalledWith("anthropic", "claude-opus-4-7");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("without modelRegistry (never captured), discovered models fall back to api-appropriate defaults", async () => {
|
|
339
|
+
const mod = await importFresh();
|
|
340
|
+
const { pi, registerProvider } = makeMockPi();
|
|
341
|
+
|
|
342
|
+
// No session_start fires — modelRegistryRef stays null and the probe is
|
|
343
|
+
// null, so every discovered id hits the fallback table.
|
|
344
|
+
globalThis.fetch = vi.fn(async () =>
|
|
345
|
+
new Response(
|
|
346
|
+
JSON.stringify({ data: [{ id: "cc/claude-opus-4-7" }] }),
|
|
347
|
+
{ status: 200 },
|
|
348
|
+
),
|
|
349
|
+
) as any;
|
|
350
|
+
|
|
351
|
+
writeProvidersJson(tmpHome, {
|
|
352
|
+
proxy: {
|
|
353
|
+
baseUrl: "https://llmproxy.example.com/v1",
|
|
354
|
+
apiKey: "sk-test",
|
|
355
|
+
api: "anthropic-messages",
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await mod.reloadProviders(pi);
|
|
360
|
+
|
|
361
|
+
const [, config] = registerProvider.mock.calls[0];
|
|
362
|
+
const [opus] = config.models;
|
|
363
|
+
// anthropic-messages fallback: 200k / 64k / no reasoning / zero cost / text+image.
|
|
364
|
+
expect(opus.contextWindow).toBe(200_000);
|
|
365
|
+
expect(opus.maxTokens).toBe(64_000);
|
|
366
|
+
expect(opus.reasoning).toBe(false);
|
|
367
|
+
expect(opus.input).toEqual(["text", "image"]);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("discovered unknown model falls back to api-appropriate defaults (openai-completions → 128k)", async () => {
|
|
371
|
+
const mod = await importFresh();
|
|
372
|
+
const { pi, registerProvider } = makeMockPi();
|
|
373
|
+
|
|
374
|
+
// Default beforeEach fetch stub returns `m1` and `m2` — neither exists in catalog.
|
|
375
|
+
writeProvidersJson(tmpHome, {
|
|
376
|
+
"my-llm": {
|
|
377
|
+
baseUrl: "https://api.example.com/v1",
|
|
378
|
+
apiKey: "sk-abc",
|
|
379
|
+
api: "openai-completions",
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await mod.reloadProviders(pi);
|
|
384
|
+
|
|
385
|
+
const [, config] = registerProvider.mock.calls[0];
|
|
386
|
+
for (const m of config.models) {
|
|
387
|
+
expect(m.contextWindow).toBe(128_000);
|
|
388
|
+
expect(m.maxTokens).toBe(16_384);
|
|
389
|
+
expect(m.reasoning).toBe(false);
|
|
390
|
+
expect(m.cost).toEqual({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0 });
|
|
391
|
+
expect(m.input).toEqual(["text", "image"]);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
});
|
|
@@ -112,10 +112,13 @@ describe("autoStartServer", () => {
|
|
|
112
112
|
|
|
113
113
|
const result = await autoStartServer(baseConfig, deps);
|
|
114
114
|
|
|
115
|
-
expect(deps.notify).
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
)
|
|
115
|
+
expect(deps.notify).toHaveBeenCalledTimes(1);
|
|
116
|
+
const [msg, level] = (deps.notify as any).mock.calls[0];
|
|
117
|
+
expect(msg).toMatch(/Dashboard server failed to start: exited/);
|
|
118
|
+
// Spec requirement (fix-windows-server-parity): failure notification
|
|
119
|
+
// MUST include the absolute path to ~/.pi/dashboard/server.log.
|
|
120
|
+
expect(msg).toMatch(/server\.log/);
|
|
121
|
+
expect(level).toBe("warning");
|
|
119
122
|
expect(result.server).toBeUndefined();
|
|
120
123
|
});
|
|
121
124
|
|
|
@@ -164,4 +167,92 @@ describe("autoStartServer", () => {
|
|
|
164
167
|
|
|
165
168
|
expect(result.server).toEqual({ host: "myhost.local", port: 8000, piPort: 9999 });
|
|
166
169
|
});
|
|
170
|
+
|
|
171
|
+
describe("onLaunchStart / onLaunchEnd callbacks", () => {
|
|
172
|
+
it("fires onLaunchStart then onLaunchEnd(true) when launch succeeds", async () => {
|
|
173
|
+
const onLaunchStart = vi.fn();
|
|
174
|
+
const onLaunchEnd = vi.fn();
|
|
175
|
+
const deps = makeDeps({
|
|
176
|
+
launchServer: vi.fn().mockResolvedValue({ success: true, message: "ok" }),
|
|
177
|
+
onLaunchStart,
|
|
178
|
+
onLaunchEnd,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await autoStartServer(baseConfig, deps);
|
|
182
|
+
|
|
183
|
+
expect(onLaunchStart).toHaveBeenCalledTimes(1);
|
|
184
|
+
expect(onLaunchEnd).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(onLaunchEnd).toHaveBeenCalledWith(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("fires onLaunchStart then onLaunchEnd(false) when launch fails", async () => {
|
|
189
|
+
const onLaunchStart = vi.fn();
|
|
190
|
+
const onLaunchEnd = vi.fn();
|
|
191
|
+
const deps = makeDeps({
|
|
192
|
+
launchServer: vi.fn().mockResolvedValue({ success: false, message: "boom" }),
|
|
193
|
+
isDashboardRunning: vi.fn().mockResolvedValue({ running: false }),
|
|
194
|
+
onLaunchStart,
|
|
195
|
+
onLaunchEnd,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await autoStartServer(baseConfig, deps);
|
|
199
|
+
|
|
200
|
+
expect(onLaunchStart).toHaveBeenCalledTimes(1);
|
|
201
|
+
expect(onLaunchEnd).toHaveBeenCalledTimes(1);
|
|
202
|
+
expect(onLaunchEnd).toHaveBeenCalledWith(false);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("fires onLaunchEnd(true) when launch fails but recheck finds running server", async () => {
|
|
206
|
+
// Race scenario: another agent started the server during our launch attempt.
|
|
207
|
+
const onLaunchStart = vi.fn();
|
|
208
|
+
const onLaunchEnd = vi.fn();
|
|
209
|
+
const deps = makeDeps({
|
|
210
|
+
launchServer: vi.fn().mockResolvedValue({ success: false, message: "EADDRINUSE" }),
|
|
211
|
+
isDashboardRunning: vi.fn()
|
|
212
|
+
.mockResolvedValueOnce({ running: false }) // before launch
|
|
213
|
+
.mockResolvedValueOnce({ running: true }), // after launch (recheck)
|
|
214
|
+
onLaunchStart,
|
|
215
|
+
onLaunchEnd,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
await autoStartServer(baseConfig, deps);
|
|
219
|
+
|
|
220
|
+
expect(onLaunchStart).toHaveBeenCalledTimes(1);
|
|
221
|
+
expect(onLaunchEnd).toHaveBeenCalledWith(true);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("does NOT fire onLaunchStart when mDNS finds a local server (no launch happens)", async () => {
|
|
225
|
+
const onLaunchStart = vi.fn();
|
|
226
|
+
const onLaunchEnd = vi.fn();
|
|
227
|
+
const local: DiscoveredServer = {
|
|
228
|
+
host: "localhost", port: 8000, piPort: 9999,
|
|
229
|
+
isLocal: true, source: "mdns",
|
|
230
|
+
};
|
|
231
|
+
const deps = makeDeps({
|
|
232
|
+
discoverDashboard: vi.fn().mockResolvedValue([local]),
|
|
233
|
+
onLaunchStart,
|
|
234
|
+
onLaunchEnd,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await autoStartServer(baseConfig, deps);
|
|
238
|
+
|
|
239
|
+
expect(onLaunchStart).not.toHaveBeenCalled();
|
|
240
|
+
expect(onLaunchEnd).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("does NOT fire onLaunchStart when health check finds an already-running server", async () => {
|
|
244
|
+
const onLaunchStart = vi.fn();
|
|
245
|
+
const onLaunchEnd = vi.fn();
|
|
246
|
+
const deps = makeDeps({
|
|
247
|
+
isDashboardRunning: vi.fn().mockResolvedValue({ running: true }),
|
|
248
|
+
onLaunchStart,
|
|
249
|
+
onLaunchEnd,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await autoStartServer(baseConfig, deps);
|
|
253
|
+
|
|
254
|
+
expect(onLaunchStart).not.toHaveBeenCalled();
|
|
255
|
+
expect(onLaunchEnd).not.toHaveBeenCalled();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
167
258
|
});
|
|
@@ -17,6 +17,22 @@ describe("server-launcher", () => {
|
|
|
17
17
|
it("should point to a file that actually exists on disk", () => {
|
|
18
18
|
expect(existsSync(resolveServerCliPath())).toBe(true);
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
it("uses require.resolve so it adapts to installed layout", () => {
|
|
22
|
+
// Regression: the monorepo-relative path math
|
|
23
|
+
// (`<extension>/../../server/src/cli.ts`) produced
|
|
24
|
+
// `<scope>/server/src/cli.ts` instead of
|
|
25
|
+
// `<scope>/pi-dashboard-server/src/cli.ts` when the extension
|
|
26
|
+
// was installed into `node_modules/@blackbelt-technology/`. The
|
|
27
|
+
// resolver must locate the server via package name, not sibling
|
|
28
|
+
// path arithmetic.
|
|
29
|
+
const cliPath = resolveServerCliPath();
|
|
30
|
+
// Either layout is fine; we just must NOT produce the broken
|
|
31
|
+
// `@blackbelt-technology/server/src/cli.ts` shape.
|
|
32
|
+
expect(cliPath).not.toMatch(/@blackbelt-technology[\\/]+server[\\/]+src[\\/]+cli\.ts$/);
|
|
33
|
+
// And must land on pi-dashboard-server (installed) or packages/server (dev).
|
|
34
|
+
expect(cliPath).toMatch(/(pi-dashboard-server|packages[\\/]+server)[\\/]+src[\\/]+cli\.ts$/);
|
|
35
|
+
});
|
|
20
36
|
});
|
|
21
37
|
|
|
22
38
|
describe("buildSpawnArgs", () => {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* forwards all pi events, and relays commands back.
|
|
6
6
|
*/
|
|
7
7
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
import { Loader } from "@mariozechner/pi-tui";
|
|
8
9
|
import { ConnectionManager } from "./connection.js";
|
|
9
10
|
import { detectSessionSource } from "./source-detector.js";
|
|
10
11
|
import { mapEventToProtocol } from "./event-forwarder.js";
|
|
@@ -25,7 +26,7 @@ import { expandPromptTemplateFromDisk } from "./prompt-expander.js";
|
|
|
25
26
|
import { PromptBus } from "./prompt-bus.js";
|
|
26
27
|
import { DashboardDefaultAdapter } from "./dashboard-default-adapter.js";
|
|
27
28
|
import { registerAskUserTool } from "./ask-user-tool.js";
|
|
28
|
-
import { activate as activateProviderRegister, onProviderChanged } from "./provider-register.js";
|
|
29
|
+
import { activate as activateProviderRegister, onProviderChanged, reloadProviders } from "./provider-register.js";
|
|
29
30
|
import type { FlowInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
30
31
|
import { startMetricsMonitor, stopMetricsMonitor, collectMetrics } from "./process-metrics.js";
|
|
31
32
|
import { scanChildProcesses } from "./process-scanner.js";
|
|
@@ -216,6 +217,17 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
216
217
|
// Reload auth credentials when dashboard notifies of changes
|
|
217
218
|
if (msg.type === "credentials_updated") {
|
|
218
219
|
try {
|
|
220
|
+
// Hot-reload providers.json diff BEFORE refreshing the registry,
|
|
221
|
+
// so any newly added providers are registered before getAvailable() runs.
|
|
222
|
+
const diff = await reloadProviders(pi).catch((err) => {
|
|
223
|
+
console.error("[dashboard] reloadProviders failed:", err);
|
|
224
|
+
return { added: [], removed: [], changed: [] };
|
|
225
|
+
});
|
|
226
|
+
if (diff.added.length || diff.removed.length || diff.changed.length) {
|
|
227
|
+
console.log(
|
|
228
|
+
`[dashboard] hot-reloaded providers: added=${JSON.stringify(diff.added)} removed=${JSON.stringify(diff.removed)} changed=${JSON.stringify(diff.changed)}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
219
231
|
cachedModelRegistry?.authStorage?.reload?.();
|
|
220
232
|
cachedModelRegistry?.refresh?.();
|
|
221
233
|
} catch (err) { console.error("[dashboard] credentials reload failed:", err); }
|
|
@@ -951,17 +963,72 @@ function initBridge(pi: ExtensionAPI) {
|
|
|
951
963
|
}
|
|
952
964
|
|
|
953
965
|
// Discover or auto-start server (non-blocking — connection will reconnect)
|
|
966
|
+
//
|
|
967
|
+
// When a real launchServer() is about to run (not on mDNS/health-check
|
|
968
|
+
// paths), mount an animated TUI widget above the editor using pi-tui's
|
|
969
|
+
// Loader (a real Component, self-animating at 80ms, like pi-flows'
|
|
970
|
+
// architect-widget). The previous implementation used
|
|
971
|
+
// ctx.ui.setStatus(...) which only writes a footer string and relies on
|
|
972
|
+
// the TUI render loop being ticked elsewhere — on the cold-start path
|
|
973
|
+
// nothing else requests renders, so the spinner never animated and often
|
|
974
|
+
// never appeared. setWidget(key, factory, {placement:"aboveEditor"}) gives
|
|
975
|
+
// us a managed component that owns its own render loop and is always
|
|
976
|
+
// visible while the launch is in flight.
|
|
977
|
+
let spinnerTimer: NodeJS.Timeout | null = null;
|
|
978
|
+
let spinnerStart = 0;
|
|
979
|
+
let activeLoader: Loader | null = null;
|
|
980
|
+
const stopSpinner = () => {
|
|
981
|
+
if (spinnerTimer) {
|
|
982
|
+
clearInterval(spinnerTimer);
|
|
983
|
+
spinnerTimer = null;
|
|
984
|
+
}
|
|
985
|
+
activeLoader = null;
|
|
986
|
+
ctx.ui.setWidget("pi-dashboard-launch", undefined);
|
|
987
|
+
};
|
|
954
988
|
autoStartServer(config, {
|
|
955
989
|
discoverDashboard,
|
|
956
990
|
isDashboardRunning,
|
|
957
991
|
launchServer,
|
|
958
992
|
notify: (msg, level) => ctx.ui.notify(msg, level),
|
|
993
|
+
onLaunchStart: () => {
|
|
994
|
+
spinnerStart = Date.now();
|
|
995
|
+
const buildMessage = () => {
|
|
996
|
+
const elapsed = Math.floor((Date.now() - spinnerStart) / 1000);
|
|
997
|
+
return `starting dashboard server … (${elapsed}s)`;
|
|
998
|
+
};
|
|
999
|
+
ctx.ui.setWidget(
|
|
1000
|
+
"pi-dashboard-launch",
|
|
1001
|
+
(tui: unknown, theme: { fg: (role: string, s: string) => string }) => {
|
|
1002
|
+
const loader = new Loader(
|
|
1003
|
+
tui as ConstructorParameters<typeof Loader>[0],
|
|
1004
|
+
(s: string) => theme.fg("accent", s),
|
|
1005
|
+
(s: string) => theme.fg("muted", s),
|
|
1006
|
+
buildMessage(),
|
|
1007
|
+
);
|
|
1008
|
+
activeLoader = loader;
|
|
1009
|
+
// Loader has stop() but no dispose(); wire dispose so that
|
|
1010
|
+
// setExtensionWidget's teardown stops the 80ms animation interval.
|
|
1011
|
+
(loader as Loader & { dispose?: () => void }).dispose = () => loader.stop();
|
|
1012
|
+
return loader;
|
|
1013
|
+
},
|
|
1014
|
+
{ placement: "aboveEditor" },
|
|
1015
|
+
);
|
|
1016
|
+
// Refresh the elapsed-seconds label every second. Frame animation is
|
|
1017
|
+
// driven by the Loader's own 80ms interval.
|
|
1018
|
+
spinnerTimer = setInterval(() => {
|
|
1019
|
+
activeLoader?.setMessage(buildMessage());
|
|
1020
|
+
}, 1000);
|
|
1021
|
+
},
|
|
1022
|
+
onLaunchEnd: () => {
|
|
1023
|
+
stopSpinner();
|
|
1024
|
+
},
|
|
959
1025
|
}).then((result) => {
|
|
1026
|
+
stopSpinner(); // safety net — covers onLaunchEnd not firing
|
|
960
1027
|
if (result.server && result.server.piPort !== config.piPort) {
|
|
961
1028
|
// Server found on a different piPort than configured — update connection URL
|
|
962
1029
|
connection.updateUrl(`ws://${result.server.host === 'localhost' ? 'localhost' : result.server.host}:${result.server.piPort}`);
|
|
963
1030
|
}
|
|
964
|
-
}).catch(() => {});
|
|
1031
|
+
}).catch(() => { stopSpinner(); });
|
|
965
1032
|
|
|
966
1033
|
// Send initial git info
|
|
967
1034
|
sendGitInfoIfChanged(ctx.cwd);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Dev build-on-reload helper.
|
|
3
3
|
* Builds the Vite client and requests server shutdown.
|
|
4
4
|
*/
|
|
5
|
-
import { execSync as defaultExecSync } from "
|
|
5
|
+
import { execSync as defaultExecSync } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
6
6
|
|
|
7
7
|
export interface DevBuildOptions {
|
|
8
8
|
packageRoot: string;
|