pi-cockpit 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,94 +1,130 @@
1
- # pi-cockpit
2
-
3
- A list-mode **agent cockpit** for [Pi](https://pi.dev): a status stack pinned **above the editor** showing live teammates and the current todo plan, plus a Starship-style footer. Everything is drawn through Pi's public extension APIs only (`setWidget` / `setFooter`) — no core patches.
4
-
5
- It is the third plugin of the `pi-maestro-flow` project (alongside `pi-maestro-flow` and `pi-maestro-teammate`). Since `pi-maestro-flow@0.6.1` it is an exact-pinned dependency of `pi-maestro-flow` — installed together and auto-registered into Pi's `settings.packages` on postinstall — but it also installs and runs standalone.
6
-
7
- ```
8
- ┌─ AGENTS · 2 running ─────────────────────────────┐ ← setWidget(aboveEditor)
9
- │ ⠋ explorer #a1f3c2 map auth read routes.ts │
10
- │ ⠋ explorer #b9e014 trace jwt read jwt.ts │
11
- ├─ TODO · 1/4 ─────────────────────────────────────┤
12
- │ 01 ✓ map auth entrypoints │
13
- │ 02 ⠋ trace jwt verify │
14
- │ 03 · implement refresh │
15
- │ 04 · add tests │
16
- └──────────────────────────────────────────────────┘
17
- > dispatch a goal… ← editor (Pi native)
18
- pi/stream-70b · ctx [████░░░░] 42% · $0.52 · 01:23 ← setFooter
19
- ```
20
-
21
- ## Install
22
-
23
- `pi-cockpit` comes automatically with the orchestration layer — installing `pi-maestro-flow@0.6.1` pulls `pi-cockpit@0.1.1` and registers it into `settings.packages` on postinstall, no manual setup required. To use it on its own:
24
-
25
- ```bash
26
- pi install npm:pi-cockpit # standalone from npm
27
- # or, for one run:
28
- pi -e ./packages/pi-cockpit
29
- ```
30
-
31
- ## What it shows
32
-
33
- - **AGENTS** — every running teammate as a table row (status spinner · role · id · label · live tail), sorted running-first. Collapses to a one-line `N agents running` summary in compact mode.
34
- - **TODO** — the active plan as numbered rows with four states (done ✓ / in-progress spinner / blocked ! / pending ·). Collapses to a segmented progress bar + current step + percent.
35
- - **Footer** — `provider/model · context gauge · ↑in ↓out · $cost · elapsed · git branch`. Since v0.1.1, `bash_bg` background-job state lives on a **dedicated second footer row** so it no longer competes with the primary line.
36
-
37
- Toggle each block between list and compact with `/cockpit`.
38
-
39
- ## Data sources (and the dependency this implies)
40
-
41
- | Block | Source | Available on bare Pi? |
42
- |-------|--------|------------------------|
43
- | AGENTS | `pi.events` channels `teammate:started` / `teammate:message` / `teammate:complete`, broadcast by **pi-maestro-teammate** | **No** — without that extension no events fire, the block stays hidden |
44
- | TODO | the `todo-state` snapshot the **pi-maestro-flow** `todo` tool persists after every mutation (re-read on each `tool_execution_end` and on `session_start`) | **No** — without the `todo` tool the block stays hidden |
45
- | Footer | `ctx.model`, `ctx.getContextUsage()`, session usage totals, `footerData.getGitBranch()` | **Yes** |
46
-
47
- So on a stock Pi (no teammate / no todo tool) the extension loads without error, the status stack renders nothing, and only the footer appears. The roster is **self-accumulated from event deltas** — teammates already running before the extension loads are not back-filled (the teammate extension broadcasts deltas, never a full roster). The todo list **is** back-filled on `session_start` from the persisted snapshot.
48
-
49
- This is the whole coupling story: cockpit has **no package dependency on `pi-maestro-flow`** and only a peer dependency on `pi-maestro-teammate`. It observes the other two plugins through public channels alone — teammate's event broadcasts for AGENTS, and the `todo-state` snapshot flow's `todo` tool persists for TODO. Remove either source and the matching block simply hides; the footer always remains.
50
-
51
- ## Configuration
52
-
53
- `~/.pi/agent/cockpit.json` (created on first run):
54
-
55
- ```json
56
- {
57
- "enabled": true,
58
- "agentsMode": "list",
59
- "todoMode": "list",
60
- "hideNativeAgents": false
61
- }
62
- ```
63
-
64
- - `agentsMode` / `todoMode`: `"list"` or `"compact"`.
65
- - `hideNativeAgents`: when `true`, clears the teammate extension's own `teammate-agents` widget (it draws a similar list *below* the editor) so the two don't duplicate. Off by default — the two widgets use different keys and placements and can coexist.
66
-
67
- ## Command
68
-
69
- `/cockpit` opens an overlay to toggle `enabled`, `agentsMode`, `todoMode`, and `hideNativeAgents` (`e` / `a` / `t` / `n`, `Esc` to close). Changes persist immediately.
70
-
71
- ## Terminal feasibility — what this design deliberately does NOT do
72
-
73
- The original mockup was a browser page; a terminal is an ANSI stream with no DOM, no CSS, no focus, no hover. Four mockup effects are **out of scope** here, with replacements:
74
-
75
- | Mockup effect | Why it can't work in a TUI | Replacement |
76
- |---------------|----------------------------|-------------|
77
- | Input-focus "power-up" glow (`:has(:focus)`) | no focus pseudo-class; `render(width)` can't see focus | the stack's header dot turns accent-green while an agent is running |
78
- | Hover-to-expand chips/rows | no hover in a terminal | list mode shows everything; compact mode is one line; `/cockpit` toggles |
79
- | Scanlines / logo light-up animation | no overlay/animation layer | a braille spinner frame, advanced on each redraw |
80
- | In-stream thinking-collapse / edit progress bar / colored bash stdout | built-in message & built-in tool rendering is **not** replaceable by extensions (`renderCall`/`renderResult` only apply to tools *you* register) | the conversation stream is left to Pi's native renderer — it is context, not a cockpit deliverable |
81
-
82
- ## Local development
83
-
84
- ```bash
85
- cd packages/pi-cockpit
86
- node --test --experimental-transform-types tests/agents-store.test.ts tests/todo-store.test.ts tests/render.test.ts tests/footer.test.ts
87
- ../../node_modules/.bin/tsc -p tsconfig.json # type-check (uses the monorepo root's tsc + types)
88
- ```
89
-
90
- The package lives inside the `pi-maestro-flow` monorepo under `packages/` so it resolves `@earendil-works/*` types from the root `node_modules`. Since v0.6.1 it is also an exact-pinned dependency of `pi-maestro-flow` and ships as its own npm package (`pi-cockpit`).
91
-
92
- ## License
93
-
94
- MIT
1
+ # pi-cockpit
2
+
3
+ A responsive **agent cockpit** for [Pi](https://pi.dev): wide terminals get a docked Maestro operations sidebar, narrow terminals automatically fall back to the existing Todo and Agent widgets, and every layout keeps the Starship-style footer.
4
+
5
+ It is the third plugin of the `pi-maestro-flow` project (alongside `pi-maestro-flow` and `pi-maestro-teammate`). It is installed and registered with `pi-maestro-flow`, but it also runs standalone. Current version: **0.5.1**.
6
+
7
+ The dock is a non-capturing top-right overlay. Cockpit reserves its columns by wrapping the active TUI renderer at runtime, so the Pi workspace reflows instead of rendering underneath it. No Pi source files are modified.
8
+
9
+ ```
10
+ ┌─ AGENTS · 2 running ─────────────────────────────┐ ← setWidget(aboveEditor)
11
+ │ ⠋ explorer #a1f3c2 map auth read routes.ts │
12
+ │ ⠋ explorer #b9e014 trace jwt read jwt.ts │
13
+ ├─ TODO · 1/4 ─────────────────────────────────────┤
14
+ │ 01 ✓ map auth entrypoints │
15
+ │ 02 ⠋ trace jwt verify │
16
+ │ 03 · implement refresh │
17
+ │ 04 · add tests │
18
+ └──────────────────────────────────────────────────┘
19
+ > dispatch a goal… ← editor (Pi native)
20
+ pi/stream-70b · ctx [████░░░░] 42% · $0.52 · 01:23 ← setFooter
21
+ ```
22
+
23
+ ## Install
24
+
25
+ `pi-cockpit` comes automatically with the orchestration layer — installing `pi-maestro-flow` pulls `pi-cockpit` and registers it into `settings.packages` on postinstall, no manual setup required. To use it on its own:
26
+
27
+ ```bash
28
+ pi install npm:pi-cockpit # standalone from npm
29
+ # or, for one run:
30
+ pi -e ./packages/pi-cockpit
31
+ ```
32
+
33
+ ## What it shows
34
+
35
+ - **SIDEBAR** — Workflow Session/Run progress, Goal state and budget, Todo tasks, Teammate roster, background jobs, and read-only Team Swarm progress. Empty sections disappear and constrained heights retain current or failed work before secondary detail.
36
+ - **AGENTS** — every running teammate as a table row (status spinner · role · id · label · live tail), sorted running-first. Completed/failed agents show their total duration. On narrow terminals this returns to the below-editor widget.
37
+ - **TODO** — the active plan as numbered rows with four states (done ✓ / in-progress spinner / blocked ! / pending ·). On narrow terminals this returns to the above-editor widget.
38
+ - **Footer** — `provider/model · context gauge · ↑in ↓out · $cost · elapsed · git branch`. `bash_bg` background-job state lives on a **dedicated second footer row** so it no longer competes with the primary line.
39
+ - **Thinking timer** — while the model is thinking, the folded thinking row shows a spinner and running elapsed time; when the run ends, it settles to the actual duration (e.g. `thoughts · 8.4s`).
40
+ - **Quiet mode** — compresses the seven built-in tool calls (read/bash/edit/write/grep/find/ls) into single-line ✓/✗/⋯ summaries and folds thinking blocks. Two glyph sets available: `check` (✓/✗/⋯) and `dot` (●/○/◌). Toggle with `/cockpit quiet`; turning off requires `/reload` to restore native tool renderers.
41
+
42
+ Toggle each block between list and compact with `/cockpit`.
43
+
44
+ ## Data sources (and the dependency this implies)
45
+
46
+ | Block | Source | Available on bare Pi? |
47
+ |-------|--------|------------------------|
48
+ | MAESTRO | Versioned `cockpit:maestro-query` / `maestro:ui-snapshot` full snapshots from **pi-maestro-flow** | **No** — Workflow, Goal, and Swarm sections stay hidden |
49
+ | AGENTS | `pi.events` channels `teammate:started` / `teammate:message` / `teammate:complete`, broadcast by **pi-maestro-teammate** | **No** — without that extension no events fire, the block stays hidden |
50
+ | TODO | the `todo-state` snapshot the **pi-maestro-flow** `todo` tool persists after every mutation (re-read on each `tool_execution_end` and on `session_start`) | **No** — without the `todo` tool the block stays hidden |
51
+ | Footer | `ctx.model`, `ctx.getContextUsage()`, session usage totals, `footerData.getGitBranch()` | **Yes** |
52
+
53
+ So on a stock Pi the extension loads without error, the Maestro sections stay empty, and the footer remains available. The roster is **self-accumulated from event deltas**; Todo is back-filled from the latest durable `todo-state`. Workflow, Goal, and Swarm use a versioned full-replacement snapshot with generation fencing and a query path for cold-start recovery.
54
+
55
+ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional producers through public event contracts; removing a producer hides only the matching section.
56
+
57
+ ## Configuration
58
+
59
+ `~/.pi/agent/cockpit.json` (created on first run):
60
+
61
+ ```json
62
+ {
63
+ "enabled": true,
64
+ "quietMode": false,
65
+ "quietSymbols": "check",
66
+ "agentsMode": "list",
67
+ "todoMode": "list",
68
+ "todoExpanded": false,
69
+ "hideNativeAgents": true,
70
+ "icons": { "mode": "auto" },
71
+ "sidebar": {
72
+ "mode": "auto",
73
+ "width": 40,
74
+ "density": "comfortable"
75
+ },
76
+ "theme": ""
77
+ }
78
+ ```
79
+
80
+ - `quietMode`: when `true`, compresses built-in tool rendering and folds thinking blocks.
81
+ - `quietSymbols`: `"check"` (✓/✗/⋯) or `"dot"` (●/○/◌) lifecycle glyphs for quiet tool rows.
82
+ - `agentsMode` / `todoMode`: `"list"` or `"compact"`.
83
+ - `todoExpanded`: when `true`, expands the todo widget by default.
84
+ - `hideNativeAgents`: when `true`, clears the teammate extension's own `teammate-agents` widget (it draws a similar list *below* the editor) so the two don't duplicate. On by default.
85
+ - `sidebar.mode`: `"auto"` or `"on"` enables the dock when at least 72 main columns plus 32 sidebar columns fit; `"off"` always uses widgets.
86
+ - `sidebar.width`: persisted dock width, rounded and clamped to `32..56`; default `40`.
87
+ - `sidebar.density`: `"comfortable"` or `"compact"`.
88
+ - `icons.mode`: `"auto"` (detect Nerd Font), `"nerd"`, or `"ascii"`.
89
+ - `theme`: named theme override; empty string follows the Pi session theme.
90
+
91
+ ## Commands
92
+
93
+ - `/cockpit` — opens the settings overlay.
94
+ - `/cockpit sidebar` — reports the current sidebar mode, width, and density.
95
+ - `/cockpit sidebar auto|on|off` — selects dock behavior.
96
+ - `/cockpit sidebar resize` or `Ctrl+Shift+R` — enters temporary Resize mode. Left/Right adjusts one column, Shift+Left/Shift+Right adjusts four, Enter accepts, and Escape rolls back. Mouse reporting is active only during Resize mode.
97
+ - `/cockpit quiet` — toggles quiet mode; `/cockpit bg` shows background jobs.
98
+ - `/theme` — switch theme with live preview; `/theme <name>` applies directly. Pi ships no standalone theme command; cockpit provides one.
99
+
100
+ ## Sidebar compatibility
101
+
102
+ The split-pane wrapper depends on Pi's current TUI renderer shape and is verified against Pi `0.82.1`. A render integration failure disables the split and retries the original renderer at full width.
103
+
104
+ Do not enable `pi-cockpit`'s dock and `pi-atelier@0.7.0`'s sidebar together. Both reserve columns by wrapping the same renderer, and `pi-atelier@0.7.0` does not participate in Cockpit's split-owner marker protocol. Use `"sidebar": { "mode": "off" }` when running Atelier.
105
+
106
+ ## Terminal feasibility — what this design deliberately does NOT do
107
+
108
+ The original mockup was a browser page; a terminal is an ANSI stream with no DOM, no CSS, no focus, no hover. Four mockup effects are **out of scope** here, with replacements:
109
+
110
+ | Mockup effect | Why it can't work in a TUI | Replacement |
111
+ |---------------|----------------------------|-------------|
112
+ | Input-focus "power-up" glow (`:has(:focus)`) | no focus pseudo-class; `render(width)` can't see focus | the stack's header dot turns accent-green while an agent is running |
113
+ | Hover-to-expand chips/rows | no hover in a terminal | list mode shows everything; compact mode is one line; `/cockpit` toggles |
114
+ | Scanlines / logo light-up animation | no overlay/animation layer | a braille spinner frame, advanced on each redraw |
115
+ | In-stream thinking-collapse / edit progress bar / colored bash stdout | built-in message & built-in tool rendering is **not** replaceable by extensions (`renderCall`/`renderResult` only apply to tools *you* register) | the conversation stream is left to Pi's native renderer — it is context, not a cockpit deliverable |
116
+
117
+ ## Local development
118
+
119
+ ```bash
120
+ cd packages/pi-cockpit
121
+ npm test
122
+ npm run typecheck
123
+ npm pack --dry-run
124
+ ```
125
+
126
+ The package lives inside the `pi-maestro-flow` monorepo under `packages/` so it resolves `@earendil-works/*` types from the root `node_modules`. It is also an exact-pinned dependency of `pi-maestro-flow` and ships as its own npm package (`pi-cockpit`).
127
+
128
+ ## License
129
+
130
+ MIT. The split-pane behavior is adapted from `pi-atelier` under its MIT license; attribution is retained in `src/split-pane.ts`.
package/package.json CHANGED
@@ -1,70 +1,71 @@
1
- {
2
- "name": "pi-cockpit",
3
- "version": "0.5.0",
4
- "description": "Agent cockpit for Pi: a list-mode status stack (live teammates + todo plan) pinned above the editor, plus a Starship-style footer. Renders only via public extension APIs (setWidget/setFooter).",
5
- "type": "module",
6
- "keywords": [
7
- "pi-package",
8
- "pi",
9
- "pi-extension",
10
- "tui",
11
- "teammate",
12
- "todo"
13
- ],
14
- "license": "MIT",
15
- "files": [
16
- "src/",
17
- "themes/",
18
- "README.md"
19
- ],
20
- "scripts": {
21
- "test": "node --test --experimental-transform-types tests/config.test.ts tests/quiet-tools.test.ts tests/agents-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts",
22
- "typecheck": "tsc --noEmit"
23
- },
24
- "pi": {
25
- "extensions": [
26
- "./src/extension/index.ts"
27
- ],
28
- "themes": [
29
- "./themes"
30
- ]
31
- },
32
- "peerDependencies": {
33
- "@earendil-works/pi-agent-core": "*",
34
- "@earendil-works/pi-ai": "*",
35
- "@earendil-works/pi-coding-agent": "*",
36
- "@earendil-works/pi-tui": "*",
37
- "pi-maestro-teammate": "^1.0.0"
38
- },
39
- "peerDependenciesMeta": {
40
- "@earendil-works/pi-agent-core": {
41
- "optional": true
42
- },
43
- "@earendil-works/pi-ai": {
44
- "optional": true
45
- },
46
- "@earendil-works/pi-coding-agent": {
47
- "optional": true
48
- },
49
- "@earendil-works/pi-tui": {
50
- "optional": true
51
- },
52
- "pi-maestro-teammate": {
53
- "optional": true
54
- }
55
- },
56
- "devDependencies": {
57
- "@earendil-works/pi-agent-core": "0.82.1",
58
- "@earendil-works/pi-ai": "0.82.1",
59
- "@earendil-works/pi-coding-agent": "0.82.1",
60
- "@earendil-works/pi-tui": "0.82.1",
61
- "@types/node": "^22.0.0",
62
- "pi-maestro-teammate": "1.3.0",
63
- "typescript": "^5.5.0"
64
- },
65
- "main": "./src/index.ts",
66
- "exports": {
67
- ".": "./src/index.ts",
68
- "./src/*": "./src/*"
69
- }
70
- }
1
+ {
2
+ "name": "pi-cockpit",
3
+ "version": "0.6.0",
4
+ "description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-extension",
10
+ "tui",
11
+ "teammate",
12
+ "todo"
13
+ ],
14
+ "license": "MIT",
15
+ "files": [
16
+ "src/",
17
+ "themes/",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test --experimental-transform-types tests/config.test.ts tests/public-events.test.ts tests/maestro-store.test.ts tests/split-pane.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/quiet-tools.test.ts tests/agents-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/settings-view.test.ts tests/thinking-fold.test.ts tests/thinking-timer.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "pi": {
25
+ "extensions": [
26
+ "./src/extension/index.ts"
27
+ ],
28
+ "themes": [
29
+ "./themes"
30
+ ]
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-agent-core": "*",
34
+ "@earendil-works/pi-ai": "*",
35
+ "@earendil-works/pi-coding-agent": "*",
36
+ "@earendil-works/pi-tui": "*",
37
+ "pi-maestro-teammate": "^1.0.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@earendil-works/pi-agent-core": {
41
+ "optional": true
42
+ },
43
+ "@earendil-works/pi-ai": {
44
+ "optional": true
45
+ },
46
+ "@earendil-works/pi-coding-agent": {
47
+ "optional": true
48
+ },
49
+ "@earendil-works/pi-tui": {
50
+ "optional": true
51
+ },
52
+ "pi-maestro-teammate": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@earendil-works/pi-agent-core": "0.82.1",
58
+ "@earendil-works/pi-ai": "0.82.1",
59
+ "@earendil-works/pi-coding-agent": "0.82.1",
60
+ "@earendil-works/pi-tui": "0.82.1",
61
+ "@types/node": "^22.0.0",
62
+ "pi-maestro-teammate": "1.3.1",
63
+ "typescript": "^5.5.0"
64
+ },
65
+ "main": "./src/index.ts",
66
+ "exports": {
67
+ ".": "./src/index.ts",
68
+ "./v1/events": "./src/public/v1/events.ts",
69
+ "./src/*": "./src/*"
70
+ }
71
+ }
@@ -68,7 +68,7 @@ export interface MessagePayload {
68
68
  progress?: ProgressPayload[];
69
69
  }
70
70
  export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exitCode">
71
- & Partial<Pick<TeammateCompleteEvent, "durationMs">>;
71
+ & Partial<Pick<TeammateCompleteEvent, "durationMs" | "wakeable">>;
72
72
 
73
73
  /**
74
74
  * How long a failed agent stays on screen after it completes.
@@ -78,6 +78,14 @@ export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exi
78
78
  */
79
79
  export const FAILED_LINGER_MS = 30_000;
80
80
 
81
+ /**
82
+ * How long a sleeping (wakeable) row stays visible after completion.
83
+ *
84
+ * Matches the native widget's idle-hide window so both surfaces agree on
85
+ * when an idle agent stops occupying screen space.
86
+ */
87
+ export const SLEEPING_LINGER_MS = 60_000;
88
+
81
89
  /**
82
90
  * How long a completed agent's tombstone suppresses self-healing.
83
91
  *
@@ -170,6 +178,14 @@ export class AgentsStore {
170
178
  return at !== undefined && now - at < COMPLETED_TOMBSTONE_MS;
171
179
  }
172
180
 
181
+ private canMaterialize(id: string, parentId: string | undefined, now: number): boolean {
182
+ if (this.isTombstoned(parentId, now)) return false;
183
+ if (!this.isTombstoned(id, now)) return true;
184
+ // An explicit parent restart clears only the graph container's tombstone.
185
+ // Its next full snapshot is authoritative for the child lifecycle too.
186
+ return parentId !== undefined && parentId !== id && this.roster.has(parentId);
187
+ }
188
+
173
189
  applyStarted(p: StartedPayload, now: number = Date.now()): void {
174
190
  const id = p.correlationId;
175
191
  // An explicit start is authoritative: a woken or re-dispatched agent
@@ -203,9 +219,11 @@ export class AgentsStore {
203
219
  for (const progress of p.progress) this.applyProgress(p.correlationId, progress, now);
204
220
  }
205
221
  const targetId = p.taskCorrelationId ?? p.correlationId;
222
+ const parentId = p.taskCorrelationId && p.taskCorrelationId !== p.correlationId
223
+ ? p.correlationId
224
+ : undefined;
206
225
  if (!this.roster.has(targetId)
207
- && !this.isTombstoned(targetId, now)
208
- && !this.isTombstoned(p.correlationId, now)) {
226
+ && this.canMaterialize(targetId, parentId, now)) {
209
227
  // Self-heal the roster: a running agent must stay visible even when its
210
228
  // `started` delta was missed (cold start, event reorder, or a foreground
211
229
  // dispatch that detached to background). Materialize the row from this
@@ -273,6 +291,13 @@ export class AgentsStore {
273
291
  row.failedAt = now;
274
292
  row.lastActivityAt = now;
275
293
  delete row.activeTool;
294
+ } else if (row && p.wakeable && p.exitCode === 0) {
295
+ // A wakeable agent enters sleeping state after completion.
296
+ // Keep the row visible so the user can see it is idle but recallable.
297
+ row.status = "sleeping";
298
+ row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
299
+ row.lastActivityAt = now;
300
+ delete row.activeTool;
276
301
  } else {
277
302
  this.roster.delete(id);
278
303
  // Tombstone even when the row never existed: a `complete` that beats
@@ -282,13 +307,16 @@ export class AgentsStore {
282
307
  }
283
308
  }
284
309
 
285
- /** Drop failed rows that have had their time on screen. Returns true if any went. */
310
+ /** Drop failed or expired sleeping rows that have had their time on screen. Returns true if any went. */
286
311
  prune(now = Date.now()): boolean {
287
312
  let changed = false;
288
313
  for (const [id, row] of this.roster) {
289
314
  if (row.failedAt !== undefined && now - row.failedAt >= FAILED_LINGER_MS) {
290
315
  this.roster.delete(id);
291
316
  changed = true;
317
+ } else if (row.status === "sleeping" && row.finishedAt !== undefined && now - row.finishedAt >= SLEEPING_LINGER_MS) {
318
+ this.roster.delete(id);
319
+ changed = true;
292
320
  }
293
321
  }
294
322
  for (const [id, at] of this.completedAt) {
@@ -297,18 +325,18 @@ export class AgentsStore {
297
325
  return changed;
298
326
  }
299
327
 
300
- /** True while a failed row is still counting down, so the redraw loop must run. */
328
+ /** True while a failed or sleeping row is still counting down, so the redraw loop must run. */
301
329
  hasLingering(): boolean {
302
330
  for (const row of this.roster.values()) {
303
331
  if (row.failedAt !== undefined) return true;
332
+ if (row.status === "sleeping") return true;
304
333
  }
305
334
  return false;
306
335
  }
307
336
 
308
337
  private applyProgress(parentCorrelationId: string, p: ProgressPayload, now: number): void {
309
338
  if (!this.roster.has(p.correlationId)
310
- && !this.isTombstoned(p.correlationId, now)
311
- && !this.isTombstoned(parentCorrelationId, now)) {
339
+ && this.canMaterialize(p.correlationId, parentCorrelationId, now)) {
312
340
  // Self-heal: see applyMessage. A graph child whose `started` delta was
313
341
  // missed still materializes from its first progress event — unless the
314
342
  // child or its graph parent already completed, in which case this late
package/src/config.ts CHANGED
@@ -3,6 +3,9 @@ import { join } from "node:path";
3
3
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
  import { type CockpitConfig, DEFAULT_CONFIG } from "./types.ts";
5
5
 
6
+ const SIDEBAR_MIN_WIDTH = 32;
7
+ const SIDEBAR_MAX_WIDTH = 56;
8
+
6
9
  export function getConfigPath(): string {
7
10
  return join(getAgentDir(), "cockpit.json");
8
11
  }
@@ -14,9 +17,17 @@ export function mergeConfig(base: CockpitConfig, over: unknown): CockpitConfig {
14
17
  const isMode = (v: unknown): v is "list" | "compact" => v === "list" || v === "compact";
15
18
  const isQuietSymbolMode = (v: unknown): v is "check" | "dot" => v === "check" || v === "dot";
16
19
  const isIconMode = (v: unknown): v is "auto" | "nerd" | "ascii" => v === "auto" || v === "nerd" || v === "ascii";
20
+ const isSidebarMode = (v: unknown): v is "auto" | "on" | "off" => v === "auto" || v === "on" || v === "off";
21
+ const isSidebarDensity = (v: unknown): v is "comfortable" | "compact" => v === "comfortable" || v === "compact";
17
22
  const iconsRaw = o.icons && typeof o.icons === "object" && !Array.isArray(o.icons)
18
23
  ? (o.icons as Record<string, unknown>)
19
24
  : undefined;
25
+ const sidebarRaw = o.sidebar && typeof o.sidebar === "object" && !Array.isArray(o.sidebar)
26
+ ? (o.sidebar as Record<string, unknown>)
27
+ : undefined;
28
+ const sidebarWidth = sidebarRaw && typeof sidebarRaw.width === "number" && Number.isFinite(sidebarRaw.width)
29
+ ? Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, Math.round(sidebarRaw.width)))
30
+ : base.sidebar.width;
20
31
  return {
21
32
  enabled: typeof o.enabled === "boolean" ? o.enabled : base.enabled,
22
33
  quietMode: typeof o.quietMode === "boolean" ? o.quietMode : base.quietMode,
@@ -26,6 +37,11 @@ export function mergeConfig(base: CockpitConfig, over: unknown): CockpitConfig {
26
37
  todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
27
38
  hideNativeAgents: typeof o.hideNativeAgents === "boolean" ? o.hideNativeAgents : base.hideNativeAgents,
28
39
  icons: { mode: iconsRaw && isIconMode(iconsRaw.mode) ? iconsRaw.mode : base.icons.mode },
40
+ sidebar: {
41
+ mode: sidebarRaw && isSidebarMode(sidebarRaw.mode) ? sidebarRaw.mode : base.sidebar.mode,
42
+ width: sidebarWidth,
43
+ density: sidebarRaw && isSidebarDensity(sidebarRaw.density) ? sidebarRaw.density : base.sidebar.density,
44
+ },
29
45
  theme: typeof o.theme === "string" ? o.theme : base.theme,
30
46
  };
31
47
  }