pi-cockpit 0.7.0 → 0.9.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 +24 -1
- package/package.json +75 -71
- package/src/agents-store.ts +63 -12
- package/src/ambient.ts +57 -9
- package/src/bash-bg-overlay.ts +7 -5
- package/src/config.ts +57 -1
- package/src/editor-bottom.ts +128 -0
- package/src/index.ts +663 -99
- package/src/mouse-reporting.ts +65 -0
- package/src/public/v1/events.ts +20 -0
- package/src/render.ts +18 -2
- package/src/settings/cockpit-provider.ts +682 -0
- package/src/settings/coordinator.ts +323 -0
- package/src/settings/i18n.ts +158 -0
- package/src/settings/locale-state.ts +138 -0
- package/src/settings/registry.ts +160 -0
- package/src/settings/settings-shell.ts +1182 -0
- package/src/settings-view.ts +96 -2
- package/src/sidebar-controller.ts +143 -2
- package/src/sidebar-render.ts +86 -15
- package/src/split-pane.ts +55 -12
- package/src/theme-picker.ts +9 -9
- package/src/tick-policy.ts +5 -3
- package/src/title-gen.ts +60 -0
- package/src/title-llm.ts +115 -0
- package/src/types.ts +58 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
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
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
|
+
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.8.0**.
|
|
6
6
|
|
|
7
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
8
|
|
|
@@ -73,6 +73,16 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
|
|
|
73
73
|
"width": 40,
|
|
74
74
|
"density": "comfortable"
|
|
75
75
|
},
|
|
76
|
+
"title": {
|
|
77
|
+
"enabled": true,
|
|
78
|
+
"showSession": true,
|
|
79
|
+
"showCwd": false,
|
|
80
|
+
"showModel": false,
|
|
81
|
+
"showThinking": false,
|
|
82
|
+
"showGit": false,
|
|
83
|
+
"showMaestro": false,
|
|
84
|
+
"maxLength": 80
|
|
85
|
+
},
|
|
76
86
|
"theme": ""
|
|
77
87
|
}
|
|
78
88
|
```
|
|
@@ -86,11 +96,24 @@ Cockpit has no package dependency on `pi-maestro-flow`. It observes optional pro
|
|
|
86
96
|
- `sidebar.width`: persisted dock width, rounded and clamped to `32..56`; default `40`.
|
|
87
97
|
- `sidebar.density`: `"comfortable"` or `"compact"`.
|
|
88
98
|
- `icons.mode`: `"auto"` (detect Nerd Font), `"nerd"`, or `"ascii"`.
|
|
99
|
+
- `title.enabled`: master switch for the terminal tab title (session summary + working state + opt-in tags). On by default.
|
|
100
|
+
- `title.showSession`: include the session summary (the `session_info` name, else a short session id) right after `pi`. On by default.
|
|
101
|
+
- `title.showCwd`: include the working directory after the session. Off by default — the title stays short, since the tab strip has no room for a wall of tags.
|
|
102
|
+
- `title.showModel`: include the active model tag (`m:gpt-5.6-sol`). Off by default.
|
|
103
|
+
- `title.showThinking`: include the thinking level tag (`t:high`); skipped while off. Off by default.
|
|
104
|
+
- `title.showGit`: include the git branch tag (`git:main`, `git:detached`); read synchronously from `.git/HEAD`, so it costs no process spawn. Off by default.
|
|
105
|
+
- `title.showMaestro`: include the Maestro workflow status tag (`wf:running`, `wf:done`). Off by default.
|
|
106
|
+
- `title.maxLength`: hard cap on the composed title; the middle is ellided to keep the head and the working-state tail. Clamped to `20..200`; default `80`.
|
|
107
|
+
- `title.generationModel`: `"provider/model"` (e.g. `"maestro-qwen/qwen3.8-max-preview"`) of a model registered with the `/api-manager` command (or any other provider pi resolves). When set, the session title is generated by that model over the first completed turn (user prompt + assistant reply) through its OpenAI-compatible endpoint, with a 10s timeout. Empty (default) uses the offline rule-based extractor instead. Generation is best-effort — on any failure the title silently falls back to the rule-based one. Editable from the `/cockpit` settings overlay (`z` or cursor to the `title gen model` row, Enter, type, Enter to save; empty clears back to rule-based).
|
|
108
|
+
|
|
109
|
+
The tab title is `frame + pi - <session> - <working state>`. The session part follows Claude Code's chain (`sessionTitle ?? agentTitle ?? haikuTitle ?? default`): the `/session name` title wins, otherwise the generated title, otherwise a short session id. The frame is Claude Code's title chrome: `⠂`/`⠐` braille spinner while a turn runs, static `✳` when idle; failure replaces it with `✗`. On quit, Cockpit clears the title so no stale tab label lingers (Claude Code's `CLEAR_TERMINAL_TITLE`).
|
|
89
110
|
- `theme`: named theme override; empty string follows the Pi session theme.
|
|
90
111
|
|
|
91
112
|
## Commands
|
|
92
113
|
|
|
93
114
|
- `/cockpit` — opens the settings overlay.
|
|
115
|
+
- `/maestro-settings` — opens the unified settings shell (Cockpit, Flow, Teammate and integrations).
|
|
116
|
+
- `Alt+J` — opens the background Bash jobs overlay (live status, command, cwd, duration, output tail).
|
|
94
117
|
- `/cockpit sidebar` — reports the current sidebar mode, width, and density.
|
|
95
118
|
- `/cockpit sidebar auto|on|off` — selects dock behavior.
|
|
96
119
|
- `/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.
|
package/package.json
CHANGED
|
@@ -1,71 +1,75 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-cockpit",
|
|
3
|
-
"version": "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/viewport-stability.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/tick-policy.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
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"pi-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"@earendil-works/pi-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"@
|
|
62
|
-
"pi-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-cockpit",
|
|
3
|
+
"version": "0.9.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/editor-bottom.test.ts tests/sidebar-render.test.ts tests/sidebar-controller.test.ts tests/viewport-stability.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/tick-policy.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts tests/title-gen.test.ts tests/title-llm.test.ts tests/settings-registry.test.ts tests/settings-coordinator.test.ts tests/settings-locale.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts",
|
|
22
|
+
"typecheck": "tsc --noEmit"
|
|
23
|
+
},
|
|
24
|
+
"pi": {
|
|
25
|
+
"extensions": [
|
|
26
|
+
"./src/extension/index.ts"
|
|
27
|
+
],
|
|
28
|
+
"themes": [
|
|
29
|
+
"./themes"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"pi-maestro-settings-core": "0.1.0",
|
|
34
|
+
"proper-lockfile": "4.1.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@earendil-works/pi-agent-core": "*",
|
|
38
|
+
"@earendil-works/pi-ai": "*",
|
|
39
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
40
|
+
"@earendil-works/pi-tui": "*",
|
|
41
|
+
"pi-maestro-teammate": "^1.6.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"@earendil-works/pi-agent-core": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"@earendil-works/pi-ai": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"@earendil-works/pi-coding-agent": {
|
|
51
|
+
"optional": true
|
|
52
|
+
},
|
|
53
|
+
"@earendil-works/pi-tui": {
|
|
54
|
+
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"pi-maestro-teammate": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@earendil-works/pi-agent-core": "0.83.0",
|
|
62
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
63
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
64
|
+
"@earendil-works/pi-tui": "0.83.0",
|
|
65
|
+
"@types/node": "^22.0.0",
|
|
66
|
+
"pi-maestro-teammate": "1.7.0",
|
|
67
|
+
"typescript": "^5.5.0"
|
|
68
|
+
},
|
|
69
|
+
"main": "./src/index.ts",
|
|
70
|
+
"exports": {
|
|
71
|
+
".": "./src/index.ts",
|
|
72
|
+
"./v1/events": "./src/public/v1/events.ts",
|
|
73
|
+
"./src/*": "./src/*"
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/agents-store.ts
CHANGED
|
@@ -114,6 +114,13 @@ export function mapAgentStatus(status: unknown): AgentStatus {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function activityFromProgressStatus(status: unknown): "running" | "sleeping" | undefined {
|
|
118
|
+
if (typeof status !== "string") return undefined;
|
|
119
|
+
return status === "sleeping" || status === "completed" || status === "failed" || status === "terminated"
|
|
120
|
+
? "sleeping"
|
|
121
|
+
: "running";
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
export const AGENT_STALL_TIMEOUT_MS = 30_000;
|
|
118
125
|
export type AgentDisplayStatus = AgentStatus | "result-ready" | "stalled";
|
|
119
126
|
|
|
@@ -138,9 +145,10 @@ function terminalTime(
|
|
|
138
145
|
now: number,
|
|
139
146
|
): number {
|
|
140
147
|
const completedAt = normalizeStartedAt(payload.completedAt, Number.NaN);
|
|
141
|
-
|
|
148
|
+
const turnStartedAt = row.turnStartedAt ?? row.startedAt;
|
|
149
|
+
if (Number.isFinite(completedAt)) return Math.max(turnStartedAt, completedAt);
|
|
142
150
|
if (typeof payload.durationMs === "number" && Number.isFinite(payload.durationMs)) {
|
|
143
|
-
return
|
|
151
|
+
return turnStartedAt + Math.max(0, payload.durationMs);
|
|
144
152
|
}
|
|
145
153
|
return row.finishedAt ?? now;
|
|
146
154
|
}
|
|
@@ -198,6 +206,14 @@ export class AgentsStore {
|
|
|
198
206
|
role: deriveRole(p.agent, p.name),
|
|
199
207
|
task: prev?.task ?? clean(p.name),
|
|
200
208
|
status: p.status === undefined ? prev?.status ?? "running" : mapAgentStatus(p.status),
|
|
209
|
+
phase: typeof p.phase === "string" ? clean(p.phase) : prev?.phase,
|
|
210
|
+
lastOutcome: p.lastOutcome
|
|
211
|
+
? {
|
|
212
|
+
status: p.lastOutcome.status,
|
|
213
|
+
...(p.lastOutcome.message ? { message: truncateTail(p.lastOutcome.message) } : {}),
|
|
214
|
+
settledAt: p.lastOutcome.settledAt,
|
|
215
|
+
}
|
|
216
|
+
: prev?.lastOutcome,
|
|
201
217
|
tail: prev?.tail ?? "",
|
|
202
218
|
startedAt: prev?.startedAt ?? normalizeStartedAt(p.startedAt, now),
|
|
203
219
|
lastActivityAt: normalizeStartedAt(p.lastActivityAt, now),
|
|
@@ -207,7 +223,15 @@ export class AgentsStore {
|
|
|
207
223
|
? { parentCorrelationId: prev.parentCorrelationId }
|
|
208
224
|
: {}),
|
|
209
225
|
};
|
|
210
|
-
if (row.status
|
|
226
|
+
if (row.status === "running") {
|
|
227
|
+
row.turnStartedAt = now;
|
|
228
|
+
delete row.finishedAt;
|
|
229
|
+
delete row.failedAt;
|
|
230
|
+
} else if (row.status === "sleeping") {
|
|
231
|
+
row.finishedAt = row.lastOutcome?.settledAt ?? prev?.finishedAt ?? now;
|
|
232
|
+
} else if (row.status !== "done" && row.status !== "failed" && row.status !== "terminated") {
|
|
233
|
+
delete row.finishedAt;
|
|
234
|
+
}
|
|
211
235
|
this.roster.set(id, row);
|
|
212
236
|
}
|
|
213
237
|
|
|
@@ -239,7 +263,7 @@ export class AgentsStore {
|
|
|
239
263
|
correlationId: targetId,
|
|
240
264
|
agent: p.agent ?? "",
|
|
241
265
|
name: p.name,
|
|
242
|
-
status: p.status,
|
|
266
|
+
status: activityFromProgressStatus(p.status),
|
|
243
267
|
lastActivityAt: p.lastActivityAt,
|
|
244
268
|
...(p.taskCorrelationId && p.taskCorrelationId !== p.correlationId
|
|
245
269
|
? { spawnedBy: p.correlationId }
|
|
@@ -278,6 +302,8 @@ export class AgentsStore {
|
|
|
278
302
|
delete row.finishedAt;
|
|
279
303
|
}
|
|
280
304
|
}
|
|
305
|
+
if (typeof p.phase === "string") row.phase = clean(p.phase);
|
|
306
|
+
else if (row.status === "sleeping") delete row.phase;
|
|
281
307
|
if (typeof p.taskIndex === "number") row.taskIndex = p.taskIndex;
|
|
282
308
|
if (Array.isArray(p.dependencies)) row.dependencies = [...p.dependencies];
|
|
283
309
|
}
|
|
@@ -307,23 +333,38 @@ export class AgentsStore {
|
|
|
307
333
|
row.status = "terminated";
|
|
308
334
|
row.taskStatus = "terminated";
|
|
309
335
|
row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
|
|
336
|
+
row.lastOutcome = { status: "terminated", settledAt: row.finishedAt };
|
|
310
337
|
row.lastActivityAt = now;
|
|
311
338
|
delete row.activeTool;
|
|
312
339
|
delete row.failedAt;
|
|
340
|
+
} else if (row && id === p.correlationId && p.wakeable) {
|
|
341
|
+
// A recoverable logical agent is sleeping regardless of the previous
|
|
342
|
+
// run outcome. Failure remains visible as orthogonal outcome metadata.
|
|
343
|
+
row.status = "sleeping";
|
|
344
|
+
row.taskStatus = "sleeping";
|
|
345
|
+
row.phase = undefined;
|
|
346
|
+
row.finishedAt = terminalTime(row, p, now);
|
|
347
|
+
row.lastActivityAt = now;
|
|
348
|
+
row.lastOutcome = {
|
|
349
|
+
status: failedByExitCode ? "failed" : "completed",
|
|
350
|
+
...(failedByExitCode && row.error ? { message: row.error } : {}),
|
|
351
|
+
settledAt: row.finishedAt,
|
|
352
|
+
};
|
|
353
|
+
if (failedByExitCode) row.failedAt = now;
|
|
354
|
+
else delete row.failedAt;
|
|
355
|
+
delete row.activeTool;
|
|
313
356
|
} else if (row && (row.status === "failed" || failedByExitCode)) {
|
|
314
357
|
row.status = "failed";
|
|
315
358
|
row.taskStatus = "failed";
|
|
316
359
|
row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
|
|
360
|
+
row.lastOutcome = {
|
|
361
|
+
status: "failed",
|
|
362
|
+
...(row.error ? { message: row.error } : {}),
|
|
363
|
+
settledAt: row.finishedAt,
|
|
364
|
+
};
|
|
317
365
|
row.failedAt = now;
|
|
318
366
|
row.lastActivityAt = now;
|
|
319
367
|
delete row.activeTool;
|
|
320
|
-
} else if (row && id === p.correlationId && p.wakeable && p.exitCode === 0) {
|
|
321
|
-
// A wakeable agent enters sleeping state after completion.
|
|
322
|
-
// Keep the row visible so the user can see it is idle but recallable.
|
|
323
|
-
row.status = "sleeping";
|
|
324
|
-
row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
|
|
325
|
-
row.lastActivityAt = now;
|
|
326
|
-
delete row.activeTool;
|
|
327
368
|
} else {
|
|
328
369
|
this.roster.delete(id);
|
|
329
370
|
// Tombstone even when the row never existed: a `complete` that beats
|
|
@@ -364,6 +405,14 @@ export class AgentsStore {
|
|
|
364
405
|
return false;
|
|
365
406
|
}
|
|
366
407
|
|
|
408
|
+
/** True while any agent (foreground or background) is still running, so elapsed/stall repaints keep the loop alive. */
|
|
409
|
+
hasActive(): boolean {
|
|
410
|
+
for (const row of this.roster.values()) {
|
|
411
|
+
if (row.status === "running") return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
367
416
|
private applyProgress(parentCorrelationId: string, p: ProgressPayload, now: number): void {
|
|
368
417
|
if (!this.roster.has(p.correlationId)
|
|
369
418
|
&& this.canMaterialize(p.correlationId, parentCorrelationId, now)) {
|
|
@@ -375,7 +424,7 @@ export class AgentsStore {
|
|
|
375
424
|
correlationId: p.correlationId,
|
|
376
425
|
agent: p.agent,
|
|
377
426
|
name: p.name,
|
|
378
|
-
status: p.status,
|
|
427
|
+
status: activityFromProgressStatus(p.status),
|
|
379
428
|
startedAt: p.startedAt,
|
|
380
429
|
lastActivityAt: p.lastActivityAt,
|
|
381
430
|
...(parentCorrelationId !== p.correlationId
|
|
@@ -403,6 +452,8 @@ export class AgentsStore {
|
|
|
403
452
|
delete row.finishedAt;
|
|
404
453
|
}
|
|
405
454
|
}
|
|
455
|
+
if (typeof p.phase === "string") row.phase = clean(p.phase);
|
|
456
|
+
else if (row.status === "sleeping") delete row.phase;
|
|
406
457
|
row.resultReadyAt = p.resultReadyAt === undefined
|
|
407
458
|
? undefined
|
|
408
459
|
: normalizeStartedAt(p.resultReadyAt, now);
|
package/src/ambient.ts
CHANGED
|
@@ -21,6 +21,22 @@ export interface AmbientState {
|
|
|
21
21
|
hideLiveDuration?: boolean;
|
|
22
22
|
/** Separator glyph between label and elapsed time (e.g. " · "). */
|
|
23
23
|
separator?: string;
|
|
24
|
+
/** Session summary — the session_info name, else a short session id. */
|
|
25
|
+
session?: string;
|
|
26
|
+
/** Active model short id. */
|
|
27
|
+
model?: string;
|
|
28
|
+
/** Active thinking level (omitted while off). */
|
|
29
|
+
thinking?: string;
|
|
30
|
+
/** Git branch name, or "detached". */
|
|
31
|
+
gitBranch?: string;
|
|
32
|
+
/** Maestro workflow status (run status when a run is active). */
|
|
33
|
+
maestro?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Leading title glyph: spinner frames while a turn runs, a static marker
|
|
36
|
+
* when idle (Claude Code's `✳` / braille frames). Failure keeps its own
|
|
37
|
+
* mark and ignores this. Absent when the caller opts out.
|
|
38
|
+
*/
|
|
39
|
+
frame?: string;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
function liveAgents(agents: readonly AgentRow[]): AgentRow[] {
|
|
@@ -51,24 +67,56 @@ export function workingMessage(state: AmbientState, now = Date.now()): string |
|
|
|
51
67
|
return `\x1b[3m${text}\x1b[23m`;
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
/** Composition options for titleFor. */
|
|
71
|
+
export interface TitleOptions {
|
|
72
|
+
/** Hard cap on the composed title. The middle is ellided, keeping head + state tail. */
|
|
73
|
+
maxLength?: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
/**
|
|
55
77
|
* The terminal tab title.
|
|
56
78
|
*
|
|
57
79
|
* A developer with several tabs open cannot otherwise tell which run finished and
|
|
58
80
|
* which one needs them. Failure outranks progress, because that is the state that
|
|
59
|
-
* actually requires a human.
|
|
81
|
+
* actually requires a human. The session summary (when present) sits right after
|
|
82
|
+
* "pi"; model / thinking / git / maestro tags follow the working state. Fields
|
|
83
|
+
* are only appended when present, so callers decide what to expose per config.
|
|
60
84
|
*/
|
|
61
|
-
export function titleFor(
|
|
62
|
-
|
|
85
|
+
export function titleFor(
|
|
86
|
+
state: AmbientState,
|
|
87
|
+
marks: { ok: string; fail: string },
|
|
88
|
+
sep = " - ",
|
|
89
|
+
opts?: TitleOptions,
|
|
90
|
+
): string {
|
|
91
|
+
const head = ["pi"];
|
|
92
|
+
if (state.session) head.push(state.session);
|
|
93
|
+
if (state.cwd) head.push(state.cwd);
|
|
94
|
+
const base = head.join(sep);
|
|
63
95
|
const broken = failedAgents(state.agents).length + failedJobs(state.jobs).length;
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
const prefix = state.frame ? `${state.frame} ` : "";
|
|
97
|
+
let title: string;
|
|
98
|
+
if (broken > 0) {
|
|
99
|
+
title = `${marks.fail} ${base}${sep}${broken} failed`;
|
|
100
|
+
} else if (state.running) {
|
|
66
101
|
const live = liveAgents(state.agents).length;
|
|
67
|
-
|
|
102
|
+
title = live > 0 ? `${prefix}${base}${sep}${live} agents` : `${prefix}${base}${sep}working`;
|
|
103
|
+
} else {
|
|
104
|
+
const jobs = state.jobs.filter((job) => job.status === "running").length;
|
|
105
|
+
title = jobs > 0 ? `${prefix}${base}${sep}${jobs} bg` : `${prefix}${base}`;
|
|
106
|
+
}
|
|
107
|
+
const tags: string[] = [];
|
|
108
|
+
if (state.model) tags.push(`m:${state.model}`);
|
|
109
|
+
if (state.thinking) tags.push(`t:${state.thinking}`);
|
|
110
|
+
if (state.gitBranch) tags.push(`git:${state.gitBranch}`);
|
|
111
|
+
if (state.maestro) tags.push(`wf:${state.maestro}`);
|
|
112
|
+
if (tags.length > 0) title = `${title}${sep}${tags.join(sep)}`;
|
|
113
|
+
if (opts?.maxLength !== undefined && title.length > opts.maxLength) {
|
|
114
|
+
const cap = opts.maxLength;
|
|
115
|
+
const headLen = Math.max(8, Math.floor(cap * 0.4));
|
|
116
|
+
const tailLen = Math.max(8, Math.floor(cap * 0.5));
|
|
117
|
+
title = `${title.slice(0, headLen)}…${title.slice(-tailLen)}`;
|
|
68
118
|
}
|
|
69
|
-
|
|
70
|
-
if (jobs > 0) return `${base}${sep}${jobs} bg`;
|
|
71
|
-
return base;
|
|
119
|
+
return title;
|
|
72
120
|
}
|
|
73
121
|
|
|
74
122
|
/**
|
package/src/bash-bg-overlay.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
|
+
Key,
|
|
3
4
|
type Component,
|
|
4
5
|
type Focusable,
|
|
6
|
+
matchesKey,
|
|
5
7
|
truncateToWidth,
|
|
6
8
|
visibleWidth,
|
|
7
9
|
wrapTextWithAnsi,
|
|
@@ -56,7 +58,7 @@ export class BashBgOverlay implements Component, Focusable {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
handleInput(data: string): void {
|
|
59
|
-
if (data
|
|
61
|
+
if (matchesKey(data, Key.escape)) {
|
|
60
62
|
if (this.mode === "detail") this.mode = "list";
|
|
61
63
|
else this.params.close();
|
|
62
64
|
this.params.requestRender();
|
|
@@ -64,20 +66,20 @@ export class BashBgOverlay implements Component, Focusable {
|
|
|
64
66
|
}
|
|
65
67
|
// Plain letters stay reserved for future filter/command modes, so navigation
|
|
66
68
|
// and refresh use arrows and a non-letter key only.
|
|
67
|
-
if (data
|
|
69
|
+
if (matchesKey(data, Key.up)) {
|
|
68
70
|
this.move(-1);
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
71
|
-
if (data
|
|
73
|
+
if (matchesKey(data, Key.down)) {
|
|
72
74
|
this.move(1);
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
75
|
-
if (data
|
|
77
|
+
if (matchesKey(data, Key.enter)) {
|
|
76
78
|
if (this.selectedJob()) this.mode = this.mode === "detail" ? "list" : "detail";
|
|
77
79
|
this.params.requestRender();
|
|
78
80
|
return;
|
|
79
81
|
}
|
|
80
|
-
if (data === "\x12" || data
|
|
82
|
+
if (data === "\x12" || matchesKey(data, Key.f5)) {
|
|
81
83
|
// Ctrl+R / F5 — refresh is acknowledged immediately even though the
|
|
82
84
|
// authoritative snapshot only arrives later. Schedule the ack's own
|
|
83
85
|
// expiry so it disappears even without any follow-up repaint.
|
package/src/config.ts
CHANGED
|
@@ -30,6 +30,13 @@ export function mergeConfig(base: CockpitConfig, over: unknown): CockpitConfig {
|
|
|
30
30
|
const sidebarWidth = sidebarRaw && typeof sidebarRaw.width === "number" && Number.isFinite(sidebarRaw.width)
|
|
31
31
|
? Math.min(SIDEBAR_MAX_WIDTH, Math.max(SIDEBAR_MIN_WIDTH, Math.round(sidebarRaw.width)))
|
|
32
32
|
: base.sidebar.width;
|
|
33
|
+
const titleRaw = o.title && typeof o.title === "object" && !Array.isArray(o.title)
|
|
34
|
+
? (o.title as Record<string, unknown>)
|
|
35
|
+
: undefined;
|
|
36
|
+
const isBool = (v: unknown): v is boolean => typeof v === "boolean";
|
|
37
|
+
const titleMaxLength = titleRaw && typeof titleRaw.maxLength === "number" && Number.isFinite(titleRaw.maxLength)
|
|
38
|
+
? Math.min(200, Math.max(20, Math.round(titleRaw.maxLength)))
|
|
39
|
+
: base.title.maxLength;
|
|
33
40
|
return {
|
|
34
41
|
enabled: typeof o.enabled === "boolean" ? o.enabled : base.enabled,
|
|
35
42
|
staticMode: typeof o.staticMode === "boolean" ? o.staticMode : base.staticMode,
|
|
@@ -40,12 +47,26 @@ export function mergeConfig(base: CockpitConfig, over: unknown): CockpitConfig {
|
|
|
40
47
|
todoMode: isMode(o.todoMode) ? o.todoMode : base.todoMode,
|
|
41
48
|
todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
|
|
42
49
|
hideNativeAgents: typeof o.hideNativeAgents === "boolean" ? o.hideNativeAgents : base.hideNativeAgents,
|
|
50
|
+
pinEditorBottom: typeof o.pinEditorBottom === "boolean" ? o.pinEditorBottom : base.pinEditorBottom,
|
|
43
51
|
icons: { mode: iconsRaw && isIconMode(iconsRaw.mode) ? iconsRaw.mode : base.icons.mode },
|
|
44
52
|
sidebar: {
|
|
45
53
|
mode: sidebarRaw && isSidebarMode(sidebarRaw.mode) ? sidebarRaw.mode : base.sidebar.mode,
|
|
46
54
|
width: sidebarWidth,
|
|
47
55
|
density: sidebarRaw && isSidebarDensity(sidebarRaw.density) ? sidebarRaw.density : base.sidebar.density,
|
|
48
56
|
},
|
|
57
|
+
title: {
|
|
58
|
+
enabled: isBool(titleRaw?.enabled) ? titleRaw!.enabled : base.title.enabled,
|
|
59
|
+
showSession: isBool(titleRaw?.showSession) ? titleRaw!.showSession : base.title.showSession,
|
|
60
|
+
showCwd: isBool(titleRaw?.showCwd) ? titleRaw!.showCwd : base.title.showCwd,
|
|
61
|
+
showModel: isBool(titleRaw?.showModel) ? titleRaw!.showModel : base.title.showModel,
|
|
62
|
+
showThinking: isBool(titleRaw?.showThinking) ? titleRaw!.showThinking : base.title.showThinking,
|
|
63
|
+
showGit: isBool(titleRaw?.showGit) ? titleRaw!.showGit : base.title.showGit,
|
|
64
|
+
showMaestro: isBool(titleRaw?.showMaestro) ? titleRaw!.showMaestro : base.title.showMaestro,
|
|
65
|
+
generationModel: titleRaw && typeof titleRaw.generationModel === "string"
|
|
66
|
+
? titleRaw.generationModel
|
|
67
|
+
: base.title.generationModel,
|
|
68
|
+
maxLength: titleMaxLength,
|
|
69
|
+
},
|
|
49
70
|
theme: typeof o.theme === "string" ? o.theme : base.theme,
|
|
50
71
|
};
|
|
51
72
|
}
|
|
@@ -76,6 +97,39 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
76
97
|
}
|
|
77
98
|
}
|
|
78
99
|
|
|
100
|
+
export function mergeConfigDocument(raw: unknown, config: CockpitConfig): Record<string, unknown> {
|
|
101
|
+
const root = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
102
|
+
? { ...(raw as Record<string, unknown>) }
|
|
103
|
+
: {};
|
|
104
|
+
const icons = root.icons && typeof root.icons === "object" && !Array.isArray(root.icons)
|
|
105
|
+
? { ...(root.icons as Record<string, unknown>), mode: config.icons.mode }
|
|
106
|
+
: { mode: config.icons.mode };
|
|
107
|
+
const sidebar = root.sidebar && typeof root.sidebar === "object" && !Array.isArray(root.sidebar)
|
|
108
|
+
? {
|
|
109
|
+
...(root.sidebar as Record<string, unknown>),
|
|
110
|
+
mode: config.sidebar.mode,
|
|
111
|
+
width: config.sidebar.width,
|
|
112
|
+
density: config.sidebar.density,
|
|
113
|
+
}
|
|
114
|
+
: { ...config.sidebar };
|
|
115
|
+
return {
|
|
116
|
+
...root,
|
|
117
|
+
enabled: config.enabled,
|
|
118
|
+
staticMode: config.staticMode,
|
|
119
|
+
quietMode: config.quietMode,
|
|
120
|
+
quietSymbols: config.quietSymbols,
|
|
121
|
+
toolPalette: config.toolPalette,
|
|
122
|
+
agentsMode: config.agentsMode,
|
|
123
|
+
todoMode: config.todoMode,
|
|
124
|
+
todoExpanded: config.todoExpanded,
|
|
125
|
+
hideNativeAgents: config.hideNativeAgents,
|
|
126
|
+
pinEditorBottom: config.pinEditorBottom,
|
|
127
|
+
icons,
|
|
128
|
+
sidebar,
|
|
129
|
+
theme: config.theme,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
79
133
|
export interface SaveResult {
|
|
80
134
|
ok: boolean;
|
|
81
135
|
error?: string;
|
|
@@ -95,7 +149,9 @@ export function saveConfig(config: CockpitConfig): SaveResult {
|
|
|
95
149
|
try {
|
|
96
150
|
const dir = getAgentDir();
|
|
97
151
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
98
|
-
|
|
152
|
+
let raw: unknown = {};
|
|
153
|
+
try { if (existsSync(path)) raw = JSON.parse(readFileSync(path, "utf8")); } catch { /* replace malformed config */ }
|
|
154
|
+
writeFileSync(tmp, JSON.stringify(mergeConfigDocument(raw, config), null, 2) + "\n", "utf8");
|
|
99
155
|
renameSync(tmp, path);
|
|
100
156
|
return { ok: true };
|
|
101
157
|
} catch (err) {
|