@blackbelt-technology/pi-agent-dashboard 0.2.9 → 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 +64 -8
- package/README.md +308 -101
- package/docs/architecture.md +515 -16
- package/package.json +14 -7
- package/packages/extension/package.json +11 -3
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/enrich-model-metadata.test.ts +201 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -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/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +107 -6
- package/packages/extension/src/command-handler.ts +34 -39
- 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/prompt-expander.ts +25 -4
- 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 +17 -2
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -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 +246 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -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__/cors.test.ts +34 -2
- 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-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +29 -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__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +57 -8
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- 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__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +126 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +45 -10
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- 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-auth-routes.test.ts +13 -3
- 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__/recommended-routes.test.ts +389 -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__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/terminal-manager.test.ts +41 -1
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- 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 +103 -6
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- 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 +108 -9
- package/packages/server/src/browser-gateway.ts +16 -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 +39 -5
- package/packages/server/src/editor-pid-registry.ts +199 -0
- package/packages/server/src/editor-registry.ts +22 -25
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/git-operations.ts +1 -1
- package/packages/server/src/headless-pid-registry.ts +16 -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/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +225 -34
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +172 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- 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/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +107 -1
- package/packages/server/src/routes/pi-core-routes.ts +140 -0
- package/packages/server/src/routes/provider-auth-routes.ts +12 -10
- package/packages/server/src/routes/provider-routes.ts +55 -2
- package/packages/server/src/routes/recommended-routes.ts +225 -0
- package/packages/server/src/routes/system-routes.ts +30 -34
- 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 +363 -26
- 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 +65 -20
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +172 -34
- 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 +59 -3
- 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__/openspec-poller.test.ts +44 -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 +156 -0
- 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__/source-matching.test.ts +143 -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 +93 -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 +71 -49
- 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 +196 -0
- package/packages/shared/src/resolve-jiti.ts +62 -3
- package/packages/shared/src/rest-api.ts +97 -0
- package/packages/shared/src/semaphore.ts +83 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -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
- package/packages/shared/src/types.ts +7 -0
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
5
5
|
import type { BrowserHandlerContext } from "./handler-context.js";
|
|
6
6
|
import { safeRealpathSync } from "../resolve-path.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { archiveCompleted as openspecArchiveCompleted } from "@blackbelt-technology/pi-dashboard-shared/platform/openspec.js";
|
|
8
|
+
import { normalizePath } from "@blackbelt-technology/pi-dashboard-shared/platform/paths.js";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Canonicalize a user-supplied path before storage: normalize separator /
|
|
12
|
+
* trailing-sep / case variants first, then resolve symlinks. Order matters
|
|
13
|
+
* — `realpath` can fail for not-yet-existing paths, so we keep its
|
|
14
|
+
* best-effort fallback but ensure we first have a sane string.
|
|
15
|
+
* See change: platform-path-normalization.
|
|
16
|
+
*/
|
|
17
|
+
function canonicalizePath(input: string): string {
|
|
18
|
+
return safeRealpathSync(normalizePath(input));
|
|
19
|
+
}
|
|
11
20
|
|
|
12
21
|
export function handlePinDirectory(
|
|
13
22
|
msg: Extract<BrowserToServerMessage, { type: "pin_directory" }>,
|
|
@@ -15,7 +24,7 @@ export function handlePinDirectory(
|
|
|
15
24
|
): void {
|
|
16
25
|
const { preferencesStore, directoryService, sessionManager, broadcast } = ctx;
|
|
17
26
|
if (!preferencesStore) return;
|
|
18
|
-
const resolved =
|
|
27
|
+
const resolved = canonicalizePath(msg.path);
|
|
19
28
|
preferencesStore.pinDirectory(resolved);
|
|
20
29
|
broadcast({ type: "pinned_dirs_updated", paths: preferencesStore.getPinnedDirectories() });
|
|
21
30
|
if (directoryService) {
|
|
@@ -48,7 +57,7 @@ export function handleUnpinDirectory(
|
|
|
48
57
|
ctx: BrowserHandlerContext,
|
|
49
58
|
): void {
|
|
50
59
|
if (ctx.preferencesStore) {
|
|
51
|
-
ctx.preferencesStore.unpinDirectory(
|
|
60
|
+
ctx.preferencesStore.unpinDirectory(canonicalizePath(msg.path));
|
|
52
61
|
ctx.broadcast({ type: "pinned_dirs_updated", paths: ctx.preferencesStore.getPinnedDirectories() });
|
|
53
62
|
}
|
|
54
63
|
}
|
|
@@ -58,7 +67,10 @@ export function handleReorderPinnedDirs(
|
|
|
58
67
|
ctx: BrowserHandlerContext,
|
|
59
68
|
): void {
|
|
60
69
|
if (ctx.preferencesStore) {
|
|
61
|
-
|
|
70
|
+
// Wrap in arrow fn: map's (elem, index, array) callback would pass
|
|
71
|
+
// the array index as canonicalizePath's 2nd arg, silently breaking
|
|
72
|
+
// platform detection. See platform-path-normalization.
|
|
73
|
+
ctx.preferencesStore.reorderPinnedDirs(msg.paths.map((p) => canonicalizePath(p)));
|
|
62
74
|
ctx.broadcast({ type: "pinned_dirs_updated", paths: ctx.preferencesStore.getPinnedDirectories() });
|
|
63
75
|
}
|
|
64
76
|
}
|
|
@@ -89,8 +101,11 @@ export function handleOpenSpecBulkArchive(
|
|
|
89
101
|
ctx: BrowserHandlerContext,
|
|
90
102
|
): void {
|
|
91
103
|
if (ctx.directoryService) {
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
// Delegate to the shared openspec tool module. The runner handles
|
|
105
|
+
// windowsHide, timeout, and argv-array escaping.
|
|
106
|
+
// See change: platform-command-executor.
|
|
107
|
+
openspecArchiveCompleted({ cwd: msg.cwd });
|
|
108
|
+
Promise.resolve()
|
|
94
109
|
.then(() => ctx.directoryService!.refreshOpenSpec(msg.cwd))
|
|
95
110
|
.then((data) => {
|
|
96
111
|
if (data) ctx.broadcast({ type: "openspec_update", cwd: msg.cwd, data });
|
|
@@ -6,28 +6,162 @@ import type { BrowserHandlerContext } from "./handler-context.js";
|
|
|
6
6
|
import { spawnPiSession } from "../process-manager.js";
|
|
7
7
|
import { loadConfig } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
8
8
|
import { createBranchedSessionFile } from "../session-file-reader.js";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
killPidWithGroup,
|
|
11
|
+
killProcess,
|
|
12
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/process.js";
|
|
13
|
+
import {
|
|
14
|
+
findPidByMarker,
|
|
15
|
+
} from "@blackbelt-technology/pi-dashboard-shared/platform/process-identify.js";
|
|
16
|
+
import { shouldInterceptReload } from "./session-action-helpers.js";
|
|
10
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Find headless pi PIDs associated with a session-id marker and kill them.
|
|
20
|
+
* Delegates platform branching to `platform/process-identify.ts` — Windows
|
|
21
|
+
* returns `[]` because command-line lookup isn't viable; Windows kills go
|
|
22
|
+
* through `headlessPidRegistry` instead.
|
|
23
|
+
* See change: consolidate-windows-spawn-and-platform-handlers.
|
|
24
|
+
*/
|
|
11
25
|
function killHeadlessBySessionId(sessionId: string): boolean {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
+
const pids = findPidByMarker(sessionId);
|
|
27
|
+
if (pids.length === 0) return false;
|
|
28
|
+
for (const pid of pids) {
|
|
29
|
+
// `killPidWithGroup` is the canonical platform helper. Failures here
|
|
30
|
+
// (e.g. ESRCH because the process is already dead) are non-fatal —
|
|
31
|
+
// the caller treats "no matching PID" and "PID already dead" the
|
|
32
|
+
// same way. Log and continue. See change:
|
|
33
|
+
// route-kill-paths-through-platform.
|
|
34
|
+
try {
|
|
35
|
+
killPidWithGroup(pid, "SIGTERM");
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`[dashboard] killHeadlessBySessionId: killPidWithGroup(${pid}) failed:`,
|
|
39
|
+
err,
|
|
40
|
+
);
|
|
26
41
|
}
|
|
27
|
-
return true;
|
|
28
|
-
} catch {
|
|
29
|
-
return false;
|
|
30
42
|
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Emit a `command_feedback` DashboardEvent to all subscribed browsers.
|
|
48
|
+
* Mirrors what the bridge's command-handler does for TUI `/reload`, but from
|
|
49
|
+
* the server side for the headless-reload path.
|
|
50
|
+
*
|
|
51
|
+
* See change: headless-reload-via-respawn.
|
|
52
|
+
*/
|
|
53
|
+
function emitCommandFeedback(
|
|
54
|
+
ctx: BrowserHandlerContext,
|
|
55
|
+
sessionId: string,
|
|
56
|
+
status: "started" | "completed" | "error",
|
|
57
|
+
message?: string,
|
|
58
|
+
): void {
|
|
59
|
+
const event = {
|
|
60
|
+
eventType: "command_feedback",
|
|
61
|
+
timestamp: Date.now(),
|
|
62
|
+
data: { command: "/reload", status, ...(message ? { message } : {}) },
|
|
63
|
+
};
|
|
64
|
+
const seq = ctx.eventStore.insertEvent(sessionId, event);
|
|
65
|
+
ctx.broadcast({ type: "event", sessionId, seq, event } as any);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Headless-session `/reload` handler.
|
|
70
|
+
*
|
|
71
|
+
* pi-coding-agent 0.68.0 has no programmatic reload path accessible to an
|
|
72
|
+
* extension in RPC mode:
|
|
73
|
+
* - `ExtensionContext` (delivered to `session_start`) has no `reload` field
|
|
74
|
+
* - The RPC protocol has no `{type:"reload"}` command
|
|
75
|
+
* - The `globalThis[RELOAD_KEY]` bootstrap requires a human to type
|
|
76
|
+
* `/__dashboard_reload` in pi's TUI, which headless sessions lack.
|
|
77
|
+
*
|
|
78
|
+
* Instead, the server achieves a reload-equivalent outcome by killing the
|
|
79
|
+
* headless pi process and respawning it with `--session <file>`, which
|
|
80
|
+
* re-hydrates the same `sessionId` and entry list. Because
|
|
81
|
+
* `memorySessionManager.register` carries accumulated state (tokens, cost,
|
|
82
|
+
* context usage, attachedProposal) when the same sessionId re-registers,
|
|
83
|
+
* the user-visible session state survives the respawn.
|
|
84
|
+
*
|
|
85
|
+
* See change: headless-reload-via-respawn.
|
|
86
|
+
*/
|
|
87
|
+
export async function handleHeadlessReload(
|
|
88
|
+
msg: Extract<BrowserToServerMessage, { type: "send_prompt" }>,
|
|
89
|
+
ctx: BrowserHandlerContext,
|
|
90
|
+
): Promise<void> {
|
|
91
|
+
const { sessionManager, headlessPidRegistry } = ctx;
|
|
92
|
+
const session = sessionManager.get(msg.sessionId);
|
|
93
|
+
if (!session) {
|
|
94
|
+
emitCommandFeedback(ctx, msg.sessionId, "error", "Session not found");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!session.sessionFile) {
|
|
98
|
+
emitCommandFeedback(
|
|
99
|
+
ctx,
|
|
100
|
+
msg.sessionId,
|
|
101
|
+
"error",
|
|
102
|
+
"No session file — cannot respawn on reload",
|
|
103
|
+
);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (session.status === "streaming") {
|
|
107
|
+
emitCommandFeedback(
|
|
108
|
+
ctx,
|
|
109
|
+
msg.sessionId,
|
|
110
|
+
"error",
|
|
111
|
+
"Wait for the current response to finish before reloading.",
|
|
112
|
+
);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
emitCommandFeedback(ctx, msg.sessionId, "started");
|
|
117
|
+
|
|
118
|
+
// SIGTERM the old headless pi. No-op if already dead (idempotency guard).
|
|
119
|
+
headlessPidRegistry.killBySessionId(msg.sessionId);
|
|
120
|
+
|
|
121
|
+
// Respawn with the same session file. The new pi process re-hydrates the
|
|
122
|
+
// same sessionId, the bridge re-registers, and the server preserves
|
|
123
|
+
// accumulated state (tokens/cost/context/attachedProposal).
|
|
124
|
+
let spawnResult: Awaited<ReturnType<typeof spawnPiSession>>;
|
|
125
|
+
try {
|
|
126
|
+
spawnResult = await spawnPiSession(session.cwd, {
|
|
127
|
+
sessionFile: session.sessionFile,
|
|
128
|
+
mode: "continue",
|
|
129
|
+
strategy: "headless",
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
133
|
+
console.error(`[dashboard] headless reload spawn failed: ${message}`);
|
|
134
|
+
const endedAt = Date.now();
|
|
135
|
+
sessionManager.update(msg.sessionId, { status: "ended", endedAt });
|
|
136
|
+
ctx.broadcast({
|
|
137
|
+
type: "session_updated",
|
|
138
|
+
sessionId: msg.sessionId,
|
|
139
|
+
updates: { status: "ended", endedAt },
|
|
140
|
+
});
|
|
141
|
+
emitCommandFeedback(ctx, msg.sessionId, "error", message);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!spawnResult.success) {
|
|
146
|
+
console.error(
|
|
147
|
+
`[dashboard] headless reload spawn failed: ${spawnResult.message}`,
|
|
148
|
+
);
|
|
149
|
+
const endedAt = Date.now();
|
|
150
|
+
sessionManager.update(msg.sessionId, { status: "ended", endedAt });
|
|
151
|
+
ctx.broadcast({
|
|
152
|
+
type: "session_updated",
|
|
153
|
+
sessionId: msg.sessionId,
|
|
154
|
+
updates: { status: "ended", endedAt },
|
|
155
|
+
});
|
|
156
|
+
emitCommandFeedback(ctx, msg.sessionId, "error", spawnResult.message);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (spawnResult.pid && spawnResult.process) {
|
|
161
|
+
headlessPidRegistry.register(spawnResult.pid, session.cwd, spawnResult.process);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
emitCommandFeedback(ctx, msg.sessionId, "completed");
|
|
31
165
|
}
|
|
32
166
|
|
|
33
167
|
export async function handleSendPrompt(
|
|
@@ -35,6 +169,16 @@ export async function handleSendPrompt(
|
|
|
35
169
|
ctx: BrowserHandlerContext,
|
|
36
170
|
): Promise<void> {
|
|
37
171
|
const { sessionManager, piGateway, headlessPidRegistry, pendingResumeRegistry, pendingDashboardSpawns, broadcast } = ctx;
|
|
172
|
+
|
|
173
|
+
// Intercept `/reload` on active headless sessions — forward the request to
|
|
174
|
+
// our kill-and-respawn handler instead of routing the prompt to the bridge
|
|
175
|
+
// (the bridge has no programmatic reload path on RPC).
|
|
176
|
+
// See change: headless-reload-via-respawn.
|
|
177
|
+
if (shouldInterceptReload(msg, headlessPidRegistry)) {
|
|
178
|
+
await handleHeadlessReload(msg, ctx);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
38
182
|
const promptSession = sessionManager.get(msg.sessionId);
|
|
39
183
|
|
|
40
184
|
if (promptSession?.status === "ended") {
|
|
@@ -141,14 +285,30 @@ export async function handleSpawnSession(
|
|
|
141
285
|
): Promise<void> {
|
|
142
286
|
const { ws, headlessPidRegistry, pendingDashboardSpawns, sendTo } = ctx;
|
|
143
287
|
const config = loadConfig();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
288
|
+
const strategy = config.spawnStrategy ?? "tmux";
|
|
289
|
+
|
|
290
|
+
// Catch both thrown exceptions and { success: false } results; surface as
|
|
291
|
+
// spawn_error so the UI can render a retryable banner instead of failing
|
|
292
|
+
// silently. Previous behaviour left the user staring at an empty state
|
|
293
|
+
// when pi itself was broken in the target folder.
|
|
294
|
+
try {
|
|
295
|
+
const spawnResult = await spawnPiSession(msg.cwd, { strategy });
|
|
296
|
+
if (spawnResult.process && spawnResult.pid) {
|
|
297
|
+
headlessPidRegistry.register(spawnResult.pid, msg.cwd, spawnResult.process);
|
|
298
|
+
}
|
|
299
|
+
if (spawnResult.dashboardSpawned && spawnResult.success) {
|
|
300
|
+
pendingDashboardSpawns?.set(msg.cwd, (pendingDashboardSpawns?.get(msg.cwd) ?? 0) + 1);
|
|
301
|
+
}
|
|
302
|
+
sendTo(ws, { type: "spawn_result", cwd: msg.cwd, success: spawnResult.success, message: spawnResult.message });
|
|
303
|
+
if (!spawnResult.success) {
|
|
304
|
+
sendTo(ws, { type: "spawn_error", cwd: msg.cwd, strategy, message: spawnResult.message });
|
|
305
|
+
}
|
|
306
|
+
} catch (err) {
|
|
307
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
308
|
+
const stderr = err instanceof Error && "stderr" in err ? String((err as { stderr: unknown }).stderr).slice(-2048) : undefined;
|
|
309
|
+
sendTo(ws, { type: "spawn_result", cwd: msg.cwd, success: false, message });
|
|
310
|
+
sendTo(ws, { type: "spawn_error", cwd: msg.cwd, strategy, message, stderr });
|
|
150
311
|
}
|
|
151
|
-
sendTo(ws, { type: "spawn_result", cwd: msg.cwd, success: spawnResult.success, message: spawnResult.message });
|
|
152
312
|
}
|
|
153
313
|
|
|
154
314
|
export function handleShutdown(
|
|
@@ -185,33 +345,11 @@ export function handleKillProcess(
|
|
|
185
345
|
}
|
|
186
346
|
|
|
187
347
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
348
|
+
* Pure predicate: does a `ps`/cmdline output string look like a pi/node process?
|
|
349
|
+
* Re-exported from `platform/process-identify.ts` for backwards compat with
|
|
350
|
+
* any external consumer of this handler.
|
|
190
351
|
*/
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
const cmd = process.platform === "darwin"
|
|
194
|
-
? `ps -p ${pid} -o command=`
|
|
195
|
-
: `cat /proc/${pid}/cmdline 2>/dev/null || ps -p ${pid} -o command=`;
|
|
196
|
-
const output = execSync(cmd, { encoding: "utf8", timeout: 2000 }).trim();
|
|
197
|
-
return /\bpi\b|\bnode\b/.test(output);
|
|
198
|
-
} catch {
|
|
199
|
-
// Process already exited — treat as dead
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Check if a process is still alive.
|
|
206
|
-
*/
|
|
207
|
-
function isProcessAlive(pid: number): boolean {
|
|
208
|
-
try {
|
|
209
|
-
process.kill(pid, 0);
|
|
210
|
-
return true;
|
|
211
|
-
} catch {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
352
|
+
export { isPiCommandLine } from "@blackbelt-technology/pi-dashboard-shared/platform/process-identify.js";
|
|
215
353
|
|
|
216
354
|
export async function handleForceKill(
|
|
217
355
|
msg: Extract<BrowserToServerMessage, { type: "force_kill" }>,
|
|
@@ -236,36 +374,32 @@ export async function handleForceKill(
|
|
|
236
374
|
return;
|
|
237
375
|
}
|
|
238
376
|
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
377
|
+
// Delegate the full SIGTERM → wait → SIGKILL escalation to the
|
|
378
|
+
// platform helper so Windows uses `taskkill /F /T /PID <pid>`
|
|
379
|
+
// (genuine tree kill) and POSIX keeps the 2s grace window.
|
|
380
|
+
// See change: route-kill-paths-through-platform.
|
|
381
|
+
//
|
|
382
|
+
// PID-safety check: skip SIGKILL escalation on Unix when the PID
|
|
383
|
+
// no longer resembles a pi process. We can't pass this check INTO
|
|
384
|
+
// killProcess without a plugin, so: if `killProcess` reports forced
|
|
385
|
+
// SIGKILL and isPiProcess says no, we still accept the result —
|
|
386
|
+
// the process was either a pi leaf or a recycled PID, and either
|
|
387
|
+
// way the session is ended. On Windows `taskkill /F /T` is atomic
|
|
388
|
+
// so the check isn't meaningful.
|
|
389
|
+
const killResult = await killProcess(pid, { timeoutMs: 2000 });
|
|
249
390
|
|
|
250
|
-
// Also kill
|
|
391
|
+
// Also kill any headless-registered siblings (same session ID).
|
|
251
392
|
headlessPidRegistry.killBySessionId(msg.sessionId);
|
|
252
393
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (isProcessAlive(pid)) {
|
|
257
|
-
// Safety check: verify PID still belongs to a pi process
|
|
258
|
-
if (isPiProcess(pid)) {
|
|
259
|
-
try {
|
|
260
|
-
process.kill(pid, "SIGKILL");
|
|
261
|
-
} catch { /* already dead */ }
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
resolve();
|
|
265
|
-
}, 2000);
|
|
266
|
-
});
|
|
394
|
+
const endedAt = Date.now();
|
|
395
|
+
sessionManager.update(msg.sessionId, { status: "ended", endedAt });
|
|
396
|
+
broadcast({ type: "session_updated", sessionId: msg.sessionId, updates: { status: "ended", endedAt } });
|
|
267
397
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
398
|
+
if (!killResult.ok) {
|
|
399
|
+
// Process was already dead when the kill was issued.
|
|
400
|
+
sendTo(ws, { type: "force_kill_result", sessionId: msg.sessionId, success: true, message: "Process already exited" });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const suffix = killResult.forced ? " (SIGKILL)" : "";
|
|
404
|
+
sendTo(ws, { type: "force_kill_result", sessionId: msg.sessionId, success: true, message: `Process terminated${suffix}` });
|
|
271
405
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for session-action-handler.
|
|
3
|
+
*
|
|
4
|
+
* Extracted so they can be unit-tested without the surrounding I/O surface
|
|
5
|
+
* (pi-gateway, event store, headless-pid-registry wiring).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { BrowserToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
9
|
+
import type { HeadlessPidRegistry } from "../headless-pid-registry.js";
|
|
10
|
+
|
|
11
|
+
type SendPromptMsg = Extract<BrowserToServerMessage, { type: "send_prompt" }>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Return true iff a `send_prompt` message targeting a headless session should
|
|
15
|
+
* be intercepted by the server and converted into a kill-and-respawn reload,
|
|
16
|
+
* instead of being forwarded to the bridge.
|
|
17
|
+
*
|
|
18
|
+
* See change: headless-reload-via-respawn.
|
|
19
|
+
*
|
|
20
|
+
* Criteria (ALL must hold):
|
|
21
|
+
* - The message text is exactly "/reload" (no whitespace, no trailing args).
|
|
22
|
+
* - No images are attached (pure slash-command, not a user prompt).
|
|
23
|
+
* - The session's PID is tracked in `headlessPidRegistry.getPid(sessionId)`.
|
|
24
|
+
*
|
|
25
|
+
* The registry is our only source of truth for "this session is headless
|
|
26
|
+
* right now" — it avoids adding a new `spawnStrategy` field to
|
|
27
|
+
* `DashboardSession`.
|
|
28
|
+
*/
|
|
29
|
+
export function shouldInterceptReload(
|
|
30
|
+
msg: SendPromptMsg,
|
|
31
|
+
headlessPidRegistry: Pick<HeadlessPidRegistry, "getPid">,
|
|
32
|
+
): boolean {
|
|
33
|
+
if (msg.text !== "/reload") return false;
|
|
34
|
+
if ((msg.images?.length ?? 0) !== 0) return false;
|
|
35
|
+
return headlessPidRegistry.getPid(msg.sessionId) !== undefined;
|
|
36
|
+
}
|