pi-cockpit 0.5.1 → 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 +130 -107
- package/package.json +4 -3
- package/src/agents-store.ts +35 -7
- package/src/config.ts +16 -0
- package/src/index.ts +225 -32
- package/src/maestro-store.ts +273 -0
- package/src/public/v1/events.ts +136 -0
- package/src/settings-view.ts +34 -1
- package/src/sidebar-controller.ts +266 -0
- package/src/sidebar-render.ts +312 -0
- package/src/split-pane.ts +377 -0
- package/src/types.ts +12 -0
package/README.md
CHANGED
|
@@ -1,107 +1,130 @@
|
|
|
1
|
-
# pi-cockpit
|
|
2
|
-
|
|
3
|
-
A
|
|
4
|
-
|
|
5
|
-
It is the third plugin of the `pi-maestro-flow` project (alongside `pi-maestro-flow` and `pi-maestro-teammate`).
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
│
|
|
13
|
-
|
|
14
|
-
│
|
|
15
|
-
│
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
pi
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"README.md"
|
|
19
19
|
],
|
|
20
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",
|
|
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
22
|
"typecheck": "tsc --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"pi": {
|
|
@@ -65,6 +65,7 @@
|
|
|
65
65
|
"main": "./src/index.ts",
|
|
66
66
|
"exports": {
|
|
67
67
|
".": "./src/index.ts",
|
|
68
|
+
"./v1/events": "./src/public/v1/events.ts",
|
|
68
69
|
"./src/*": "./src/*"
|
|
69
70
|
}
|
|
70
71
|
}
|
package/src/agents-store.ts
CHANGED
|
@@ -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
|
-
&&
|
|
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
|
-
&&
|
|
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
|
}
|