pi-cockpit 0.21.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 +3 -3
- package/src/agents-store.ts +214 -6
- package/src/index.ts +12 -8
- package/src/settings/settings-shell.ts +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Responsive Maestro operations sidebar, fallback status widgets, and footer for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@earendil-works/pi-ai": "*",
|
|
44
44
|
"@earendil-works/pi-coding-agent": "*",
|
|
45
45
|
"@earendil-works/pi-tui": "*",
|
|
46
|
-
"pi-maestro-teammate": "^2.
|
|
46
|
+
"pi-maestro-teammate": "^2.5.0",
|
|
47
47
|
"typebox": "*"
|
|
48
48
|
},
|
|
49
49
|
"peerDependenciesMeta": {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
73
73
|
"@earendil-works/pi-tui": "0.84.4",
|
|
74
74
|
"@types/node": "^22.0.0",
|
|
75
|
-
"pi-maestro-teammate": "2.
|
|
75
|
+
"pi-maestro-teammate": "2.5.0",
|
|
76
76
|
"typebox": "1.3.7",
|
|
77
77
|
"typescript": "^5.5.0"
|
|
78
78
|
},
|
package/src/agents-store.ts
CHANGED
|
@@ -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
|
-
|
|
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/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 {
|
|
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,
|
|
@@ -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
|
-
|
|
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) {
|