pi-cockpit 0.20.0 → 0.22.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 CHANGED
@@ -1,85 +1,85 @@
1
- {
2
- "name": "pi-cockpit",
3
- "version": "0.20.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/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.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/model-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/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
22
- "typecheck": "tsc --noEmit",
23
- "prepack": "node ../../scripts/bundle-settings-core.mjs",
24
- "postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
25
- },
26
- "pi": {
27
- "extensions": [
28
- "./src/extension/index.ts"
29
- ],
30
- "themes": [
31
- "./themes"
32
- ]
33
- },
34
- "dependencies": {
35
- "pi-maestro-settings-core": "0.2.1",
36
- "proper-lockfile": "4.1.2"
37
- },
38
- "bundledDependencies": [
39
- "pi-maestro-settings-core"
40
- ],
41
- "peerDependencies": {
42
- "@earendil-works/pi-agent-core": "*",
43
- "@earendil-works/pi-ai": "*",
44
- "@earendil-works/pi-coding-agent": "*",
45
- "@earendil-works/pi-tui": "*",
46
- "pi-maestro-teammate": "^2.1.0",
47
- "typebox": "*"
48
- },
49
- "peerDependenciesMeta": {
50
- "@earendil-works/pi-agent-core": {
51
- "optional": true
52
- },
53
- "@earendil-works/pi-ai": {
54
- "optional": true
55
- },
56
- "@earendil-works/pi-coding-agent": {
57
- "optional": true
58
- },
59
- "@earendil-works/pi-tui": {
60
- "optional": true
61
- },
62
- "pi-maestro-teammate": {
63
- "optional": true
64
- },
65
- "typebox": {
66
- "optional": true
67
- }
68
- },
69
- "devDependencies": {
70
- "@earendil-works/pi-agent-core": "0.84.4",
71
- "@earendil-works/pi-ai": "0.84.4",
72
- "@earendil-works/pi-coding-agent": "0.84.4",
73
- "@earendil-works/pi-tui": "0.84.4",
74
- "@types/node": "^22.0.0",
75
- "pi-maestro-teammate": "2.3.0",
76
- "typebox": "1.3.7",
77
- "typescript": "^5.5.0"
78
- },
79
- "main": "./src/index.ts",
80
- "exports": {
81
- ".": "./src/index.ts",
82
- "./v1/events": "./src/public/v1/events.ts",
83
- "./src/*": "./src/*"
84
- }
85
- }
1
+ {
2
+ "name": "pi-cockpit",
3
+ "version": "0.22.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/edit-guard.test.ts tests/agents-store.test.ts tests/endpoint-store.test.ts tests/session-ui-state.test.ts tests/session-tabs.test.ts tests/agent-bar.test.ts tests/supervision-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-overlay.test.ts tests/agent-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/agent-scroll.test.ts tests/session-bar.test.ts tests/session-detail.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/teammate-message.test.ts tests/input-routing.test.ts tests/input-history-store.test.ts tests/unified-editor.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/zen-render.test.ts tests/zen-browse.test.ts tests/zen-sheet.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/model-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/tui-i18n.test.ts tests/settings-shell.test.ts tests/settings-cockpit-provider.test.ts tests/settings-cross-package.test.ts tests/ui-primitives.test.ts tests/settings-shell-secret.test.ts tests/settings-shell-listcrud.test.ts tests/capturing-overlay.test.ts tests/claude-editor.test.ts tests/fullscreen-controller.test.ts tests/transcript-selection.test.ts tests/terminal-cleanup.test.ts tests/terminal-capability.test.ts tests/usage-core.test.ts tests/usage-extension.test.ts tests/usage-history.test.ts tests/usage-sparkline.test.ts",
22
+ "typecheck": "tsc --noEmit",
23
+ "prepack": "node ../../scripts/bundle-settings-core.mjs",
24
+ "postpack": "node ../../scripts/bundle-settings-core.mjs --clean"
25
+ },
26
+ "pi": {
27
+ "extensions": [
28
+ "./src/extension/index.ts"
29
+ ],
30
+ "themes": [
31
+ "./themes"
32
+ ]
33
+ },
34
+ "dependencies": {
35
+ "pi-maestro-settings-core": "0.2.1",
36
+ "proper-lockfile": "4.1.2"
37
+ },
38
+ "bundledDependencies": [
39
+ "pi-maestro-settings-core"
40
+ ],
41
+ "peerDependencies": {
42
+ "@earendil-works/pi-agent-core": "*",
43
+ "@earendil-works/pi-ai": "*",
44
+ "@earendil-works/pi-coding-agent": "*",
45
+ "@earendil-works/pi-tui": "*",
46
+ "pi-maestro-teammate": "^2.5.0",
47
+ "typebox": "*"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@earendil-works/pi-agent-core": {
51
+ "optional": true
52
+ },
53
+ "@earendil-works/pi-ai": {
54
+ "optional": true
55
+ },
56
+ "@earendil-works/pi-coding-agent": {
57
+ "optional": true
58
+ },
59
+ "@earendil-works/pi-tui": {
60
+ "optional": true
61
+ },
62
+ "pi-maestro-teammate": {
63
+ "optional": true
64
+ },
65
+ "typebox": {
66
+ "optional": true
67
+ }
68
+ },
69
+ "devDependencies": {
70
+ "@earendil-works/pi-agent-core": "0.84.4",
71
+ "@earendil-works/pi-ai": "0.84.4",
72
+ "@earendil-works/pi-coding-agent": "0.84.4",
73
+ "@earendil-works/pi-tui": "0.84.4",
74
+ "@types/node": "^22.0.0",
75
+ "pi-maestro-teammate": "2.5.0",
76
+ "typebox": "1.3.7",
77
+ "typescript": "^5.5.0"
78
+ },
79
+ "main": "./src/index.ts",
80
+ "exports": {
81
+ ".": "./src/index.ts",
82
+ "./v1/events": "./src/public/v1/events.ts",
83
+ "./src/*": "./src/*"
84
+ }
85
+ }
@@ -11,19 +11,227 @@ import type {
11
11
  } from "pi-maestro-teammate/v1/types";
12
12
  import type {
13
13
  RuntimeAgentReadEntityV2,
14
+ RuntimeReadModelChangeV2,
14
15
  RuntimeReadModelDeltaV2,
15
16
  RuntimeReadModelOwnershipV2,
16
17
  RuntimeReadModelSnapshotV2,
17
- } from "pi-maestro-teammate/v2/runtime";
18
- import {
19
- RuntimeReadModelProjectionV2,
20
- parseRuntimeReadModelDeltaV2,
21
- parseRuntimeReadModelOwnershipV2,
22
- parseRuntimeReadModelSnapshotV2,
18
+ RuntimeReadModelSourceV2,
23
19
  } from "pi-maestro-teammate/v2/runtime";
24
20
  import { sanitizeExtensionStatusText } from "./extension-status.ts";
25
21
  import { EXPERT_LEADER_NAME, type AgentRow, type AgentStatus } from "./types.ts";
26
22
 
23
+ export const RUNTIME_READ_MODEL_DELTA_EVENT = "teammate:runtime-read-model-delta-v2";
24
+ export const RUNTIME_READ_MODEL_QUERY_EVENT = "teammate:runtime-read-model-query-v2";
25
+ export const RUNTIME_READ_MODEL_SNAPSHOT_EVENT = "teammate:runtime-read-model-snapshot-v2";
26
+ export const RUNTIME_READ_MODEL_UNAVAILABLE_EVENT = "teammate:runtime-read-model-unavailable-v2";
27
+
28
+ /** Keep Cockpit loadable when its optional Teammate peer is absent. */
29
+ export function runtimeV2ReadEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
30
+ return env.PI_RUNTIME_BROKER?.trim().toLowerCase() === "sqlite"
31
+ && (env.PI_RUNTIME_V2_READ === undefined || env.PI_RUNTIME_V2_READ === "1");
32
+ }
33
+
34
+ function plainObject(value: unknown): value is Record<string, unknown> {
35
+ return typeof value === "object" && value !== null && !Array.isArray(value);
36
+ }
37
+
38
+ function safeInteger(value: unknown, minimum = 0): value is number {
39
+ return Number.isSafeInteger(value) && (value as number) >= minimum;
40
+ }
41
+
42
+ function identifier(value: unknown): value is string {
43
+ return typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.includes("\0");
44
+ }
45
+
46
+ function parseRuntimeReadModelOwnershipV2(value: unknown): RuntimeReadModelOwnershipV2 | undefined {
47
+ if (!plainObject(value)
48
+ || !identifier(value.workspaceId)
49
+ || !identifier(value.sessionId)
50
+ || !identifier(value.sourceId)
51
+ || !safeInteger(value.generation, 1)) return undefined;
52
+ return {
53
+ workspaceId: value.workspaceId,
54
+ sessionId: value.sessionId,
55
+ sourceId: value.sourceId,
56
+ generation: value.generation,
57
+ };
58
+ }
59
+
60
+ function parseSource(value: unknown): RuntimeReadModelSourceV2 | undefined {
61
+ if (!plainObject(value)
62
+ || !identifier(value.streamId)
63
+ || !safeInteger(value.revision, 1)
64
+ || !safeInteger(value.generation, 1)) return undefined;
65
+ const projection = value.projection === undefined
66
+ ? undefined
67
+ : parseRuntimeReadModelOwnershipV2(value.projection);
68
+ if (value.projection !== undefined && !projection) return undefined;
69
+ return {
70
+ streamId: value.streamId,
71
+ revision: value.revision,
72
+ generation: value.generation,
73
+ ...(projection ? { projection } : {}),
74
+ };
75
+ }
76
+
77
+ const AGENT_STATUSES = new Set<RuntimeAgentReadEntityV2["status"]>([
78
+ "pending", "running", "retrying", "sleeping", "completed", "failed", "terminated",
79
+ ]);
80
+
81
+ function parseRuntimeEntity(value: unknown): RuntimeAgentReadEntityV2 | undefined {
82
+ if (!plainObject(value)
83
+ || !identifier(value.correlationId)
84
+ || !safeInteger(value.generation, 1)
85
+ || !identifier(value.agent)
86
+ || !AGENT_STATUSES.has(value.status as RuntimeAgentReadEntityV2["status"])
87
+ || !safeInteger(value.startedAt)
88
+ || !safeInteger(value.lastActivityAt)) return undefined;
89
+ for (const key of ["name", "task", "spawnedBy", "parentCorrelationId", "phase", "requestedModel", "resolvedModel", "lastMessage", "error"] as const) {
90
+ if (value[key] !== undefined && typeof value[key] !== "string") return undefined;
91
+ }
92
+ if (value.dependencies !== undefined
93
+ && (!Array.isArray(value.dependencies) || value.dependencies.some((item) => !safeInteger(item)))) return undefined;
94
+ if (value.attemptedModels !== undefined
95
+ && (!Array.isArray(value.attemptedModels) || value.attemptedModels.some((item) => typeof item !== "string"))) return undefined;
96
+ if (value.projection !== undefined && !parseRuntimeReadModelOwnershipV2(value.projection)) return undefined;
97
+ return structuredClone(value) as unknown as RuntimeAgentReadEntityV2;
98
+ }
99
+
100
+ function parseRuntimeReadModelDeltaV2(value: unknown): RuntimeReadModelDeltaV2 | undefined {
101
+ if (!plainObject(value)
102
+ || value.version !== 2
103
+ || value.revision !== 1
104
+ || value.kind !== "agent-runs-delta"
105
+ || !safeInteger(value.baseCursor)
106
+ || !safeInteger(value.nextCursor, 1)
107
+ || value.nextCursor <= value.baseCursor
108
+ || !Array.isArray(value.changes)) return undefined;
109
+ const source = parseSource(value.source);
110
+ if (!source) return undefined;
111
+ const changes: RuntimeReadModelChangeV2[] = [];
112
+ for (const change of value.changes) {
113
+ if (!plainObject(change)) return undefined;
114
+ if (change.kind === "upsert") {
115
+ const entity = parseRuntimeEntity(change.entity);
116
+ if (!entity) return undefined;
117
+ changes.push({ kind: "upsert", entity });
118
+ continue;
119
+ }
120
+ if (change.kind === "tombstone"
121
+ && identifier(change.correlationId)
122
+ && safeInteger(change.generation, 1)) {
123
+ changes.push({ kind: "tombstone", correlationId: change.correlationId, generation: change.generation });
124
+ continue;
125
+ }
126
+ return undefined;
127
+ }
128
+ return {
129
+ version: 2,
130
+ revision: 1,
131
+ kind: "agent-runs-delta",
132
+ baseCursor: value.baseCursor,
133
+ nextCursor: value.nextCursor,
134
+ source,
135
+ changes,
136
+ };
137
+ }
138
+
139
+ function parseRuntimeReadModelSnapshotV2(value: unknown): RuntimeReadModelSnapshotV2 | undefined {
140
+ if (!plainObject(value)
141
+ || value.version !== 2
142
+ || value.revision !== 1
143
+ || value.kind !== "agent-runs-snapshot"
144
+ || !safeInteger(value.cursor)
145
+ || !Array.isArray(value.agents)) return undefined;
146
+ const source = parseSource(value.source);
147
+ if (!source) return undefined;
148
+ const agents = value.agents.map(parseRuntimeEntity);
149
+ if (agents.some((agent) => agent === undefined)) return undefined;
150
+ return {
151
+ version: 2,
152
+ revision: 1,
153
+ kind: "agent-runs-snapshot",
154
+ cursor: value.cursor,
155
+ source,
156
+ agents: agents as RuntimeAgentReadEntityV2[],
157
+ };
158
+ }
159
+
160
+ function cloneRuntimeEntity(entity: RuntimeAgentReadEntityV2): RuntimeAgentReadEntityV2 {
161
+ return structuredClone(entity);
162
+ }
163
+
164
+ class RuntimeReadModelProjectionV2 {
165
+ readonly #agents = new Map<string, RuntimeAgentReadEntityV2>();
166
+ #cursor = 0;
167
+ #source: RuntimeReadModelSourceV2 = { streamId: "uninitialized", revision: 1, generation: 1 };
168
+ #initialized = false;
169
+
170
+ get cursor(): number { return this.#cursor; }
171
+
172
+ applySnapshot(input: unknown): boolean {
173
+ const snapshot = parseRuntimeReadModelSnapshotV2(input);
174
+ if (!snapshot) return false;
175
+ if (this.#initialized && snapshot.source.streamId === this.#source.streamId) {
176
+ if (snapshot.source.generation < this.#source.generation) return false;
177
+ if (snapshot.source.generation === this.#source.generation && snapshot.cursor < this.#cursor) return false;
178
+ }
179
+ const next = new Map<string, RuntimeAgentReadEntityV2>();
180
+ for (const agent of snapshot.agents) {
181
+ const existing = next.get(agent.correlationId);
182
+ if (existing && existing.generation > agent.generation) continue;
183
+ next.set(agent.correlationId, cloneRuntimeEntity(agent));
184
+ }
185
+ this.#agents.clear();
186
+ for (const [id, agent] of next) this.#agents.set(id, agent);
187
+ this.#cursor = snapshot.cursor;
188
+ this.#source = { ...snapshot.source };
189
+ this.#initialized = true;
190
+ return true;
191
+ }
192
+
193
+ applyDelta(input: unknown): boolean {
194
+ const delta = parseRuntimeReadModelDeltaV2(input);
195
+ if (!delta
196
+ || delta.baseCursor !== this.#cursor
197
+ || delta.nextCursor <= this.#cursor
198
+ || delta.source.streamId !== this.#source.streamId
199
+ || delta.source.generation !== this.#source.generation
200
+ || JSON.stringify(delta.source.projection) !== JSON.stringify(this.#source.projection)
201
+ || delta.source.revision !== delta.nextCursor) return false;
202
+ const next = new Map([...this.#agents].map(([id, agent]) => [id, cloneRuntimeEntity(agent)]));
203
+ for (const change of delta.changes) {
204
+ if (change.kind === "upsert") {
205
+ const current = next.get(change.entity.correlationId);
206
+ if (current && change.entity.generation < current.generation) return false;
207
+ next.set(change.entity.correlationId, cloneRuntimeEntity(change.entity));
208
+ continue;
209
+ }
210
+ const current = next.get(change.correlationId);
211
+ if (current && change.generation < current.generation) return false;
212
+ if (!current || change.generation === current.generation) next.delete(change.correlationId);
213
+ }
214
+ this.#agents.clear();
215
+ for (const [id, agent] of next) this.#agents.set(id, agent);
216
+ this.#cursor = delta.nextCursor;
217
+ this.#source = { ...delta.source };
218
+ return true;
219
+ }
220
+
221
+ snapshot(): RuntimeReadModelSnapshotV2 {
222
+ return {
223
+ version: 2,
224
+ revision: 1,
225
+ kind: "agent-runs-snapshot",
226
+ cursor: this.#cursor,
227
+ source: { ...this.#source },
228
+ agents: [...this.#agents.values()]
229
+ .sort((left, right) => left.correlationId.localeCompare(right.correlationId))
230
+ .map(cloneRuntimeEntity),
231
+ };
232
+ }
233
+ }
234
+
27
235
  const STATUS_TEXT_MAX = 48;
28
236
  /** Selected-session output kept for the expandable fixed detail region. */
29
237
  export const SESSION_CONTENT_MAX = 2_048;
package/src/config.ts CHANGED
@@ -76,6 +76,7 @@ const isCurrencyMode = (v: unknown): v is CurrencyMode => v === "usd" || v === "
76
76
  agentsMode: isMode(o.agentsMode) ? o.agentsMode : base.agentsMode,
77
77
  todoMode: isMode(o.todoMode) ? o.todoMode : base.todoMode,
78
78
  todoExpanded: typeof o.todoExpanded === "boolean" ? o.todoExpanded : base.todoExpanded,
79
+ todoDurationChart: typeof o.todoDurationChart === "boolean" ? o.todoDurationChart : base.todoDurationChart,
79
80
  stackStyle: isStackStyle(o.stackStyle) ? o.stackStyle : base.stackStyle,
80
81
  hideNativeAgents: typeof o.hideNativeAgents === "boolean" ? o.hideNativeAgents : base.hideNativeAgents,
81
82
  pinEditorBottom: typeof o.pinEditorBottom === "boolean" ? o.pinEditorBottom : base.pinEditorBottom,
@@ -168,6 +169,7 @@ export function mergeConfigDocument(raw: unknown, config: CockpitConfig): Record
168
169
  agentsMode: config.agentsMode,
169
170
  todoMode: config.todoMode,
170
171
  todoExpanded: config.todoExpanded,
172
+ todoDurationChart: config.todoDurationChart,
171
173
  stackStyle: config.stackStyle,
172
174
  hideNativeAgents: config.hideNativeAgents,
173
175
  pinEditorBottom: config.pinEditorBottom,
package/src/index.ts CHANGED
@@ -4,7 +4,18 @@ import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext, type Exte
4
4
  import type { TUI } from "@earendil-works/pi-tui";
5
5
  import { ambientKeysShouldYield, capturingOverlayVisible } from "./capturing-overlay.ts";
6
6
  import { Key, decodeKittyPrintable, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
7
- import { AgentReadStoreRouter, effectiveAgentStatus, type CompletePayload, type MessagePayload, type StartedPayload } from "./agents-store.ts";
7
+ import {
8
+ AgentReadStoreRouter,
9
+ RUNTIME_READ_MODEL_DELTA_EVENT,
10
+ RUNTIME_READ_MODEL_QUERY_EVENT,
11
+ RUNTIME_READ_MODEL_SNAPSHOT_EVENT,
12
+ RUNTIME_READ_MODEL_UNAVAILABLE_EVENT,
13
+ effectiveAgentStatus,
14
+ runtimeV2ReadEnabled,
15
+ type CompletePayload,
16
+ type MessagePayload,
17
+ type StartedPayload,
18
+ } from "./agents-store.ts";
8
19
  import { AmbientSurfaceCache, nextUiPromptDepth, shouldHideWorkingDuration, statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
9
20
  import { generateTitleWithModel } from "./title-llm.ts";
10
21
  import { suggestTitle } from "./title-gen.ts";
@@ -120,13 +131,6 @@ import {
120
131
  type CockpitConfig,
121
132
  } from "./types.ts";
122
133
  import type { MailboxHostRegistry } from "pi-maestro-teammate/v1/mailbox";
123
- import {
124
- RUNTIME_READ_MODEL_DELTA_EVENT,
125
- RUNTIME_READ_MODEL_QUERY_EVENT,
126
- RUNTIME_READ_MODEL_SNAPSHOT_EVENT,
127
- RUNTIME_READ_MODEL_UNAVAILABLE_EVENT,
128
- runtimeV2ReadEnabled,
129
- } from "pi-maestro-teammate/v2/runtime";
130
134
 
131
135
  export {
132
136
  EndpointStore,
@@ -357,6 +361,27 @@ export default function (pi: ExtensionAPI): void {
357
361
  // thinking label / teammate tree still stream above the visible viewport.
358
362
  let stabilityTui: TUI | undefined;
359
363
  let viewportStabilityPatch: ViewportStabilityPatch | undefined;
364
+ const ensureViewportStability = (tui: TUI): void => {
365
+ // Native fullscreen owns a fixed application viewport and has no main-screen
366
+ // applyLineResets hook. Do not cache that expected miss: the same dynamic TUI
367
+ // reference can later dispatch to the regular renderer.
368
+ if (tui.mode === "fullscreen") return;
369
+ if (stabilityTui === tui && viewportStabilityPatch?.active) return;
370
+ viewportStabilityPatch?.detach();
371
+ const patch = attachViewportStability(tui);
372
+ viewportStabilityPatch = patch;
373
+ stabilityTui = patch.active ? tui : undefined;
374
+ };
375
+ const requestCapturedRender = (): void => {
376
+ try {
377
+ const tui = capturedTui;
378
+ if (!tui) return;
379
+ ensureViewportStability(tui);
380
+ tui.requestRender();
381
+ } catch {
382
+ // tui may be gone between sessions
383
+ }
384
+ };
360
385
  // True while Cockpit owns a capturing overlay (bash jobs, theme picker,
361
386
  // settings panel). Split-pane resize must yield to the overlay's focus and
362
387
  // refuse to start while one is open; otherwise the resize listener — a
@@ -455,13 +480,7 @@ export default function (pi: ExtensionAPI): void {
455
480
  // while the model thinks, then the settled duration. Pi owns the fold.
456
481
  const thinkingTimer = new ThinkingFoldTimer({
457
482
  getTui: () => capturedTui,
458
- requestRender: () => {
459
- try {
460
- capturedTui?.requestRender();
461
- } catch {
462
- // tui may be gone between sessions
463
- }
464
- },
483
+ requestRender: requestCapturedRender,
465
484
  getBaseLabel: () => (config.quietMode ? quietThinkingLabel() : undefined),
466
485
  getGlyphs: () => resolveGlyphs(config.icons.mode),
467
486
  isThinkingHidden: () => (lastCtx ? readHideThinkingBlock(lastCtx.cwd) : false),
@@ -601,7 +620,7 @@ export default function (pi: ExtensionAPI): void {
601
620
  if (refreshAmbientSurface) refreshAmbient(now);
602
621
  if (agentPriorityChanged) publishUiOwnership();
603
622
  try {
604
- capturedTui?.requestRender();
623
+ requestCapturedRender();
605
624
  activeAgentOverlayRender?.();
606
625
  sidebarController?.requestRender();
607
626
  } catch {
@@ -691,6 +710,7 @@ export default function (pi: ExtensionAPI): void {
691
710
  sidebar: ownsDock,
692
711
  goal: ownsDock,
693
712
  todoExpanded: effectiveTodoExpanded(),
713
+ todoDurationChart: config.todoDurationChart,
694
714
  quiet: config.enabled && config.quietMode,
695
715
  quietSymbols: config.quietSymbols,
696
716
  static: config.staticMode,
@@ -747,13 +767,6 @@ export default function (pi: ExtensionAPI): void {
747
767
  };
748
768
 
749
769
 
750
- const ensureViewportStability = (tui: TUI): void => {
751
- if (stabilityTui === tui) return;
752
- viewportStabilityPatch?.detach();
753
- viewportStabilityPatch = attachViewportStability(tui);
754
- stabilityTui = tui;
755
- };
756
-
757
770
  const sessionListOverlayActive = (): boolean =>
758
771
  capturingOverlayActive || ambientKeysShouldYield(capturedTui);
759
772
 
@@ -1916,6 +1929,7 @@ export default function (pi: ExtensionAPI): void {
1916
1929
  sidebar: false,
1917
1930
  goal: false,
1918
1931
  todoExpanded: config.todoExpanded,
1932
+ todoDurationChart: config.todoDurationChart,
1919
1933
  quiet: false,
1920
1934
  quietSymbols: config.quietSymbols,
1921
1935
  static: config.staticMode,
@@ -195,6 +195,8 @@ export interface CockpitUiOwnershipV1 {
195
195
  /** True only while the dock renders Maestro Goal state. */
196
196
  goal: boolean;
197
197
  todoExpanded: boolean;
198
+ /** Whether Flow should render the duration chart after a terminal todo advance. */
199
+ todoDurationChart?: boolean;
198
200
  quiet: boolean;
199
201
  quietSymbols: "check" | "dot";
200
202
  /** Static mode: per-second churn (elapsed ticks, spinners) is suppressed. */
@@ -68,6 +68,7 @@ const CONFIG_KEYS = [
68
68
  "agentsMode",
69
69
  "todoMode",
70
70
  "todoExpanded",
71
+ "todoDurationChart",
71
72
  "stackStyle",
72
73
  "hideNativeAgents",
73
74
  "toolPalette",
@@ -164,6 +165,8 @@ const CATALOGS = {
164
165
  "cockpit.agentsMode": "Agent display",
165
166
  "cockpit.todoMode": "Todo display",
166
167
  "cockpit.todoExpanded": "Todo expanded",
168
+ "cockpit.todoDurationChart": "Advance duration chart",
169
+ "cockpit.todoDurationChart.description": "Show a task-duration chart after advance completes the entire Todo list.",
167
170
  "cockpit.stackStyle": "Stack projection",
168
171
  "cockpit.hideNativeAgents": "Hide native agent widget",
169
172
  "cockpit.icons.mode": "Icon mode",
@@ -249,6 +252,8 @@ const CATALOGS = {
249
252
  "cockpit.agentsMode": "Agent 显示",
250
253
  "cockpit.todoMode": "Todo 显示",
251
254
  "cockpit.todoExpanded": "展开 Todo",
255
+ "cockpit.todoDurationChart": "Advance 耗时图",
256
+ "cockpit.todoDurationChart.description": "当 advance 完成整个 Todo 列表后显示任务耗时统计图。",
252
257
  "cockpit.stackStyle": "栈投影",
253
258
  "cockpit.hideNativeAgents": "隐藏原生 Agent 面板",
254
259
  "cockpit.icons.mode": "图标模式",
@@ -367,8 +372,9 @@ const DEFINITIONS: readonly SettingDefinition[] = [
367
372
  enumDefinition("agentsMode", "cockpit.group.panels", 0, "cockpit.agentsMode", ["list", "compact"], "live"),
368
373
  enumDefinition("todoMode", "cockpit.group.panels", 1, "cockpit.todoMode", ["list", "compact"], "live"),
369
374
  booleanDefinition("todoExpanded", "cockpit.group.panels", 2, "cockpit.todoExpanded", "live"),
375
+ booleanDefinition("todoDurationChart", "cockpit.group.panels", 3, "cockpit.todoDurationChart", "live", "cockpit.todoDurationChart.description"),
370
376
  enumDefinition("stackStyle", "cockpit.group.layout", 4, "cockpit.stackStyle", ["classic", "zen"], "live"),
371
- booleanDefinition("hideNativeAgents", "cockpit.group.panels", 3, "cockpit.hideNativeAgents", "live"),
377
+ booleanDefinition("hideNativeAgents", "cockpit.group.panels", 4, "cockpit.hideNativeAgents", "live"),
372
378
  enumDefinition("sidebar.mode", "cockpit.group.sidebar", 0, "cockpit.sidebar.mode", ["auto", "on", "off"], "live"),
373
379
  {
374
380
  key: "sidebar.width",
@@ -789,7 +795,7 @@ function setUsageValue(usage: CockpitConfig["usage"], field: string, value: Json
789
795
  }
790
796
 
791
797
  function validValue(key: CockpitSettingKey, value: JsonValue): boolean {
792
- if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
798
+ if (["enabled", "staticMode", "pinEditorBottom", "doubleEscapeClearInput", "fullscreenInput", "copyOnSelect", "historyEnabled", "quietMode", "todoExpanded", "todoDurationChart", "hideNativeAgents"].includes(key)) return typeof value === "boolean";
793
799
  if (key === "sidebar.width") return typeof value === "number" && Number.isSafeInteger(value) && value >= 32 && value <= 56;
794
800
  if (key === "title.maxLength") return typeof value === "number" && Number.isSafeInteger(value) && value >= 20 && value <= 200;
795
801
  if (key === "title.generationModel") return typeof value === "string";
@@ -1283,7 +1283,15 @@ export class MaestroSettingsShell implements Component, Focusable {
1283
1283
  const change = this.params.coordinator.changes(providerId).find((entry) => entry.key === definition.key && entry.scope === scope);
1284
1284
  if (change?.operation === "unset") return this.t("settings.inherited");
1285
1285
  if (change?.operation === "set") return this.formatValue(definition, change.value);
1286
- return this.displayConfigured(definition, this.configuredValue(this.params.coordinator.baseline(providerId), definition.key, scope));
1286
+ const baseline = this.params.coordinator.baseline(providerId);
1287
+ const configured = this.configuredValue(baseline, definition.key, scope);
1288
+ if (!configured || configured.state === "absent") {
1289
+ const effective = baseline?.effective.values.find((value) => value.key === definition.key);
1290
+ if (effective) {
1291
+ return `${this.t("settings.state.absent")} · ${this.t("settings.effective")} ${this.formatValue(definition, effective.value)}`;
1292
+ }
1293
+ }
1294
+ return this.displayConfigured(definition, configured);
1287
1295
  }
1288
1296
 
1289
1297
  private configuredValue(snapshot: SettingsSnapshot | undefined, key: string, scope: SettingsScope) {
package/src/types.ts CHANGED
@@ -260,6 +260,8 @@ export interface CockpitConfig {
260
260
  agentsMode: ViewMode;
261
261
  todoMode: ViewMode;
262
262
  todoExpanded: boolean;
263
+ /** Show the duration chart after a terminal todo advance. */
264
+ todoDurationChart: boolean;
263
265
  /** Above-editor stack projection: classic Todo/Agents widgets or the unified Zen stack. */
264
266
  stackStyle: StackStyle;
265
267
  hideNativeAgents: boolean;
@@ -318,6 +320,7 @@ export const DEFAULT_CONFIG: CockpitConfig = {
318
320
  agentsMode: "list",
319
321
  todoMode: "list",
320
322
  todoExpanded: false,
323
+ todoDurationChart: true,
321
324
  stackStyle: "classic",
322
325
  hideNativeAgents: true,
323
326
  pinEditorBottom: false,
@@ -10,7 +10,8 @@ interface ViewportTuiInternals {
10
10
  previousLines?: string[];
11
11
  previousViewportTop?: number;
12
12
  previousHeight?: number;
13
- terminal?: { rows: number };
13
+ previousWidth?: number;
14
+ terminal?: { columns: number; rows: number };
14
15
  }
15
16
 
16
17
  interface ViewportStabilityMarker {
@@ -44,18 +45,6 @@ function markerOf(fn: ApplyLineResets): ViewportStabilityMarker | undefined {
44
45
  return (fn as ApplyLineResets & Record<symbol, ViewportStabilityMarker | undefined>)[VIEWPORT_STABILITY_MARKER];
45
46
  }
46
47
 
47
- const ANSI_ESCAPE = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1b\\))/g;
48
- const TRAILING_DURATION = /(\s(?:·\s*)?)(?:\d+(?:\.\d+)?s|\d+m\d{1,2}s)$/;
49
-
50
- function differsOnlyByTrailingDuration(previous: string, next: string): boolean {
51
- const previousPlain = previous.replace(ANSI_ESCAPE, "");
52
- const nextPlain = next.replace(ANSI_ESCAPE, "");
53
- if (previousPlain === nextPlain) return false;
54
- if (!TRAILING_DURATION.test(previousPlain) || !TRAILING_DURATION.test(nextPlain)) return false;
55
- return previousPlain.replace(TRAILING_DURATION, "$1<duration>")
56
- === nextPlain.replace(TRAILING_DURATION, "$1<duration>");
57
- }
58
-
59
48
  function isKittyImageLine(line: string | undefined): boolean {
60
49
  return typeof line === "string" && line.includes("\x1b_G");
61
50
  }
@@ -110,13 +99,17 @@ function resolveApplyLineResetsSlot(internals: ViewportTuiInternals): ApplyLineR
110
99
  }
111
100
 
112
101
  /**
113
- * Prevents timer-only churn above the visible viewport from forcing a full
114
- * redraw. The next frame keeps the terminal's actual hidden text, so the diff
115
- * baseline remains truthful; status, content, layout and image changes retain
116
- * pi-tui's native redraw path.
102
+ * Treats the hidden viewport prefix as immutable scrollback while frame size is
103
+ * stable. The next frame keeps the terminal's actual hidden text, so visible
104
+ * changes can use pi-tui's differential path instead of clearing and replaying
105
+ * the screen. Structural, resize and image changes retain the native path.
117
106
  */
118
107
  export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
119
108
  try {
109
+ // Native fullscreen has an application-owned fixed viewport. Its inherited
110
+ // applyLineResets method is not part of that renderer's diff path, so patching
111
+ // it would report a false-positive attachment on the shared TUI base class.
112
+ if (tui.mode === "fullscreen") return { active: false, detach() {} };
120
113
  const internals = tui as unknown as ViewportTuiInternals;
121
114
  const slot = resolveApplyLineResetsSlot(internals);
122
115
  if (!slot) return { active: false, detach() {} };
@@ -137,6 +130,7 @@ export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
137
130
  && Number.isFinite(viewportTop)
138
131
  && viewportTop > 0
139
132
  && this.previousHeight === this.terminal?.rows
133
+ && this.previousWidth === this.terminal?.columns
140
134
  ) {
141
135
  const hiddenEnd = Math.min(previousLines.length, Math.trunc(viewportTop));
142
136
  const hiddenHasKittyImage = previousLines.slice(0, hiddenEnd).some(isKittyImageLine)
@@ -144,10 +138,7 @@ export function attachViewportStability(tui: TUI): ViewportStabilityPatch {
144
138
  if (!hiddenHasKittyImage) {
145
139
  for (let index = 0; index < hiddenEnd; index += 1) {
146
140
  const previous = previousLines[index];
147
- const next = nextLines[index];
148
- if (previous !== undefined && next !== undefined && differsOnlyByTrailingDuration(previous, next)) {
149
- nextLines[index] = previous;
150
- }
141
+ if (previous !== undefined) nextLines[index] = previous;
151
142
  }
152
143
  }
153
144
  }