pi-cockpit 0.4.0 → 0.5.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/package.json +70 -70
- package/src/agents-store.ts +47 -3
- package/src/config.ts +5 -2
- package/src/footer.ts +1 -0
- package/src/index.ts +155 -5
- package/src/quiet-tools.ts +310 -0
- package/src/render.ts +1 -0
- package/src/settings-view.ts +166 -129
- package/src/stack-widget.ts +11 -2
- package/src/thinking-fold.ts +86 -0
- package/src/thinking-timer.ts +224 -0
- package/src/types.ts +18 -0
package/package.json
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-cockpit",
|
|
3
|
-
"version": "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/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/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.
|
|
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.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
|
+
}
|
package/src/agents-store.ts
CHANGED
|
@@ -78,6 +78,22 @@ 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 completed agent's tombstone suppresses self-healing.
|
|
83
|
+
*
|
|
84
|
+
* `complete` deletes a row, but the teammate extension can still publish
|
|
85
|
+
* progress afterwards: the flush gate may re-arm after disposal, a graph task
|
|
86
|
+
* that returned result-ready carries `status:"running"` until its lifecycle
|
|
87
|
+
* confirms (up to a 60s deadline), and nested/IPC deltas reorder freely. Any
|
|
88
|
+
* such late delta would otherwise self-heal a ghost row that no second
|
|
89
|
+
* `complete` ever removes — the footer keeps showing an agent as running long
|
|
90
|
+
* after its tool result was already returned. The tombstone blocks that
|
|
91
|
+
* rebuild. It must outlive the lifecycle-confirmation deadline with margin.
|
|
92
|
+
* An explicit `started` (a genuine new lifecycle, e.g. a woken agent) clears
|
|
93
|
+
* the tombstone, so legitimate reuse is never suppressed.
|
|
94
|
+
*/
|
|
95
|
+
export const COMPLETED_TOMBSTONE_MS = 120_000;
|
|
96
|
+
|
|
81
97
|
function deriveRole(agent: string | undefined, name: string | undefined): string {
|
|
82
98
|
if (agent && !agent.startsWith("graph(")) return clean(agent);
|
|
83
99
|
return clean(name) || "agent";
|
|
@@ -145,9 +161,21 @@ function latestTool(tools: MessagePayload["recentTools"]): string | undefined {
|
|
|
145
161
|
// always represented as long as it keeps emitting activity.
|
|
146
162
|
export class AgentsStore {
|
|
147
163
|
private readonly roster = new Map<string, AgentRow>();
|
|
164
|
+
/** correlationId -> completion time; suppresses post-complete self-healing. */
|
|
165
|
+
private readonly completedAt = new Map<string, number>();
|
|
166
|
+
|
|
167
|
+
private isTombstoned(id: string | undefined, now: number): boolean {
|
|
168
|
+
if (id === undefined) return false;
|
|
169
|
+
const at = this.completedAt.get(id);
|
|
170
|
+
return at !== undefined && now - at < COMPLETED_TOMBSTONE_MS;
|
|
171
|
+
}
|
|
148
172
|
|
|
149
173
|
applyStarted(p: StartedPayload, now: number = Date.now()): void {
|
|
150
174
|
const id = p.correlationId;
|
|
175
|
+
// An explicit start is authoritative: a woken or re-dispatched agent
|
|
176
|
+
// reuses its correlationId and must reappear even if an earlier run was
|
|
177
|
+
// tombstoned.
|
|
178
|
+
this.completedAt.delete(id);
|
|
151
179
|
const prev = this.roster.get(id);
|
|
152
180
|
const row: AgentRow = {
|
|
153
181
|
...prev,
|
|
@@ -175,11 +203,16 @@ export class AgentsStore {
|
|
|
175
203
|
for (const progress of p.progress) this.applyProgress(p.correlationId, progress, now);
|
|
176
204
|
}
|
|
177
205
|
const targetId = p.taskCorrelationId ?? p.correlationId;
|
|
178
|
-
if (!this.roster.has(targetId)
|
|
206
|
+
if (!this.roster.has(targetId)
|
|
207
|
+
&& !this.isTombstoned(targetId, now)
|
|
208
|
+
&& !this.isTombstoned(p.correlationId, now)) {
|
|
179
209
|
// Self-heal the roster: a running agent must stay visible even when its
|
|
180
210
|
// `started` delta was missed (cold start, event reorder, or a foreground
|
|
181
211
|
// dispatch that detached to background). Materialize the row from this
|
|
182
212
|
// event instead of dropping it; later deltas refine the placeholder.
|
|
213
|
+
// A tombstoned target (or graph parent) means `complete` already ran, so
|
|
214
|
+
// this delta is a late ghost, not a missed start — dropping it keeps the
|
|
215
|
+
// row deleted.
|
|
183
216
|
this.applyStarted({
|
|
184
217
|
correlationId: targetId,
|
|
185
218
|
agent: p.agent ?? "",
|
|
@@ -242,6 +275,9 @@ export class AgentsStore {
|
|
|
242
275
|
delete row.activeTool;
|
|
243
276
|
} else {
|
|
244
277
|
this.roster.delete(id);
|
|
278
|
+
// Tombstone even when the row never existed: a `complete` that beats
|
|
279
|
+
// the first delta must still suppress the self-heal that follows.
|
|
280
|
+
this.completedAt.set(id, now);
|
|
245
281
|
}
|
|
246
282
|
}
|
|
247
283
|
}
|
|
@@ -255,6 +291,9 @@ export class AgentsStore {
|
|
|
255
291
|
changed = true;
|
|
256
292
|
}
|
|
257
293
|
}
|
|
294
|
+
for (const [id, at] of this.completedAt) {
|
|
295
|
+
if (now - at >= COMPLETED_TOMBSTONE_MS) this.completedAt.delete(id);
|
|
296
|
+
}
|
|
258
297
|
return changed;
|
|
259
298
|
}
|
|
260
299
|
|
|
@@ -267,9 +306,13 @@ export class AgentsStore {
|
|
|
267
306
|
}
|
|
268
307
|
|
|
269
308
|
private applyProgress(parentCorrelationId: string, p: ProgressPayload, now: number): void {
|
|
270
|
-
if (!this.roster.has(p.correlationId)
|
|
309
|
+
if (!this.roster.has(p.correlationId)
|
|
310
|
+
&& !this.isTombstoned(p.correlationId, now)
|
|
311
|
+
&& !this.isTombstoned(parentCorrelationId, now)) {
|
|
271
312
|
// Self-heal: see applyMessage. A graph child whose `started` delta was
|
|
272
|
-
// missed still materializes from its first progress event
|
|
313
|
+
// missed still materializes from its first progress event — unless the
|
|
314
|
+
// child or its graph parent already completed, in which case this late
|
|
315
|
+
// snapshot entry is a ghost and the row must stay deleted.
|
|
273
316
|
this.applyStarted({
|
|
274
317
|
correlationId: p.correlationId,
|
|
275
318
|
agent: p.agent,
|
|
@@ -339,5 +382,6 @@ export class AgentsStore {
|
|
|
339
382
|
|
|
340
383
|
clear(): void {
|
|
341
384
|
this.roster.clear();
|
|
385
|
+
this.completedAt.clear();
|
|
342
386
|
}
|
|
343
387
|
}
|
package/src/config.ts
CHANGED
|
@@ -8,16 +8,19 @@ export function getConfigPath(): string {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
// Flat, field-by-field merge: type-safe and forward-compatible (unknown keys ignored).
|
|
11
|
-
function
|
|
11
|
+
export function mergeConfig(base: CockpitConfig, over: unknown): CockpitConfig {
|
|
12
12
|
if (!over || typeof over !== "object" || Array.isArray(over)) return base;
|
|
13
13
|
const o = over as Record<string, unknown>;
|
|
14
14
|
const isMode = (v: unknown): v is "list" | "compact" => v === "list" || v === "compact";
|
|
15
|
+
const isQuietSymbolMode = (v: unknown): v is "check" | "dot" => v === "check" || v === "dot";
|
|
15
16
|
const isIconMode = (v: unknown): v is "auto" | "nerd" | "ascii" => v === "auto" || v === "nerd" || v === "ascii";
|
|
16
17
|
const iconsRaw = o.icons && typeof o.icons === "object" && !Array.isArray(o.icons)
|
|
17
18
|
? (o.icons as Record<string, unknown>)
|
|
18
19
|
: undefined;
|
|
19
20
|
return {
|
|
20
21
|
enabled: typeof o.enabled === "boolean" ? o.enabled : base.enabled,
|
|
22
|
+
quietMode: typeof o.quietMode === "boolean" ? o.quietMode : base.quietMode,
|
|
23
|
+
quietSymbols: isQuietSymbolMode(o.quietSymbols) ? o.quietSymbols : base.quietSymbols,
|
|
21
24
|
agentsMode: isMode(o.agentsMode) ? o.agentsMode : base.agentsMode,
|
|
22
25
|
todoMode: isMode(o.todoMode) ? o.todoMode : base.todoMode,
|
|
23
26
|
todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
|
|
@@ -46,7 +49,7 @@ export function loadConfig(notify?: (msg: string, level: "warning" | "info") =>
|
|
|
46
49
|
return structuredClone(DEFAULT_CONFIG);
|
|
47
50
|
}
|
|
48
51
|
try {
|
|
49
|
-
return
|
|
52
|
+
return mergeConfig(DEFAULT_CONFIG, JSON.parse(readFileSync(path, "utf8")));
|
|
50
53
|
} catch (err) {
|
|
51
54
|
notify?.(`pi-cockpit config parse error: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
52
55
|
return structuredClone(DEFAULT_CONFIG);
|
package/src/footer.ts
CHANGED
|
@@ -202,6 +202,7 @@ export function renderFooter(p: FooterParts): string[] {
|
|
|
202
202
|
if (width <= 0) return [""];
|
|
203
203
|
const ell = theme.fg("dim", g.ellipsis);
|
|
204
204
|
const sep = theme.fg("dim", g.separator.trim());
|
|
205
|
+
|
|
205
206
|
const identitySeparator = ` ${sep} `;
|
|
206
207
|
const visibleStatuses = (p.extensionStatuses ?? []).filter(
|
|
207
208
|
(status) => isVisibleExtensionStatus(status, p.thinking),
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,9 @@ import { statusText, titleFor, workingMessage, type AmbientState } from "./ambie
|
|
|
7
7
|
import { BashBgStore } from "./bash-bg-store.ts";
|
|
8
8
|
import { BashBgOverlay } from "./bash-bg-overlay.ts";
|
|
9
9
|
import { renderBashBgSummary } from "./bash-bg-widget.ts";
|
|
10
|
+
import { registerQuietTools } from "./quiet-tools.ts";
|
|
11
|
+
import { ensureThinkingFolded, readHideThinkingBlock } from "./thinking-fold.ts";
|
|
12
|
+
import { ThinkingFoldTimer } from "./thinking-timer.ts";
|
|
10
13
|
import { TodoStore } from "./todo-store.ts";
|
|
11
14
|
import { makeTodoWidget, makeAgentWidget, terminalRows } from "./stack-widget.ts";
|
|
12
15
|
import { activeThemeName, ThemePicker } from "./theme-picker.ts";
|
|
@@ -35,6 +38,9 @@ const FOOTER_UTILS: WidthUtils = { measure: visibleWidth, clip: truncateToWidth
|
|
|
35
38
|
const BASH_BG_OVERLAY_KEY = "alt+j";
|
|
36
39
|
const COCKPIT_STATUS_KEY = "cockpit";
|
|
37
40
|
const WIDTH_POLL_INTERVAL_MS = 250;
|
|
41
|
+
// Quiet mode's rename of pi's hidden-thinking label. The live thinking timer
|
|
42
|
+
// derives its final "thoughts · 8.4s" labels from the same word.
|
|
43
|
+
const QUIET_THINKING_LABEL = "thoughts";
|
|
38
44
|
|
|
39
45
|
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
40
46
|
try {
|
|
@@ -67,6 +73,34 @@ export default function (pi: ExtensionAPI): void {
|
|
|
67
73
|
// Persisted rather than toasted: a config that failed to load silently downgrades
|
|
68
74
|
// the whole session to defaults, so it belongs in a slot that does not scroll away.
|
|
69
75
|
let configProblem: string | undefined;
|
|
76
|
+
let quietToolsRegistered = false;
|
|
77
|
+
|
|
78
|
+
// Live labels for folded thinking rows: a spinner with running elapsed
|
|
79
|
+
// time while the model thinks, then the actual duration once it settles.
|
|
80
|
+
// Pi owns the fold; this only renames the label it already renders.
|
|
81
|
+
const thinkingTimer = new ThinkingFoldTimer({
|
|
82
|
+
getTui: () => capturedTui,
|
|
83
|
+
requestRender: () => {
|
|
84
|
+
try {
|
|
85
|
+
capturedTui?.requestRender();
|
|
86
|
+
} catch {
|
|
87
|
+
// tui may be gone between sessions
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
getBaseLabel: () => (config.quietMode ? QUIET_THINKING_LABEL : undefined),
|
|
91
|
+
getGlyphs: () => resolveGlyphs(config.icons.mode),
|
|
92
|
+
isThinkingHidden: () => (lastCtx ? readHideThinkingBlock(lastCtx.cwd) : false),
|
|
93
|
+
isEnabled: () => config.enabled,
|
|
94
|
+
setGlobalLabel: (label) => {
|
|
95
|
+
const ctx = lastCtx;
|
|
96
|
+
if (!ctx || !isTuiContext(ctx)) return;
|
|
97
|
+
try {
|
|
98
|
+
ctx.ui.setHiddenThinkingLabel(label);
|
|
99
|
+
} catch {
|
|
100
|
+
// non-TUI or mid-teardown
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
});
|
|
70
104
|
|
|
71
105
|
// The streaming line, the tab title and the footer status slot are all fed from
|
|
72
106
|
// the same snapshots the widgets read, so they can never disagree with them.
|
|
@@ -135,6 +169,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
135
169
|
agents: config.enabled && config.hideNativeAgents,
|
|
136
170
|
footer: config.enabled,
|
|
137
171
|
todoExpanded: config.todoExpanded,
|
|
172
|
+
quiet: config.enabled && config.quietMode,
|
|
173
|
+
quietSymbols: config.quietSymbols,
|
|
138
174
|
});
|
|
139
175
|
};
|
|
140
176
|
const setTodoExpanded = (expanded: boolean): void => {
|
|
@@ -145,6 +181,45 @@ export default function (pi: ExtensionAPI): void {
|
|
|
145
181
|
req();
|
|
146
182
|
};
|
|
147
183
|
|
|
184
|
+
// Quiet mode live toggle: widgets and footer read config on every render so
|
|
185
|
+
// they switch immediately. Tool rendering is a one-way latch (registerTool
|
|
186
|
+
// cannot be undone), so turning ON registers immediately but turning OFF
|
|
187
|
+
// requires /reload to restore the default tool shells.
|
|
188
|
+
const applyQuietMode = (ctx: ExtensionContext, was: boolean, now: boolean): void => {
|
|
189
|
+
// Broadcast so cross-extension surfaces (e.g. pi-maestro-flow's todo tool
|
|
190
|
+
// rendering) can follow quiet mode regardless of which path toggled it.
|
|
191
|
+
publishUiOwnership();
|
|
192
|
+
if (now && !was) {
|
|
193
|
+
if (!quietToolsRegistered) {
|
|
194
|
+
registerQuietTools(pi, () => config);
|
|
195
|
+
quietToolsRegistered = true;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
ctx.ui.setHiddenThinkingLabel(QUIET_THINKING_LABEL);
|
|
199
|
+
} catch { /* non-TUI */ }
|
|
200
|
+
// The label only renames an already-hidden block; the fold itself
|
|
201
|
+
// rides pi's native toggle, which also persists it. Report what
|
|
202
|
+
// actually happened instead of promising a fold we could not reach.
|
|
203
|
+
const folded = ensureThinkingFolded(capturedTui, ctx.cwd, true);
|
|
204
|
+
ctx.ui.notify(
|
|
205
|
+
folded
|
|
206
|
+
? "quiet mode on — tools compressed, thinking folded"
|
|
207
|
+
: "quiet mode on — tools compressed",
|
|
208
|
+
"info",
|
|
209
|
+
);
|
|
210
|
+
} else if (!now && was) {
|
|
211
|
+
try {
|
|
212
|
+
ctx.ui.setHiddenThinkingLabel(undefined);
|
|
213
|
+
} catch { /* non-TUI */ }
|
|
214
|
+
ctx.ui.notify(
|
|
215
|
+
quietToolsRegistered
|
|
216
|
+
? "quiet mode off — /reload to restore default tool rendering"
|
|
217
|
+
: "quiet mode off",
|
|
218
|
+
"info",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
148
223
|
const uninstallUi = (ctx: ExtensionContext): void => {
|
|
149
224
|
ctx.ui.setWidget(STACK_WIDGET_KEY, undefined);
|
|
150
225
|
ctx.ui.setWidget(AGENT_WIDGET_KEY, undefined);
|
|
@@ -302,6 +377,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
302
377
|
pi.on("session_start", (_e, ctx) => {
|
|
303
378
|
lastCtx = ctx;
|
|
304
379
|
activeTools.clear();
|
|
380
|
+
thinkingTimer.reset();
|
|
305
381
|
ensureConfigExists();
|
|
306
382
|
config = loadConfig((m, l) => {
|
|
307
383
|
try {
|
|
@@ -311,6 +387,19 @@ export default function (pi: ExtensionAPI): void {
|
|
|
311
387
|
}
|
|
312
388
|
});
|
|
313
389
|
invalidateUsageCache();
|
|
390
|
+
// Quiet mode: register compact tool renderers and fold thinking blocks.
|
|
391
|
+
// Tools cannot be unregistered, so this is a one-way latch per process.
|
|
392
|
+
if (config.quietMode && !quietToolsRegistered) {
|
|
393
|
+
registerQuietTools(pi, () => config);
|
|
394
|
+
quietToolsRegistered = true;
|
|
395
|
+
}
|
|
396
|
+
if (config.quietMode) {
|
|
397
|
+
try {
|
|
398
|
+
ctx.ui.setHiddenThinkingLabel(QUIET_THINKING_LABEL);
|
|
399
|
+
} catch {
|
|
400
|
+
// non-TUI mode
|
|
401
|
+
}
|
|
402
|
+
}
|
|
314
403
|
// The theme is deliberately NOT re-applied here. ctx.ui.setTheme writes
|
|
315
404
|
// through to pi's own settings, so pi already restores the user's choice on
|
|
316
405
|
// its own. Replaying cockpit's copy would overwrite whatever the user set
|
|
@@ -324,6 +413,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
324
413
|
applyUi(ctx);
|
|
325
414
|
publishUiOwnership();
|
|
326
415
|
}
|
|
416
|
+
// applyUi captured the TUI synchronously, so the native toggle is
|
|
417
|
+
// reachable already; a no-op when cockpit is disabled or non-TUI.
|
|
418
|
+
if (config.quietMode) ensureThinkingFolded(capturedTui, ctx.cwd, true);
|
|
327
419
|
pi.events.emit(BASH_BG_QUERY_EVENT, undefined);
|
|
328
420
|
req();
|
|
329
421
|
});
|
|
@@ -335,10 +427,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
335
427
|
agents: false,
|
|
336
428
|
footer: false,
|
|
337
429
|
todoExpanded: config.todoExpanded,
|
|
430
|
+
quiet: false,
|
|
431
|
+
quietSymbols: config.quietSymbols,
|
|
338
432
|
});
|
|
339
433
|
lastCtx = undefined;
|
|
340
434
|
running = false;
|
|
341
435
|
activeTools.clear();
|
|
436
|
+
thinkingTimer.reset();
|
|
342
437
|
invalidateUsageCache();
|
|
343
438
|
agents.clear();
|
|
344
439
|
bashBg.clear();
|
|
@@ -352,12 +447,21 @@ export default function (pi: ExtensionAPI): void {
|
|
|
352
447
|
pi.on("agent_end", () => {
|
|
353
448
|
running = false;
|
|
354
449
|
activeTools.clear();
|
|
450
|
+
thinkingTimer.stop();
|
|
355
451
|
syncTick();
|
|
356
452
|
req();
|
|
357
453
|
});
|
|
358
454
|
|
|
455
|
+
// --- live labels for folded thinking rows ---
|
|
456
|
+
pi.on("message_update", (e) => {
|
|
457
|
+
thinkingTimer.onAssistantMessageEvent(e.assistantMessageEvent);
|
|
458
|
+
});
|
|
459
|
+
|
|
359
460
|
// --- redraw triggers for the footer's live data ---
|
|
360
|
-
pi.on("message_end", (
|
|
461
|
+
pi.on("message_end", (e, ctx) => {
|
|
462
|
+
// Settle an interrupted thinking run: message_end fires for aborted
|
|
463
|
+
// and failed messages too, while the row is still mounted.
|
|
464
|
+
if (e.message.role === "assistant") thinkingTimer.onAssistantMessageEnd();
|
|
361
465
|
invalidateUsageCache();
|
|
362
466
|
if (isTuiContext(ctx)) req();
|
|
363
467
|
});
|
|
@@ -474,7 +578,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
474
578
|
|
|
475
579
|
// --- /cockpit: toggle list/compact + enabled + hide-native ---
|
|
476
580
|
pi.registerCommand("cockpit", {
|
|
477
|
-
description: "Open pi-cockpit settings;
|
|
581
|
+
description: "Open pi-cockpit settings; /cockpit quiet toggles quiet mode, /cockpit bg shows jobs",
|
|
582
|
+
getArgumentCompletions: (prefix) => {
|
|
583
|
+
const query = prefix.trim().toLowerCase();
|
|
584
|
+
const candidates = ["quiet", "quiet on", "quiet off", "bg", "jobs", "todo", "todo expand", "todo collapse"];
|
|
585
|
+
const matches = candidates.filter((c) => c.startsWith(query));
|
|
586
|
+
return matches.length > 0 ? matches.map((c) => ({ value: c, label: c })) : null;
|
|
587
|
+
},
|
|
478
588
|
handler: async (args, ctx) => {
|
|
479
589
|
if (!ctx.hasUI) return;
|
|
480
590
|
const action = args.trim().toLowerCase();
|
|
@@ -491,6 +601,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
491
601
|
setTodoExpanded(action.endsWith("expand"));
|
|
492
602
|
return;
|
|
493
603
|
}
|
|
604
|
+
if (action === "quiet" || action === "quiet toggle") {
|
|
605
|
+
const was = config.quietMode;
|
|
606
|
+
config = { ...config, quietMode: !was };
|
|
607
|
+
saveConfig(config);
|
|
608
|
+
applyQuietMode(ctx, was, config.quietMode);
|
|
609
|
+
req();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (action === "quiet on" || action === "quiet off") {
|
|
613
|
+
const target = action === "quiet on";
|
|
614
|
+
if (config.quietMode === target) return;
|
|
615
|
+
const was = config.quietMode;
|
|
616
|
+
config = { ...config, quietMode: target };
|
|
617
|
+
saveConfig(config);
|
|
618
|
+
applyQuietMode(ctx, was, target);
|
|
619
|
+
req();
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
494
622
|
await openSettings(ctx);
|
|
495
623
|
},
|
|
496
624
|
});
|
|
@@ -499,6 +627,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
499
627
|
ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
|
500
628
|
let settingsCursor = 0;
|
|
501
629
|
let saveState: SaveState = { kind: "idle" };
|
|
630
|
+
// Mirror of pi's hideThinkingBlock, read when the panel opens. The
|
|
631
|
+
// overlay captures input, so Ctrl+T cannot flip it while this is open
|
|
632
|
+
// and the mirror stays exact until the panel closes.
|
|
633
|
+
let thinkingHidden = readHideThinkingBlock(ctx.cwd);
|
|
502
634
|
// The theme row expands in place rather than closing the panel and opening
|
|
503
635
|
// a second overlay: pi's `theme` is a live Proxy, so both surfaces repaint
|
|
504
636
|
// in the previewed colours, and Esc lands back on the row it was invoked
|
|
@@ -511,6 +643,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
511
643
|
};
|
|
512
644
|
const apply = (key: string): void => {
|
|
513
645
|
const wasEnabled = config.enabled;
|
|
646
|
+
const wasQuiet = config.quietMode;
|
|
514
647
|
if (key === "theme") {
|
|
515
648
|
// Delegate: the picker previews live and reverts on Esc, neither of
|
|
516
649
|
// which a blind one-key cycle through the name list can do.
|
|
@@ -518,6 +651,18 @@ export default function (pi: ExtensionAPI): void {
|
|
|
518
651
|
tui.requestRender();
|
|
519
652
|
return;
|
|
520
653
|
}
|
|
654
|
+
if (key === "thinkingFold") {
|
|
655
|
+
// Pass-through row: pi owns hideThinkingBlock, so bring it to the
|
|
656
|
+
// wanted state through the native toggle instead of saving config.
|
|
657
|
+
// pi persists the flip synchronously through its settingsManager.
|
|
658
|
+
const target = !thinkingHidden;
|
|
659
|
+
const ok = ensureThinkingFolded(tui, ctx.cwd, target);
|
|
660
|
+
if (ok) thinkingHidden = target;
|
|
661
|
+
saveState = ok
|
|
662
|
+
? { kind: "saved" }
|
|
663
|
+
: { kind: "failed", message: "editor unreachable" };
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
521
666
|
config = applyRow(config, key);
|
|
522
667
|
saveState = { kind: "saving" };
|
|
523
668
|
const result = saveConfig(config);
|
|
@@ -537,6 +682,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
537
682
|
} else {
|
|
538
683
|
publishUiOwnership();
|
|
539
684
|
}
|
|
685
|
+
if (wasQuiet !== config.quietMode) {
|
|
686
|
+
applyQuietMode(ctx, wasQuiet, config.quietMode);
|
|
687
|
+
}
|
|
540
688
|
req();
|
|
541
689
|
};
|
|
542
690
|
const ui = {
|
|
@@ -546,7 +694,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
546
694
|
// rows saying nothing the picker does not already say.
|
|
547
695
|
if (sub) return sub.render(width);
|
|
548
696
|
const paint: PaintTheme = theme;
|
|
549
|
-
const rows = buildRows(config);
|
|
697
|
+
const rows = buildRows(config, { thinkingHidden });
|
|
550
698
|
settingsCursor = Math.max(0, Math.min(settingsCursor, rows.length - 1));
|
|
551
699
|
const w = Math.min(width, 52);
|
|
552
700
|
const labelWidth = Math.max(...rows.map((r) => visibleWidth(r.label)));
|
|
@@ -576,7 +724,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
576
724
|
}
|
|
577
725
|
// The theme row opens /theme; /settings additionally pairs a light
|
|
578
726
|
// and a dark theme, which neither of cockpit's surfaces can express.
|
|
579
|
-
|
|
727
|
+
// Thinking fold is pi's setting too (Ctrl+T), mirrored live here.
|
|
728
|
+
lines.push(paint.fg("dim", "theme & thinking are stored by pi"));
|
|
729
|
+
lines.push(paint.fg("dim", "/settings pairs light+dark"));
|
|
580
730
|
lines.push(paint.fg("dim", "↑↓ move · Enter change · letter jumps · Esc close"));
|
|
581
731
|
return lines.map((line) => truncateToWidth(line, width, "…"));
|
|
582
732
|
},
|
|
@@ -589,7 +739,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
589
739
|
tui.requestRender();
|
|
590
740
|
return;
|
|
591
741
|
}
|
|
592
|
-
const rows = buildRows(config);
|
|
742
|
+
const rows = buildRows(config, { thinkingHidden });
|
|
593
743
|
if (matchesKey(data, Key.escape)) {
|
|
594
744
|
done(undefined);
|
|
595
745
|
return;
|