@iseoane/agent-depot 0.5.0 → 0.5.2
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/CHANGELOG.md +34 -0
- package/README.es.md +78 -33
- package/README.md +66 -25
- package/dist/src/cli.js +55 -0
- package/dist/src/git-source.js +21 -0
- package/dist/src/package-cli.js +2 -1
- package/dist/src/package-flow.js +14 -5
- package/dist/src/package-host-plans.js +12 -9
- package/dist/src/package-host-state.js +133 -5
- package/dist/src/package-owned-skills.js +8 -6
- package/dist/src/process-runner.js +9 -1
- package/dist/src/self-update.js +141 -0
- package/dist/src/skill-discovery.js +5 -0
- package/dist/src/tui/app.js +7 -7
- package/dist/src/tui/catalog-browse.js +36 -10
- package/dist/src/tui/catalog-ownership.js +12 -0
- package/dist/src/tui/catalog-panel.js +10 -2
- package/dist/src/tui/catalog-state.js +8 -3
- package/dist/src/tui/catalog-tree.js +6 -2
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/package-rows.js +9 -3
- package/dist/src/tui/render.js +1 -1
- package/dist/src/tui/window.js +3 -4
- package/package.json +32 -12
- package/resources/pi/agents-panel/contract.ts +62 -0
- package/resources/pi/agents-panel/index.ts +471 -0
- package/skills/doctor-md-agents/SKILL.md +2 -1
- package/skills/doctor-md-agents/scripts/scan_corrections.py +141 -13
- package/skills/doctor-md-skill/references/supported-harnesses.md +2 -0
- package/skills/doctor-md-skill/scripts/collect_sessions.py +26 -2
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { Key, matchesKey, stripTerminalSequences, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AGENT_SNAPSHOT_REQUEST_EVENT,
|
|
5
|
+
AGENT_SNAPSHOT_RESPONSE_EVENT,
|
|
6
|
+
parseAgentSnapshotResponse,
|
|
7
|
+
type AgentSnapshot,
|
|
8
|
+
} from "./contract.js";
|
|
9
|
+
|
|
10
|
+
const TICK_MS = 1000;
|
|
11
|
+
const RESPONSE_TIMEOUT_MS = 2000;
|
|
12
|
+
const MAX_NOTIFY_CHARS = 4000;
|
|
13
|
+
const OVERLAY_MARGIN = 1;
|
|
14
|
+
const OVERLAY_MAX_HEIGHT_PERCENT = 80;
|
|
15
|
+
const MAX_AGENTS = 12;
|
|
16
|
+
|
|
17
|
+
type SnapshotState =
|
|
18
|
+
| { readonly kind: "waiting" }
|
|
19
|
+
| { readonly kind: "unavailable" }
|
|
20
|
+
| { readonly kind: "available"; readonly requestId: string; readonly agents: readonly AgentSnapshot[] };
|
|
21
|
+
|
|
22
|
+
export interface PanelEventBus {
|
|
23
|
+
emit(channel: string, data: unknown): void;
|
|
24
|
+
on(channel: string, handler: (data: unknown) => void): () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PanelTheme {
|
|
28
|
+
fg(color: "accent" | "dim" | "muted" | "success" | "error" | "warning", text: string): string;
|
|
29
|
+
bold(text: string): string;
|
|
30
|
+
style(text: string, options: { readonly bg: "selectedBg" }): string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PanelCommandContext {
|
|
34
|
+
readonly mode: string;
|
|
35
|
+
readonly hasUI: boolean;
|
|
36
|
+
readonly ui: {
|
|
37
|
+
notify(message: string, type: "info"): void;
|
|
38
|
+
custom(
|
|
39
|
+
factory: (
|
|
40
|
+
tui: { requestRender(): void; terminal?: { readonly rows: number } },
|
|
41
|
+
theme: PanelTheme,
|
|
42
|
+
keybindings: unknown,
|
|
43
|
+
done: (result: undefined) => void,
|
|
44
|
+
) => Component,
|
|
45
|
+
options: {
|
|
46
|
+
readonly overlay: true;
|
|
47
|
+
readonly overlayOptions: {
|
|
48
|
+
readonly width: "80%";
|
|
49
|
+
readonly minWidth: 40;
|
|
50
|
+
readonly maxHeight: string;
|
|
51
|
+
readonly anchor: "center";
|
|
52
|
+
readonly margin: number;
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
): Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface PanelExtensionAPI {
|
|
60
|
+
readonly events: PanelEventBus;
|
|
61
|
+
on(event: "session_shutdown", handler: () => void): void;
|
|
62
|
+
registerCommand(name: string, options: {
|
|
63
|
+
readonly description: string;
|
|
64
|
+
readonly handler: (args: string, ctx: PanelCommandContext) => Promise<void>;
|
|
65
|
+
}): void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class SnapshotChannel {
|
|
69
|
+
private current: SnapshotState = { kind: "waiting" };
|
|
70
|
+
private nextRequestId = 0;
|
|
71
|
+
private latestRequestId: string | undefined;
|
|
72
|
+
private readonly listeners = new Set<() => void>();
|
|
73
|
+
|
|
74
|
+
constructor(private readonly pi: Pick<PanelExtensionAPI, "events">) {
|
|
75
|
+
pi.events.on(AGENT_SNAPSHOT_RESPONSE_EVENT, (payload) => {
|
|
76
|
+
const response = parseAgentSnapshotResponse(payload);
|
|
77
|
+
if (!response || response.requestId !== this.latestRequestId) return;
|
|
78
|
+
this.current = { kind: "available", requestId: response.requestId, agents: response.agents };
|
|
79
|
+
this.notify();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
get state(): SnapshotState {
|
|
84
|
+
return this.current;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
subscribe(listener: () => void): () => void {
|
|
88
|
+
this.listeners.add(listener);
|
|
89
|
+
return () => this.listeners.delete(listener);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
request(): string {
|
|
93
|
+
const requestId = String(++this.nextRequestId);
|
|
94
|
+
this.latestRequestId = requestId;
|
|
95
|
+
this.pi.events.emit(AGENT_SNAPSHOT_REQUEST_EVENT, { version: 1, requestId });
|
|
96
|
+
return requestId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async requestSnapshot(timeoutMs = RESPONSE_TIMEOUT_MS): Promise<SnapshotState> {
|
|
100
|
+
const requestId = this.request();
|
|
101
|
+
if (this.current.kind === "available" && this.current.requestId === requestId) return this.current;
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
let settled = false;
|
|
105
|
+
let unsubscribe = () => {};
|
|
106
|
+
const timeout = setTimeout(() => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
this.markUnavailable(requestId);
|
|
110
|
+
unsubscribe();
|
|
111
|
+
resolve(this.latestRequestId === requestId ? this.current : { kind: "unavailable" });
|
|
112
|
+
}, timeoutMs);
|
|
113
|
+
const finish = (state: SnapshotState) => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
settled = true;
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
unsubscribe();
|
|
118
|
+
resolve(state);
|
|
119
|
+
};
|
|
120
|
+
unsubscribe = this.subscribe(() => {
|
|
121
|
+
const state = this.current;
|
|
122
|
+
if (state.kind === "available" && state.requestId === requestId) finish(state);
|
|
123
|
+
if (state.kind === "unavailable" && this.latestRequestId === requestId) finish(state);
|
|
124
|
+
});
|
|
125
|
+
if (this.current.kind === "available" && this.current.requestId === requestId) finish(this.current);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
markUnavailable(requestId: string): void {
|
|
130
|
+
if (this.latestRequestId !== requestId || this.current.kind === "unavailable") return;
|
|
131
|
+
this.current = { kind: "unavailable" };
|
|
132
|
+
this.notify();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private notify(): void {
|
|
136
|
+
for (const listener of this.listeners) listener();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface PanelLine {
|
|
141
|
+
readonly text: string;
|
|
142
|
+
readonly selected?: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sanitize(text: string): string {
|
|
146
|
+
return stripTerminalSequences(text)
|
|
147
|
+
.replace(/[\p{Cc}\p{Cf}]+/gu, " ")
|
|
148
|
+
.replace(/\s+/gu, " ")
|
|
149
|
+
.trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatElapsed(ms: number): string {
|
|
153
|
+
const total = Math.max(0, Math.round(ms / 1000));
|
|
154
|
+
const hours = Math.floor(total / 3600);
|
|
155
|
+
const minutes = Math.floor((total % 3600) / 60);
|
|
156
|
+
const seconds = total % 60;
|
|
157
|
+
if (hours) return `${hours}h ${String(minutes).padStart(2, "0")}m`;
|
|
158
|
+
if (minutes) return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
159
|
+
return `${seconds}s`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatTokens(count: number): string {
|
|
163
|
+
if (count < 1000) return String(count);
|
|
164
|
+
if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`;
|
|
165
|
+
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function usageLine(usage: NonNullable<AgentSnapshot["usage"]>): string {
|
|
169
|
+
const values: string[] = [];
|
|
170
|
+
if (usage.input !== undefined) values.push(`${formatTokens(usage.input)} in`);
|
|
171
|
+
if (usage.output !== undefined) values.push(`${formatTokens(usage.output)} out`);
|
|
172
|
+
if (usage.cacheRead !== undefined || usage.cacheWrite !== undefined) {
|
|
173
|
+
values.push(`${formatTokens((usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0))} cached`);
|
|
174
|
+
}
|
|
175
|
+
if (usage.cost !== undefined) values.push(`$${usage.cost.toFixed(4)}`);
|
|
176
|
+
if (!values.length && usage.totalTokens !== undefined) values.push(`${formatTokens(usage.totalTokens)} tokens`);
|
|
177
|
+
return values.join(", ");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function textListing(agents: readonly AgentSnapshot[]): string {
|
|
181
|
+
const ordered = orderAgents(agents);
|
|
182
|
+
if (!ordered.length) return "No agent has been started in this session.";
|
|
183
|
+
const text = ordered.map((agent) => {
|
|
184
|
+
const head = `${sanitize(agent.id)} ${sanitize(agent.status)} ${sanitize(agent.description)}`;
|
|
185
|
+
const meta = [
|
|
186
|
+
sanitize(agent.model),
|
|
187
|
+
agent.thinking ? `@${sanitize(agent.thinking)}` : "",
|
|
188
|
+
formatElapsed(agent.elapsedMs),
|
|
189
|
+
agent.activity ? sanitize(agent.activity.label) : "",
|
|
190
|
+
agent.usage ? usageLine(agent.usage) : "",
|
|
191
|
+
].filter(Boolean);
|
|
192
|
+
return `${head}\n ${meta.join(" ")}`;
|
|
193
|
+
}).join("\n\n");
|
|
194
|
+
const chars = [...text];
|
|
195
|
+
return chars.length <= MAX_NOTIFY_CHARS ? text : `${chars.slice(0, MAX_NOTIFY_CHARS).join("")}\n(truncated)`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function orderAgents(agents: readonly AgentSnapshot[]): AgentSnapshot[] {
|
|
199
|
+
return [...agents].sort((left, right) => {
|
|
200
|
+
if (left.status === "running" && right.status !== "running") return -1;
|
|
201
|
+
if (left.status !== "running" && right.status === "running") return 1;
|
|
202
|
+
const leftStarted = Date.parse(left.startedAt);
|
|
203
|
+
const rightStarted = Date.parse(right.startedAt);
|
|
204
|
+
if (Number.isNaN(leftStarted)) return Number.isNaN(rightStarted) ? 0 : 1;
|
|
205
|
+
if (Number.isNaN(rightStarted)) return -1;
|
|
206
|
+
return rightStarted - leftStarted;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export class AgentsPanel implements Component {
|
|
211
|
+
private cursor = 0;
|
|
212
|
+
private selectedId: string | undefined;
|
|
213
|
+
private detail = false;
|
|
214
|
+
private detailScroll = 0;
|
|
215
|
+
private closed = false;
|
|
216
|
+
private timer: NodeJS.Timeout | undefined;
|
|
217
|
+
private responseTimeout: NodeJS.Timeout | undefined;
|
|
218
|
+
private pendingRequest: string | undefined;
|
|
219
|
+
private readonly unsubscribe: () => void;
|
|
220
|
+
|
|
221
|
+
constructor(
|
|
222
|
+
private readonly channel: SnapshotChannel,
|
|
223
|
+
private readonly tui: { requestRender(): void; terminal?: { readonly rows: number } },
|
|
224
|
+
private readonly theme: PanelTheme,
|
|
225
|
+
private readonly close: () => void,
|
|
226
|
+
) {
|
|
227
|
+
this.unsubscribe = channel.subscribe(() => {
|
|
228
|
+
if (channel.state.kind === "available" && channel.state.requestId === this.pendingRequest) {
|
|
229
|
+
if (this.responseTimeout) clearTimeout(this.responseTimeout);
|
|
230
|
+
this.responseTimeout = undefined;
|
|
231
|
+
this.pendingRequest = undefined;
|
|
232
|
+
}
|
|
233
|
+
tui.requestRender();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
start(): void {
|
|
238
|
+
if (this.timer) return;
|
|
239
|
+
this.requestSnapshot();
|
|
240
|
+
this.timer = setInterval(() => this.requestSnapshot(), TICK_MS);
|
|
241
|
+
this.timer.unref();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
stop(): void {
|
|
245
|
+
this.closed = true;
|
|
246
|
+
if (this.timer) clearInterval(this.timer);
|
|
247
|
+
if (this.responseTimeout) clearTimeout(this.responseTimeout);
|
|
248
|
+
this.timer = undefined;
|
|
249
|
+
this.responseTimeout = undefined;
|
|
250
|
+
this.pendingRequest = undefined;
|
|
251
|
+
this.unsubscribe();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
dispose(): void {
|
|
255
|
+
this.stop();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
invalidate(): void {}
|
|
259
|
+
|
|
260
|
+
private requestSnapshot(): void {
|
|
261
|
+
if (this.closed || this.pendingRequest) return;
|
|
262
|
+
const requestId = this.channel.request();
|
|
263
|
+
if (this.channel.state.kind === "available" && this.channel.state.requestId === requestId) return;
|
|
264
|
+
this.pendingRequest = requestId;
|
|
265
|
+
this.responseTimeout = setTimeout(() => {
|
|
266
|
+
if (this.pendingRequest !== requestId) return;
|
|
267
|
+
this.responseTimeout = undefined;
|
|
268
|
+
this.pendingRequest = undefined;
|
|
269
|
+
this.channel.markUnavailable(requestId);
|
|
270
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
271
|
+
this.responseTimeout.unref();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
handleInput(data: string): void {
|
|
275
|
+
if (data === "q") return this.finish();
|
|
276
|
+
if (matchesKey(data, Key.escape)) {
|
|
277
|
+
if (this.detail) {
|
|
278
|
+
this.detail = false;
|
|
279
|
+
this.detailScroll = 0;
|
|
280
|
+
this.tui.requestRender();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
return this.finish();
|
|
284
|
+
}
|
|
285
|
+
if (matchesKey(data, Key.enter) || data === "d") {
|
|
286
|
+
if (this.channel.state.kind !== "available" || this.channel.state.agents.length === 0) return;
|
|
287
|
+
this.detail = !this.detail;
|
|
288
|
+
this.detailScroll = 0;
|
|
289
|
+
this.tui.requestRender();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const up = matchesKey(data, Key.up) || data === "k";
|
|
293
|
+
const down = matchesKey(data, Key.down) || data === "j";
|
|
294
|
+
if (!up && !down) return;
|
|
295
|
+
if (this.detail) {
|
|
296
|
+
this.detailScroll = Math.max(0, this.detailScroll + (down ? 1 : -1));
|
|
297
|
+
} else if (this.channel.state.kind === "available") {
|
|
298
|
+
const agents = orderAgents(this.channel.state.agents);
|
|
299
|
+
this.syncSelection(agents);
|
|
300
|
+
this.cursor = down ? Math.min(Math.max(0, agents.length - 1), this.cursor + 1) : Math.max(0, this.cursor - 1);
|
|
301
|
+
this.selectedId = agents[this.cursor]?.id;
|
|
302
|
+
}
|
|
303
|
+
this.tui.requestRender();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
render(width: number): string[] {
|
|
307
|
+
const available = this.channel.state;
|
|
308
|
+
if (available.kind !== "available") {
|
|
309
|
+
const message = available.kind === "waiting"
|
|
310
|
+
? "Waiting for an agent provider."
|
|
311
|
+
: "No agent provider responded. Install an adapter that publishes agent snapshots.";
|
|
312
|
+
return this.fit(this.frame({ text: this.heading("Agents") }, [{ text: "" }, { text: this.theme.fg("muted", message) }], "q close", this.overlayRows()), Math.max(1, width));
|
|
313
|
+
}
|
|
314
|
+
const agents = orderAgents(available.agents);
|
|
315
|
+
if (agents.length === 0) {
|
|
316
|
+
this.syncSelection(agents);
|
|
317
|
+
return this.fit(this.frame({ text: this.heading("Agents") }, [{ text: "" }, { text: this.theme.fg("muted", "No agent has been started in this session.") }], "q close", this.overlayRows()), Math.max(1, width));
|
|
318
|
+
}
|
|
319
|
+
this.syncSelection(agents);
|
|
320
|
+
const selected = agents[this.cursor];
|
|
321
|
+
if (!selected) return this.fit(this.frame({ text: this.heading("Agents") }, [], "q close", this.overlayRows()), Math.max(1, width));
|
|
322
|
+
const lines = this.detail ? this.detailLines(selected, this.overlayRows()) : this.listLines(agents, this.overlayRows());
|
|
323
|
+
return this.fit(lines, Math.max(1, width));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private syncSelection(agents: readonly AgentSnapshot[]): void {
|
|
327
|
+
if (!agents.length) {
|
|
328
|
+
this.cursor = 0;
|
|
329
|
+
this.selectedId = undefined;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const selectedIndex = this.selectedId ? agents.findIndex((agent) => agent.id === this.selectedId) : -1;
|
|
333
|
+
this.cursor = selectedIndex >= 0 ? selectedIndex : Math.min(this.cursor, agents.length - 1);
|
|
334
|
+
this.selectedId = agents[this.cursor]?.id;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private finish(): void {
|
|
338
|
+
this.stop();
|
|
339
|
+
this.close();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private fit(lines: readonly PanelLine[], width: number): string[] {
|
|
343
|
+
return lines.map(({ text, selected }) => {
|
|
344
|
+
const fitted = truncateToWidth(text, width, "...", true);
|
|
345
|
+
return selected ? this.theme.style(fitted, { bg: "selectedBg" }) : fitted;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private heading(text: string): string {
|
|
350
|
+
return this.theme.fg("accent", this.theme.bold(text));
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private hint(text: string): string {
|
|
354
|
+
return this.theme.fg("dim", text);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private overlayRows(): number {
|
|
358
|
+
const rows = Math.max(1, Math.floor(this.tui.terminal?.rows || 24));
|
|
359
|
+
return Math.max(1, Math.min(Math.floor((rows * OVERLAY_MAX_HEIGHT_PERCENT) / 100), Math.max(1, rows - OVERLAY_MARGIN * 2)));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private frame(heading: PanelLine, body: readonly PanelLine[], help: string, limit: number): PanelLine[] {
|
|
363
|
+
const lines = [heading, ...body.slice(0, Math.max(0, limit - 2)), { text: this.hint(help) }];
|
|
364
|
+
return lines.length <= limit ? lines : [{ text: this.hint(help) }];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private listLines(agents: readonly AgentSnapshot[], limit: number): PanelLine[] {
|
|
368
|
+
const running = agents.filter((agent) => agent.status === "running").length;
|
|
369
|
+
const budget = Math.min(MAX_AGENTS, Math.max(1, Math.floor(Math.max(0, limit - 2) / 2)));
|
|
370
|
+
const start = Math.min(Math.max(0, this.cursor - Math.floor(budget / 2)), Math.max(0, agents.length - budget));
|
|
371
|
+
const visible = agents.slice(start, start + budget);
|
|
372
|
+
const shown = agents.length > visible.length ? `, showing ${start + 1}-${start + visible.length}` : "";
|
|
373
|
+
const body: PanelLine[] = [];
|
|
374
|
+
visible.forEach((agent, index) => {
|
|
375
|
+
const selected = start + index === this.cursor;
|
|
376
|
+
body.push({ text: this.row(agent, selected), selected }, { text: this.meta(agent), selected });
|
|
377
|
+
});
|
|
378
|
+
return this.frame({ text: this.heading(`Agents (${running} running, ${agents.length} total${shown})`) }, body, "up/down select enter details q close", limit);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private row(agent: AgentSnapshot, selected: boolean): string {
|
|
382
|
+
const mark = agent.status === "running" ? ">" : agent.status === "completed" ? "+" : agent.status === "failed" ? "x" : "-";
|
|
383
|
+
const color = agent.status === "running" ? "accent" : agent.status === "completed" ? "success" : agent.status === "failed" ? "error" : "warning";
|
|
384
|
+
return `${selected ? "*" : " "} ${this.theme.fg(color, mark)} ${this.theme.fg("muted", sanitize(agent.id).slice(0, 8))} ${sanitize(agent.description)}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private meta(agent: AgentSnapshot): string {
|
|
388
|
+
const fields = [sanitize(agent.model)];
|
|
389
|
+
if (agent.thinking) fields.push(`@${sanitize(agent.thinking)}`);
|
|
390
|
+
fields.push(formatElapsed(agent.elapsedMs));
|
|
391
|
+
if (agent.usage) fields.push(usageLine(agent.usage));
|
|
392
|
+
fields.push(agent.activity ? sanitize(agent.activity.label) : "no activity seen");
|
|
393
|
+
return ` ${this.theme.fg("dim", fields.join(" "))}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private detailLines(agent: AgentSnapshot, limit: number): PanelLine[] {
|
|
397
|
+
const lines: PanelLine[] = [
|
|
398
|
+
{ text: this.heading(sanitize(agent.description)) },
|
|
399
|
+
{ text: this.theme.fg("muted", sanitize(agent.id)) },
|
|
400
|
+
{ text: "" },
|
|
401
|
+
{ text: `${this.theme.fg("muted", "status:")} ${sanitize(agent.status)}` },
|
|
402
|
+
{ text: `${this.theme.fg("muted", "type:")} ${sanitize(agent.subagentType)}` },
|
|
403
|
+
{ text: `${this.theme.fg("muted", "model:")} ${sanitize(agent.thinking ? `${agent.model} @${agent.thinking}` : agent.model)}` },
|
|
404
|
+
{ text: `${this.theme.fg("muted", "readonly:")} ${agent.readonly ? "yes" : "no"}` },
|
|
405
|
+
{ text: `${this.theme.fg("muted", "started:")} ${sanitize(agent.startedAt)}` },
|
|
406
|
+
{ text: `${this.theme.fg("muted", "elapsed:")} ${formatElapsed(agent.elapsedMs)}${agent.endedAt ? " (final)" : ""}` },
|
|
407
|
+
{ text: `${this.theme.fg("muted", "activity:")} ${agent.activity ? sanitize(agent.activity.label) : "none observed"}` },
|
|
408
|
+
{ text: `${this.theme.fg("muted", "tokens:")} ${agent.usage ? usageLine(agent.usage) : agent.observed ? "none reported" : "not captured"}` },
|
|
409
|
+
{ text: `${this.theme.fg("muted", "telemetry:")} ${agent.kind === "local" ? "live from this process" : agent.kind === "remote" ? "held by another pi process" : agent.observed ? "captured when the run ended" : "not captured (restored record)"}` },
|
|
410
|
+
];
|
|
411
|
+
if (agent.worktree) lines.push({ text: `${this.theme.fg("muted", "worktree:")} ${sanitize(agent.worktree)}` });
|
|
412
|
+
const viewport = Math.max(0, limit - 1);
|
|
413
|
+
const maxScroll = Math.max(0, lines.length - viewport);
|
|
414
|
+
this.detailScroll = Math.min(Math.max(0, this.detailScroll), maxScroll);
|
|
415
|
+
const help = maxScroll > 0 ? "up/down scroll esc back q close" : "esc back q close";
|
|
416
|
+
if (viewport === 0) return [{ text: this.hint(help) }];
|
|
417
|
+
return [...lines.slice(this.detailScroll, this.detailScroll + viewport), { text: this.hint(help) }];
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
class AgentSnapshotPanelExtension {
|
|
422
|
+
readonly channel: SnapshotChannel;
|
|
423
|
+
private active: AgentsPanel | undefined;
|
|
424
|
+
|
|
425
|
+
constructor(private readonly pi: PanelExtensionAPI) {
|
|
426
|
+
this.channel = new SnapshotChannel(pi);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
register(): void {
|
|
430
|
+
this.pi.on("session_shutdown", () => {
|
|
431
|
+
this.active?.stop();
|
|
432
|
+
this.active = undefined;
|
|
433
|
+
});
|
|
434
|
+
this.pi.registerCommand("agents", {
|
|
435
|
+
description: "Show this session's agents",
|
|
436
|
+
handler: async (_args, ctx: PanelCommandContext) => {
|
|
437
|
+
if (ctx.mode !== "tui") {
|
|
438
|
+
if (ctx.hasUI) {
|
|
439
|
+
const state = await this.channel.requestSnapshot();
|
|
440
|
+
const text = state.kind === "available"
|
|
441
|
+
? textListing(state.agents)
|
|
442
|
+
: "No agent provider responded. Install an adapter that publishes agent snapshots.";
|
|
443
|
+
ctx.ui.notify(text, "info");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
throw new Error("The /agents panel needs an interactive terminal.");
|
|
447
|
+
}
|
|
448
|
+
let panel: AgentsPanel | undefined;
|
|
449
|
+
try {
|
|
450
|
+
await ctx.ui.custom(
|
|
451
|
+
(tui, theme, _keybindings, done) => {
|
|
452
|
+
panel = new AgentsPanel(this.channel, tui, theme, () => done(undefined));
|
|
453
|
+
this.active = panel;
|
|
454
|
+
panel.start();
|
|
455
|
+
return panel;
|
|
456
|
+
},
|
|
457
|
+
{ overlay: true, overlayOptions: { width: "80%", minWidth: 40, maxHeight: `${OVERLAY_MAX_HEIGHT_PERCENT}%`, anchor: "center", margin: OVERLAY_MARGIN } },
|
|
458
|
+
);
|
|
459
|
+
} finally {
|
|
460
|
+
panel?.stop();
|
|
461
|
+
if (this.active === panel) this.active = undefined;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export default function agentsPanel(pi: PanelExtensionAPI): void {
|
|
469
|
+
const thisExtension = new AgentSnapshotPanelExtension(pi);
|
|
470
|
+
thisExtension.register();
|
|
471
|
+
}
|
|
@@ -116,7 +116,7 @@ python3 "$SKILL_ROOT/scripts/scan_corrections.py" --out "$REPORT_DIR" \
|
|
|
116
116
|
--days <90, or N from --days> <--repo PATH... | --all-conversations>
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
-
This reads
|
|
119
|
+
This reads raw Claude Code, Codex and Pi session logs directly — every discovered session in the window, not just the sampled ones — and writes `$REPORT_DIR/corrections.json`: each user turn that looks like a correction, with session id, timestamp, cwd, branch, the matched pattern, the user's words, and a short excerpt of what the agent had just done. Pi project scope comes from the session header's `cwd`; its active parent-linked entry branch is read, while only user and assistant message text can contribute corrections. The report lists each supported source's status and file count, including missing sources. Grok Build and ZCode are not supported by this corrections pass; they are not counted as zero-correction sources. Use `source_status` as the coverage record. Carry missing or empty sources and `unsupported_sources` into the report notes; never describe this pass as all local history.
|
|
120
120
|
|
|
121
121
|
Not every `user` turn was typed by a human. Sub-agents, audit harnesses, judges and other skills all write turns that the log records as `user`, and they are long, templated and repeated verbatim across hundreds of sessions — left unfiltered they drown the real corrections. The script drops them by shape, not by name: turns above `--max-turn-chars`, turns whose normalized text repeats across `--template-threshold` or more distinct sessions, and turns carrying the self-session sentinel. Read `stats.turns_dropped_*` in the output: if the dropped counts dwarf the kept ones, say so — it means this corpus is mostly machine-driven and the corrections signal is thin.
|
|
122
122
|
|
|
@@ -216,6 +216,7 @@ Write `$REPORT_DIR/report.json`:
|
|
|
216
216
|
"harness": "<from inventory.json, or 'n/a'>",
|
|
217
217
|
"scope": "<repo name, project list, or 'all conversations'>",
|
|
218
218
|
"windows": {"corrections_days": 90, "adherence_days": 45},
|
|
219
|
+
"correction_sources": {"claude": "scanned|no_recent_sessions|not_found", "codex": "scanned|no_recent_sessions|not_found", "pi": "scanned|no_recent_sessions|not_found", "unsupported": ["grok", "zcode"]},
|
|
219
220
|
"stats": {
|
|
220
221
|
"files": 0, "rules": 0, "est_tokens": 0,
|
|
221
222
|
"sessions_scanned": 0, "sessions_judged": 0,
|