@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
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Git info gathering — detects branch, remote URL, and PR number.
|
|
3
|
+
* Delegates to the shared git tool module so there's no inline execSync
|
|
4
|
+
* and every call benefits from the runner's safety defaults (windowsHide,
|
|
5
|
+
* timeout, tolerated exit codes).
|
|
6
|
+
* See change: platform-command-executor.
|
|
3
7
|
*/
|
|
4
|
-
import
|
|
8
|
+
import * as git from "@blackbelt-technology/pi-dashboard-shared/platform/git.js";
|
|
5
9
|
import { buildGitLinks, type GitLinks } from "./git-link-builder.js";
|
|
6
10
|
|
|
7
11
|
export interface GitInfo {
|
|
@@ -11,39 +15,25 @@ export interface GitInfo {
|
|
|
11
15
|
gitPrUrl?: string;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
/** Run a shell command and return trimmed stdout, or undefined on failure. */
|
|
15
|
-
function runGit(command: string, cwd: string): string | undefined {
|
|
16
|
-
try {
|
|
17
|
-
return execSync(command, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 5000 }).trim();
|
|
18
|
-
} catch {
|
|
19
|
-
return undefined;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
18
|
/** Detect the current git branch. Returns short SHA for detached HEAD. */
|
|
24
19
|
export function detectBranch(cwd: string): string | undefined {
|
|
25
|
-
const ref =
|
|
20
|
+
const ref = git.currentBranchOr({ cwd });
|
|
26
21
|
if (!ref) return undefined;
|
|
27
22
|
if (ref === "HEAD") {
|
|
28
23
|
// Detached HEAD — return short commit SHA
|
|
29
|
-
return
|
|
24
|
+
return git.headShaOr({ cwd, short: true }) ?? "HEAD";
|
|
30
25
|
}
|
|
31
26
|
return ref;
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
/** Detect the remote origin URL. */
|
|
35
30
|
export function detectRemoteUrl(cwd: string): string | undefined {
|
|
36
|
-
return
|
|
31
|
+
return git.remoteUrlOr({ cwd });
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
/** Detect the PR number via gh CLI (best effort). */
|
|
40
35
|
export function detectPrNumber(cwd: string): number | undefined {
|
|
41
|
-
|
|
42
|
-
if (result) {
|
|
43
|
-
const num = parseInt(result, 10);
|
|
44
|
-
if (!isNaN(num)) return num;
|
|
45
|
-
}
|
|
46
|
-
return undefined;
|
|
36
|
+
return git.prNumberOr({ cwd });
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
/** Gather all git info for a directory. Returns undefined if not a git repo. */
|
|
@@ -30,6 +30,7 @@ declare module "@oh-my-pi/pi-coding-agent" {
|
|
|
30
30
|
registerCommand(name: string, options: { description?: string; handler: (args: string, ctx: any) => Promise<void> }): void;
|
|
31
31
|
registerTool(tool: any): void;
|
|
32
32
|
registerProvider(name: string, config: any): void;
|
|
33
|
+
unregisterProvider(name: string): void;
|
|
33
34
|
exec(command: string, args: string[], options?: { timeout?: number }): Promise<{ stdout: string; stderr: string; exitCode: number }>;
|
|
34
35
|
events: EventBus;
|
|
35
36
|
}
|
|
@@ -12,8 +12,43 @@
|
|
|
12
12
|
* This handles the reparenting problem: children get reparented to PID 1
|
|
13
13
|
* when the bash wrapper exits, but we captured their PGIDs while alive.
|
|
14
14
|
*/
|
|
15
|
-
import { spawnSync as defaultSpawnSync } from "
|
|
16
|
-
import type { SpawnSyncReturns } from "
|
|
15
|
+
import { spawnSync as defaultSpawnSync } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
16
|
+
import type { SpawnSyncReturns } from "@blackbelt-technology/pi-dashboard-shared/platform/exec.js";
|
|
17
|
+
import { getDefaultRegistry } from "@blackbelt-technology/pi-dashboard-shared/tool-registry/index.js";
|
|
18
|
+
import { killPidWithGroup } from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a Windows system tool name (wmic / powershell / tasklist /
|
|
22
|
+
* taskkill) to its full `.exe` path via the global tool registry. If
|
|
23
|
+
* the registry lookup fails we fall back to the bare name and let
|
|
24
|
+
* `spawnSync` do PATHEXT resolution.
|
|
25
|
+
*
|
|
26
|
+
* Spawning the FULL path bypasses any cmd.exe / PATHEXT resolution
|
|
27
|
+
* layers, keeping `windowsHide: true` honored end-to-end. See change:
|
|
28
|
+
* consolidate-windows-spawn-and-platform-handlers.
|
|
29
|
+
*
|
|
30
|
+
* Uses `getDefaultRegistry` (not `peek*`) because the bridge extension
|
|
31
|
+
* runs inside pi's process and may be the FIRST caller to construct
|
|
32
|
+
* the registry in that process. Idempotent and cached per-process.
|
|
33
|
+
*/
|
|
34
|
+
const systemToolCache = new Map<string, string>();
|
|
35
|
+
function resolveSystemTool(name: string): string {
|
|
36
|
+
const cached = systemToolCache.get(name);
|
|
37
|
+
if (cached) return cached;
|
|
38
|
+
try {
|
|
39
|
+
const reg = getDefaultRegistry();
|
|
40
|
+
if (reg.has(name)) {
|
|
41
|
+
const res = reg.resolve(name);
|
|
42
|
+
if (res.ok && res.path) {
|
|
43
|
+
systemToolCache.set(name, res.path);
|
|
44
|
+
return res.path;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch { /* registry unavailable in some test contexts */ }
|
|
48
|
+
// Cache the bare name so we only miss once per process.
|
|
49
|
+
systemToolCache.set(name, name);
|
|
50
|
+
return name;
|
|
51
|
+
}
|
|
17
52
|
|
|
18
53
|
export interface ChildProcessInfo {
|
|
19
54
|
pid: number;
|
|
@@ -24,36 +59,12 @@ export interface ChildProcessInfo {
|
|
|
24
59
|
|
|
25
60
|
/**
|
|
26
61
|
* Parse ps ETIME format into milliseconds.
|
|
27
|
-
*
|
|
62
|
+
* Re-exported from the shared platform primitive to keep the public API of
|
|
63
|
+
* this module stable while centralizing the pure helper.
|
|
64
|
+
* See change: consolidate-platform-handlers.
|
|
28
65
|
*/
|
|
29
|
-
export
|
|
30
|
-
|
|
31
|
-
if (!trimmed) return 0;
|
|
32
|
-
|
|
33
|
-
let days = 0;
|
|
34
|
-
let rest = trimmed;
|
|
35
|
-
|
|
36
|
-
const dashIdx = rest.indexOf("-");
|
|
37
|
-
if (dashIdx !== -1) {
|
|
38
|
-
days = parseInt(rest.slice(0, dashIdx), 10);
|
|
39
|
-
if (isNaN(days)) return 0;
|
|
40
|
-
rest = rest.slice(dashIdx + 1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const parts = rest.split(":").map((p) => parseInt(p, 10));
|
|
44
|
-
if (parts.some(isNaN)) return 0;
|
|
45
|
-
|
|
46
|
-
let hours = 0, minutes = 0, seconds = 0;
|
|
47
|
-
if (parts.length === 3) {
|
|
48
|
-
[hours, minutes, seconds] = parts;
|
|
49
|
-
} else if (parts.length === 2) {
|
|
50
|
-
[minutes, seconds] = parts;
|
|
51
|
-
} else {
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return ((days * 86400) + (hours * 3600) + (minutes * 60) + seconds) * 1000;
|
|
56
|
-
}
|
|
66
|
+
export { parseEtime } from "@blackbelt-technology/pi-dashboard-shared/platform/process-scan.js";
|
|
67
|
+
import { parseEtime } from "@blackbelt-technology/pi-dashboard-shared/platform/process-scan.js";
|
|
57
68
|
|
|
58
69
|
const DEFAULT_MIN_ELAPSED_MS = 30_000;
|
|
59
70
|
|
|
@@ -112,7 +123,8 @@ export function captureChildPgids(
|
|
|
112
123
|
trackedPgids: Set<number>,
|
|
113
124
|
options?: ScanOptions,
|
|
114
125
|
): void {
|
|
115
|
-
|
|
126
|
+
const platform = (options as any)?._platform ?? process.platform;
|
|
127
|
+
if (platform === "win32") return;
|
|
116
128
|
|
|
117
129
|
const spawnSync: SpawnSyncFn = options?._spawnSync ?? defaultSpawnSync;
|
|
118
130
|
|
|
@@ -244,7 +256,9 @@ export function killProcessByPgid(pgid: number, options?: ScanOptions): boolean
|
|
|
244
256
|
return killWindowsProcess(pgid, options);
|
|
245
257
|
}
|
|
246
258
|
try {
|
|
247
|
-
|
|
259
|
+
// Route through the platform helper so the pid → -pgid mapping stays
|
|
260
|
+
// in one place. See change: route-kill-paths-through-platform.
|
|
261
|
+
killPidWithGroup(pgid, "SIGTERM", { platform });
|
|
248
262
|
return true;
|
|
249
263
|
} catch {
|
|
250
264
|
return false;
|
|
@@ -282,9 +296,20 @@ function wmicDateToElapsedMs(creationDate: string): number {
|
|
|
282
296
|
function getWindowsDescendants(parentPid: number, spawnSync: SpawnSyncFn): ChildProcessInfo[] {
|
|
283
297
|
try {
|
|
284
298
|
const result = spawnSync(
|
|
285
|
-
"wmic",
|
|
299
|
+
resolveSystemTool("wmic"),
|
|
286
300
|
["process", "where", `ParentProcessId=${parentPid}`, "get", "CommandLine,CreationDate,ParentProcessId,ProcessId", "/format:list"],
|
|
287
|
-
{
|
|
301
|
+
{
|
|
302
|
+
encoding: "utf-8",
|
|
303
|
+
timeout: 5000,
|
|
304
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
305
|
+
// Critical: without this, every 10-second process scan flashes a
|
|
306
|
+
// console window. wmic is deprecated on Win 11 but still the
|
|
307
|
+
// default here; PowerShell fallback also needs windowsHide.
|
|
308
|
+
// Also: `resolveSystemTool` above returns the FULL .exe path
|
|
309
|
+
// when the registry is available, so there's no PATHEXT /
|
|
310
|
+
// cmd.exe resolution layer that could leak a console.
|
|
311
|
+
windowsHide: true,
|
|
312
|
+
},
|
|
288
313
|
);
|
|
289
314
|
if (result.status !== 0 || !result.stdout) {
|
|
290
315
|
// wmic removed in newer Windows 11 — fallback to tasklist
|
|
@@ -336,9 +361,17 @@ function getWindowsDescendantsTasklist(parentPid: number, spawnSync: SpawnSyncFn
|
|
|
336
361
|
// tasklist /FI filters by parent — but tasklist doesn't support ParentProcessId filter
|
|
337
362
|
// Use PowerShell Get-CimInstance as fallback
|
|
338
363
|
const result = spawnSync(
|
|
339
|
-
"powershell",
|
|
364
|
+
resolveSystemTool("powershell"),
|
|
340
365
|
["-NoProfile", "-Command", `Get-CimInstance Win32_Process -Filter "ParentProcessId=${parentPid}" | Select-Object ProcessId,CommandLine,CreationDate | ConvertTo-Json`],
|
|
341
|
-
{
|
|
366
|
+
{
|
|
367
|
+
encoding: "utf-8",
|
|
368
|
+
timeout: 10000,
|
|
369
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
370
|
+
// Suppress console flash for the PowerShell fallback path.
|
|
371
|
+
// `resolveSystemTool` returns the full .exe path when registry
|
|
372
|
+
// is available.
|
|
373
|
+
windowsHide: true,
|
|
374
|
+
},
|
|
342
375
|
);
|
|
343
376
|
if (result.status !== 0 || !result.stdout) return [];
|
|
344
377
|
|
|
@@ -384,7 +417,8 @@ export function scanWindowsProcesses(
|
|
|
384
417
|
export function killWindowsProcess(pid: number, options?: ScanOptions): boolean {
|
|
385
418
|
const spawnSync: SpawnSyncFn = options?._spawnSync ?? defaultSpawnSync;
|
|
386
419
|
try {
|
|
387
|
-
const result = spawnSync("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
420
|
+
const result = spawnSync(resolveSystemTool("taskkill"), ["/PID", String(pid), "/T", "/F"], {
|
|
421
|
+
windowsHide: true,
|
|
388
422
|
encoding: "utf-8",
|
|
389
423
|
timeout: 5000,
|
|
390
424
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -24,16 +24,166 @@ interface ProviderEntry {
|
|
|
24
24
|
api?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
type InputModality = "text" | "image";
|
|
28
|
+
|
|
29
|
+
export interface ModelMetadata {
|
|
30
|
+
contextWindow: number;
|
|
31
|
+
maxTokens: number;
|
|
32
|
+
reasoning: boolean;
|
|
33
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
34
|
+
input: InputModality[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A catalog probe: given (provider, id), return the catalog entry or null.
|
|
39
|
+
* In production this is `modelRegistry.find(provider, id)` from pi's
|
|
40
|
+
* ModelRegistry (which knows both built-in pi-ai models AND user-configured
|
|
41
|
+
* custom models). Exposed as a parameter so unit tests can supply a fake
|
|
42
|
+
* catalog without needing pi-ai installed.
|
|
43
|
+
*/
|
|
44
|
+
export type CatalogProbe = (provider: string, modelId: string) => {
|
|
45
|
+
contextWindow: number;
|
|
46
|
+
maxTokens: number;
|
|
47
|
+
reasoning: boolean;
|
|
48
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
49
|
+
input: readonly ("text" | "image")[];
|
|
50
|
+
} | null | undefined;
|
|
51
|
+
|
|
52
|
+
// -- Model metadata enrichment --------------------------------------------
|
|
53
|
+
//
|
|
54
|
+
// Custom-provider `/v1/models` endpoints return only { id, object, ... } —
|
|
55
|
+
// they do not advertise context_window, max_tokens, cost, or reasoning
|
|
56
|
+
// capability. Rather than hardcode 200k / 16k / $0 / no-reasoning for every
|
|
57
|
+
// discovered model (the prior behavior, which was wrong for Opus 4.6+/Sonnet
|
|
58
|
+
// 4.6+/GPT-5/Gemini 2.5), we consult pi's `modelRegistry.find(provider, id)`
|
|
59
|
+
// — which surfaces pi-ai's bundled catalog plus any custom models — for
|
|
60
|
+
// accurate metadata and fall back to api-appropriate defaults when the
|
|
61
|
+
// catalog has no match.
|
|
62
|
+
//
|
|
63
|
+
// See change: enrich-custom-provider-model-metadata.
|
|
64
|
+
|
|
65
|
+
// API type → ordered list of candidate providers in pi's catalog.
|
|
66
|
+
// Provider keys match pi-ai's MODELS export as surfaced by modelRegistry.
|
|
67
|
+
// Order matters: first match wins.
|
|
68
|
+
const CANDIDATE_PROVIDERS: Record<string, readonly string[]> = {
|
|
69
|
+
"anthropic-messages": ["anthropic", "opencode"],
|
|
70
|
+
"google-generative-ai": ["google", "google-vertex"],
|
|
71
|
+
"openai-completions": ["openai", "openrouter", "groq", "xai", "mistral"],
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Api-typed fallback defaults when the catalog has no match. Modern floors:
|
|
75
|
+
// - anthropic-messages: 200k ctx (Claude 3/4 floor), 64k maxTok
|
|
76
|
+
// - google-generative-ai: 1M ctx (Gemini 1.5+/2.x floor), 65k maxTok
|
|
77
|
+
// - openai-completions (default): 128k ctx (GPT-4o floor), 16k maxTok
|
|
78
|
+
const FALLBACK_DEFAULTS: Record<string, Omit<ModelMetadata, "input">> = {
|
|
79
|
+
"anthropic-messages": {
|
|
80
|
+
contextWindow: 200_000,
|
|
81
|
+
maxTokens: 64_000,
|
|
82
|
+
reasoning: false,
|
|
83
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
84
|
+
},
|
|
85
|
+
"google-generative-ai": {
|
|
86
|
+
contextWindow: 1_000_000,
|
|
87
|
+
maxTokens: 65_536,
|
|
88
|
+
reasoning: false,
|
|
89
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
90
|
+
},
|
|
91
|
+
"openai-completions": {
|
|
92
|
+
contextWindow: 128_000,
|
|
93
|
+
maxTokens: 16_384,
|
|
94
|
+
reasoning: false,
|
|
95
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const DEFAULT_INPUT: InputModality[] = ["text", "image"];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a discovered custom-provider model id to full metadata by consulting
|
|
103
|
+
* pi's model catalog via the supplied `probe` function. Falls back to
|
|
104
|
+
* api-appropriate defaults when no catalog entry matches OR when no probe is
|
|
105
|
+
* available (e.g., modelRegistry not yet captured from spawn-context).
|
|
106
|
+
*
|
|
107
|
+
* Strips common proxy-prefix path segments (`cc/`, `anthropic/`,
|
|
108
|
+
* `openrouter/anthropic/…`) before lookup so prefixed ids resolve to the same
|
|
109
|
+
* catalog entry as the bare id.
|
|
110
|
+
*
|
|
111
|
+
* Exported (with the `probe` parameter) for unit testing. Production callers
|
|
112
|
+
* use `registerEntry()` which injects `modelRegistry.find`.
|
|
113
|
+
*/
|
|
114
|
+
export function enrichModelMetadata(
|
|
115
|
+
discoveredId: string,
|
|
116
|
+
api?: string,
|
|
117
|
+
probe?: CatalogProbe | null,
|
|
118
|
+
): ModelMetadata {
|
|
119
|
+
const resolvedApi = api && api in CANDIDATE_PROVIDERS ? api : "openai-completions";
|
|
120
|
+
const candidates = CANDIDATE_PROVIDERS[resolvedApi] ?? CANDIDATE_PROVIDERS["openai-completions"];
|
|
121
|
+
|
|
122
|
+
// Build dedup'd list of ids to try: full, then everything after the last `/`.
|
|
123
|
+
const lookupIds: string[] = [discoveredId];
|
|
124
|
+
const lastSlash = discoveredId.lastIndexOf("/");
|
|
125
|
+
if (lastSlash >= 0 && lastSlash < discoveredId.length - 1) {
|
|
126
|
+
const bare = discoveredId.slice(lastSlash + 1);
|
|
127
|
+
if (bare && bare !== discoveredId) lookupIds.push(bare);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (probe) {
|
|
131
|
+
for (const id of lookupIds) {
|
|
132
|
+
for (const provider of candidates) {
|
|
133
|
+
let match: ReturnType<CatalogProbe> | undefined;
|
|
134
|
+
try {
|
|
135
|
+
match = probe(provider, id);
|
|
136
|
+
} catch {
|
|
137
|
+
match = undefined;
|
|
138
|
+
}
|
|
139
|
+
if (match) {
|
|
140
|
+
return {
|
|
141
|
+
contextWindow: match.contextWindow,
|
|
142
|
+
maxTokens: match.maxTokens,
|
|
143
|
+
reasoning: match.reasoning,
|
|
144
|
+
cost: match.cost,
|
|
145
|
+
input: [...match.input] as InputModality[],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// No probe, or no catalog match — use api-appropriate fallback with
|
|
153
|
+
// image-capable default (see change: enable-image-input-custom-providers).
|
|
154
|
+
const fallback = FALLBACK_DEFAULTS[resolvedApi] ?? FALLBACK_DEFAULTS["openai-completions"];
|
|
155
|
+
return {
|
|
156
|
+
...fallback,
|
|
157
|
+
input: [...DEFAULT_INPUT],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
27
161
|
// -- Config path ----------------------------------------------------------
|
|
28
162
|
|
|
29
|
-
|
|
163
|
+
// Resolved lazily so HOME can be changed in tests.
|
|
164
|
+
function configPath(): string {
|
|
165
|
+
return join(homedir(), ".pi", "agent", "providers.json");
|
|
166
|
+
}
|
|
167
|
+
const CONFIG_PATH = configPath();
|
|
168
|
+
|
|
169
|
+
// Snapshot of last-registered provider entries so reloadProviders can diff.
|
|
170
|
+
const lastRegistered = new Map<string, ProviderEntry>();
|
|
171
|
+
|
|
172
|
+
function entriesEqual(a: ProviderEntry, b: ProviderEntry): boolean {
|
|
173
|
+
return (
|
|
174
|
+
a.baseUrl === b.baseUrl &&
|
|
175
|
+
a.apiKey === b.apiKey &&
|
|
176
|
+
(a.api ?? "openai-completions") === (b.api ?? "openai-completions")
|
|
177
|
+
);
|
|
178
|
+
}
|
|
30
179
|
|
|
31
180
|
// -- Config I/O (read-only — providers section) ----------------------------
|
|
32
181
|
|
|
33
182
|
function loadProviders(): Record<string, ProviderEntry> {
|
|
34
|
-
|
|
183
|
+
const path = configPath();
|
|
184
|
+
if (existsSync(path)) {
|
|
35
185
|
try {
|
|
36
|
-
const raw = JSON.parse(readFileSync(
|
|
186
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
37
187
|
const providers: Record<string, ProviderEntry> = { ...raw.providers };
|
|
38
188
|
for (const [, entry] of Object.entries(providers) as [string, any][]) {
|
|
39
189
|
if (entry.apiKeyEnv && !entry.apiKey) {
|
|
@@ -43,8 +193,10 @@ function loadProviders(): Record<string, ProviderEntry> {
|
|
|
43
193
|
delete (entry as any).modelIds;
|
|
44
194
|
}
|
|
45
195
|
return providers;
|
|
46
|
-
} catch {
|
|
47
|
-
|
|
196
|
+
} catch (err: any) {
|
|
197
|
+
console.error(
|
|
198
|
+
`[dashboard] providers.json reload failed: ${err?.message ?? String(err)}`,
|
|
199
|
+
);
|
|
48
200
|
}
|
|
49
201
|
}
|
|
50
202
|
return {};
|
|
@@ -124,6 +276,11 @@ let currentSessionModelId = "";
|
|
|
124
276
|
|
|
125
277
|
let piRef: ExtensionAPI | null = null;
|
|
126
278
|
|
|
279
|
+
// Captured from any pi event handler's ctx.modelRegistry (first available wins).
|
|
280
|
+
// Used by getModelRegistry() to probe pi's catalog for model metadata enrichment.
|
|
281
|
+
// See change: enrich-custom-provider-model-metadata.
|
|
282
|
+
let modelRegistryRef: any = null;
|
|
283
|
+
|
|
127
284
|
// Callback for notifying the bridge when providers change
|
|
128
285
|
let onProvidersChanged: (() => void) | null = null;
|
|
129
286
|
|
|
@@ -148,13 +305,15 @@ export function onProviderChanged(callback: () => void): void {
|
|
|
148
305
|
onProvidersChanged = callback;
|
|
149
306
|
}
|
|
150
307
|
|
|
151
|
-
// -- Helper: get modelRegistry
|
|
152
|
-
|
|
308
|
+
// -- Helper: get modelRegistry --------------------------------------------
|
|
309
|
+
//
|
|
310
|
+
// pi's ModelRegistry is passed as `ctx.modelRegistry` to every extension
|
|
311
|
+
// event handler (see ExtensionContext in pi's types). We capture the first
|
|
312
|
+
// reference we see in `session_start` and reuse it thereafter. This avoids
|
|
313
|
+
// depending on pi-flows' `flow:get-spawn-context` event which is not
|
|
314
|
+
// guaranteed to be present in every install.
|
|
153
315
|
function getModelRegistry(): any {
|
|
154
|
-
|
|
155
|
-
const spawnCtx: any = {};
|
|
156
|
-
piRef.events.emit("flow:get-spawn-context", spawnCtx);
|
|
157
|
-
return spawnCtx.modelRegistry ?? null;
|
|
316
|
+
return modelRegistryRef;
|
|
158
317
|
}
|
|
159
318
|
|
|
160
319
|
// -- Provider registration (with auto-discovery) --------------------------
|
|
@@ -162,14 +321,23 @@ function getModelRegistry(): any {
|
|
|
162
321
|
async function registerEntry(pi: ExtensionAPI, name: string, entry: ProviderEntry): Promise<number> {
|
|
163
322
|
const discovered = await discoverModels(entry.baseUrl, entry.apiKey);
|
|
164
323
|
|
|
324
|
+
// Metadata (contextWindow, maxTokens, reasoning, cost, input) is resolved
|
|
325
|
+
// via pi's `modelRegistry.find(provider, id)` when the registry is
|
|
326
|
+
// reachable, with api-appropriate fallbacks otherwise — the previous
|
|
327
|
+
// hardcoded 200k / 16k / $0 / no-reasoning was silently wrong for
|
|
328
|
+
// Opus 4.6+/Sonnet 4.6+/GPT-5/Gemini-2.x proxied via OpenAI-compatible
|
|
329
|
+
// endpoints. See enrichModelMetadata above, and change:
|
|
330
|
+
// enrich-custom-provider-model-metadata.
|
|
331
|
+
const registry = getModelRegistry();
|
|
332
|
+
const probe: CatalogProbe | null =
|
|
333
|
+
registry && typeof registry.find === "function"
|
|
334
|
+
? (provider, modelId) => registry.find(provider, modelId) ?? null
|
|
335
|
+
: null;
|
|
336
|
+
|
|
165
337
|
const models = discovered.map((m) => ({
|
|
166
338
|
id: m.id,
|
|
167
339
|
name: m.id,
|
|
168
|
-
|
|
169
|
-
input: ["text"] as ("text" | "image")[],
|
|
170
|
-
contextWindow: 200000,
|
|
171
|
-
maxTokens: 16384,
|
|
172
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
340
|
+
...enrichModelMetadata(m.id, entry.api, probe),
|
|
173
341
|
}));
|
|
174
342
|
|
|
175
343
|
pi.registerProvider(name, {
|
|
@@ -179,12 +347,83 @@ async function registerEntry(pi: ExtensionAPI, name: string, entry: ProviderEntr
|
|
|
179
347
|
models,
|
|
180
348
|
});
|
|
181
349
|
|
|
350
|
+
// Record snapshot so reloadProviders can detect subsequent changes.
|
|
351
|
+
lastRegistered.set(name, {
|
|
352
|
+
baseUrl: entry.baseUrl,
|
|
353
|
+
apiKey: entry.apiKey,
|
|
354
|
+
api: entry.api ?? "openai-completions",
|
|
355
|
+
});
|
|
356
|
+
|
|
182
357
|
// Notify bridge directly (same package — no cross-package event needed)
|
|
183
358
|
onProvidersChanged?.();
|
|
184
359
|
|
|
185
360
|
return discovered.length;
|
|
186
361
|
}
|
|
187
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Diff the current providers.json against the last-registered snapshot and
|
|
365
|
+
* apply add / remove / change operations via `pi.registerProvider` and
|
|
366
|
+
* `pi.unregisterProvider`. Called by the bridge's `credentials_updated`
|
|
367
|
+
* handler so adding/editing/removing providers in the dashboard UI takes
|
|
368
|
+
* effect without a session restart.
|
|
369
|
+
*
|
|
370
|
+
* Malformed providers.json or IO errors produce an empty diff and do not
|
|
371
|
+
* throw, so the caller can still run `modelRegistry.refresh()` for other
|
|
372
|
+
* credential updates.
|
|
373
|
+
*/
|
|
374
|
+
export async function reloadProviders(
|
|
375
|
+
pi: ExtensionAPI,
|
|
376
|
+
): Promise<{ added: string[]; removed: string[]; changed: string[] }> {
|
|
377
|
+
piRef = pi;
|
|
378
|
+
const added: string[] = [];
|
|
379
|
+
const removed: string[] = [];
|
|
380
|
+
const changed: string[] = [];
|
|
381
|
+
|
|
382
|
+
let current: Record<string, ProviderEntry>;
|
|
383
|
+
try {
|
|
384
|
+
current = loadProviders();
|
|
385
|
+
} catch {
|
|
386
|
+
return { added, removed, changed };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Detect removals and changes against previous snapshot.
|
|
390
|
+
for (const [name, prev] of lastRegistered) {
|
|
391
|
+
const next = current[name];
|
|
392
|
+
if (!next) {
|
|
393
|
+
try {
|
|
394
|
+
pi.unregisterProvider(name);
|
|
395
|
+
} catch (err: any) {
|
|
396
|
+
console.error(`[dashboard] unregisterProvider("${name}") failed: ${err?.message ?? String(err)}`);
|
|
397
|
+
}
|
|
398
|
+
lastRegistered.delete(name);
|
|
399
|
+
removed.push(name);
|
|
400
|
+
} else if (!entriesEqual(prev, next)) {
|
|
401
|
+
try {
|
|
402
|
+
pi.unregisterProvider(name);
|
|
403
|
+
} catch (err: any) {
|
|
404
|
+
console.error(`[dashboard] unregisterProvider("${name}") failed: ${err?.message ?? String(err)}`);
|
|
405
|
+
}
|
|
406
|
+
lastRegistered.delete(name);
|
|
407
|
+
changed.push(name);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Register new entries and changed entries (order-dependent: unregister ran first above).
|
|
412
|
+
for (const [name, entry] of Object.entries(current)) {
|
|
413
|
+
if (lastRegistered.has(name)) continue;
|
|
414
|
+
try {
|
|
415
|
+
await registerEntry(pi, name, entry);
|
|
416
|
+
if (!added.includes(name) && !changed.includes(name)) {
|
|
417
|
+
added.push(name);
|
|
418
|
+
}
|
|
419
|
+
} catch (err: any) {
|
|
420
|
+
console.error(`[dashboard] registerProvider("${name}") failed: ${err?.message ?? String(err)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return { added, removed, changed };
|
|
425
|
+
}
|
|
426
|
+
|
|
188
427
|
// -- Extension entry point ------------------------------------------------
|
|
189
428
|
|
|
190
429
|
export function activate(pi: ExtensionAPI) {
|
|
@@ -248,6 +487,11 @@ export function activate(pi: ExtensionAPI) {
|
|
|
248
487
|
// ── Session lifecycle ──────────────────────────────────────────────
|
|
249
488
|
|
|
250
489
|
pi.on("model_select", async (_event, ctx) => {
|
|
490
|
+
// Also capture modelRegistry here as a belt-and-suspenders in case
|
|
491
|
+
// session_start ran before activate() finished in some edge case.
|
|
492
|
+
if (!modelRegistryRef && ctx.modelRegistry) {
|
|
493
|
+
modelRegistryRef = ctx.modelRegistry;
|
|
494
|
+
}
|
|
251
495
|
if (ctx.model) {
|
|
252
496
|
currentSessionProvider = ctx.model.provider ?? "";
|
|
253
497
|
currentSessionModelId = ctx.model.id ?? "";
|
|
@@ -255,6 +499,50 @@ export function activate(pi: ExtensionAPI) {
|
|
|
255
499
|
});
|
|
256
500
|
|
|
257
501
|
pi.on("session_start", async (_event, ctx) => {
|
|
502
|
+
// Capture the modelRegistry reference the first time we see it, then
|
|
503
|
+
// re-register already-registered providers so their model metadata gets
|
|
504
|
+
// enriched from pi's catalog (they were registered at activate() before
|
|
505
|
+
// any ctx was available, so they currently carry fallback defaults).
|
|
506
|
+
// See change: enrich-custom-provider-model-metadata.
|
|
507
|
+
if (!modelRegistryRef && ctx.modelRegistry) {
|
|
508
|
+
modelRegistryRef = ctx.modelRegistry;
|
|
509
|
+
if (lastRegistered.size > 0) {
|
|
510
|
+
// Force re-registration: clear snapshot so reloadProviders re-adds all
|
|
511
|
+
// entries (which will now probe the captured registry).
|
|
512
|
+
const names = Array.from(lastRegistered.keys());
|
|
513
|
+
lastRegistered.clear();
|
|
514
|
+
for (const name of names) {
|
|
515
|
+
const entry = providers[name];
|
|
516
|
+
if (entry) {
|
|
517
|
+
try {
|
|
518
|
+
await registerEntry(pi, name, entry);
|
|
519
|
+
} catch (err: any) {
|
|
520
|
+
console.error(`[dashboard] re-registerProvider("${name}") failed: ${err?.message ?? String(err)}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// If the session's currently-selected model belongs to one of the
|
|
526
|
+
// providers we just re-registered, re-apply it via pi.setModel() so
|
|
527
|
+
// the snapshot on agent.state.model picks up the enriched metadata
|
|
528
|
+
// (reasoning / contextWindow / cost). Without this, pi's session
|
|
529
|
+
// still holds the pre-enrichment descriptor with reasoning: false,
|
|
530
|
+
// causing setThinkingLevel to clamp to "off" even though the registry
|
|
531
|
+
// now has reasoning: true. See change: enrich-custom-provider-model-metadata.
|
|
532
|
+
const current = ctx.model as any;
|
|
533
|
+
if (current?.provider && current?.id && names.includes(current.provider)) {
|
|
534
|
+
try {
|
|
535
|
+
const refreshed = ctx.modelRegistry.find(current.provider, current.id);
|
|
536
|
+
if (refreshed && (pi as any).setModel) {
|
|
537
|
+
await (pi as any).setModel(refreshed);
|
|
538
|
+
}
|
|
539
|
+
} catch (err: any) {
|
|
540
|
+
console.error(`[dashboard] re-setModel after enrichment failed: ${err?.message ?? String(err)}`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
258
546
|
if (ctx.model) {
|
|
259
547
|
currentSessionProvider = ctx.model.provider ?? "";
|
|
260
548
|
currentSessionModelId = ctx.model.id ?? "";
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Auto-start logic for the dashboard server.
|
|
3
3
|
* Uses mDNS discovery first, falls back to health check, then auto-starts.
|
|
4
4
|
*/
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
5
7
|
|
|
6
8
|
export interface DiscoveredServer {
|
|
7
9
|
host: string;
|
|
@@ -16,6 +18,20 @@ export interface AutoStartDeps {
|
|
|
16
18
|
isDashboardRunning: (port: number) => Promise<{ running: boolean; portConflict?: boolean }>;
|
|
17
19
|
launchServer: (config: any) => Promise<{ success: boolean; message: string }>;
|
|
18
20
|
notify: (message: string, level: "info" | "warning") => void;
|
|
21
|
+
/**
|
|
22
|
+
* Optional callback fired immediately BEFORE `launchServer(config)` is
|
|
23
|
+
* invoked. Used by TUI-aware callers (bridge extension) to show a
|
|
24
|
+
* "starting dashboard server" spinner. NOT fired during mDNS discovery
|
|
25
|
+
* or health-check phases — only when an actual server process is
|
|
26
|
+
* about to be spawned.
|
|
27
|
+
*/
|
|
28
|
+
onLaunchStart?: () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Optional callback fired after `launchServer` resolves (success or
|
|
31
|
+
* failure), AND after the post-launch mDNS re-discovery + recheck.
|
|
32
|
+
* Passes the final success state so the caller can clear spinners.
|
|
33
|
+
*/
|
|
34
|
+
onLaunchEnd?: (success: boolean) => void;
|
|
19
35
|
}
|
|
20
36
|
|
|
21
37
|
export interface AutoStartResult {
|
|
@@ -58,8 +74,10 @@ export async function autoStartServer(
|
|
|
58
74
|
}
|
|
59
75
|
|
|
60
76
|
// 3. Auto-start server
|
|
77
|
+
deps.onLaunchStart?.();
|
|
61
78
|
const result = await deps.launchServer(config);
|
|
62
79
|
if (result.success) {
|
|
80
|
+
deps.onLaunchEnd?.(true);
|
|
63
81
|
deps.notify(`🌐 Dashboard started at http://localhost:${config.port}`, "info");
|
|
64
82
|
|
|
65
83
|
// Wait for mDNS advertisement from the newly started server (up to 10s)
|
|
@@ -79,9 +97,17 @@ export async function autoStartServer(
|
|
|
79
97
|
// Another agent may have started the server concurrently — recheck before warning
|
|
80
98
|
const recheck = await deps.isDashboardRunning(config.port);
|
|
81
99
|
if (recheck.running) {
|
|
100
|
+
deps.onLaunchEnd?.(true);
|
|
82
101
|
return { server: { host: "localhost", port: config.port, piPort: config.piPort } };
|
|
83
102
|
}
|
|
84
103
|
|
|
85
|
-
|
|
104
|
+
// Surface the log path so users can inspect the crash output without having
|
|
105
|
+
// to know the convention. See change: fix-windows-server-parity.
|
|
106
|
+
deps.onLaunchEnd?.(false);
|
|
107
|
+
const logPath = path.join(os.homedir(), ".pi", "dashboard", "server.log");
|
|
108
|
+
deps.notify(
|
|
109
|
+
`Dashboard server failed to start: ${result.message}\nSee log: ${logPath}`,
|
|
110
|
+
"warning",
|
|
111
|
+
);
|
|
86
112
|
return {};
|
|
87
113
|
}
|