@blackbelt-technology/pi-agent-dashboard 0.5.3 → 0.5.4
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 +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +10 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
ExtensionToServerMessage,
|
|
10
10
|
} from "@blackbelt-technology/pi-dashboard-shared/protocol.js";
|
|
11
11
|
import { killProcessByPgid } from "./process-scanner.js";
|
|
12
|
-
import type { FileEntry, PiSessionInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
12
|
+
import type { FileEntry, ImageContent, PiSessionInfo } from "@blackbelt-technology/pi-dashboard-shared/types.js";
|
|
13
13
|
import { filterHiddenCommands } from "./bridge-context.js";
|
|
14
14
|
import { expandPromptTemplateFromDisk } from "./prompt-expander.js";
|
|
15
15
|
import { tryDispatchExtensionCommand } from "./slash-dispatch.js";
|
|
@@ -169,7 +169,25 @@ export function createCommandHandler(
|
|
|
169
169
|
* events emitted by the dispatch path arrive before this turn returns.
|
|
170
170
|
* See change: fix-extension-slash-commands-in-dashboard.
|
|
171
171
|
*/
|
|
172
|
-
sessionPrompt?: (text: string) => void | Promise<void>;
|
|
172
|
+
sessionPrompt?: (text: string, delivery?: "steer" | "followUp") => void | Promise<void>;
|
|
173
|
+
/**
|
|
174
|
+
* Bridge-shadow-queue hooks: called AFTER pi accepts the user message,
|
|
175
|
+
* gated by `isStreaming()` captured BEFORE the send. The capture order
|
|
176
|
+
* matters — `pi.sendUserMessage` on an idle session synchronously
|
|
177
|
+
* triggers `agent_start`, which flips bridge state to streaming. Checking
|
|
178
|
+
* AFTER the send would mis-record idle sends as chip entries.
|
|
179
|
+
* Pi doesn't expose `queue_update` to extensions, so the bridge is the
|
|
180
|
+
* source of truth. See change: add-followup-edit-and-steer-cancel.
|
|
181
|
+
*/
|
|
182
|
+
onSteerSent?: (text: string) => void;
|
|
183
|
+
onFollowupSent?: (text: string) => void;
|
|
184
|
+
/**
|
|
185
|
+
* Returns true iff the agent was streaming at the moment of the call.
|
|
186
|
+
* Used to capture pre-send streaming state before `pi.sendUserMessage`
|
|
187
|
+
* runs (which may flip the flag synchronously via agent_start).
|
|
188
|
+
* See change: add-followup-edit-and-steer-cancel.
|
|
189
|
+
*/
|
|
190
|
+
isStreaming?: () => boolean;
|
|
173
191
|
},
|
|
174
192
|
): CommandHandler {
|
|
175
193
|
const getSessionId = typeof sessionIdOrGetter === "function" ? sessionIdOrGetter : () => sessionIdOrGetter;
|
|
@@ -304,7 +322,7 @@ export function createCommandHandler(
|
|
|
304
322
|
// extension-command dispatch. Do NOT emit completed here — would
|
|
305
323
|
// duplicate the dispatch path's terminal event.
|
|
306
324
|
// See change: fix-extension-slash-commands-in-dashboard.
|
|
307
|
-
await options.sessionPrompt(parsed.text);
|
|
325
|
+
await options.sessionPrompt(parsed.text, msg.delivery);
|
|
308
326
|
} else {
|
|
309
327
|
// Test / non-bridge callers: apply the extension-command dispatch
|
|
310
328
|
// branch inline before falling through to sendUserMessage. Keeps
|
|
@@ -314,12 +332,17 @@ export function createCommandHandler(
|
|
|
314
332
|
parsed.text,
|
|
315
333
|
sessionId,
|
|
316
334
|
options?.eventSink,
|
|
335
|
+
undefined, // connection — absent in non-bridge path
|
|
336
|
+
msg.delivery,
|
|
317
337
|
);
|
|
318
338
|
if (!handled) {
|
|
319
339
|
// sendUserMessage exempt from gating: only typed single-line
|
|
320
340
|
// slashes that are NOT extension commands reach this — i.e.
|
|
321
341
|
// skills, prompt templates, unrecognized slashes.
|
|
322
|
-
|
|
342
|
+
// Forward delivery so steering on slash fallback honors the
|
|
343
|
+
// dashboard's keyboard contract. See change: add-steering-message.
|
|
344
|
+
const deliverAs = msg.delivery ?? ("followUp" as const);
|
|
345
|
+
(pi.sendUserMessage as any)(parsed.text, { deliverAs });
|
|
323
346
|
}
|
|
324
347
|
}
|
|
325
348
|
return undefined;
|
|
@@ -339,11 +362,27 @@ export function createCommandHandler(
|
|
|
339
362
|
if (outgoing.startsWith("/")) {
|
|
340
363
|
outgoing = expandPromptTemplateFromDisk(outgoing, process.cwd(), pi);
|
|
341
364
|
}
|
|
342
|
-
|
|
365
|
+
// Pi owns the steering + follow-up queues natively. The helper enforces
|
|
366
|
+
// capacity-1 on follow-up by clearing pi's slot before sending. After
|
|
367
|
+
// pi accepts, notify the bridge so it updates its shadow queue — but
|
|
368
|
+
// only if the agent was already streaming BEFORE the send. Pi flips
|
|
369
|
+
// idle→streaming synchronously on the first user message, so checking
|
|
370
|
+
// after sendUserMessage gives false positives.
|
|
371
|
+
// See change: add-followup-edit-and-steer-cancel.
|
|
372
|
+
const wasStreaming = options?.isStreaming?.() ?? false;
|
|
373
|
+
sendUserMessageWithImages(pi, outgoing, msg.images, msg.delivery);
|
|
374
|
+
if (wasStreaming) {
|
|
375
|
+
const da = msg.delivery ?? "followUp";
|
|
376
|
+
if (da === "steer") options?.onSteerSent?.(outgoing);
|
|
377
|
+
else options?.onFollowupSent?.(outgoing);
|
|
378
|
+
}
|
|
343
379
|
return undefined;
|
|
344
380
|
}
|
|
345
381
|
|
|
346
382
|
case "abort":
|
|
383
|
+
// Pi owns both queues now. abort() asks pi to halt the current turn;
|
|
384
|
+
// pi's native drain logic handles any remaining queue entries naturally.
|
|
385
|
+
// See change: add-followup-edit-and-steer-cancel.
|
|
347
386
|
if (options?.abort) {
|
|
348
387
|
options.abort();
|
|
349
388
|
}
|
|
@@ -502,13 +541,20 @@ export function createCommandHandler(
|
|
|
502
541
|
}
|
|
503
542
|
|
|
504
543
|
/** Send a user message with optional image validation.
|
|
505
|
-
* Uses deliverAs: "followUp" so messages queue properly when the agent is streaming.
|
|
544
|
+
* Uses deliverAs: "followUp" by default so messages queue properly when the agent is streaming.
|
|
545
|
+
* Pass deliverAs: "steer" for steering messages (delivered after current turn).
|
|
546
|
+
* See change: add-steering-message. */
|
|
506
547
|
function sendUserMessageWithImages(
|
|
507
548
|
pi: ExtensionAPI,
|
|
508
549
|
text: string,
|
|
509
550
|
images?: Array<{ type: string; data: string; mimeType: string }>,
|
|
551
|
+
delivery?: "steer" | "followUp",
|
|
510
552
|
): void {
|
|
511
|
-
const
|
|
553
|
+
const deliverAs = delivery ?? ("followUp" as const);
|
|
554
|
+
const sendOptions = { deliverAs };
|
|
555
|
+
// v2: follow-up is a multi-entry queue. Sends APPEND — the cap-1 invariant
|
|
556
|
+
// is gone. See change: add-followup-edit-and-steer-cancel design.md Decision 8.
|
|
557
|
+
// (The bridge's shadow queue records the append via onFollowupSent.)
|
|
512
558
|
if (images && images.length > 0) {
|
|
513
559
|
const validMimeTypes = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
514
560
|
const validImages = images.filter((img) => {
|
|
@@ -10,6 +10,11 @@ import type { PromptAdapter, PromptRequest, PromptResponse, PromptClaim } from "
|
|
|
10
10
|
|
|
11
11
|
export class DashboardDefaultAdapter implements PromptAdapter {
|
|
12
12
|
readonly name = "dashboard-default";
|
|
13
|
+
/**
|
|
14
|
+
* Last-resort fallback. Any plugin adapter with default priority (1000)
|
|
15
|
+
* or lower beats this. See change: route-flow-asks-to-upper-slot.
|
|
16
|
+
*/
|
|
17
|
+
readonly priority = 9999;
|
|
13
18
|
|
|
14
19
|
onRequest(_prompt: PromptRequest): PromptClaim {
|
|
15
20
|
return {
|
|
@@ -41,6 +41,14 @@ export interface PromptResponse {
|
|
|
41
41
|
|
|
42
42
|
export interface PromptAdapter {
|
|
43
43
|
name: string;
|
|
44
|
+
/**
|
|
45
|
+
* Routing priority. Lower numbers run first. Default 1000.
|
|
46
|
+
* `DashboardDefaultAdapter` uses 9999 so any plugin adapter with the
|
|
47
|
+
* default priority (1000) or lower beats it automatically.
|
|
48
|
+
*
|
|
49
|
+
* See change: route-flow-asks-to-upper-slot.
|
|
50
|
+
*/
|
|
51
|
+
priority?: number;
|
|
44
52
|
/**
|
|
45
53
|
* Called when a new prompt arrives.
|
|
46
54
|
* Return a PromptClaim to participate, or null/undefined to skip.
|
|
@@ -92,11 +100,18 @@ export class PromptBus {
|
|
|
92
100
|
/**
|
|
93
101
|
* Register an adapter. Returns an unsubscribe function.
|
|
94
102
|
* Re-registering with the same name replaces the previous adapter.
|
|
103
|
+
*
|
|
104
|
+
* Adapters are kept sorted by `priority` (default 1000, lower first).
|
|
105
|
+
* `Array.prototype.sort` is stable in V8 ≥ ES2019 so equal priorities
|
|
106
|
+
* preserve insertion order. See change: route-flow-asks-to-upper-slot.
|
|
95
107
|
*/
|
|
96
108
|
registerAdapter(adapter: PromptAdapter): () => void {
|
|
97
109
|
// Replace existing adapter with same name
|
|
98
110
|
this.adapters = this.adapters.filter(a => a.name !== adapter.name);
|
|
99
111
|
this.adapters.push(adapter);
|
|
112
|
+
this.adapters.sort(
|
|
113
|
+
(a, b) => (a.priority ?? 1000) - (b.priority ?? 1000),
|
|
114
|
+
);
|
|
100
115
|
|
|
101
116
|
return () => {
|
|
102
117
|
this.adapters = this.adapters.filter(a => a !== adapter);
|
|
@@ -8,10 +8,14 @@
|
|
|
8
8
|
* dashboard-spawned headless `pi --mode rpc` AND a `connection` is wired
|
|
9
9
|
* → emit `dispatch_extension_command` to the server (server forwards to
|
|
10
10
|
* the per-session RPC keeper UDS and emits the terminal command_feedback).
|
|
11
|
-
* - Path D
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* - Path D: `pi.dispatchCommand` absent AND the bridge is NOT headless
|
|
12
|
+
* (tmux / wt) OR no `connection` was supplied → emit
|
|
13
|
+
* `command_feedback {status:"error"}` with a hint to enable
|
|
14
|
+
* `useRpcKeeper: true` for headless sessions.
|
|
15
|
+
* Note: pi.sendUserMessage() hardcodes expandPromptTemplates: false, which
|
|
16
|
+
* skips _tryExecuteExtensionCommand; extension commands sent this way
|
|
17
|
+
* become regular LLM messages. This is a pi limitation — the bridge has
|
|
18
|
+
* no mechanism to dispatch extension commands outside the RPC path.
|
|
15
19
|
*
|
|
16
20
|
* If `text` is NOT an extension command, return `false` so the caller can
|
|
17
21
|
* fall through to its existing template-expansion / sendUserMessage path.
|
|
@@ -21,7 +25,8 @@
|
|
|
21
25
|
* Path C does NOT emit a terminal event — the server emits it.
|
|
22
26
|
*
|
|
23
27
|
* See change: fix-extension-slash-commands-in-dashboard,
|
|
24
|
-
* add-rpc-stdin-dispatch-with-keeper-sidecar
|
|
28
|
+
* add-rpc-stdin-dispatch-with-keeper-sidecar,
|
|
29
|
+
* fix-slash-dispatch-delivery.
|
|
25
30
|
*/
|
|
26
31
|
import crypto from "node:crypto";
|
|
27
32
|
import type { ExtensionToServerMessage } from "@blackbelt-technology/pi-dashboard-shared/protocol.js";
|
|
@@ -38,9 +43,6 @@ export interface DispatchConnection {
|
|
|
38
43
|
send(msg: ExtensionToServerMessage): void;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
const PI_071_REQUIRED =
|
|
42
|
-
"Extension slash commands cannot be dispatched from the dashboard yet — requires pi 0.71+ (`pi.dispatchCommand`). Invoke from the pi TUI, or use the extension's tools directly.";
|
|
43
|
-
|
|
44
46
|
function emitFeedback(
|
|
45
47
|
sink: FeedbackSink | undefined,
|
|
46
48
|
sessionId: string,
|
|
@@ -64,8 +66,8 @@ function emitFeedback(
|
|
|
64
66
|
* Try to dispatch a slash command as an extension command.
|
|
65
67
|
*
|
|
66
68
|
* @returns `true` if the helper handled the text (extension command detected;
|
|
67
|
-
* dispatch attempted or
|
|
68
|
-
* through to template expansion or `sendUserMessage`.
|
|
69
|
+
* dispatch attempted or error feedback emitted). The caller MUST NOT
|
|
70
|
+
* fall through to template expansion or `sendUserMessage`.
|
|
69
71
|
* @returns `false` if `text` is not an extension slash command. The caller
|
|
70
72
|
* SHOULD continue with its existing fallback path.
|
|
71
73
|
*/
|
|
@@ -75,6 +77,7 @@ export async function tryDispatchExtensionCommand(
|
|
|
75
77
|
sessionId: string,
|
|
76
78
|
sink: FeedbackSink | undefined,
|
|
77
79
|
connection?: DispatchConnection,
|
|
80
|
+
delivery?: "steer" | "followUp",
|
|
78
81
|
): Promise<boolean> {
|
|
79
82
|
// Defensive: pi.getCommands() can throw on a stale ctx during dispose.
|
|
80
83
|
let commands: Array<{ name: string; source?: string }> = [];
|
|
@@ -88,12 +91,13 @@ export async function tryDispatchExtensionCommand(
|
|
|
88
91
|
|
|
89
92
|
if (!isExtensionSlashCommand(text, commands)) return false;
|
|
90
93
|
|
|
91
|
-
emitFeedback(sink, sessionId, text, "started");
|
|
92
|
-
|
|
93
94
|
// Path B (preferred when available): pi 0.71+ exposes dispatchCommand.
|
|
95
|
+
// Note: as of pi 0.74.1, dispatchCommand does NOT exist in the ExtensionAPI.
|
|
96
|
+
// This path is dead code until pi ships the API; preserved for future use.
|
|
94
97
|
if (hasDispatchCommand(pi)) {
|
|
98
|
+
emitFeedback(sink, sessionId, text, "started");
|
|
95
99
|
try {
|
|
96
|
-
await (pi as any).dispatchCommand(text, { streamingBehavior: "followUp" });
|
|
100
|
+
await (pi as any).dispatchCommand(text, { streamingBehavior: delivery ?? "followUp" });
|
|
97
101
|
emitFeedback(sink, sessionId, text, "completed");
|
|
98
102
|
} catch (err: any) {
|
|
99
103
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -108,6 +112,7 @@ export async function tryDispatchExtensionCommand(
|
|
|
108
112
|
// terminal event for this path — that would duplicate the reducer's
|
|
109
113
|
// started→terminal upsert. See change: add-rpc-stdin-dispatch-with-keeper-sidecar.
|
|
110
114
|
if (connection && isHeadlessRpcSession()) {
|
|
115
|
+
emitFeedback(sink, sessionId, text, "started");
|
|
111
116
|
connection.send({
|
|
112
117
|
type: "dispatch_extension_command",
|
|
113
118
|
sessionId,
|
|
@@ -117,7 +122,17 @@ export async function tryDispatchExtensionCommand(
|
|
|
117
122
|
return true;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
// Path D
|
|
121
|
-
|
|
125
|
+
// Path D: No dispatchCommand, not headless (tmux / wt) or no connection.
|
|
126
|
+
// Extension commands can only be dispatched through the RPC keeper, which
|
|
127
|
+
// is available for headless sessions (`pi --mode rpc`). For tmux/wt sessions
|
|
128
|
+
// there is no injection channel — the command becomes a regular LLM message.
|
|
129
|
+
// To enable extension command dispatch for headless sessions:
|
|
130
|
+
// { "spawnStrategy": "headless", "useRpcKeeper": true }
|
|
131
|
+
// See change: fix-slash-dispatch-delivery.
|
|
132
|
+
const RPC_KEEPER_HINT =
|
|
133
|
+
"Extension slash commands cannot be dispatched from the dashboard for " +
|
|
134
|
+
"non-headless (tmux/wt) sessions. If you're using headless mode, add " +
|
|
135
|
+
'"useRpcKeeper": true to your dashboard config (~/.pi/dashboard/config.json).';
|
|
136
|
+
emitFeedback(sink, sessionId, text, "error", RPC_KEEPER_HINT);
|
|
122
137
|
return true;
|
|
123
138
|
}
|
|
@@ -11,16 +11,24 @@ import { readSessionMeta } from "@blackbelt-technology/pi-dashboard-shared/sessi
|
|
|
11
11
|
* a .meta.json sidecar with source information.
|
|
12
12
|
*/
|
|
13
13
|
export function detectSessionSource(hasUI?: boolean, sessionFile?: string): SessionSource {
|
|
14
|
-
//
|
|
14
|
+
// A TUI is attached → the session is a CLI/TUI session, not headless,
|
|
15
|
+
// regardless of any .meta.json sidecar (which can be stamped "dashboard"
|
|
16
|
+
// by event-wiring's pendingDashboardSpawns by-cwd matcher when an
|
|
17
|
+
// unrelated dashboard Spawn for the same cwd happened around the same time).
|
|
18
|
+
if (hasUI) {
|
|
19
|
+
if (process.env.ZED_TERM) return "tui"; // pi TUI inside Zed's terminal
|
|
20
|
+
if (process.env.TMUX) return "tmux";
|
|
21
|
+
return "tui";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// No TUI attached → headless. Check for .meta.json sidecar written by
|
|
25
|
+
// the dashboard server when it spawned this session.
|
|
15
26
|
if (sessionFile) {
|
|
16
27
|
const meta = readSessionMeta(sessionFile);
|
|
17
28
|
if (meta?.source === "dashboard") return "dashboard";
|
|
18
29
|
}
|
|
19
30
|
|
|
20
|
-
if (process.env.ZED_TERM)
|
|
21
|
-
if (hasUI) return "tui";
|
|
22
|
-
return "zed";
|
|
23
|
-
}
|
|
31
|
+
if (process.env.ZED_TERM) return "zed";
|
|
24
32
|
if (process.env.TMUX) return "tmux";
|
|
25
33
|
return "tui";
|
|
26
34
|
}
|
|
@@ -11,8 +11,25 @@
|
|
|
11
11
|
* See change: fix-provider-retry-infinite-loop.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Matches terminal billing/quota error categories observed in production
|
|
16
|
+
* across providers. The orderer uses this to surface a synthesized
|
|
17
|
+
* `auto_retry_end{success:false}` BEFORE the bridge forwards `agent_end`,
|
|
18
|
+
* so the dashboard's `retryState` clears before `lastError` is set.
|
|
19
|
+
*
|
|
20
|
+
* Coverage (verified via fixture tests):
|
|
21
|
+
* - Codex / Anthropic / generic: usage_limit_reached, usage_not_included,
|
|
22
|
+
* quota_exceeded, credit_balance, insufficient_quota, monthly limit,
|
|
23
|
+
* daily limit, hourly limit, "reset after Nh|Nm|Ns".
|
|
24
|
+
* - Gemini / Google: "monthly spending cap", "spending cap",
|
|
25
|
+
* RESOURCE_EXHAUSTED.
|
|
26
|
+
* - Generic catch-all for "exceeded ... (quota|cap|spending)" within
|
|
27
|
+
* ~40 chars (avoids a string with no terminal-meaning context).
|
|
28
|
+
*
|
|
29
|
+
* See change: fix-retry-banner-stuck-on-limit-exceeded.
|
|
30
|
+
*/
|
|
14
31
|
export const USAGE_LIMIT_PATTERN =
|
|
15
|
-
/usage[_ ]limit[_ ]reached|usage_not_included|quota[_ ]exceeded|monthly limit|hourly limit|reset after \d+[hms]/i;
|
|
32
|
+
/usage[_ ]limit[_ ]reached|usage_not_included|insufficient_quota|credit[_ ]balance|quota[_ ]exceeded|resource[_ ]exhausted|monthly[_ ]limit|monthly[_ ]spending[_ ]cap|hourly[_ ]limit|daily[_ ]limit|spending[_ ]cap|exceeded[^"]{0,40}(quota|cap|spending)|reset after \d+[hms]/i;
|
|
16
33
|
|
|
17
34
|
export interface SyntheticEventEnvelope {
|
|
18
35
|
eventType: "auto_retry_end";
|
|
@@ -4,18 +4,26 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The actual CLI is `../src/cli.ts`. This wrapper exists because a
|
|
6
6
|
* `#!/usr/bin/env` shebang cannot interpolate a dynamic `--import`
|
|
7
|
-
* loader path. The wrapper resolves jiti from
|
|
8
|
-
* and re-execs Node with
|
|
7
|
+
* loader path. The wrapper resolves jiti from `process.argv[1]`'s
|
|
8
|
+
* module graph at runtime and re-execs Node with
|
|
9
|
+
* `--import <jiti-url> cli.ts <args>`.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* in
|
|
13
|
-
*
|
|
11
|
+
* Since `@blackbelt-technology/pi-dashboard-server` declares `jiti` as
|
|
12
|
+
* a direct runtime dependency, `createRequire(argv[1]).resolve("jiti/...")`
|
|
13
|
+
* SHALL succeed in any well-formed npm install layout (flat, scoped,
|
|
14
|
+
* hoisted, pnpm). A miss therefore indicates a corrupted install, not
|
|
15
|
+
* a missing prerequisite. The error message reflects that.
|
|
14
16
|
*
|
|
15
|
-
*
|
|
17
|
+
* No tsx fallback: jiti is the sole supported TypeScript loader.
|
|
18
|
+
* Mirrors the resolution shape in
|
|
19
|
+
* `packages/shared/src/platform/binary-lookup.ts::ToolResolver.resolveJiti`
|
|
20
|
+
* (cannot import the .ts module before a TS loader is registered, so
|
|
21
|
+
* the lookup is inlined).
|
|
22
|
+
*
|
|
23
|
+
* See change: replace-tsx-with-jiti, enable-standalone-npm-install.
|
|
16
24
|
*/
|
|
17
25
|
import { createRequire } from "node:module";
|
|
18
|
-
import { realpathSync } from "node:fs";
|
|
26
|
+
import { realpathSync, readFileSync } from "node:fs";
|
|
19
27
|
import { spawn } from "node:child_process";
|
|
20
28
|
import { dirname, join, resolve } from "node:path";
|
|
21
29
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
@@ -23,7 +31,35 @@ import { pathToFileURL, fileURLToPath } from "node:url";
|
|
|
23
31
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
24
32
|
const cliPath = resolve(here, "..", "src", "cli.ts");
|
|
25
33
|
|
|
26
|
-
//
|
|
34
|
+
// Metadata short-circuit: --version / -v / version SHALL NOT require jiti.
|
|
35
|
+
// See change: fix-electron-cold-launch-probe-cascade (Bug B).
|
|
36
|
+
//
|
|
37
|
+
// Why: every metadata-only consumer (npmGlobal probe, doctor, the user
|
|
38
|
+
// asking "what's installed") was previously blocked when the wrapper
|
|
39
|
+
// couldn't resolve jiti — even when the install was perfectly capable
|
|
40
|
+
// of answering the query from its sibling package.json. Reading the
|
|
41
|
+
// version is a pure metadata operation; gating it on a TS loader was
|
|
42
|
+
// over-aggressive.
|
|
43
|
+
//
|
|
44
|
+
// Falls through (no exit) on read/parse error so the legacy install
|
|
45
|
+
// hint still surfaces for genuinely corrupt installs.
|
|
46
|
+
const metaArg = process.argv[2];
|
|
47
|
+
if (metaArg === "--version" || metaArg === "-v" || metaArg === "version") {
|
|
48
|
+
try {
|
|
49
|
+
const pkgPath = resolve(here, "..", "package.json");
|
|
50
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
51
|
+
if (pkg && typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
52
|
+
process.stdout.write(pkg.version + "\n");
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
/* fall through to jiti-resolve path so the legacy error still fires */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Mirrors packages/shared/src/platform/binary-lookup.ts JITI_PACKAGES.
|
|
61
|
+
// Kept in sync by repo-lint: packages/shared/src/__tests__/jiti-packages-parity.test.ts.
|
|
62
|
+
// See change: enable-standalone-npm-install task 7.3.
|
|
27
63
|
const JITI_PACKAGES = ["jiti", "@mariozechner/jiti"];
|
|
28
64
|
|
|
29
65
|
/** Resolve pi's jiti register hook as a file:// URL. Returns null on miss. */
|
|
@@ -51,17 +87,29 @@ function resolveJitiUrl() {
|
|
|
51
87
|
|
|
52
88
|
const loader = resolveJitiUrl();
|
|
53
89
|
if (!loader) {
|
|
90
|
+
// jiti is a direct dep of @blackbelt-technology/pi-dashboard-server, so a
|
|
91
|
+
// miss here means the install is corrupted (deleted node_modules entry,
|
|
92
|
+
// partial extract, etc.). The legacy "install pi globally" hint is kept
|
|
93
|
+
// as a workaround for users who can't reinstall the dashboard cleanly.
|
|
54
94
|
process.stderr.write(
|
|
55
|
-
"pi-dashboard: cannot find jiti
|
|
56
|
-
"
|
|
95
|
+
"pi-dashboard: cannot find jiti.\n" +
|
|
96
|
+
"This is unexpected: jiti ships as a direct dependency of pi-dashboard-server.\n" +
|
|
97
|
+
"Your install may be corrupted. Try:\n" +
|
|
98
|
+
" npm install -g @blackbelt-technology/pi-agent-dashboard\n" +
|
|
99
|
+
"Workaround: install pi globally (provides a fallback jiti):\n" +
|
|
100
|
+
" npm install -g @earendil-works/pi-coding-agent\n" +
|
|
101
|
+
"Please report at https://github.com/BlackBeltTechnology/pi-agent-dashboard/issues\n",
|
|
57
102
|
);
|
|
58
103
|
process.exit(1);
|
|
59
104
|
}
|
|
60
105
|
|
|
61
106
|
// Mirrors shouldUrlWrapEntry() in packages/shared/src/platform/node-spawn.ts:
|
|
62
|
-
// jiti
|
|
63
|
-
//
|
|
64
|
-
|
|
107
|
+
// jiti misnormalises file:/// URL entries on Windows (verified live on
|
|
108
|
+
// Node 22.18.0 + jiti 2.7.0 in a standalone install — the entry gets
|
|
109
|
+
// re-prepended with cwd as if it were a relative specifier). Pass the
|
|
110
|
+
// RAW path on every platform; Node's drive-letter heuristic handles
|
|
111
|
+
// `C:\…` entries directly. See change: fix-windows-standalone-spawn.
|
|
112
|
+
const entry = cliPath;
|
|
65
113
|
|
|
66
114
|
const child = spawn(
|
|
67
115
|
process.execPath,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blackbelt-technology/pi-dashboard-server",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Dashboard server for monitoring and interacting with pi agent sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
"postinstall": "node scripts/fix-pty-permissions.cjs"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@blackbelt-technology/dashboard-plugin-runtime": "^0.5.
|
|
36
|
-
"@blackbelt-technology/pi-dashboard-extension": "^0.5.
|
|
37
|
-
"@blackbelt-technology/pi-dashboard-shared": "^0.5.
|
|
35
|
+
"@blackbelt-technology/dashboard-plugin-runtime": "^0.5.4",
|
|
36
|
+
"@blackbelt-technology/pi-dashboard-extension": "^0.5.4",
|
|
37
|
+
"@blackbelt-technology/pi-dashboard-shared": "^0.5.4",
|
|
38
|
+
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
38
39
|
"@fastify/compress": "^8.3.1",
|
|
39
40
|
"@fastify/cookie": "^11.0.2",
|
|
40
41
|
"@fastify/cors": "^11.0.0",
|
|
@@ -42,12 +43,15 @@
|
|
|
42
43
|
"@fastify/reply-from": "^12.6.1",
|
|
43
44
|
"@fastify/static": "^8.0.0",
|
|
44
45
|
"@fastify/websocket": "^11.0.0",
|
|
46
|
+
"@fission-ai/openspec": "^1.3.0",
|
|
45
47
|
"bonjour-service": "^1.3.0",
|
|
46
48
|
"diff": "^8.0.3",
|
|
47
49
|
"fastify": "^5.0.0",
|
|
50
|
+
"jiti": "^2.7.0",
|
|
48
51
|
"jsonwebtoken": "^9.0.3",
|
|
49
|
-
"node-pty": "
|
|
52
|
+
"node-pty": "1.2.0-beta.13",
|
|
50
53
|
"proper-lockfile": "^4.1.2",
|
|
54
|
+
"tsx": "^4.21.0",
|
|
51
55
|
"ws": "^8.18.0"
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for browserGateway.registerHandler — the reverse channel that
|
|
3
|
+
* plugins use to receive Browser→Server custom message types.
|
|
4
|
+
*
|
|
5
|
+
* See change: adopt-server-driven-intent-rendering.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
import { createBrowserGateway } from "../browser-gateway.js";
|
|
9
|
+
|
|
10
|
+
function makeMockDeps() {
|
|
11
|
+
// Minimal mock dependencies for createBrowserGateway. We only need the
|
|
12
|
+
// gateway's registerHandler + the message dispatch loop, not session
|
|
13
|
+
// management.
|
|
14
|
+
return {
|
|
15
|
+
sessionManager: {
|
|
16
|
+
listActive: () => [],
|
|
17
|
+
listAll: () => [],
|
|
18
|
+
getSession: () => undefined,
|
|
19
|
+
registerSession: () => {},
|
|
20
|
+
unregisterSession: () => {},
|
|
21
|
+
updateSession: () => {},
|
|
22
|
+
detachAll: () => {},
|
|
23
|
+
attachExtension: () => {},
|
|
24
|
+
detachExtension: () => {},
|
|
25
|
+
markEnded: () => {},
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
} as any,
|
|
28
|
+
eventStore: {
|
|
29
|
+
append: () => {},
|
|
30
|
+
getEvents: () => [],
|
|
31
|
+
getLatestEvent: () => undefined,
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
} as any,
|
|
34
|
+
piGateway: {
|
|
35
|
+
send: () => {},
|
|
36
|
+
sendToSession: () => {},
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
|
+
} as any,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("browserGateway.registerHandler", () => {
|
|
43
|
+
it("stores and looks up handlers by type", () => {
|
|
44
|
+
const deps = makeMockDeps();
|
|
45
|
+
const gateway = createBrowserGateway(deps.sessionManager, deps.eventStore, deps.piGateway);
|
|
46
|
+
|
|
47
|
+
const handler = vi.fn();
|
|
48
|
+
gateway.registerHandler("plugin_action", handler);
|
|
49
|
+
|
|
50
|
+
// We can't easily invoke the WS message loop without a real WebSocket
|
|
51
|
+
// connection, so we verify only that registration succeeds without
|
|
52
|
+
// throwing. End-to-end dispatch is verified in section 19 manual smoke.
|
|
53
|
+
expect(typeof gateway.registerHandler).toBe("function");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("multiple handlers for different types can be registered", () => {
|
|
57
|
+
const deps = makeMockDeps();
|
|
58
|
+
const gateway = createBrowserGateway(deps.sessionManager, deps.eventStore, deps.piGateway);
|
|
59
|
+
|
|
60
|
+
const handlerA = vi.fn();
|
|
61
|
+
const handlerB = vi.fn();
|
|
62
|
+
gateway.registerHandler("plugin_action", handlerA);
|
|
63
|
+
gateway.registerHandler("plugin_other", handlerB);
|
|
64
|
+
|
|
65
|
+
// No throw on registration. (Last-write-wins for the same type
|
|
66
|
+
// is implicit Map semantics; not validated here.)
|
|
67
|
+
expect(true).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression-prevention lint: forbid the `env: { ...process.env }` anti-pattern
|
|
3
|
+
* in `packages/server/src/cli.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Why this is forbidden:
|
|
6
|
+
*
|
|
7
|
+
* `launchDashboardServer` (the shared spawn primitive) internally computes the
|
|
8
|
+
* spawn env as `ToolResolver.buildSpawnEnv(process.env)`, which augments PATH
|
|
9
|
+
* with managed-dir, bundled-node, and pi-bin prepends. Caller-supplied `env`
|
|
10
|
+
* is then overlaid on top with caller-wins semantics.
|
|
11
|
+
*
|
|
12
|
+
* Passing `env: { ...process.env }` re-supplies the raw, un-augmented PATH
|
|
13
|
+
* back over the augmented base, silently defeating the entire purpose of
|
|
14
|
+
* `buildSpawnEnv` — the spawned daemon then cannot find `pi` (or any other
|
|
15
|
+
* tool resolved via PATH augmentation) in environments where the launching
|
|
16
|
+
* shell's PATH lacks those prepends (e.g. `.desktop` launchers, systemd-user
|
|
17
|
+
* units, non-interactive logins that don't init nvm).
|
|
18
|
+
*
|
|
19
|
+
* See: openspec/changes/fix-cli-env-clobber/proposal.md
|
|
20
|
+
* See: dashboard-server capability spec — constraint C22 (env merge contract)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from "vitest";
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = path.dirname(__filename);
|
|
30
|
+
const CLI_SOURCE = path.resolve(__dirname, "..", "cli.ts");
|
|
31
|
+
|
|
32
|
+
describe("cli.ts env clobber lint", () => {
|
|
33
|
+
it("does not pass `env: { ...process.env }` to launchDashboardServer", () => {
|
|
34
|
+
const source = fs.readFileSync(CLI_SOURCE, "utf8");
|
|
35
|
+
const pattern = /env:\s*\{\s*\.\.\.process\.env\s*\}/;
|
|
36
|
+
const match = source.match(pattern);
|
|
37
|
+
|
|
38
|
+
expect(
|
|
39
|
+
match,
|
|
40
|
+
"packages/server/src/cli.ts must not contain `env: { ...process.env }`. " +
|
|
41
|
+
"This pattern clobbers the augmented PATH from ToolResolver.buildSpawnEnv. " +
|
|
42
|
+
"Omit `env` to let the shared primitive's resolver-merged env take effect. " +
|
|
43
|
+
"See openspec/changes/fix-cli-env-clobber/proposal.md.",
|
|
44
|
+
).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-lint — guards Phase 3.0.a/3.0.b of change:
|
|
3
|
+
* eliminate-electron-runtime-install.
|
|
4
|
+
*
|
|
5
|
+
* Under R3, `packages/server/src/cli.ts::runForeground` MUST NOT reach into
|
|
6
|
+
* any of the deleted bootstrap modules. The tool-registry resolve is the
|
|
7
|
+
* sole point at which startup verifies pi is reachable; failure throws a
|
|
8
|
+
* hard error citing a corrupted node_modules/ tree (no degraded mode).
|
|
9
|
+
*
|
|
10
|
+
* This test asserts the cli.ts source text contains:
|
|
11
|
+
* 1. ZERO references to the deleted module names (`bootstrap-install`,
|
|
12
|
+
* `bootstrap-install-from-list`, `installable-list`, `bootstrap-state`,
|
|
13
|
+
* `bootstrap-queue`, `managed-workspace-materialize`,
|
|
14
|
+
* `defaultInstallableList`, `writeInstallableList`,
|
|
15
|
+
* `updateBootstrapCompatibility`, `BootstrapStateStore`).
|
|
16
|
+
* 2. The `[bootstrap] ready` log line, proving the tool-registry
|
|
17
|
+
* resolve path is in place.
|
|
18
|
+
* 3. The hard-throw branch citing a "corrupted node_modules" message.
|
|
19
|
+
*
|
|
20
|
+
* If you intentionally rename / move the resolve step, update this lint
|
|
21
|
+
* to match — but reaching back into any of the forbidden symbols means
|
|
22
|
+
* runtime install has crept back into the standalone arm.
|
|
23
|
+
*/
|
|
24
|
+
import { describe, it, expect } from "vitest";
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const CLI_PATH = path.resolve(HERE, "..", "cli.ts");
|
|
31
|
+
|
|
32
|
+
const FORBIDDEN_SYMBOLS = [
|
|
33
|
+
"bootstrap-install",
|
|
34
|
+
"bootstrap-install-from-list",
|
|
35
|
+
"installable-list",
|
|
36
|
+
"bootstrap-state",
|
|
37
|
+
"bootstrap-queue",
|
|
38
|
+
"managed-workspace-materialize",
|
|
39
|
+
"defaultInstallableList",
|
|
40
|
+
"writeInstallableList",
|
|
41
|
+
"updateBootstrapCompatibility",
|
|
42
|
+
"BootstrapStateStore",
|
|
43
|
+
"bootstrapInstall",
|
|
44
|
+
"bootstrapInstallFromList",
|
|
45
|
+
"runDegradedModeBootstrap",
|
|
46
|
+
"maybeSeedDefaultInstallableList",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
describe("cli.ts has no bootstrap-install references (Phase 3.0.b)", () => {
|
|
50
|
+
it("does not reference any deleted bootstrap modules or symbols", () => {
|
|
51
|
+
const src = fs.readFileSync(CLI_PATH, "utf-8");
|
|
52
|
+
const offenders = FORBIDDEN_SYMBOLS.filter((sym) => src.includes(sym));
|
|
53
|
+
expect(
|
|
54
|
+
offenders,
|
|
55
|
+
`cli.ts must not reference deleted bootstrap symbols under R3:\n ${offenders.join(", ")}\n\nSee change: eliminate-electron-runtime-install (Phase 3.0).`,
|
|
56
|
+
).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("contains the [bootstrap] ready log line proving tool-registry resolve is wired", () => {
|
|
60
|
+
const src = fs.readFileSync(CLI_PATH, "utf-8");
|
|
61
|
+
expect(src).toContain("[bootstrap] ready (pi resolved via");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws hard on pi resolution failure (no degraded mode)", () => {
|
|
65
|
+
const src = fs.readFileSync(CLI_PATH, "utf-8");
|
|
66
|
+
// The hard-throw branch in runForeground cites "corrupted node_modules".
|
|
67
|
+
expect(src).toMatch(/corrupted node_modules/i);
|
|
68
|
+
});
|
|
69
|
+
});
|