@blackbelt-technology/pi-agent-dashboard 0.4.0 → 0.4.1
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 +30 -8
- package/README.md +386 -494
- package/docs/architecture.md +63 -9
- package/package.json +8 -5
- package/packages/extension/package.json +6 -4
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +40 -8
- 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-list.test.ts +137 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/ask-user-tool.ts +5 -4
- package/packages/extension/src/bridge.ts +102 -15
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +43 -0
- package/packages/extension/src/server-launcher.ts +15 -3
- package/packages/server/package.json +5 -5
- 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__/pi-version-skew.test.ts +72 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +34 -6
- package/packages/server/src/cli.ts +56 -9
- package/packages/server/src/pi-version-skew.ts +12 -1
- package/packages/server/src/restart-helper.ts +13 -2
- package/packages/shared/package.json +1 -1
- 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__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/platform/index.ts +1 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/protocol.ts +23 -0
- package/packages/shared/src/state-replay.ts +9 -0
- package/packages/shared/src/tool-registry/definitions.ts +92 -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.
|
|
@@ -62,7 +74,7 @@ make clean # Destroy all cloned VMs
|
|
|
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"}`. **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,10 +98,13 @@ 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 |
|
|
91
104
|
| `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) |
|
|
105
|
+
| `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 `multiselect` method). Tool description instructs agents: *"UI provides a Select all toggle; do not add one."* See change: ask-user-multiselect-polyfill. |
|
|
106
|
+
| `src/extension/multiselect-polyfill.ts` | `polyfillMultiselect(ctx, title, options, opts)` — thin wrapper around `ctx.ui.custom<T>()` that resolves to `string[]` (confirmed) or `undefined` (cancelled). Used for both single-question and batch sub-question `multiselect` paths in `ask-user-tool.ts`. |
|
|
107
|
+
| `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. |
|
|
93
108
|
| `src/shared/openspec-activity-detector.ts` | Detects OpenSpec activity from tool events; auto-attach requires only changeName (phase optional) |
|
|
94
109
|
| `src/shared/openspec-poller.ts` | OpenSpec CLI polling (shared, used by server DirectoryService) |
|
|
95
110
|
| `src/shared/state-replay.ts` | Synthesizes events from pi entries (shared, used by server + bridge) |
|
|
@@ -131,7 +146,9 @@ make clean # Destroy all cloned VMs
|
|
|
131
146
|
| `src/server/pi-resource-scanner.ts` | Discovers pi extensions, skills, prompts from local, global, and package sources |
|
|
132
147
|
| `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")` |
|
|
133
148
|
| `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) |
|
|
149
|
+
| `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 |
|
|
150
|
+
| `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 |
|
|
151
|
+
| `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
152
|
| `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
153
|
| `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
154
|
| `src/shared/tool-registry/types.ts` | `ToolDefinition`, `Strategy`, `StrategyResult`, `Resolution`, `Source`, `UnknownToolError`, `ModuleResolutionError` |
|
|
@@ -141,7 +158,7 @@ make clean # Destroy all cloned VMs
|
|
|
141
158
|
| `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
159
|
| `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
160
|
| `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. |
|
|
161
|
+
| `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
162
|
| `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
163
|
| `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
164
|
| `src/client/lib/tools-api.ts` | Client-side fetch helpers for `/api/tools*` (`fetchTools`, `rescanAll`, `rescanOne`, `setOverride`, `clearOverride`, `downloadDiagnostics`) |
|
|
@@ -166,6 +183,8 @@ make clean # Destroy all cloned VMs
|
|
|
166
183
|
| `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
184
|
| `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
185
|
| `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. |
|
|
186
|
+
| `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. |
|
|
187
|
+
| `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
188
|
| `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
189
|
| `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
190
|
| `src/server/editor-registry.ts` | Detects available native editors (running processes + CLI) |
|
|
@@ -190,7 +209,10 @@ make clean # Destroy all cloned VMs
|
|
|
190
209
|
| `src/client/hooks/useAuthStatus.ts` | Client auth status hook and login redirect helper |
|
|
191
210
|
| `src/server/localhost-guard.ts` | Network access guard: `createNetworkGuard` (loopback/trusted/authenticated), `isBypassedHost` (CIDR/wildcard/exact), netmask-to-CIDR helpers |
|
|
192
211
|
| `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
|
|
212
|
+
| `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 |
|
|
213
|
+
| `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. |
|
|
214
|
+
| `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. |
|
|
215
|
+
| `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
216
|
| `src/client/components/KnownServersSection.tsx` | Settings section: list/add/remove persisted known remote servers |
|
|
195
217
|
| `src/client/components/NetworkDiscoverySection.tsx` | Settings section: mDNS network scan with "Add" action and label prompt |
|
|
196
218
|
| `src/client/lib/known-servers-api.ts` | Client-side fetch helpers for known servers CRUD and discovery endpoints |
|
|
@@ -239,14 +261,14 @@ make clean # Destroy all cloned VMs
|
|
|
239
261
|
| `src/client/lib/folder-encoding.ts` | Base64url encode/decode for folder paths in URL routes |
|
|
240
262
|
| `src/shared/editor-types.ts` | Editor instance types shared across components |
|
|
241
263
|
| `src/client/components/TerminalCard.tsx` | Sidebar card for terminal sessions (cyan accent) |
|
|
242
|
-
| `src/client/App.tsx` | React app with WebSocket integration |
|
|
264
|
+
| `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. |
|
|
243
265
|
| `src/client/components/MobileShell.tsx` | Two-panel mobile shell with slide transitions and swipe-back |
|
|
244
266
|
| `src/client/components/MobileActionMenu.tsx` | Kebab menu for session actions on mobile (includes OpenSpec commands) |
|
|
245
267
|
| `src/client/components/MobileOverlay.tsx` | Hamburger button and sidebar overlay for mobile |
|
|
246
268
|
| `src/client/components/SessionHeader.tsx` | Session header with OpenSpec attach/detach, flow launcher, MobileAttachButton |
|
|
247
269
|
| `src/client/hooks/useSwipeBack.ts` | iOS-style left-edge swipe-back gesture (40px edge zone, document-level listeners) |
|
|
248
270
|
| `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
|
|
271
|
+
| `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
272
|
| `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
273
|
| `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
274
|
| `src/client/lib/mobile-depth.ts` | Pure function computing MobileShell depth from route state |
|
|
@@ -322,7 +344,7 @@ make clean # Destroy all cloned VMs
|
|
|
322
344
|
| `packages/electron/scripts/test-electron-install.sh` | Full e2e Docker test: install, wizard, server launch, health check |
|
|
323
345
|
| `packages/electron/scripts/test-electron-install-inner.sh` | Inner test script run inside Docker container |
|
|
324
346
|
| `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 |
|
|
347
|
+
| `.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. |
|
|
326
348
|
|
|
327
349
|
## Build & Restart Workflow
|
|
328
350
|
|