@blackbelt-technology/pi-agent-dashboard 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +104 -35
- package/README.md +390 -494
- package/docs/architecture.md +423 -20
- package/package.json +11 -8
- package/packages/extension/package.json +11 -4
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +91 -15
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +170 -61
- package/packages/extension/src/bridge.ts +199 -19
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +73 -0
- package/packages/extension/src/server-launcher.ts +15 -3
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +11 -5
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +72 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +34 -6
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +61 -15
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/pi-version-skew.ts +12 -1
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/restart-helper.ts +13 -2
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/platform/index.ts +1 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +79 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +20 -1
- package/packages/shared/src/tool-registry/definitions.ts +92 -0
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
package/AGENTS.md
CHANGED
|
@@ -30,6 +30,18 @@ pi-dashboard # Start dashboard server
|
|
|
30
30
|
pi-dashboard --dev # Start with Vite proxy
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Running Tests
|
|
34
|
+
|
|
35
|
+
Pipe test output to a tmp file, then grep — avoids re-running to inspect errors:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm test 2>&1 | tee /tmp/pi-test.log # run once, capture all output
|
|
39
|
+
grep -nE 'FAIL|Error|✗|✘' /tmp/pi-test.log # find failures
|
|
40
|
+
grep -n -A 20 'FAIL ' /tmp/pi-test.log # failure + context
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Always grep the file — never rerun `npm test` just to see errors.
|
|
44
|
+
|
|
33
45
|
## Cross-Platform QA Testing
|
|
34
46
|
|
|
35
47
|
VM-based QA testing for verifying clean-state installation and runtime across platforms.
|
|
@@ -57,12 +69,12 @@ make clean # Destroy all cloned VMs
|
|
|
57
69
|
|
|
58
70
|
| File | Purpose |
|
|
59
71
|
|------|---------|
|
|
60
|
-
| `src/shared/protocol.ts` | Extension↔Server WebSocket messages |
|
|
61
|
-
| `src/shared/browser-protocol.ts` | Server↔Browser WebSocket messages (all message types including PromptBus `prompt_request`/`prompt_dismiss`/`prompt_cancel` must be in the `ServerToBrowserMessage`
|
|
72
|
+
| `src/shared/protocol.ts` | Extension↔Server WebSocket messages. Phase-1 Extension UI System adds `ui_modules_list` / `ui_data_list` (extension → server) and `ui_management` (server → extension); these messages MUST stay in `ExtensionToServerMessage` / `ServerToExtensionMessage` unions or esbuild strips the switch cases in production. See change: add-extension-ui-modal. |
|
|
73
|
+
| `src/shared/browser-protocol.ts` | Server↔Browser WebSocket messages (all message types including PromptBus `prompt_request`/`prompt_dismiss`/`prompt_cancel` and Extension UI System Phase-1 `ui_modules_list`/`ui_data_list` (server → browser) + `ui_management` (browser → server) must be in the `ServerToBrowserMessage` / `BrowserToServerMessage` unions — `as any` switch cases are stripped by esbuild in production). **`prompt_request.metadata.toolCallId`** (change: fix-interactive-ui-reorder): optional field set by the bridge's `ctx.ui.{select, input, confirm, editor}` wrappers when the prompt is bound to a tool execution; consumed by the client reducer's `addInteractiveRequest` to pair the resulting `interactiveUi` row with its parent `toolResult`. The `metadata` field is typed `Record<string, unknown>`, so the addition is opt-in and forward/backward-compatible. |
|
|
62
74
|
| `src/shared/types.ts` | Data models (Session, Workspace, Event) |
|
|
63
75
|
| `src/shared/config.ts` | Shared config loader (`~/.pi/dashboard/config.json`). Includes `openspec: OpenSpecPollConfig` block (`pollIntervalSeconds` 5–3600, `maxConcurrentSpawns` 1–16, `changeDetection` `"mtime"\|"always"`, `jitterSeconds` 0–60) with clamping via `parseOpenSpecPollConfig`. See change: optimize-openspec-poll-burst |
|
|
64
76
|
| `src/shared/semaphore.ts` | Tiny FIFO semaphore (`createSemaphore(max)` → `{run, setMax, size}`). Used by `directory-service.ts` to cap concurrent `openspec` CLI spawns. Supports live resize via `setMax(n)` for runtime reconfig. |
|
|
65
|
-
| `src/extension/bridge.ts` | Main extension entry point (composes sync/tracker/flow modules, tracks `isAgentStreaming` in persistent BridgeState) |
|
|
77
|
+
| `src/extension/bridge.ts` | Main extension entry point (composes sync/tracker/flow modules, tracks `isAgentStreaming` in persistent BridgeState). **Invariant**: bridge code MUST NOT call `pi.newSession(...)` / `ctx.fork(...)` / `ctx.switchSession(...)` — enforced by `packages/extension/src/__tests__/no-session-replacement-calls.test.ts`. pi 0.69+ invalidates captured `pi`/`ctx` after these calls; the bridge re-captures state in `session_start` keyed on `event.reason ∈ {"new","fork","resume"}`. **PromptBus patch site**: patches `ctx.ui.select/input/confirm/editor/multiselect` in `session_start`; the TUI adapter handles `select/input/confirm/editor` only. `multiselect` is bridge-attached (pi has no native API) and routed exclusively through the bus to the `DashboardDefaultAdapter`'s browser dialog — decoded via `decodeMultiselectAnswer`. There is intentionally NO TUI adapter arm for multiselect: pi 0.70 RPC mode's `ctx.ui.custom` is a no-op, so any TUI arm awaiting it would auto-cancel the dashboard render in <1s (changes: fix-multiselect-auto-cancel-on-dashboard, fix-multiselect-tui-arm-self-cancel; regression-pinned by `no-tui-multiselect-arm-regression.test.ts`). **Per-message-fork entryId stamping** (change: fix-per-message-fork): `message_start` events now stamp a `nonce` (no entryId, since the user entry isn't persisted yet on pi 0.69+); `message_end` enrichment is deferred via `setTimeout(0)` (NOT `queueMicrotask`, which resolves inside pi's awaited extension dispatcher and misses `appendMessage`); a wrapped `ctx.sessionManager.appendMessage` emits `entry_persisted { entryId, nonce }` so the client reducer can back-fill the user-message bubble's `entryId`. |
|
|
66
78
|
| `src/extension/bridge-context.ts` | Shared mutable state type + helpers for bridge modules |
|
|
67
79
|
| `src/extension/session-sync.ts` | Session register, replay, and switch/fork handling |
|
|
68
80
|
| `src/extension/model-tracker.ts` | Model/thinking-level/git/name change detection |
|
|
@@ -86,39 +98,75 @@ make clean # Destroy all cloned VMs
|
|
|
86
98
|
| `src/client/components/BranchPicker.tsx` | Typeahead branch picker with keyboard navigation |
|
|
87
99
|
| `src/client/components/BranchSwitchDialog.tsx` | Checkout orchestration: dirty-state stash, pop prompt |
|
|
88
100
|
| `src/client/lib/git-api.ts` | Client-side fetch helpers for git API endpoints |
|
|
101
|
+
| `src/client/hooks/useImagePaste.ts` | Reusable clipboard-image-paste hook with controlled/uncontrolled modes. Uncontrolled (`useImagePaste()` with no args): hook owns `pendingImages` in local `useState` — used by `ExploreDialog` whose lifetime IS the dialog. Controlled (`useImagePaste({ images, onImagesChange })`): caller owns the array — used by `<CommandInput>` so App can key pending images by `sessionId`. `imageError` stays local in BOTH modes (auto-clears after 3 s, no value in lifting). Supports image/png, image/jpeg, image/gif, image/webp; 10 MB base64 cap. See change: lift-pending-images-to-app |
|
|
89
102
|
| `src/extension/prompt-bus.ts` | PromptBus — unified prompt routing to registered adapters (TUI, dashboard, custom). First-response-wins, cross-adapter dismissal. |
|
|
90
103
|
| `src/extension/dashboard-default-adapter.ts` | Built-in PromptBus adapter that renders prompts as generic interactive dialogs in dashboard chat |
|
|
104
|
+
| `src/extension/ui-modules.ts` | Extension UI System Phases 1+2: `refreshUiModules(ctx)` partitions `probe.modules` by `kind` — `management-modal` → `ui_modules_list` (Phase 1, last-write-wins on duplicate `id`); `footer-segment`/`agent-metric`/`breadcrumb`/`gate`/`toast` → one `ext_ui_decorator` per descriptor (Phase 2, last-write-wins on `(kind,namespace,id)`; `namespace` validated against `/^[a-z0-9-]+$/`; `removed: true` forwarded verbatim). `subscribeUiInvalidate(ctx)` throttles `ui:invalidate` re-probes to 20/sec (50 ms leading + trailing) and emits one warning per offending burst (`INVALIDATE_RATE_CAP_PER_SEC = 20`). `handleUiManagement(ctx, msg)` re-emits browser-originated `ui_management` on `pi.events` with `_reply` injected and forwards synchronous `data.items` as `ui_data_list`. Wired into `bridge.ts` at `session_start` and the `onReconnect` callback. See changes: add-extension-ui-modal, add-extension-ui-decorations. |
|
|
105
|
+
| `src/client/components/extension-ui/GenericExtensionDialog.tsx` | Phase-1 modal renderer for `ExtensionUiModule`. Supports `view.kind ∈ {table, grid, form}`, dispatches `ui_management { action: "list", event: dataEvent }` on mount for table/grid, gates `UiAction.confirm` through `ConfirmDialog`. MDI icons resolved via `mdi-icon-lookup.ts` (unknown keys render no icon). |
|
|
106
|
+
| `src/client/components/extension-ui/decorator-utils.ts` | Phase-2 helper: `decoratorsOfKind(decorators, kind)` — typed filter over `Session.uiDecorators` returning only descriptors of the requested `DecoratorKind`. Used by every Phase-2 slot component. See change: add-extension-ui-decorations. |
|
|
107
|
+
| `src/client/components/extension-ui/FooterSegmentSlot.tsx` | Phase-2 slot. Renders all `kind: "footer-segment"` descriptors as inline pills mounted inside `SessionHeader.tsx` to the right of the model/thinking-level info. Supports MDI `payload.icon` (resolved via `mdi-icon-lookup`) and `payload.tooltip` (HTML title attribute). |
|
|
108
|
+
| `src/client/components/extension-ui/AgentMetricSlot.tsx` | Phase-2 slot. Renders `agent-metric` descriptors whose `payload.agentId` matches the `FlowAgentCard.agent.agentName` of the parent. Mounted inside `FlowAgentCard.tsx`. Orphan descriptors (no matching agent) render nothing. |
|
|
109
|
+
| `src/client/components/extension-ui/BreadcrumbSlot.tsx` | Phase-2 slot. Renders the most-recently-cached `breadcrumb` descriptor as a horizontal step indicator at the top of `FlowDashboard.tsx`. Steps with `status: "done"` show a check; `"error"` show a red alert; the active step is matched against `payload.current` (or first `status: "active"`). |
|
|
110
|
+
| `src/client/components/extension-ui/GateSlot.tsx` | Phase-2 slot. Aggregates `gate` descriptors targeting the same `flowId` via the pure helper `aggregateGateState(decorators, flowId)`: most-restrictive-wins (any `available: false` blocks; reasons concatenated). Mounted inline in `FlowLaunchDialog.tsx`; the dialog also reads `aggregateGateState` directly to disable the Run button. |
|
|
111
|
+
| `src/client/components/extension-ui/ToastSlot.tsx` | Phase-2 slot. Top-right fixed tray mounted in `App.tsx`. Reads toast descriptors across every session in the `sessions` Map, stacks without deduplication, auto-dismisses each toast after `payload.durationMs` (default 5000ms; `0` = sticky), caps simultaneous-display at 5 (FIFO eviction — cache is unaffected). Manual dismiss via the close button latches the descriptor key into a local `dismissed` set so cache replay does not resurrect dismissed toasts. |
|
|
112
|
+
| `src/client/lib/mdi-icon-lookup.ts` | `resolveMdiIcon(key) → string \| null` — looks up an MDI key string against `@mdi/js` exports; allowlists by `mdi`-prefix, returns `null` for unknown keys (no error). XSS-safe icon vocabulary for extension descriptors. |
|
|
91
113
|
| `src/client/lib/prompt-component-registry.ts` | Client-side component registry mapping prompt type strings to render metadata (placement, component) |
|
|
92
|
-
| `src/extension/ask-user-tool.ts` | `ask_user` tool registration (bundled in bridge, registered at session_start to avoid static tool-name conflicts with other extensions) |
|
|
93
|
-
| `src/
|
|
94
|
-
| `src/
|
|
114
|
+
| `src/extension/ask-user-tool.ts` | `ask_user` tool registration (bundled in bridge, registered at session_start to avoid static tool-name conflicts with other extensions). `multiselect` dispatches through `polyfillMultiselect` (since pi-coding-agent's `ExtensionUIContext` has no native `multiselect` method). Tool description instructs agents: *"UI provides a Select all toggle; do not add one."* **Schema shape**: `parameters` is a single flat `Type.Object` (root `type: "object"`) — NOT a root `Type.Union` — because OpenAI's function-calling validator rejects root-level `anyOf` with *"schema must be a JSON Schema of 'type: \"object\"'"*. To restore Anthropic-friendly per-method strictness after commit a53933f, the root object now carries a body-level `oneOf` discriminator over `method` (confirm/select/multiselect/input/batch) with per-arm `required` + `minItems` constraints; sub-questions use the same flat-object + `oneOf` pattern with no `batch` arm (no nesting). `prepareArguments` still provides runtime rescue/normalization (`params` unwrap, `question`→`title`, stringified `options`/`questions`, batch synthesis/title backfill, `{label,value}` → labels), and `execute` retains empty-options guardrails. See changes: ask-user-multiselect-polyfill, refactor(schema)-restructure-ask-user-tool-schemas, fix-multiselect-auto-cancel-on-dashboard. |
|
|
115
|
+
| `src/extension/multiselect-polyfill.ts` | `polyfillMultiselect(ctx, title, options, opts)` — primary path delegates to bridge-patched `ctx.ui.multiselect` (PromptBus → `DashboardDefaultAdapter` → client `MultiselectRenderer`). Legacy fallback uses `ctx.ui.custom<T>()` + `MultiSelectList` for older / non-bridge contexts; this fallback is **a no-op in pi 0.70 RPC mode** (dashboard headless sessions) — pi-coding-agent defines `custom` as `async () => undefined` there — and is only effective in pure-TUI sessions if a future pi version restores `ctx.ui.custom` in RPC mode. Resolves to `string[]` (confirmed, including empty `[]`) or `undefined` (cancelled). Used for both single-question and batch sub-question `multiselect` paths in `ask-user-tool.ts`. See changes: fix-multiselect-auto-cancel-on-dashboard, fix-multiselect-tui-arm-self-cancel. |
|
|
116
|
+
| `src/extension/multiselect-list.ts` | `MultiSelectList` component implementing pi-tui's `Component` interface. Keyboard contract: `↑↓`/`k`/`j` navigate, `Space` toggles current, `Enter` confirms (selected values in original option order), `Escape` cancels. **No "select all" binding in TUI** — the dashboard adapter provides that affordance. |
|
|
117
|
+
| `src/shared/openspec-activity-detector.ts` | Detects OpenSpec activity from tool events; auto-attach requires only changeName (phase optional). Bash arm rejects flag-shaped capture tokens (`name.startsWith("-")` → `null`) so `openspec archive --help` and similar discovery commands no longer rename unnamed sessions to `--help`. See change: fix-openspec-flag-rename-bug. |
|
|
118
|
+
| `src/shared/openspec-poller.ts` | OpenSpec CLI polling (shared, used by server DirectoryService). `buildOpenSpecData(listResult, statusResults, designProbeFactory?, specsProbeFactory?)` accepts two optional probe factories: a `DesignProbeFactory` that promotes the `design` artifact's status from `"ready"` to `"done"` when local file evidence (R1/R2/R3) satisfies design (see change: fix-openspec-design-detection), and a `SpecsProbeFactory` that promotes the `specs` artifact from `"ready"` to `"done"` when at least one `specs/**/*.md` file exists locally (see change: fix-openspec-specs-mtime-gate-blind-spot). Both overrides are promote-only, single-artifact-only, never demote; the post-override loop re-derives change-level `isComplete`. `pollOpenSpec` (sync) and `pollOpenSpecAsync` (async) wire real-fs probes via `createFsProbeFactory(cwd)` and `createFsSpecsProbeFactory(cwd)`. `directory-service.ts` does the same on the mtime-gated path. |
|
|
119
|
+
| `src/shared/openspec-design-evidence.ts` | Pure rule evaluator + real-fs probe factory for the OpenSpec design-artifact override. `evaluateLocalDesignSatisfaction(changeDir, probe)` short-circuits R1 (`^design.*\.md$` file present) → R2 (`design/` folder with `*.md`) → R3 (`tasks.md` contains a `^\s*-\s+\[[ xX]\]\s` checkbox). `createFsDesignEvidenceProbe()` returns a defensive sync probe (try/catch on every fs call). Probe-injection keeps `buildOpenSpecData` pure and unit-testable without filesystem mocks. See change: fix-openspec-design-detection. |
|
|
120
|
+
| `src/shared/openspec-specs-evidence.ts` | Pure rule evaluator + real-fs probe factory for the OpenSpec specs-artifact override. `evaluateLocalSpecsSatisfaction(changeDir, probe)` returns true iff at least one `*.md` file exists under `<changeDir>/specs/` (iterative DFS, short-circuits on first match). `createFsSpecsEvidenceProbe()` returns a defensive sync probe — every `readdirSync` is wrapped in try/catch so missing dirs / permission errors / symlink loops yield `false` rather than throwing. Mirror-shape of `openspec-design-evidence.ts`; consumed by `buildOpenSpecData` as a defense-in-depth layer over the `directory-service.ts` mtime gate (which now also watches `specs/**`). See change: fix-openspec-specs-mtime-gate-blind-spot. |
|
|
121
|
+
| `.pi/skills/openspec-shared/scripts/effective-status.sh` | Bash wrapper around `openspec status --change <name> --json` that applies the same R1/R2/R3 promotion as the dashboard so OpenSpec workflow skills (`openspec-{continue,ff,apply,verify}-change`) and dashboard session-card buttons cannot disagree about a change's next-ready artifact. Inlines the rule logic via `find` + `grep -E` and uses `jq` for JSON mutation; falls back to raw CLI output if `jq` is absent. **Repo-lint** `packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts` blocks raw `openspec status ... --json` calls in any of the four governed skills (opt-out: `ban:openspec-status-ok`). Parity test: `packages/shared/src/__tests__/openspec-effective-status-script.test.ts`. See change: fix-openspec-design-detection. |
|
|
95
122
|
| `src/shared/state-replay.ts` | Synthesizes events from pi entries (shared, used by server + bridge) |
|
|
123
|
+
| `src/shared/dashboard-plugin/slot-types.ts` | Frozen slot taxonomy: `SlotId` union, `Multiplicity`, `PayloadTier` enums, `SLOT_DEFINITIONS` record. Adding a slot = minor; removing = major. |
|
|
124
|
+
| `src/shared/dashboard-plugin/manifest-types.ts` | `PluginManifest` and `PluginClaim` interfaces; includes `tab` field for `settings-section` claims and `fixture` flag for test-only plugins. |
|
|
125
|
+
| `src/shared/dashboard-plugin/slot-props.ts` | `SlotPropsMap` and `SlotProps<SlotId>` — typed prop contracts per slot id. Type-level test asserts all slot ids are covered. |
|
|
126
|
+
| `src/shared/dashboard-plugin/plugin-status.ts` | `PluginStatus` (for `/api/health`) and `PluginConfigUpdate` (WebSocket broadcast payload). |
|
|
127
|
+
| `src/shared/plugin-bridge-register.ts` | Plugin bridge entry management: `registerPluginBridge`, `deregisterPluginBridge`, `registerAllPluginBridges`. Manages `dashboard-<plugin-id>` keys in `settings.json#dashboardPluginBridges`; never touches user-owned `packages[]` entries. Atomic write (tmp+rename). |
|
|
128
|
+
| `packages/dashboard-plugin-runtime/src/slot-registry.ts` | `createSlotRegistry()` — typed `Map<SlotId, ClaimEntry[]>` pre-sorted by `(priority, pluginId)`. Filter helpers: `forSession`, `forFolder`, `forCommand`, `forTab`, `forToolName`. |
|
|
129
|
+
| `packages/dashboard-plugin-runtime/src/manifest-validator.ts` | Hand-rolled manifest validator. Throws `ManifestValidationError` with `pluginId` and `reason`. No Zod dependency. |
|
|
130
|
+
| `packages/dashboard-plugin-runtime/src/plugin-context.tsx` | `PluginContextProvider`, `CurrentPluginLayer`, `usePluginConfig<T>()`, `useAllSessions`, `useSessionState`, `usePluginLogger`, `usePluginSend`, `usePluginRouter`, `useSlotRegistry`, `applyPluginConfigUpdate`. Per-plugin context layer scopes hooks to the contributing plugin's id. |
|
|
131
|
+
| `packages/dashboard-plugin-runtime/src/slot-consumers.tsx` | One component per slot id: `SidebarFolderSectionSlot`, `SessionCardBadgeSlot`, `SessionCardActionBarSlot`, `ContentViewSlot`, `ContentHeaderStickySlot`, `ContentInlineFooterSlot`, `AnchoredPopoverSlot`, `CommandRouteSlot`, `SettingsSectionSlot`, `ToolRendererSlot`. Each wraps contributions in `SlotErrorBoundary`. |
|
|
132
|
+
| `packages/dashboard-plugin-runtime/src/slot-error-boundary.tsx` | Per-claim React error boundary. Logs `[slot-error-boundary] Plugin "<id>" slot "<slot>" threw:` and renders nothing for the failing claim without suppressing siblings. |
|
|
133
|
+
| `packages/dashboard-plugin-runtime/src/vite-plugin/index.ts` | `viteDashboardPluginsPlugin(repoRoot?)` — generates `packages/client/src/generated/plugin-registry.tsx` with named imports (for tree-shaking). Watches manifests during dev; regenerates + triggers HMR on changes. Filters `fixture:true` plugins in production. |
|
|
134
|
+
| `packages/dashboard-plugin-runtime/src/server/loader.ts` | `discoverPlugins(repoRoot?)` (single-process-cache glob), `loadServerEntries(deps)` (per-plugin dynamic-import + `registerPlugin` invocation, failure-isolated), `getPluginStatusStore()`. |
|
|
135
|
+
| `packages/dashboard-plugin-runtime/src/server/server-context.ts` | `createServerPluginContext(deps, pluginId)` factory — creates a `ServerPluginContext` scoped to a plugin with namespaced logger and typed config accessors. |
|
|
136
|
+
| `packages/dashboard-plugin-runtime/src/server/config-validator.ts` | `validatePluginConfig`, `applySchemaDefaults` — Ajv JSON-Schema 7 validation for plugin config writes. |
|
|
137
|
+
| `packages/dashboard-plugin-runtime/src/server/plugin-status-store.ts` | In-memory `PluginStatusStore` — consumed by `/api/health.plugins[]`. |
|
|
138
|
+
| `src/server/routes/plugin-config-routes.ts` | `POST /api/config/plugins/:id` — validates `:id`, validates body against `configSchema`, merges into `plugins.<id>.*`, atomic-writes, broadcasts `plugin_config_update`. Auth-gated. |
|
|
139
|
+
| `packages/demo-plugin/` | Private workspace (`"private": true`, `"fixture": true`). Exercises `settings-section` (DemoSettings form) and `tool-renderer` (DemoToolRenderer green box for `toolName: "DashboardDemo"`). Excluded from production bundles. |
|
|
96
140
|
| `src/shared/stats-extractor.ts` | Extracts token/cost stats from turn_end events |
|
|
97
141
|
| `src/server/session-stats-reader.ts` | Reads cumulative stats + context usage from session JSONL files at startup |
|
|
98
|
-
| `src/server/server.ts` | HTTP + WebSocket server (composes route modules + wiring) |
|
|
142
|
+
| `src/server/server.ts` | HTTP + WebSocket server (composes route modules + wiring). Exports two pure helpers consumed by the bootstrap-state subscribe callback: `makeBootstrapTransitionHandler({ flushQueue, onTransitionToReady })` (stateful gate — fires both callbacks once per `installing → ready` transition, ignores the initial `ready` snapshot) and `runPostInstallRepair({ registry, directoryService, browserGateway })` (full `registry.rescan()` + per-cwd `refreshOpenSpec` with selective `openspec_update` broadcast on prior-empty / data-differs + per-cwd `refreshPiResources`; per-cwd failures isolated). The centralized hook covers all three install entry points (`runDegradedModeBootstrap`, REST `triggerUpgradePi`, REST `triggerRetry`). DEBUG-gated single-line diagnostic on completion. See change: fix-openspec-buttons-after-bootstrap-install. |
|
|
99
143
|
| `src/server/routes/session-routes.ts` | REST routes: sessions, events, session-diff |
|
|
100
144
|
| `src/server/routes/git-routes.ts` | REST routes: git branches, checkout, init, stash-pop |
|
|
101
|
-
| `src/server/routes/file-routes.ts` | REST routes: file read, browse, readme, pinned-dirs |
|
|
145
|
+
| `src/server/routes/file-routes.ts` | REST routes: file read, browse (with `detect=0\|1` opt-in classifier), browse-flags (bulk classifier), browse-mkdir, readme, pinned-dirs. See change: split-browse-flags |
|
|
102
146
|
| `src/server/routes/openspec-routes.ts` | REST routes: openspec-archive, pi-resources, pi-resource-file |
|
|
103
147
|
| `src/server/routes/system-routes.ts` | REST routes: config, health, shutdown, tunnel, editors |
|
|
104
|
-
| `src/server/event-wiring.ts` | Pi gateway → browser gateway event forwarding (replay suppression with `skipReplayInsert` dedup, flows refresh dedup, context usage extraction) |
|
|
148
|
+
| `src/server/event-wiring.ts` | Pi gateway → browser gateway event forwarding (replay suppression with `skipReplayInsert` dedup, flows refresh dedup, context usage extraction). Phase-1 Extension UI System: caches `ui_modules_list` on `Session.uiModules` and broadcasts; caches `ui_data_list` on `Session.uiDataMap[event]` with a per-event item cap (default 1000, last-write-wins on overflow) and broadcasts. Phase-2: `ext_ui_decorator` switch arm caches descriptors under `Session.uiDecorators[`${kind}:${namespace}:${id}`]` (upsert, or delete when `removed: true`) and broadcasts the message verbatim to subscribers; deleting an absent key is a no-op but still broadcasts. See changes: add-extension-ui-modal, add-extension-ui-decorations. |
|
|
105
149
|
| `src/server/idle-timer.ts` | Auto-shutdown idle timer with sleep-wake resilience |
|
|
106
150
|
| `src/server/session-bootstrap.ts` | Startup session discovery and OpenSpec polling init |
|
|
107
151
|
| `src/server/pi-gateway.ts` | Extension WebSocket gateway (port 9999) |
|
|
108
152
|
| `src/server/browser-gateway.ts` | Browser WebSocket gateway (dispatches to handler modules) |
|
|
109
153
|
| `src/server/browser-handlers/handler-context.ts` | Shared context type for browser message handlers |
|
|
110
|
-
| `src/server/browser-handlers/subscription-handler.ts` | Subscribe/unsubscribe with async batched replay, backpressure, lazy loading |
|
|
111
|
-
| `src/server/browser-handlers/session-action-handler.ts` | Send prompt, abort, resume, spawn, shutdown, force kill, flow control. `handleForceKill` delegates the SIGTERM→wait→SIGKILL escalation to `killProcess` from `platform/process.ts` so Windows gets `taskkill /F /T /PID` (genuine tree kill). No direct `process.kill()` anywhere (enforced by `no-direct-process-kill.test.ts`). `handleSendPrompt` also intercepts `/reload` on headless sessions (gated by `headlessPidRegistry.getPid`) and delegates to `handleHeadlessReload`, which SIGTERMs the pi process and respawns with `--session <file> --mode continue` — pi 0.68.0 has no extension-accessible reload path in RPC mode, so kill-and-respawn is the only way to reload settings/extensions/skills for headless sessions. See changes: route-kill-paths-through-platform, headless-reload-via-respawn. |
|
|
154
|
+
| `src/server/browser-handlers/subscription-handler.ts` | Subscribe/unsubscribe with async batched replay, backpressure, lazy loading. Exports `replayUiState(ws, sessionId, ctx)` for the Extension UI System; called immediately after every `replayPendingUiRequests` site (4 sites). Replay sends the cached `ui_modules_list` (Phase 1) → one `ui_data_list` per `(event, items)` entry (Phase 1) → one `ext_ui_decorator` per `Session.uiDecorators` cache entry (Phase 2; never with `removed: true` since deleted entries are absent). Replay ordering: events → pending UI requests → ui_modules_list → ui_data_list → ext_ui_decorator. See changes: add-extension-ui-modal, add-extension-ui-decorations. |
|
|
155
|
+
| `src/server/browser-handlers/session-action-handler.ts` | Send prompt, abort, resume, spawn, shutdown, force kill, flow control. `handleSpawnSession` accepts an optional `SpawnSessionBrowserMessage.attachProposal` and enqueues it into `pendingAttachRegistry` BEFORE awaiting `spawnPiSession(...)` so a fast `session_register` cannot lose the intent (see change: add-folder-task-checker-and-spawn-attach). `handleForceKill` delegates the SIGTERM→wait→SIGKILL escalation to `killProcess` from `platform/process.ts` so Windows gets `taskkill /F /T /PID` (genuine tree kill). No direct `process.kill()` anywhere (enforced by `no-direct-process-kill.test.ts`). `handleSendPrompt` also intercepts `/reload` on headless sessions (gated by `headlessPidRegistry.getPid`) and delegates to `handleHeadlessReload`, which SIGTERMs the pi process and respawns with `--session <file> --mode continue` — pi 0.68.0 has no extension-accessible reload path in RPC mode, so kill-and-respawn is the only way to reload settings/extensions/skills for headless sessions. See changes: route-kill-paths-through-platform, headless-reload-via-respawn. |
|
|
112
156
|
| `src/server/browser-handlers/session-action-helpers.ts` | Pure helpers for session-action-handler. `shouldInterceptReload(msg, headlessPidRegistry)` gates the headless-reload interception: exact `/reload` text, no images, PID tracked in registry. Extracted for testability. |
|
|
113
157
|
| `src/client/components/ImageLightbox.tsx` | Full-size image lightbox with zoom/pan (useZoomPan), Esc/backdrop close |
|
|
114
158
|
| `src/client/components/CollapsedToolGroup.tsx` | Collapsed group of repeated tool calls with expand toggle |
|
|
115
159
|
| `src/client/lib/group-tool-calls.ts` | Groups consecutive identical tool calls for chat display |
|
|
116
|
-
| `src/
|
|
160
|
+
| `src/client/lib/collapse-retried-errors.ts` | Two pure helpers used by `ChatView` to remove duplicate tool cards: `findRetriedErrorIds(messages)` returns ids of error `toolResult`s immediately superseded by a successful retry of the same tool (skipping `assistant`/`thinking`/`turnSeparator`/`rawEvent`/`commandFeedback`); `findActiveInteractiveToolResultIds(messages)` returns ids of `running` `toolResult`s paired with a *pending* `interactiveUi` message that follows them, so the running tool card can be hidden while the `InteractiveUiCard` is the sole interactive surface. See change: collapse-duplicate-tool-cards |
|
|
161
|
+
| `src/client/components/RetriedErrorBadge.tsx` | One-line `⚠ <toolName> failed — retried ›` pill rendered by `ChatView` in place of full `ToolCallStep` for ids returned by `findRetriedErrorIds`. Click toggles an expanded view that re-uses `<ToolCallStep status="error">` with a "Hide failed attempt" toggle so the original validation message + `Received arguments:` JSON is still recoverable. See change: collapse-duplicate-tool-cards |
|
|
162
|
+
| `src/server/browser-handlers/session-meta-handler.ts` | Rename, hide, unhide, attach/detach proposal, fetch, list. Attach/detach apply an **idempotent auto-rename rule** via the pure helpers in `packages/server/src/proposal-attach-naming.ts`: attach renames when name is empty OR equals the current `attachedProposal` (auto-set witness); detach reverts the name only when that equality holds. The same helpers are reused by the REST endpoints in `session-api.ts` to keep WS and REST in lockstep. See change: fix-mobile-attach-proposal-display. |
|
|
163
|
+
| `src/server/proposal-attach-naming.ts` | Pure decision-matrix helpers `attachRenameTarget(session, changeName)` and `detachShouldClearName(session)` used by both the WS handler and the REST endpoint to compute the idempotent auto-rename / auto-revert. Equality witness: `name === attachedProposal`. See change: fix-mobile-attach-proposal-display. |
|
|
117
164
|
| `src/server/browser-handlers/terminal-handler.ts` | Create, kill, rename terminals |
|
|
118
165
|
| `src/server/browser-handlers/directory-handler.ts` | Pin/unpin dirs, reorder, openspec refresh, pi-gateway forwards |
|
|
119
166
|
| `src/server/memory-event-store.ts` | In-memory event buffer with LRU eviction, per-session cap, payload truncation |
|
|
120
167
|
| `src/server/memory-session-manager.ts` | Pure in-memory session registry |
|
|
121
|
-
| `src/client/components/FolderOpenSpecSection.tsx` | Folder-level OpenSpec UI: collapsible change list, refresh, bulk archive, archive button |
|
|
168
|
+
| `src/client/components/FolderOpenSpecSection.tsx` | Folder-level OpenSpec UI: collapsible change list, refresh, bulk archive, archive button. Each change row's `N/M tasks` indicator is a button that opens the existing `TasksPopover` (one popover at a time; opening another row swaps the popover); a `mdiPlay` icon button rendered after the artifact letters invokes `onSpawnAttached(cwd, changeName)` to spawn a pi session with the change pre-attached. Both buttons stop propagation so the surrounding folder collapse is unaffected. See change: add-folder-task-checker-and-spawn-attach. |
|
|
169
|
+
| `src/server/pending-attach-registry.ts` | In-memory FIFO queue of pending `attachProposal` intents per cwd. `enqueue(cwd, changeName)` is called from `handleSpawnSession` when the browser sets `SpawnSessionBrowserMessage.attachProposal`; `consume(cwd)` is called from `event-wiring.ts`'s `pi-gateway.onSessionRegistered` hook on every `session_register`, and the resolved name is applied via the shared `applyAttachProposal(sessionId, changeName, ctx)` helper in `session-meta-handler.ts` (same code as `handleAttachProposal`). Per-cwd cap = 8 (silent drop + warn), 60 s TTL on every read/write to avoid stranded intents from failed spawns. Cwd is normalized via `safeRealpathSync` + trailing-sep strip so `/p`, `/p/`, and symlink variants share a queue. In-memory only — not persisted. See change: add-folder-task-checker-and-spawn-attach. |
|
|
122
170
|
| `src/client/components/ArchiveBrowserView.tsx` | Searchable archive browser: date-grouped list, two-level nav to artifact reader |
|
|
123
171
|
| `src/client/hooks/useArchiveListing.ts` | Fetch hook + pure helpers (groupByDate, filterEntries) for archive endpoint |
|
|
124
172
|
| `src/server/openspec-archive.ts` | Scans `openspec/changes/archive/` and returns structured ArchiveEntry list |
|
|
@@ -127,11 +175,14 @@ make clean # Destroy all cloned VMs
|
|
|
127
175
|
| `src/client/components/PinDirectoryDialog.tsx` | Dialog to pin a directory (wraps PathPicker) |
|
|
128
176
|
| `src/client/components/PathPicker.tsx` | Reusable keyboard-first path picker with typeahead directory list |
|
|
129
177
|
| `src/client/lib/browse-api.ts` | Client-side browse API helper for PathPicker |
|
|
130
|
-
| `src/server/browse.ts` | Directory listing
|
|
178
|
+
| `src/server/browse.ts` | Directory listing + classification for the browse API. Two responsibilities kept deliberately separate: `listDirectories(dir, q, { detect })` does enumeration (cheap, single `readdir`; per-entry `.git`/`.pi` probes only when `detect: true`), and `classifyPaths(paths)` does bulk classification (≤ `MAX_FLAG_PATHS = 100`, `fs.access` fan-out bounded by `createSemaphore(32)`, any error → `{ isGit: false, isPi: false }`). `parseFlagsQuery(rawPaths)` validates the `paths=<json-array>` query for `GET /api/browse/flags`. Worktree-safe: detection uses `fs.access` (never `readdir`) so `.git` regular-files in worktrees still classify correctly. See change: split-browse-flags |
|
|
131
179
|
| `src/server/pi-resource-scanner.ts` | Discovers pi extensions, skills, prompts from local, global, and package sources |
|
|
132
|
-
| `src/server/package-manager-wrapper.ts` | Thin adapter around pi's `DefaultPackageManager` with operation serialization, progress forwarding, and session reload; delegates module resolution to `ToolRegistry.resolveModule("pi-coding-agent")` |
|
|
180
|
+
| `src/server/package-manager-wrapper.ts` | Thin adapter around pi's `DefaultPackageManager` with operation serialization, progress forwarding, and session reload; delegates module resolution to `ToolRegistry.resolveModule("pi-coding-agent")`. Includes `move(req)` for scope-to-scope package moves: hybrid execution — npm/git/https sources reinstall at dest then remove from origin (single `busy` lock, coalesced reload), filesystem-path sources settings-edit only (uses `settingsManager.getGlobalSettings/getProjectSettings/setPackages/setProjectPackages` to preserve filter objects). Identity preflight via `computeIdentity` before any side-effect; partial-success (install OK, remove failed) is delivered through the `package_operation_complete` WS event's `partialSuccess` field. See change: unify-package-management-ui. |
|
|
181
|
+
| `src/server/package-source-helpers.ts` | Pure helpers `parseSourceKind(source)` (npm/git/https/abs-path/rel-path) and `computeIdentity(source, settingsDir?)` (npm = bare name, git/https = url-no-ref, path = resolved absolute) mirroring pi's source-kind and dedup rules from `docs/packages.md`. Used by `package-manager-wrapper`'s `move()` arm-selection and identity preflight; safe for cross-platform tests (no `process.platform` branching). See change: unify-package-management-ui. |
|
|
133
182
|
| `src/shared/tool-registry/registry.ts` | `ToolRegistry` service — single-source resolver for every external binary/module (pi, pi-coding-agent, openspec, npm, node, tsx, git, zrok, pi-dashboard). Ordered strategy chain per tool, per-resolution diagnostic trail, in-memory cache, override-aware |
|
|
134
|
-
| `src/shared/tool-registry/definitions.ts` | Registers the standard tool set. Each definition declares an ordered strategy chain (override → bare-import → managed → npm-global → where) and a classifier (strategy → source) |
|
|
183
|
+
| `src/shared/tool-registry/definitions.ts` | Registers the standard tool set. Each definition declares an ordered strategy chain (override → bare-import → managed → npm-global → where) and a classifier (strategy → source). Includes build-time tools `electron` and `node-pty` (registered as `kind: "module"` returning the package directory; resolved hoist-aware via `bareImportPackageDirStrategy` with optional `searchPaths`). See change: register-build-time-tools |
|
|
184
|
+
| `packages/shared/bin/pi-dashboard-resolve-tool.cjs` | Shell-callable resolver wrapper for the tool registry. CommonJS, dependency-free, invokable as `node packages/shared/bin/pi-dashboard-resolve-tool.cjs <tool-name> [--json]`. Mirrors the `override` + `bare-import` strategy semantics for build-time tools (electron, node-pty) so CI workflows / Dockerfiles / postinstall scripts can resolve hoisted-vs-nested layouts without depending on the shared package's TS build. **Strategy chain MUST stay in sync with `tool-registry/definitions.ts`** — enforced socially via cross-reference comments and via `no-hardcoded-node-modules-paths.test.ts`. See change: register-build-time-tools |
|
|
185
|
+
| `src/shared/__tests__/no-hardcoded-node-modules-paths.test.ts` | Repo-level lint: scans `.github/workflows/publish.yml`, `.github/workflows/ci.yml`, `packages/electron/scripts/Dockerfile.build`, and the two `fix-pty-permissions.cjs` postinstalls for hardcoded `node_modules/electron` / `node_modules/node-pty` substrings outside the explicit allowlist. Fails with `file:line:col` citation. Replaces hand-rolled inline `require.resolve(...)` patterns with the registered tool registry. Mirrors `no-direct-process-kill.test.ts` and `no-raw-node-import.test.ts`. See change: register-build-time-tools |
|
|
135
186
|
| `src/shared/tool-registry/strategies.ts` | Reusable resolution strategies: `overrideStrategy`, `managedBinStrategy`, `managedModuleStrategy`, `npmGlobalStrategy`, `whereStrategy`, `bareImportStrategy` (uses `createRequire` for sync probe). All take injectable `StrategyDeps` for tests |
|
|
136
187
|
| `src/shared/tool-registry/overrides.ts` | Read/write `~/.pi/dashboard/tool-overrides.json` (machine-local, separate from `config.json`). Lazy-loaded, atomic write via tmp+rename, malformed-file tolerant |
|
|
137
188
|
| `src/shared/tool-registry/types.ts` | `ToolDefinition`, `Strategy`, `StrategyResult`, `Resolution`, `Source`, `UnknownToolError`, `ModuleResolutionError` |
|
|
@@ -141,7 +192,7 @@ make clean # Destroy all cloned VMs
|
|
|
141
192
|
| `packages/server/src/bootstrap-state.ts` | In-memory bootstrap state store (`createBootstrapState()`, `BootstrapState { status, progress, error, version, compatibility, bridgeRegistrationError }`, `BootstrapStateStore` with get/set/subscribe/dispose plus side-channel `setLastInstallPackages` / `getLastInstallPackages` so `POST /api/bootstrap/retry` re-runs the exact failed set instead of a hard-coded default). Partial `set()` supports `undefined` = clear semantics; `setLastInstallPackages` does NOT trigger subscribers (it's not part of the broadcast snapshot). |
|
|
142
193
|
| `packages/server/src/routes/bootstrap-routes.ts` | REST routes: `GET /api/bootstrap/status`, `POST /api/bootstrap/upgrade-pi` (202+ticketId or 409), `POST /api/bootstrap/retry` (202 if failed, else 409). Trigger callbacks are injected so CLI wires them to `bootstrapInstall` while tests wire them to spies. |
|
|
143
194
|
| `packages/server/src/bootstrap-queue.ts` | In-memory ticket queue (`createBootstrapQueue()`, `enqueue(handler)`, `flushAll()`, `size()`, `clear(reason)`, `onTicketComplete(listener)`). `server.ts` flushes on bootstrap-state transition to "ready" and wires `onTicketComplete` → `bootstrap_ticket_complete` WS broadcast so browsers holding a 202 ticketId learn the queued op's outcome. `session-api.ts gateOrEnqueue` uses the queue to defer session spawn during installs. On `clear`, pending tickets are rejected directly (reject closure stored on the entry) so no caller hangs at shutdown. |
|
|
144
|
-
| `packages/server/src/pi-version-skew.ts` | Pi compatibility range reader (`readPiCompatibility` reads `piCompatibility` from `packages/server/package.json`; `readCurrentPiVersion` via `createRequire`), comparator (`parseVersion`, `compareVersions`, `isBelow`, `isAbove` supporting `0.x` wildcard), + `updateBootstrapCompatibility(store, pkgPath)` that writes the result into `bootstrapState.compatibility` with a 60 s cache. Below-minimum adds a 503-blocking `error` message. |
|
|
195
|
+
| `packages/server/src/pi-version-skew.ts` | Pi compatibility range reader (`readPiCompatibility` reads `piCompatibility` from `packages/server/package.json`; `readCurrentPiVersion` via `createRequire`, with `fs.realpathSync` on the registry-resolved bin path so symlinked npm-global launchers resolve to the real module's `package.json`), comparator (`parseVersion`, `compareVersions`, `isBelow`, `isAbove` supporting `0.x` wildcard), + `updateBootstrapCompatibility(store, pkgPath)` that writes the result into `bootstrapState.compatibility` with a 60 s cache. Below-minimum adds a 503-blocking `error` message. **CLI surface**: `cli.ts` calls `logCompatibilityWarning(bootstrapState)` after each `updateBootstrapCompatibility(...)` site and emits a stderr warning — 3-line red block on below-minimum (`⚠ pi X is below the required minimum Y … Run: pi-dashboard upgrade-pi`), single-line advisory on below-recommended, silent when in range. **Currently pinned**: `minimum: "0.70.0"`, `recommended: "0.70.0"`, `maximum: null` (lockstep — no backward compatibility for older pi versions). See changes: pi-zero-seventy-compat, warn-pi-version-skew-in-cli. |
|
|
145
196
|
| `packages/client/src/hooks/useBootstrapStatus.ts` | Client hook for bootstrap state. Fetches `/api/bootstrap/status` on mount, subscribes to `bootstrap-status` `CustomEvent` dispatched by `useMessageHandler` on `bootstrap_status_update` WS broadcasts. Exposes `{ state, isLoading, error, refresh, retry, upgradePi }`. |
|
|
146
197
|
| `packages/client/src/components/BootstrapBanner.tsx` | Banner mounted in `App.tsx` above `<MobileShell>`. Hidden at status="ready" with no compatibility hints; blue "Installing pi…" when installing; red "pi install failed — [Retry]" when failed; amber upgrade hints when `compatibility.upgradeRecommended` or `upgradeDashboard` is true. |
|
|
147
198
|
| `src/client/lib/tools-api.ts` | Client-side fetch helpers for `/api/tools*` (`fetchTools`, `rescanAll`, `rescanOne`, `setOverride`, `clearOverride`, `downloadDiagnostics`) |
|
|
@@ -153,10 +204,11 @@ make clean # Destroy all cloned VMs
|
|
|
153
204
|
| `src/server/meta-persistence.ts` | Per-session debounced `.meta.json` writer |
|
|
154
205
|
| `src/server/session-scanner.ts` | Startup session discovery by scanning `~/.pi/agent/sessions/` |
|
|
155
206
|
| `src/server/migrate-persistence.ts` | One-time migration from `sessions.json` + `state.json` to `.meta.json` |
|
|
156
|
-
| `src/server/session-order-manager.ts` | Per-cwd session ordering with persistence |
|
|
157
|
-
| `src/server/directory-service.ts` | Server-side session discovery, event loading, and OpenSpec polling. Uses mtime-gated per-directory cache (`DirCache`), shared FIFO semaphore, and deterministic per-cwd `phaseOffsetMs(cwd, jitterSeconds)` jitter (FNV-1a 32-bit hash). `refreshOpenSpec(cwd)` bypasses the
|
|
207
|
+
| `src/server/session-order-manager.ts` | Per-cwd session ordering with persistence. **Move-to-front semantic** (change: top-of-tier-on-status-change): exports `moveToFront(cwd, sessionId)` — idempotent `remove + unshift` used by `server.ts onChange`'s ended→alive user-intent branch so the just-resumed card always surfaces at the top of the alive tier, even on repeated end→resume cycles. Bridge auto-reattach short-circuits before any mutation via `pendingResumeIntents.consume()`. |
|
|
208
|
+
| `src/server/directory-service.ts` | Server-side session discovery, event loading, and OpenSpec polling. Uses mtime-gated per-directory cache (`DirCache`), shared FIFO semaphore, and deterministic per-cwd `phaseOffsetMs(cwd, jitterSeconds)` jitter (FNV-1a 32-bit hash). **Per-change watch set** (`perChangeArtifactPaths`) covers `<change>/`, `tasks.md`, `proposal.md`, `design.md`, plus `specs/`, every immediate `specs/<cap>/`, and every `specs/<cap>/spec.md` — `readdirSync` of `specs/` is try/catch-wrapped so missing dirs yield an empty fan-out (change: fix-openspec-specs-mtime-gate-blind-spot, which also wires `createFsSpecsProbeFactory(cwd)` into `buildOpenSpecData` as a second probe-factory argument so multi-spec authoring lights up green even when the gate misfires). **TOCTOU-safe stamping** (change: fix-openspec-mtime-gate-toctou): the per-change status loop captures `preCallMtime` *before* awaiting `runOpenSpecStatus` and stamps THAT value into the cache; if the post-call effective mtime differs, the entry is racy and the cache is left untouched (next gated tick re-polls). DEBUG-gated `console.warn` cites this change name when the discard branch fires. **Refresh contract**: `refreshOpenSpec(cwd)` is the user-clicked path and bypasses the gate via `pollOne(cwd, true)` — it's the manual escape hatch when the gate's heuristic is wrong. `pollDirectoryGated`, `onDirectoryAdded`, and the post-archive refresh in `handleOpenSpecBulkArchive` all call `pollOne(cwd, false)` so periodic / internal paths stay O(1) status spawns. `reconfigurePolling(cfg)` applies live config changes without a restart. Pi-resources scan lives on its own 5×-interval timer so it doesn't stack with the openspec burst. See changes: fix-openspec-specs-mtime-gate-blind-spot, fix-openspec-mtime-gate-toctou, fix-openspec-mtime-gate-blind-spots, optimize-openspec-poll-burst |
|
|
158
209
|
| `src/server/pending-fork-registry.ts` | Tracks pending fork operations for session placement |
|
|
159
210
|
| `src/server/pending-resume-registry.ts` | Queues prompts for auto-resume of ended sessions |
|
|
211
|
+
| `src/server/pending-resume-intent-registry.ts` | In-memory `Map<sessionId, timestamp>` (default 60 s TTL, lazy expiry on read). `record(id)` is called by `handleResumeSession` (WS) and the REST `POST /api/session/:id/resume` handler immediately before `spawnPiSession`; `consume(id)` is called by `server.ts`'s `sessionManager.onChange` hook in the ended→alive branch. When `consume` returns `false` (no user intent tagged), the branch returns early WITHOUT mutating `sessionOrder` or broadcasting `sessions_reordered` — this stops bridge auto-reattach on dashboard reboot from clobbering the user's drag-order. The `if (!order.includes(sessionId))` guard inside the branch keeps drag-to-resume's dropped slot intact when the intent is present. In-memory only — NOT persisted across server restarts. See change: preserve-session-order-on-reboot. |
|
|
160
212
|
| `src/server/json-store.ts` | Atomic JSON file read/write helpers |
|
|
161
213
|
| `src/server/process-manager.ts` | Session spawning: dispatches via `platform/spawn-mechanism.ts` `selectMechanism` → `tmux` / `wt` / `wsl-tmux` / `headless`. All mechanisms forward `sessionFile`/`mode` uniformly via `sessionFlagsToArgv`. Windows headless uses `spawnDetached` primitive with `detached: true` (PGID-equivalent via libuv) and stderr-to-file-fd (crash-visible). |
|
|
162
214
|
| `src/shared/platform/detached-spawn.ts` | Three primitives: `spawnDetached` (libuv-correct detached defaults on every OS), `waitForNoCrash` (negative liveness — did it survive the window?), `waitForReady` (positive liveness — did a probe turn true?). All spawn sites with long-lived detached children delegate here. `SpawnDetachedOptions` exposes optional `stdoutFd` (defaults to `"ignore"`) and `logFd` (stderr, defaults to `"ignore"`); the bridge server-launcher sets both to the same fd for parity with the CLI. |
|
|
@@ -166,6 +218,8 @@ make clean # Destroy all cloned VMs
|
|
|
166
218
|
| `src/shared/platform/spawn-mechanism.ts` | `SpawnMechanism` enum (`tmux`/`wt`/`wsl-tmux`/`headless`) + pure `selectMechanism({ platform, userStrategy, electronMode, available })` selector. `buildWtArgs` builds argv for Windows Terminal `new-tab`. `sessionFlagsToArgv` is the uniform flag builder every mechanism MUST call. |
|
|
167
219
|
| `src/shared/platform/process-identify.ts` | `findPidByMarker` + `isProcessLikePi` + `isPiCommandLine` — consolidates the three `process.platform === "win32"` branches that previously lived inside `session-action-handler.ts`. Windows stubs are documented (command-line lookup goes via `headlessPidRegistry` instead). |
|
|
168
220
|
| `src/shared/platform/process.ts` | **Sole source of process termination + liveness primitives**: `isProcessAlive(pid)` (signal 0), `killProcess(pid, {timeoutMs})` (Windows `taskkill /F /T /PID`, POSIX `SIGTERM` → wait → `SIGKILL` tree kill), `killPidWithGroup(pid, sig)` (POSIX `kill(-pid, sig)`, Windows direct kill). Every `process.kill(...)` call outside this file is banned by `no-direct-process-kill.test.ts`. See change: route-kill-paths-through-platform. |
|
|
221
|
+
| `src/shared/platform/node-spawn.ts` | **Sole source of `node --import <loader> <entry>` argv construction**: `toFileUrl(pathOrUrl)` (idempotent path → file:// URL, handles Windows drive letters on POSIX hosts), `spawnNodeScript(opts)` (wraps both loader and entry positions as file:// URLs before delegating to `platform/exec.ts::spawn`). Every `spawn(process.execPath, ["--import", ...])` call outside this file with raw path arguments is banned by `no-raw-node-import.test.ts`. Fixes `ERR_UNSUPPORTED_ESM_URL_SCHEME` on non-C: Windows drives (e.g. `B:\`) where Node's drive-letter heuristic for entry-script paths has known gaps. See change: fix-windows-entry-script-url. |
|
|
222
|
+
| `src/shared/__tests__/no-raw-node-import.test.ts` | Repo-level lint: scans `packages/*/src/` (excluding `platform/node-spawn.ts` + `resolve-jiti.ts` + `__tests__/`) for `spawn(...)` argv with `"--import"` / `"--loader"` followed by raw identifiers not wrapped in `toFileUrl(...)` / `pathToFileURL(...)`. Mirrors `no-direct-process-kill.test.ts` pattern. |
|
|
169
223
|
| `src/shared/__tests__/no-direct-process-kill.test.ts` | Repo-level lint: scans `packages/*/src/` (excluding platform/ and `__tests__/`) for `process.kill(` calls and fails with file:line if any are found. Mirrors `no-direct-child-process.test.ts`. |
|
|
170
224
|
| `src/shared/__tests__/bootstrap/` | In-memory bootstrap resolution harness (memfs-backed). `harness.ts` (withFakeEnv + layer), `fixtures/` (managed/npm-g/electron/dev-monorepo/settings-json layouts), `assertions.ts` (snapshotTrail + snapshotSettingsDelta with `<HOME>` / `<NPM_ROOT>` normalization), `scenarios.ts` (1080-cell cube: platform × dash × pi × settings × env), `scenarios-skipped.ts` (bulk-skip manifest with documented reasons), `cube.test.ts` (fail-closed sweep), `families/*.test.ts` (30+ registered scenario cells across A-K). Run via `npm run test:bootstrap`. See change: bootstrap-resolution-harness. |
|
|
171
225
|
| `src/server/editor-registry.ts` | Detects available native editors (running processes + CLI) |
|
|
@@ -190,9 +244,13 @@ make clean # Destroy all cloned VMs
|
|
|
190
244
|
| `src/client/hooks/useAuthStatus.ts` | Client auth status hook and login redirect helper |
|
|
191
245
|
| `src/server/localhost-guard.ts` | Network access guard: `createNetworkGuard` (loopback/trusted/authenticated), `isBypassedHost` (CIDR/wildcard/exact), netmask-to-CIDR helpers |
|
|
192
246
|
| `src/server/server-pid.ts` | PID file management for daemon mode |
|
|
193
|
-
| `src/client/components/ServerSelector.tsx` | Server selector dropdown showing persisted known servers with
|
|
247
|
+
| `src/client/components/ServerSelector.tsx` | Server selector dropdown showing persisted known servers. Availability probing runs **only on dropdown open** (once per open, not on mount, not on a timer, not while closed). Accepts `inFlightSwitchKey` to render a spinner on the entry being switched to. Unreachable entries are `disabled`, rendered with `opacity-50` + `cursor-not-allowed`, and clicks are no-ops. Reachable clicks delegate to the transactional staging-socket switch (see `server-switch.ts`). See change: safe-server-switch |
|
|
248
|
+
| `packages/client/src/lib/staging-socket.ts` | `openStagingSocket(url, {timeoutMs}): Promise<WebSocket>` — single-settle helper that resolves on first `OPEN`, rejects on error/close/timeout, and closes the socket on timeout to avoid leaks. Used by the transactional server-switch. |
|
|
249
|
+
| `packages/client/src/lib/server-switch.ts` | `performServerSwitch(target, deps)` — extracted two-phase transaction (stage → commit) from `App.tsx`'s `handleServerSwitch`. Guarantees the ordering `clearInMemoryState` → `setWsUrl` → `persistLastServer`, and never persists localStorage or clears state on staging failure. Fully unit-tested. |
|
|
250
|
+
| `packages/client/src/components/ConnectionStatusBanner.tsx` | Disconnection banner: appears only after the active WebSocket has been non-`OPEN` for >3s continuously; hidden immediately on reconnect; suppressed during in-flight staging switch. Mounted above `<MobileShell>` in `App.tsx`. |
|
|
194
251
|
| `src/client/components/KnownServersSection.tsx` | Settings section: list/add/remove persisted known remote servers |
|
|
195
|
-
| `src/client/components/NetworkDiscoverySection.tsx` | Settings section: mDNS network scan with "Add" action and label prompt |
|
|
252
|
+
| `src/client/components/NetworkDiscoverySection.tsx` | Settings section: mDNS network scan with "Add" action and label prompt. When the scan finds zero servers (common — Wi-Fi AP isolation, mesh routers, VLANs, VPN, firewall), renders a diagnostic block listing failure causes plus an inline manual-add form that accepts a free-form host input parsed via `parseHostInput()`. Surfaces scan errors instead of swallowing them. See change: diagnose-empty-mdns-scan |
|
|
253
|
+
| `src/client/lib/parse-host-input.ts` | Pure helper `parseHostInput(input, defaultPort = 8000) → {host, port} \| null` accepting full URLs (`http://192.168.16.202:8000`), `host:port`, bare hostnames, and bracketed IPv6 (`[::1]:8000`). Rejects empty input, bare IPv6 (ambiguous with `host:port`), invalid ports, malformed URLs. 12 unit tests in `parse-host-input.test.ts`. See change: diagnose-empty-mdns-scan |
|
|
196
254
|
| `src/client/lib/known-servers-api.ts` | Client-side fetch helpers for known servers CRUD and discovery endpoints |
|
|
197
255
|
| `src/server/routes/known-servers-routes.ts` | REST routes: known servers CRUD, on-demand mDNS discovery scan |
|
|
198
256
|
| `src/server/terminal-manager.ts` | PTY lifecycle, ring buffer, spawn/attach/kill terminals |
|
|
@@ -204,17 +262,20 @@ make clean # Destroy all cloned VMs
|
|
|
204
262
|
| `public/manifest.json` | PWA web app manifest for installability |
|
|
205
263
|
| `public/sw.js` | Minimal service worker for PWA installability |
|
|
206
264
|
| `src/client/components/ZrokInstallGuide.tsx` | OS-aware zrok installation guide view (macOS/Linux/Windows) |
|
|
207
|
-
| `src/server/cli.ts` | CLI entry point with subcommands (start/stop/restart/status); `findPortHolders` is cross-platform (netstat/taskkill on Windows, lsof on Unix) and `server.log` is opened append-mode with timestamped headers |
|
|
265
|
+
| `src/server/cli.ts` | CLI entry point with subcommands (start/stop/restart/status); `findPortHolders` is cross-platform (netstat/taskkill on Windows, lsof on Unix) and `server.log` is opened append-mode with timestamped headers. The local post-install `registry.rescan("pi")` block was removed and ownership of the post-install rescan + force-refresh moved to the centralized `bootstrapState.subscribe` hook in `server.ts`. See change: fix-openspec-buttons-after-bootstrap-install. |
|
|
208
266
|
| `src/server/restart-helper.ts` | Cross-platform `/api/restart` orchestrator: spawns a detached `node -e` child using only Node built-ins (net, http) — no sh/lsof/curl dependency; exports pure `buildOrchestratorScript(params)` for testing |
|
|
209
267
|
| `src/shared/resolve-jiti.ts` | Resolves pi's jiti register hook as a `file://` URL (required for `node --import` on Windows); exports pure `buildJitiRegisterUrl(pkgJsonPath)` helper and `resolveJitiFromAnchor(anchorPath)` for managed-install/system-pi callers |
|
|
210
268
|
| `src/shared/platform/paths.ts` | OS-aware path primitives: `normalizePath`, `samePath` (filesystem-level equality), `parsePathInput` (picker input), `withTrailingSep`, `isFilesystemRoot`. All accept optional trailing `platform: NodeJS.Platform` for testability. Windows multi-drive invariant: A:\, B:\, C:\ never merge; bare `B:` input treated as drive root, not cwd-relative. Exported from `platform/index.ts` as `paths.*` namespace alongside `git.*`, `openspec.*`, `npm.*` |
|
|
211
269
|
| `src/client/lib/session-grouping.ts` | `inferPlatform(samples)` heuristic (backslash/drive-letter = Windows, leading `/` = POSIX) + `groupSessionsByDirectory` that uses `normalizePath`-keyed Maps so sessions group under their pinned folder across separator/case/trailing drift |
|
|
212
|
-
| `src/shared/platform/` | Unified cross-OS primitives (see `index.ts` barrel). Sub-modules: `exec.ts` (**the only module that imports `node:child_process`** — wraps `execSync`/`exec`/`execFile`/`spawn`/`spawnSync` with `windowsHide: true` by default; enforced by `no-direct-child-process.test.ts`), `runner.ts` (the Recipe engine — `run(recipe, input)` resolves binaries via `ToolResolver`, applies timeout / tolerate / error normalization, returns `Result<T>`), `git.ts` / `openspec.ts` / `npm.ts` (Recipe-based tool modules — typed functions like `git.diff(...)`, `openspec.list(...)`, `npm.rootGlobal()` that never touch `child_process` or `process.platform`), `binary-lookup.ts` (`where`/`which`, `.cmd` ext, `ToolResolver` class), `process.ts` (`findPortHolders`, `killProcess`, `isProcessAlive`, `killPidWithGroup`, `parseNetstatListeners`), `process-scan.ts` (`isProcessRunning` via pgrep/tasklist, `parseEtime`), `shell.ts` (`detectShell` for SHELL/COMSPEC, `getTerminalEnvHints`), `commands.ts` (`openBrowser`, `isVirtualMachine`). All exported helpers that depend on OS take an optional `platform: NodeJS.Platform` parameter so tests can exercise both branches without mutating `process.platform`. See changes: consolidate-platform-handlers, platform-command-executor. |
|
|
270
|
+
| `src/shared/platform/` | Unified cross-OS primitives (see `index.ts` barrel). Sub-modules: `exec.ts` (**the only module that imports `node:child_process`** — wraps `execSync`/`exec`/`execFile`/`spawn`/`spawnSync` with `windowsHide: true` by default; enforced by `no-direct-child-process.test.ts`), `runner.ts` (the Recipe engine — `run(recipe, input)` resolves binaries via `ToolResolver`, applies timeout / tolerate / error normalization, returns `Result<T>`), `git.ts` / `openspec.ts` / `npm.ts` (Recipe-based tool modules — typed functions like `git.diff(...)`, `openspec.list(...)`, `npm.rootGlobal()` that never touch `child_process` or `process.platform`), `binary-lookup.ts` (`where`/`which`, `.cmd` ext, `ToolResolver` class; **`isAppImageSelfHit(path, opts?)`** — pure helper that flags a candidate binary path as the running Electron AppImage launcher when `realpath(path) === realpath(process.execPath)`, when `path` lives under `process.env.APPDIR` (squashfs mount), or when `realpath(path) === realpath(process.env.APPIMAGE)`. Defensive try/catch around every `realpath`. Consumed by `whereStrategy` (Layer 2 — every registry-resolved tool inherits the guard) and `detectPiDashboardCli` / `detectPi` / `detectSystemNode` (Layer 1 — belt-and-braces). See change: fix-electron-appimage-cli-self-detection), `process.ts` (`findPortHolders`, `killProcess`, `isProcessAlive`, `killPidWithGroup`, `parseNetstatListeners`), `process-scan.ts` (`isProcessRunning` via pgrep/tasklist, `parseEtime`), `shell.ts` (`detectShell` for SHELL/COMSPEC, `getTerminalEnvHints`), `commands.ts` (`openBrowser`, `isVirtualMachine`). All exported helpers that depend on OS take an optional `platform: NodeJS.Platform` parameter so tests can exercise both branches without mutating `process.platform`. See changes: consolidate-platform-handlers, platform-command-executor. |
|
|
213
271
|
| `src/shared/rest-api.ts` | REST API type definitions |
|
|
214
272
|
| `scripts/reload-all.sh` | Build bridge + reload all pi sessions |
|
|
215
273
|
| `scripts/sync-versions.js` | Post-bump release helper. Reads every workspace `package.json`, enforces lockstep versions, rewrites every inter-package dep specifier (e.g. `"@blackbelt-technology/pi-dashboard-shared": "^<ver>"`) to the current bumped version. Called by `release-cut` skill AND by `.github/workflows/publish.yml` (defensively) between `npm version -ws` and `npm run build`. Required because the npm CLI does not implement the pnpm/yarn `workspace:` protocol — we use plain semver ranges and sync them at bump time. Cross-ref specifiers use plain `"^<ver>"`; `packages/electron` is `"private": true` so `npm publish -ws` skips it automatically. |
|
|
216
|
-
| `src/client/components/PiResourcesView.tsx` | Content area view for browsing pi extensions, skills, and prompts (
|
|
217
|
-
| `src/client/components/
|
|
274
|
+
| `src/client/components/PiResourcesView.tsx` | Content area view for browsing pi extensions, skills, and prompts. Two tabs: **Resources** (browse-only — loose `.pi/{skills,extensions,prompts}` files plus per-package nested resource trees that contribute to the session; renamed from "Installed" but the internal route id stays `"installed"`) and **Packages** (workspace-scope manage surface, hosts `PackageBrowser`). `MergedScopeSection` filters out installed packages with zero contributed resources — those are visible only in the Packages tab. No standalone manage rows in the Resources tab. See change: unify-workspace-package-management. |
|
|
275
|
+
| `src/client/components/InstalledPackagesList.tsx` | Shared rich-row list of installed packages (used in both Settings → Packages and Pi Resources → Installed). Composes `<PackageRow>` per entry with `useInstalledPackages` + `usePackageOperations` (extended hook surface: `move`, `moveStateFor`, `clearMove`). Per-row expand-chevron reveals an inline tree of contained skills/extensions/prompts, populated from a `containedResources: Map<source, PiPackageInfo>` prop the caller projects from `usePiResources`. Move → button computed from `currentScope` and gated by an `otherScopePackages` prop (compared via `computeDestIdentity` from `lib/installed-list-helpers.ts` — client-side mirror of the server identity rules for npm/git/https; path sources fall back to literal). Partial-success banner rendered inline when the move's `package_operation_complete` event carries `partialSuccess`, with Cleanup button that re-POSTs `/api/packages/remove` against `fromScope`. See change: unify-package-management-ui. |
|
|
276
|
+
| `src/client/lib/move-tracker.ts` | In-flight move tracker (singleton). Decoupled from `package-queue` because moves are moveId-keyed (not source-keyed) and have partial-success semantics. Listens on `pi-package-event` → `package_operation_complete` events that carry a `moveId`; updates state per-moveId; auto-clears successful moves after 3 s; keeps partial-success states sticky until the user clicks Cleanup or Dismiss. Exposed through `usePackageOperations.moveStateFor(source) / clearMove(moveId)`. See change: unify-package-management-ui. |
|
|
277
|
+
| `src/client/lib/packages-api.ts` | Client-side fetch helpers for package endpoints that don't fit the `package-queue` source-keyed model. Currently hosts `movePackage(args): Promise<MoveResponse>` (POSTs `/api/packages/move`, returns a discriminated union `{ok:true, moveId, phases}` / `{ok:false, status, code, message}` so callers can branch on stable error codes without try/catch). See change: unify-package-management-ui. |
|
|
278
|
+
| `src/client/components/PackageBrowser.tsx` | Reusable inline package browser: npm search, type filters, manual URL input. **Workspace-scope manage surface** — renders an "Installed Packages" section above search using `PackageRow` + `classifySource(pkg.source)`, mirroring `UnifiedPackagesSection` (Settings → Pi Ecosystem). Every source shape (`npm:`, absolute path, relative path, `file://`, `git@`, `https://...git`) gets working `Update` / `Uninstall` actions; `operations.remove(pkg.source)` flows the raw source verbatim, no regex reshape. Cross-scope `installedInfo` map is keyed by `pkg.source` (search-result rows synthesize `npm:${pkg.name}` at lookup time). The legacy "Installed" filter pill is removed — the dedicated section above search replaces it. See changes: unify-workspace-package-management, fix-local-path-install-spinner. |
|
|
218
279
|
| `src/client/components/PackageCard.tsx` | Package card with type badges, downloads, install/uninstall actions |
|
|
219
280
|
| `src/client/components/PackageReadmeDialog.tsx` | Dialog overlay showing package README with install/uninstall action |
|
|
220
281
|
| `src/client/components/PackageInstallConfirmDialog.tsx` | Confirmation dialog before package install (name, source, scope) |
|
|
@@ -225,9 +286,11 @@ make clean # Destroy all cloned VMs
|
|
|
225
286
|
| `src/client/components/MissingRequiredBanner.tsx` | Top-of-page banner when any `required` recommended extension is missing from `~/.pi/agent/settings.json` `packages[]` |
|
|
226
287
|
| `src/shared/recommended-extensions.ts` | Static `RECOMMENDED_EXTENSIONS` manifest (pi-anthropic-messages, @tintinweb/pi-subagents, pi-flows, pi-web-access, pi-agent-browser); enriched server-side with live description/version and installed/active cross-reference |
|
|
227
288
|
| `src/server/routes/recommended-routes.ts` | `GET /api/packages/recommended` — enrichment + 60s cache, invalidated on successful install/remove/update |
|
|
228
|
-
| `src/client/hooks/usePackageOperations.ts` |
|
|
289
|
+
| `src/client/hooks/usePackageOperations.ts` | Thin React subscriber over the singleton `packageQueue`. Public API (`operation`, `install/remove/update`, `clearOperation`) preserved for backwards-compat with `PackageBrowser`, `RecommendedExtensions`, `MissingRequiredBanner`, `PiResourcesView`, `SettingsPanel`. Adds `statusFor(source)` (`idle\|queued\|running\|success\|error`), `messageFor(source)`, `queueDepth` for richer per-row state. The single `pi-package-event` window listener now lives inside `package-queue`, not the hook. See change: package-install-queue. |
|
|
290
|
+
| `src/client/lib/package-queue.ts` | Module-level singleton FIFO queue scheduling pi package install/remove/update operations across the entire client. Owns the running op (at most one) plus a queue keyed by `source`, the per-source status map, and the *one* `pi-package-event` window listener that advances the queue on `package_operation_complete`. Dedup on duplicate enqueue, retry-once on HTTP 409 `PackageOperationBusyError` (500 ms backoff), success auto-clear after 3 s, sticky error until next enqueue. **WS-vs-HTTP race fix**: the `matchesRunning(opId, source)` predicate falls back to `source` matching when `running.operationId` is still `null` (HTTP response not yet parsed) so completions/progress that arrive before fetch resolves — common for fast local-path installs — are not silently dropped; once `operationId` is set it takes priority. Exports `__resetForTests()`. See changes: package-install-queue, fix-local-path-install-spinner. |
|
|
229
291
|
| `src/client/hooks/usePiResources.ts` | Fetch + 30s polling hook for pi resources API |
|
|
230
292
|
| `src/client/components/MarkdownPreviewView.tsx` | Generic reusable markdown preview with back button, tabs, loading/error states |
|
|
293
|
+
| `src/client/components/MarkdownContent.tsx` | Shared markdown renderer for chat/previews; supports GFM, raw HTML, syntax highlighting, Mermaid, table copy buttons, and strips raw HTML `ref` attributes so pasted/generated JSX-like markup cannot trip React ref validation. |
|
|
231
294
|
| `src/client/hooks/useOpenSpecReader.ts` | Maps OpenSpec artifacts to file paths, fetches content, concatenates specs |
|
|
232
295
|
| `src/client/components/interactive-renderers/` | Registry + renderers for interactive UI dialogs (confirm, select, multiselect, input, editor, notify) |
|
|
233
296
|
| `src/shared/terminal-types.ts` | TerminalSession type and control messages |
|
|
@@ -239,23 +302,26 @@ make clean # Destroy all cloned VMs
|
|
|
239
302
|
| `src/client/lib/folder-encoding.ts` | Base64url encode/decode for folder paths in URL routes |
|
|
240
303
|
| `src/shared/editor-types.ts` | Editor instance types shared across components |
|
|
241
304
|
| `src/client/components/TerminalCard.tsx` | Sidebar card for terminal sessions (cyan accent) |
|
|
242
|
-
| `src/client/App.tsx` | React app with WebSocket integration |
|
|
305
|
+
| `src/client/App.tsx` | React app with WebSocket integration. Owns per-session ephemeral input state alongside `sessionStates`: `drafts: Map<sid, string>` (persisted to `localStorage`, see `chat-input-draft-and-history`) and `pendingImagesMap: Map<sid, ImageContent[]>` (in-memory only, NOT persisted — see change: lift-pending-images-to-app). Both are passed into `<CommandInput>` as controlled props (`draft`/`onDraftChange`, `images`/`onImagesChange`) so they survive content-area route changes that unmount the chat panel and don't leak across session switches. `wrappedHandleSend` clears both via `clearDraftForSession` + `clearImagesForSession` after a successful send. Module-level `EMPTY_IMAGES = Object.freeze([])` provides a stable empty-array reference so sessions with no pending images don't trigger child re-renders. **Desktop back-arrow** (change: fix-desktop-back-navigation): the session-header back button on desktop dispatches through `useDesktopBack({...})` which wraps the pure `selectDesktopBackTarget` helper — pops content-area overlays in priority order before falling back to `navigate("/")`, so a cold-load `/session/:id` back click never hits the silent-no-op `window.history.back()` path. `useContentViews` and `useOpenSpecActions` accept `navigate`/`settingsMatch`/`tunnelSetupMatch` so sidebar-triggered overlays auto-close `/settings` / `/tunnel-setup` before opening, fixing the bug where hidden overlays masked Settings. |
|
|
243
306
|
| `src/client/components/MobileShell.tsx` | Two-panel mobile shell with slide transitions and swipe-back |
|
|
244
307
|
| `src/client/components/MobileActionMenu.tsx` | Kebab menu for session actions on mobile (includes OpenSpec commands) |
|
|
245
308
|
| `src/client/components/MobileOverlay.tsx` | Hamburger button and sidebar overlay for mobile |
|
|
246
|
-
| `src/client/components/SessionHeader.tsx` | Session header with OpenSpec attach/detach, flow launcher, MobileAttachButton |
|
|
309
|
+
| `src/client/components/SessionHeader.tsx` | Session header with OpenSpec attach/detach, flow launcher, MobileAttachButton. **Mobile** branch additionally renders a read-only `mobile-header-attached-chip` (paperclip + `session.attachedProposal`) between the title and the `MobileAttachButton` so attach/detach state is visible without opening the popover. See change: fix-mobile-attach-proposal-display. **Two-row mobile layout** (change: fix-mobile-header-and-orientation): when `session.attachedProposal` is non-empty, `MobileHeader` becomes a `flex-col` with row 1 = back+title+attach+kebab and row 2 = the existing chip span (now without the `max-w-[55%]` constraint since it no longer competes with the title for horizontal space). When `attachedProposal` is null/empty the header collapses to a single-row container exactly as before — no empty row 2 reserved. **Attached-proposal artifact summary** (change: add-attached-proposal-header-summary): both the desktop branch and `MobileHeader` look up `session.attachedProposal` in `openspecChanges` and, when found, render the existing `ArtifactLettersButton` (P/D/T/S letters colored by status, single button → opens proposal artifact via `onReadArtifact` prop threaded from `App.tsx → useContentViews`) plus a `(completedTasks/totalTasks)` counter (`data-testid="attached-proposal-task-counter"`, only when `totalTasks > 0`). Surface is gated on the explicit `attachedProposal` only — auto-detected `openspecChange` does not trigger it. |
|
|
310
|
+
| `src/client/hooks/useMobile.tsx` | `MobileProvider` + `useMobile()` hook. Predicate is **width OR height** — the wrapped `useMediaQuery("(max-width: 767px), (max-height: 599px)")` flips to mobile whenever the viewport is < 768px wide OR < 600px tall. The height arm catches landscape phones (iPhone 14 in landscape is 844×390, Pixel 8 landscape 915×412) so they get the single-panel mobile layout instead of the cramped desktop two-panel one. Documented side effect: shrinking a desktop window to <600px tall also enters mobile mode (pinned by a regression test in `useMobile.test.tsx`). See change: fix-mobile-header-and-orientation. |
|
|
247
311
|
| `src/client/hooks/useSwipeBack.ts` | iOS-style left-edge swipe-back gesture (40px edge zone, document-level listeners) |
|
|
248
|
-
| `src/client/components/ChatView.tsx` | Chat message view with scroll-lock: pauses auto-scroll when user scrolls up, floating scroll-to-bottom button, per-session scroll position persistence |
|
|
249
|
-
| `src/client/components/CommandInput.tsx` | Chat textarea + autocomplete + controlled draft / history. Props: `sessionId`, `draft`, `onDraftChange` (lifted to App.tsx so
|
|
312
|
+
| `src/client/components/ChatView.tsx` | Chat message view with scroll-lock: pauses auto-scroll when user scrolls up, floating scroll-to-bottom button, per-session scroll position persistence. Race-safe across multi-batch `event_replay`: a `markProgrammatic()` helper raises `programmaticScroll` for ~150 ms around every self-initiated `scrollTo`, and `handleScroll` early-returns while the flag is set so the spurious onScroll that lags `scrollTo` (and would otherwise misread the now-grown `scrollHeight` as "user scrolled away") cannot flip `isNearBottom` during replay. See change: fix-chat-scroll-race-during-replay |
|
|
313
|
+
| `src/client/components/CommandInput.tsx` | Chat textarea + autocomplete + controlled draft / history / images. Props: `sessionId`, `draft`, `onDraftChange` (draft lifted to App.tsx so it survives navigation), `history` (newest-first user prompts for this session), `images`, `onImagesChange` (pending pasted images lifted to App.tsx as `pendingImagesMap` so they survive route takeover and don't leak across sessions). `ArrowUp`/`ArrowDown` walk history bash-style (only when no dropdown is open AND caret is on first/last line); `Escape` during history mode restores the in-progress draft. See changes: chat-input-draft-and-history, lift-pending-images-to-app |
|
|
250
314
|
| `src/client/lib/draft-storage.ts` | Per-session chat input draft persistence helpers: `readAllDrafts()`, `writeDraft(sid, text)`, `deleteDraft(sid)`, key prefix `chat-draft:`. Wraps `localStorage` in try/catch (private-mode / quota safe). |
|
|
251
315
|
| `src/client/lib/message-history.ts` | `extractUserPromptHistory(messages)` — pure filter+dedup over `ChatMessage[]` returning newest-first user prompts with consecutive duplicates collapsed. Drives `ArrowUp`/`ArrowDown` history recall in `CommandInput`. |
|
|
252
316
|
| `src/client/lib/mobile-depth.ts` | Pure function computing MobileShell depth from route state |
|
|
317
|
+
| `src/client/lib/desktop-back.ts` | Pure helper `selectDesktopBackTarget(state) → BackTarget` for the desktop session-header back button. Mirrors the priority chain mobile's inline `onBack` switch uses (archiveBrowserCwd → specsBrowserCwd → flowYamlPreview → diffViewSessionId → piResourceFilePreview → readmePreview → piResourcesState → previewState → navigate("/")). Returns a discriminated union `{kind:"clear", target}` or `{kind:"navigate", to:"/"}`. Pinned by a 256-combination parity test against the mobile inline switch. See change: fix-desktop-back-navigation. |
|
|
318
|
+
| `src/client/hooks/useDesktopBack.ts` | Hook wrapping `selectDesktopBackTarget` with live overlay setters + `navigate`. Returns a memoised `goBack()` callback used by `App.tsx`'s desktop session-header. Replaces the pre-fix `window.history.back()` which was a silent no-op on cold loads. See change: fix-desktop-back-navigation. |
|
|
253
319
|
| `src/client/hooks/useZoomPan.ts` | Reusable zoom/pan hook (wheel, drag, pinch, buttons) |
|
|
254
320
|
| `src/client/hooks/useMessageHandler.ts` | WebSocket message dispatch hook (extracted from App.tsx) |
|
|
255
321
|
| `src/client/hooks/useSessionActions.ts` | Session action callbacks hook (send, abort, resume, spawn, etc.) |
|
|
256
322
|
| `src/client/hooks/useOpenSpecActions.ts` | OpenSpec action callbacks hook (refresh, archive, attach, detach); calls `clearAllContentViews` before opening preview |
|
|
257
323
|
| `src/client/hooks/useContentViews.ts` | Content view state + fetch (pi resources, readme, file preview); `clearAll()` resets all hook-owned states; `onBeforeOpen` callback for cross-component clearing |
|
|
258
|
-
| `src/client/lib/event-reducer.ts` | Event-sourced state reducer (delegates flow events to flow-reducer); extracts LLM errors from `agent_end` into `lastError` |
|
|
324
|
+
| `src/client/lib/event-reducer.ts` | Event-sourced state reducer (delegates flow events to flow-reducer); extracts LLM errors from `agent_end` into `lastError`. **Assistant content-array reorder** (changes: fix-text-tool-render-order, fix-interactive-ui-reorder): on every `message_end` for `role:"assistant"`, the pure helper `reorderToolCardsForAssistantMessage(messages, content)` reorders the trailing rows so that text/thinking/toolCall blocks land in the same order as the model's `content[]` array. **Turn-boundary anchored window**: walks `messages[]` backwards from the tail collecting every row whose role is NOT a hard turn boundary (`TURN_BOUNDARY_ROLES = {user, turnSeparator, commandFeedback, rawEvent}`). Window = `[boundaryIdx + 1 ..]` — prior-turn rows separated by a hard boundary cannot leak in. **Pairing rule** (fix-interactive-ui-reorder): for each `toolCall` block, the helper claims the matching `toolResult` row AND any `interactiveUi` row whose `toolCallId` matches, emitting them as a `[toolResult, interactiveUi]` pair so `ask_user` and other PromptBus-routed tools render below their own assistant intro text. Uses `findLastUnclaimed` (most-recent first) so back-to-back assistant turns without a user response between them still claim the current message's rows correctly. **Hybrid unclaimed-row handling**: rows of "reorderable" roles (`assistant`, `toolResult`, `thinking`) that are unclaimed keep their **original suffix index** (protects prior-message rows that bled in when no boundary exists between back-to-back assistants); rows of "trailing" roles (`interactiveUi`, `bashOutput`) that are unclaimed are emitted AFTER claimed rows so free-floating ui dialogs (no `toolCallId`) sit below their tool card. Preserves React keyed reconciliation (`tool-${id}` / `ui-${id}`). Replay path inherits the fix because it routes through the same reducer. `addInteractiveRequest(state, requestId, method, params, toolCallId?)` accepts the optional 5th param so the pushed `interactiveUi` ChatMessage carries the `toolCallId`. |
|
|
259
325
|
| `src/client/hooks/usePendingPromptTimeout.ts` | 30-second safety timeout for stuck `pendingPrompt` spinners |
|
|
260
326
|
| `src/client/lib/flow-reducer.ts` | Flow state machine: all flow_* event handling |
|
|
261
327
|
| `src/client/lib/session-grouping.ts` | Pure functions: group, sort, filter sessions by directory |
|
|
@@ -292,14 +358,17 @@ make clean # Destroy all cloned VMs
|
|
|
292
358
|
| `.pi/skills/browser-visual-debug/SKILL.md` | Skill: visual debugging with a real browser (screenshots, interaction, responsive testing) via pi-agent-browser |
|
|
293
359
|
| `.pi/skills/browser-visual-debug/references/` | Dashboard recipes, responsive testing presets, agent-browser commands cheatsheet |
|
|
294
360
|
| `.pi/skills/browser-visual-debug/scripts/detect-dashboard.sh` | Auto-detect dashboard URL, mode, and Vite dev server status |
|
|
295
|
-
| `packages/electron/src/main.ts` | Electron main process: single-instance, wizard, server launch, loading page, tray |
|
|
361
|
+
| `packages/electron/src/main.ts` | Electron main process: single-instance, wizard, server launch, loading page, tray. **External-link hardening (change: harden-external-link-handling, #13)**: `createMainWindow` registers `webContents.setWindowOpenHandler` + `will-navigate` BEFORE `loadURL` so `target="_blank"` / `window.open` and bare-`<a>` external navigation both route through `shell.openExternal` (system browser) instead of replacing the dashboard. Same-origin checks use the pure `isSameOriginUrl` helper. |
|
|
362
|
+
| `packages/electron/src/lib/link-handling.ts` | Pure `isSameOriginUrl(href, serverOrigin)` helper used by the Electron shell to classify URLs as same-origin vs external. No Electron import; 15 unit tests in `packages/electron/src/__tests__/link-handling.test.ts`. See change: harden-external-link-handling (#13). |
|
|
363
|
+
| `packages/client/src/components/MarkdownContent.tsx` | ReactMarkdown-based renderer for chat bodies, thinking blocks, flow agent detail, package READMEs, markdown previews. **External-link hardening (change: harden-external-link-handling, #13)**: exports pure `isExternalHref(href)` and overrides the `a` component so external URLs render with `target="_blank" rel="noopener noreferrer"` while fragment-only and same-origin hrefs stay in-document. |
|
|
364
|
+
| `packages/client/src/__tests__/no-bare-external-anchor.test.ts` | Repo-level lint: scans client `.tsx` files for `<a href="http(s)://...">` tags missing `target="_blank"`. Per-line opt-out via `// ban:bare-anchor-ok`. See change: harden-external-link-handling. |
|
|
296
365
|
| `packages/electron/src/lib/server-lifecycle.ts` | Health check → tsx binary spawn (inlined config/health, no shared pkg imports) |
|
|
297
366
|
| `packages/server/src/extension-register.ts` | Auto-registers bundled bridge extension in pi's global settings on startup |
|
|
298
367
|
| `packages/electron/src/lib/doctor.ts` | Doctor diagnostic: checks all binaries, versions, server status, offers setup |
|
|
299
368
|
| `packages/electron/src/lib/app-menu.ts` | App menu with About dialog and Doctor on all platforms |
|
|
300
369
|
| `packages/electron/src/lib/tray.ts` | System tray with platform-specific icons (template on macOS, ico/png on Win/Linux) |
|
|
301
370
|
| `packages/electron/src/lib/dependency-installer.ts` | Async npm install of pi, openspec, tsx into ~/.pi-dashboard/ using bundled Node |
|
|
302
|
-
| `packages/electron/src/lib/dependency-detector.ts` | Detects pi, openspec, Node.js on system PATH and managed install |
|
|
371
|
+
| `packages/electron/src/lib/dependency-detector.ts` | Detects pi, openspec, Node.js on system PATH and managed install. **AppImage self-recursion guard** (change: fix-electron-appimage-cli-self-detection): `detectPiDashboardCli` rejects any candidate that matches `isAppImageSelfHit(path)` (in addition to the existing `_npx` filter) so power-user mode falls through to the standalone tsx + `cli.ts` path when the only `pi-dashboard` on PATH is the AppImage's own launcher (`packagerConfig.executableName: "pi-dashboard"` collides by design). `detectPi` and `detectSystemNode` apply the same guard symmetrically on the registry-resolved path — belt-and-braces beyond the `whereStrategy` filter. |
|
|
303
372
|
| `packages/electron/src/lib/bundled-node.ts` | Resolves bundled Node.js/npm paths in Electron resources |
|
|
304
373
|
| `packages/electron/src/lib/wizard-window.ts` | First-run setup wizard window with preload bridge |
|
|
305
374
|
| `packages/electron/forge.config.ts` | Electron Forge config: DMG, DEB, AppImage, NSIS makers, icon, extraResources |
|
|
@@ -315,14 +384,14 @@ make clean # Destroy all cloned VMs
|
|
|
315
384
|
| `packages/electron/scripts/bundle-recommended-extensions.sh` | Opt-in (via `BUNDLE_RECOMMENDED_EXTENSIONS=1`) build-time bundler that clones each id in `BUNDLED_EXTENSION_IDS` into `packages/electron/resources/bundled-extensions/<id>/`, records `.bundled-sha`, enforces SPDX allowlist (MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC) and a 15 MB total size budget. See change: bundle-first-party-extensions. |
|
|
316
385
|
| `packages/electron/src/lib/dependency-installer.ts` → `installBundledExtensions` | First-run activation of pre-bundled extensions. Copies `<resourcesPath>/bundled-extensions/<id>/` into pi's git cache (`~/.pi/agent/git/<host>/<path>/`), runs `npm install --omit=dev` if runtime deps declared, then calls `manager.addSourceToSettings(gitUrl)` + `settingsManager.flush()`. Returns activated ids so `installRecommendedExtensions` can skip them with `output: "Already installed (bundled)"`. |
|
|
317
386
|
| `packages/electron/src/lib/wizard-badge.ts` | Pure `classifyProgressBadge(output)` helper (`bundled` / `system` / null) shared between wizard HTML inline JS and unit tests. |
|
|
318
|
-
| `packages/shared/src/recommended-extensions.ts` → `BUNDLED_EXTENSION_IDS` | Single source of truth for which recommended ids ship bundled in the Electron installer. Currently `["pi-anthropic-messages"
|
|
387
|
+
| `packages/shared/src/recommended-extensions.ts` → `BUNDLED_EXTENSION_IDS` | Single source of truth for which recommended ids ship bundled in the Electron installer. **Currently `["pi-anthropic-messages"]` only** — `"pi-flows"` was removed in commit b9b3d7e because the upstream repo (BlackBeltTechnology/pi-flows) declares no SPDX license (no `LICENSE` file, no `package.json#license`), and `bundle-recommended-extensions.sh`'s allowlist (MIT/Apache-2.0/BSD-2-Clause/BSD-3-Clause/ISC) correctly rejects it — which was blocking every electron build matrix variant. Re-add `"pi-flows"` once upstream declares a license. The npm publish path is unaffected (only electron build was broken). See `openspec/changes/archive/2026-04-21-bundle-first-party-extensions/design.md` for original design + license-blocker discussion. |
|
|
319
388
|
| `packages/electron/scripts/docker-make.sh` | Docker entrypoint: bundles server, installs native deps, runs Forge make |
|
|
320
389
|
| `packages/electron/scripts/Dockerfile.build` | Docker image for cross-platform builds (node:22-bookworm-slim + build tools) |
|
|
321
390
|
| `packages/electron/scripts/test-server-launch.sh` | Docker-based test for server launch on clean Linux |
|
|
322
391
|
| `packages/electron/scripts/test-electron-install.sh` | Full e2e Docker test: install, wizard, server launch, health check |
|
|
323
392
|
| `packages/electron/scripts/test-electron-install-inner.sh` | Inner test script run inside Docker container |
|
|
324
393
|
| `packages/electron/resources/icon.png` | Master 1024×1024 app icon (π on dark navy) |
|
|
325
|
-
| `.github/workflows/publish.yml` | CI: builds DMG (macOS), DEB+AppImage (Linux), NSIS+ZIP+portable (Windows) on native runners; publishes npm + GitHub Release |
|
|
394
|
+
| `.github/workflows/publish.yml` | CI: builds DMG (macOS), DEB+AppImage (Linux), NSIS+ZIP+portable (Windows) on native runners; publishes npm + GitHub Release. **Two triggers**: (a) push of any `v*` tag (the original path — release-cut skill / hand tag), (b) `workflow_dispatch` from the GitHub Actions UI with a single required `version` input (e.g. `"0.4.1"`). The `prepare` job branches on `github.event_name`: tag-push extracts the version from `GITHUB_REF_NAME`; dispatch validates the input as semver, checks tag uniqueness on origin, bumps every workspace `package.json` via `npm version -ws`, syncs cross-ref specifiers via `scripts/sync-versions.js`, promotes `## [Unreleased]` to a dated `## [<version>]` section in `CHANGELOG.md`, commits + tags + pushes the branch. The publish, electron, and github-release jobs all `needs: prepare` and check out `ref: ${{ needs.prepare.outputs.tag }}` so both trigger paths publish the same tree. **Idempotent ordered npm publish (commit b9fcea9)**: the publish step replaced the bulk `npm publish --workspaces --include-workspace-root` call with a per-package loop that (a) **skips** already-published versions via `npm view <pkg>@<ver>` (so a re-run after partial-publish failure resumes cleanly instead of aborting on "cannot publish over previously published"), (b) publishes the **4 stable sub-packages first** (`pi-dashboard-shared` → `extension` → `server` → `web`), then the brand-new `dashboard-plugin-runtime`, then the **root metapackage last** so the registry already serves matching sub-package versions before the root tarball lands and resolves dependents like `@blackbelt-technology/pi-dashboard-extension@^X.Y.Z`. v0.4.0 and v0.4.1 shipped broken because the bulk call aborted on the first error and only the root tarball landed — `npm install @blackbelt-technology/pi-agent-dashboard@0.4.1` returned ETARGET on the sub-deps. Single-failure isolation: any non-skip failure marks the step failed via a `FAIL=1` accumulator but lets the loop finish so logs show every package's outcome. Also (commit b9fcea9): `packages/server/package.json#dependencies` now declares `@blackbelt-technology/dashboard-plugin-runtime: ^<ver>` — previously imported via workspace symlinks but missing from the published tarball, so a clean `npm install` of just the server crashed with `MODULE_NOT_FOUND` on first start. Also (commit 2728c31): every workspace `package.json` (`shared`, `extension`, `server`, `client`, `dashboard-plugin-runtime`, plus `electron`) now declares a `repository` field — required for npm provenance attestation validation when publishing with `--provenance` from GitHub Actions OIDC. |
|
|
326
395
|
|
|
327
396
|
## Build & Restart Workflow
|
|
328
397
|
|