@esso0428/pi-subagents 0.15.16 → 0.16.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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/dist/agent-history-list.d.ts.map +1 -1
- package/dist/agent-history-list.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -6
- package/dist/index.js.map +1 -1
- package/dist/output-file.js +3 -3
- package/dist/output-file.js.map +1 -1
- package/dist/ui/agent-widget.d.ts +11 -4
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +85 -19
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/ccstyle/diff/ansi-utils.js +2 -2
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +15 -10
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.js +1 -2
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +19 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js +85 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-renderer.js +1 -1
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-result.d.ts +9 -1
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-result.js +137 -5
- package/dist/ui/ccstyle/tool-result.js.map +1 -1
- package/dist/ui/conversation-blocks.d.ts.map +1 -1
- package/dist/ui/conversation-blocks.js +14 -13
- package/dist/ui/conversation-blocks.js.map +1 -1
- package/dist/ui/conversation-role.d.ts +1 -1
- package/dist/ui/conversation-role.d.ts.map +1 -1
- package/dist/ui/conversation-search.js +4 -4
- package/dist/ui/conversation-search.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +17 -1
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +116 -43
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +5 -1
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +67 -26
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/ui/deferred-ui-refresh.d.ts +15 -0
- package/dist/ui/deferred-ui-refresh.d.ts.map +1 -0
- package/dist/ui/deferred-ui-refresh.js +50 -0
- package/dist/ui/deferred-ui-refresh.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +55 -14
- package/dist/ui/fleet-list.d.ts.map +1 -1
- package/dist/ui/fleet-list.js +178 -44
- package/dist/ui/fleet-list.js.map +1 -1
- package/dist/write-execution.d.ts.map +1 -1
- package/dist/write-execution.js +1 -1
- package/dist/write-execution.js.map +1 -1
- package/package.json +6 -5
- package/src/agent-history-list.ts +1 -1
- package/src/index.ts +49 -7
- package/src/output-file.ts +3 -3
- package/src/ui/agent-widget.ts +18 -10
- package/src/ui/ccstyle/diff/ansi-utils.ts +2 -2
- package/src/ui/ccstyle/diff/diff-presentation.ts +0 -1
- package/src/ui/ccstyle/diff/diff-renderer.ts +22 -21
- package/src/ui/ccstyle/diff/line-width-safety.ts +1 -2
- package/src/ui/ccstyle/diff/shiki-highlight.ts +97 -0
- package/src/ui/ccstyle/tool-renderer.ts +3 -3
- package/src/ui/ccstyle/tool-result.ts +175 -7
- package/src/ui/conversation-role.ts +1 -1
- package/src/ui/conversation-search.ts +4 -4
- package/src/ui/conversation-timeline.ts +7 -7
- package/src/ui/conversation-viewer.ts +16 -7
- package/src/ui/deferred-ui-refresh.ts +72 -0
- package/src/ui/fleet-list.ts +185 -50
- package/src/write-execution.ts +2 -2
- package/test/agent-history-list.test.ts +0 -121
- package/test/agent-manager-history.test.ts +0 -88
- package/test/agent-menu-history-navigation.test.ts +0 -86
- package/test/output-file-history.test.ts +0 -50
- package/test/ui/agent-widget.test.ts +0 -161
- package/test/ui/conversation-blocks.test.ts +0 -103
- package/test/ui/conversation-diff.test.ts +0 -91
- package/test/ui/conversation-search.test.ts +0 -55
- package/test/ui/conversation-viewer.test.ts +0 -438
- package/test/ui/fleet-list.test.ts +0 -135
- package/test/write-execution.test.ts +0 -83
- package/tsconfig.json +0 -16
package/src/ui/fleet-list.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* fleet-list.ts — Claude Code-style "FleetView" list rendered below the editor.
|
|
3
3
|
*
|
|
4
4
|
* Shows `main` + each running/queued subagent as a navigable list. Pressing ↓ (or
|
|
5
|
-
*
|
|
5
|
+
* `j`) at an empty prompt activates the list; ↑/↓ move the selection (filled ● marker),
|
|
6
6
|
* Enter opens the selected agent's live conversation overlay, Esc returns to the prompt.
|
|
7
7
|
* A viewer stays open when its agent finishes so the final output remains readable.
|
|
8
8
|
*
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* can `consume` keys — gated on `getEditorText() === ""` so normal typing is untouched.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
14
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import
|
|
15
|
+
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
17
16
|
import { canOpenActiveAgent } from "../agent-history-list.js";
|
|
17
|
+
import type { AgentManager } from "../agent-manager.js";
|
|
18
18
|
import type { AgentRecord } from "../types.js";
|
|
19
19
|
import { getLifetimeTotal } from "../usage.js";
|
|
20
20
|
import { type AgentActivity, getDisplayName, type Theme } from "./agent-widget.js";
|
|
@@ -28,11 +28,23 @@ const FLEET_BASE_ROWS = 2; // hint + main
|
|
|
28
28
|
const FLEET_BODY_ROWS = FLEET_MAX_RENDER_ROWS - FLEET_BASE_ROWS;
|
|
29
29
|
/** Re-render cadence so elapsed/token stats tick while agents run. */
|
|
30
30
|
const TICK_MS = 200;
|
|
31
|
+
/** Normalized mouse input accepted by FleetView's widget surface. */
|
|
32
|
+
export type FleetMouseEvent = { type: string; wheelDelta?: number };
|
|
33
|
+
|
|
34
|
+
type FleetMouseResult = { handled: true; render: true };
|
|
35
|
+
|
|
36
|
+
type FleetWidgetContent = {
|
|
37
|
+
render(width: number): string[];
|
|
38
|
+
invalidate(): void;
|
|
39
|
+
dispose?(): void;
|
|
40
|
+
handleMouse?(event: FleetMouseEvent): FleetMouseResult | undefined;
|
|
41
|
+
};
|
|
42
|
+
|
|
31
43
|
/** Minimal UI surface the FleetView needs from `ctx.ui` (structural subset). */
|
|
32
44
|
export type FleetUICtx = {
|
|
33
45
|
setWidget(
|
|
34
46
|
key: string,
|
|
35
|
-
content: undefined | ((tui: any, theme: Theme) =>
|
|
47
|
+
content: undefined | ((tui: any, theme: Theme) => FleetWidgetContent),
|
|
36
48
|
options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
37
49
|
): void;
|
|
38
50
|
onTerminalInput(handler: (data: string) => { consume?: boolean; data?: string } | undefined): () => void;
|
|
@@ -62,30 +74,70 @@ export function formatFleetTokens(count: number): string {
|
|
|
62
74
|
return `↓ ${compact} tokens`;
|
|
63
75
|
}
|
|
64
76
|
|
|
77
|
+
/** A bounded slice of the agent-only roster rendered by FleetView. */
|
|
78
|
+
export type FleetAgentWindow = {
|
|
79
|
+
start: number;
|
|
80
|
+
end: number;
|
|
81
|
+
hiddenAbove: number;
|
|
82
|
+
hiddenBelow: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
65
85
|
/**
|
|
66
|
-
* Calculate a bounded agent slice
|
|
67
|
-
* `
|
|
86
|
+
* Calculate a bounded agent slice from an agent-only viewport start.
|
|
87
|
+
* `end` is exclusive; `main` is rendered separately and is not part of this
|
|
88
|
+
* window. Marker rows are reserved only when the corresponding side is hidden.
|
|
68
89
|
*/
|
|
69
90
|
export function calculateFleetAgentWindow(
|
|
70
91
|
agentCount: number,
|
|
71
|
-
|
|
72
|
-
):
|
|
92
|
+
viewportStart: number,
|
|
93
|
+
): FleetAgentWindow {
|
|
73
94
|
const count = Math.max(0, Math.floor(agentCount));
|
|
74
95
|
if (count === 0) return { start: 0, end: 0, hiddenAbove: 0, hiddenBelow: 0 };
|
|
75
96
|
|
|
76
|
-
const
|
|
97
|
+
const maxStart = Math.max(0, count - (FLEET_BODY_ROWS - 1));
|
|
98
|
+
const start = Math.min(maxStart, Math.max(0, Math.floor(viewportStart)));
|
|
99
|
+
const hasAbove = start > 0;
|
|
100
|
+
const agentBudget = Math.max(1, FLEET_BODY_ROWS - (hasAbove ? 1 : 0));
|
|
101
|
+
let end = Math.min(count, start + agentBudget);
|
|
102
|
+
if (end < count) end -= 1; // reserve the ↓ marker row
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
start,
|
|
106
|
+
end,
|
|
107
|
+
hiddenAbove: start,
|
|
108
|
+
hiddenBelow: count - end,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Return the smallest viewport adjustment that makes an agent visible. The
|
|
114
|
+
* selected index is 0-based within the agent-only roster; `main` is handled by
|
|
115
|
+
* FleetList rather than this pure helper.
|
|
116
|
+
*/
|
|
117
|
+
export function ensureFleetSelectionVisible(
|
|
118
|
+
agentCount: number,
|
|
119
|
+
selectedAgentIndex: number,
|
|
120
|
+
viewportStart: number,
|
|
121
|
+
): number {
|
|
122
|
+
const count = Math.max(0, Math.floor(agentCount));
|
|
123
|
+
if (count === 0) return 0;
|
|
124
|
+
|
|
125
|
+
const selected = Math.min(
|
|
77
126
|
count - 1,
|
|
78
|
-
Math.max(0, Math.floor(
|
|
127
|
+
Math.max(0, Math.floor(selectedAgentIndex)),
|
|
79
128
|
);
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Math.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
129
|
+
const maxStart = Math.max(0, count - (FLEET_BODY_ROWS - 1));
|
|
130
|
+
let start = Math.min(maxStart, Math.max(0, Math.floor(viewportStart)));
|
|
131
|
+
const current = calculateFleetAgentWindow(count, start);
|
|
132
|
+
|
|
133
|
+
if (selected < current.start) {
|
|
134
|
+
return Math.min(maxStart, selected);
|
|
135
|
+
}
|
|
136
|
+
if (selected < current.end) return start;
|
|
137
|
+
|
|
138
|
+
// With a possible ↓ marker, selected - 1 is the least forward shift that
|
|
139
|
+
// exposes the selected row while preserving the marker row budget.
|
|
140
|
+
return Math.min(maxStart, Math.max(start, selected - 1));
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
/**
|
|
@@ -115,14 +167,15 @@ export class FleetList {
|
|
|
115
167
|
private active = false;
|
|
116
168
|
/** 0 = `main`, 1..N = subagents. */
|
|
117
169
|
private selectedIndex = 0;
|
|
170
|
+
/** 0-based start in the agent-only roster; main is rendered separately. */
|
|
171
|
+
private viewportStart = 0;
|
|
118
172
|
/** Set while a conversation overlay is open; calling it closes the overlay. */
|
|
119
173
|
private viewerClose: (() => void) | undefined;
|
|
120
174
|
private viewingAgentId: string | undefined;
|
|
121
175
|
|
|
122
176
|
constructor(
|
|
123
177
|
private manager: AgentManager,
|
|
124
|
-
private agentActivity: Map<string, AgentActivity>,
|
|
125
|
-
private getCwd: () => string | undefined = () => undefined,
|
|
178
|
+
private agentActivity: Map<string, AgentActivity>,_getCwd: () => string | undefined = () => undefined,
|
|
126
179
|
private pi?: ExtensionAPI,
|
|
127
180
|
private getCtx: () => ExtensionContext | undefined = () => undefined,
|
|
128
181
|
) {}
|
|
@@ -137,8 +190,8 @@ export class FleetList {
|
|
|
137
190
|
}
|
|
138
191
|
|
|
139
192
|
/** Capture the UI context and (re)register the global input handler. */
|
|
140
|
-
setUICtx(ui: FleetUICtx):
|
|
141
|
-
if (ui === this.ui) return;
|
|
193
|
+
setUICtx(ui: FleetUICtx): boolean {
|
|
194
|
+
if (ui === this.ui) return false;
|
|
142
195
|
this.inputUnsub?.();
|
|
143
196
|
this.ui = ui;
|
|
144
197
|
this.widgetRegistered = false;
|
|
@@ -146,6 +199,14 @@ export class FleetList {
|
|
|
146
199
|
this.lastRenderKey = undefined;
|
|
147
200
|
this.tui = undefined;
|
|
148
201
|
this.inputUnsub = ui.onTerminalInput(data => this.handleKey(data));
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Request a render on the currently registered TUI without touching input. */
|
|
206
|
+
requestUiRefresh(force = true): boolean {
|
|
207
|
+
if (!this.tui) return false;
|
|
208
|
+
this.tui.requestRender(force);
|
|
209
|
+
return true;
|
|
149
210
|
}
|
|
150
211
|
|
|
151
212
|
/** Ensure the re-render timer is running (called when an agent spawns). */
|
|
@@ -173,6 +234,8 @@ export class FleetList {
|
|
|
173
234
|
this.lastRenderKey = undefined;
|
|
174
235
|
this.tui = undefined;
|
|
175
236
|
this.active = false;
|
|
237
|
+
this.selectedIndex = 0;
|
|
238
|
+
this.viewportStart = 0;
|
|
176
239
|
// Null last so a `viewerClose()` microtask above can't re-register the widget.
|
|
177
240
|
this.ui = undefined;
|
|
178
241
|
}
|
|
@@ -195,12 +258,14 @@ export class FleetList {
|
|
|
195
258
|
if (this.timer) { clearInterval(this.timer); this.timer = undefined; }
|
|
196
259
|
this.active = false;
|
|
197
260
|
this.selectedIndex = 0;
|
|
261
|
+
this.viewportStart = 0;
|
|
198
262
|
this.lastRosterKey = undefined;
|
|
199
263
|
this.lastRenderKey = undefined;
|
|
200
264
|
return;
|
|
201
265
|
}
|
|
202
266
|
|
|
203
267
|
this.clampSelection();
|
|
268
|
+
this.ensureSelectionVisible();
|
|
204
269
|
this.ensureTimer(); // keep stats ticking whenever the list is shown (e.g. after a re-enable)
|
|
205
270
|
const renderKey = this.renderKey(records);
|
|
206
271
|
|
|
@@ -210,6 +275,7 @@ export class FleetList {
|
|
|
210
275
|
return {
|
|
211
276
|
render: (w: number) => this.renderBar(w, theme),
|
|
212
277
|
invalidate: () => { this.widgetRegistered = false; this.tui = undefined; },
|
|
278
|
+
handleMouse: (event: FleetMouseEvent) => this.handleMouse(event),
|
|
213
279
|
};
|
|
214
280
|
}, { placement: "belowEditor" });
|
|
215
281
|
this.widgetRegistered = true;
|
|
@@ -243,7 +309,13 @@ export class FleetList {
|
|
|
243
309
|
Math.round(((record.completedAt ?? now) - record.startedAt) / 1000),
|
|
244
310
|
].join("\u001f");
|
|
245
311
|
});
|
|
246
|
-
return [
|
|
312
|
+
return [
|
|
313
|
+
this.enabled ? "1" : "0",
|
|
314
|
+
this.active ? "1" : "0",
|
|
315
|
+
String(this.selectedIndex),
|
|
316
|
+
String(this.viewportStart),
|
|
317
|
+
...agentState,
|
|
318
|
+
].join("\u001e");
|
|
247
319
|
}
|
|
248
320
|
|
|
249
321
|
// ---- Roster ----
|
|
@@ -270,8 +342,84 @@ export class FleetList {
|
|
|
270
342
|
if (this.selectedIndex < 0) this.selectedIndex = 0;
|
|
271
343
|
}
|
|
272
344
|
|
|
345
|
+
/** Keep the selected agent visible without recentering an already-valid window. */
|
|
346
|
+
private ensureSelectionVisible(): void {
|
|
347
|
+
const agents = this.agentRecords();
|
|
348
|
+
if (agents.length === 0) {
|
|
349
|
+
this.viewportStart = 0;
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (this.selectedIndex > 0) {
|
|
353
|
+
this.viewportStart = ensureFleetSelectionVisible(
|
|
354
|
+
agents.length,
|
|
355
|
+
this.selectedIndex - 1,
|
|
356
|
+
this.viewportStart,
|
|
357
|
+
);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// `main` does not constrain the agent window, but a roster shrink can make
|
|
361
|
+
// its previous start invalid. Use the same pure clamp as renderBar.
|
|
362
|
+
this.viewportStart = calculateFleetAgentWindow(agents.length, this.viewportStart).start;
|
|
363
|
+
}
|
|
364
|
+
|
|
273
365
|
// ---- Key handling ----
|
|
274
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Move the selection while keeping keyboard and wheel input on one path.
|
|
369
|
+
* Returns whether the direction was handled; inactive upward input and input
|
|
370
|
+
* that cannot activate the list are left for the editor/transcript.
|
|
371
|
+
*/
|
|
372
|
+
private moveSelection(direction: -1 | 1): boolean {
|
|
373
|
+
const max = this.roster().length - 1;
|
|
374
|
+
if (max < 0) return false;
|
|
375
|
+
|
|
376
|
+
if (!this.active) {
|
|
377
|
+
// Match ↓ activation: only a downward direction at an empty prompt with
|
|
378
|
+
// the editor focused enters the list. Upward input remains a no-op.
|
|
379
|
+
if (direction !== 1 || !this.editorHasFocus() || this.agentRecords().length === 0 || this.ui?.getEditorText() !== "") {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
this.active = true;
|
|
383
|
+
this.selectedIndex = 0;
|
|
384
|
+
this.viewportStart = 0;
|
|
385
|
+
this.update();
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
this.clampSelection();
|
|
390
|
+
if (direction === -1 && this.selectedIndex === 0) {
|
|
391
|
+
this.deactivate();
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
this.selectedIndex = direction === 1
|
|
395
|
+
? Math.min(max, this.selectedIndex + 1)
|
|
396
|
+
: Math.max(0, this.selectedIndex - 1);
|
|
397
|
+
this.ensureSelectionVisible();
|
|
398
|
+
this.update();
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Handle normalized wheel input from the registered widget. */
|
|
403
|
+
handleMouse(event: FleetMouseEvent): FleetMouseResult | undefined {
|
|
404
|
+
if (!this.enabled || !this.ui || this.viewerClose) return undefined;
|
|
405
|
+
if (event.type !== "wheel") return undefined;
|
|
406
|
+
|
|
407
|
+
const wheelDelta = event.wheelDelta;
|
|
408
|
+
const direction: -1 | 1 | undefined = wheelDelta == null
|
|
409
|
+
? undefined
|
|
410
|
+
: wheelDelta < 0
|
|
411
|
+
? -1
|
|
412
|
+
: wheelDelta > 0
|
|
413
|
+
? 1
|
|
414
|
+
: undefined;
|
|
415
|
+
if (!direction || !this.moveSelection(direction)) return undefined;
|
|
416
|
+
|
|
417
|
+
// The widget handled the normalized wheel, including at selection
|
|
418
|
+
// boundaries; explicitly request a redraw so it never falls through to
|
|
419
|
+
// Pi's transcript fallback.
|
|
420
|
+
return { handled: true, render: true };
|
|
421
|
+
}
|
|
422
|
+
|
|
275
423
|
/** Returns `{consume:true}` to swallow a key, or undefined to let it through. */
|
|
276
424
|
handleKey(data: string): { consume?: boolean; data?: string } | undefined {
|
|
277
425
|
if (!this.enabled || !this.ui) return undefined;
|
|
@@ -290,31 +438,17 @@ export class FleetList {
|
|
|
290
438
|
return undefined;
|
|
291
439
|
}
|
|
292
440
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return { consume: true };
|
|
301
|
-
}
|
|
302
|
-
return undefined;
|
|
303
|
-
}
|
|
441
|
+
const direction: -1 | 1 | undefined = matchesKey(data, "down") || matchesKey(data, "j")
|
|
442
|
+
|| (!this.active && matchesKey(data, "left"))
|
|
443
|
+
? 1
|
|
444
|
+
: matchesKey(data, "up") || matchesKey(data, "k")
|
|
445
|
+
? -1
|
|
446
|
+
: undefined;
|
|
447
|
+
if (direction && this.moveSelection(direction)) return { consume: true };
|
|
304
448
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
this.selectedIndex = Math.min(max, this.selectedIndex + 1);
|
|
309
|
-
this.update();
|
|
310
|
-
return { consume: true };
|
|
311
|
-
}
|
|
312
|
-
if (matchesKey(data, "up")) {
|
|
313
|
-
if (this.selectedIndex === 0) { this.deactivate(); return { consume: true }; }
|
|
314
|
-
this.selectedIndex -= 1;
|
|
315
|
-
this.update();
|
|
316
|
-
return { consume: true };
|
|
317
|
-
}
|
|
449
|
+
if (!this.active) return undefined;
|
|
450
|
+
|
|
451
|
+
// Active — Enter opens, Esc / Up-past-top exits.
|
|
318
452
|
if (matchesKey(data, "escape")) { this.deactivate(); return { consume: true }; }
|
|
319
453
|
if (matchesKey(data, Key.enter)) { this.openSelected(); return { consume: true }; }
|
|
320
454
|
|
|
@@ -339,6 +473,7 @@ export class FleetList {
|
|
|
339
473
|
private deactivate(): void {
|
|
340
474
|
this.active = false;
|
|
341
475
|
this.selectedIndex = 0;
|
|
476
|
+
this.viewportStart = 0;
|
|
342
477
|
this.update();
|
|
343
478
|
}
|
|
344
479
|
|
|
@@ -416,12 +551,12 @@ export class FleetList {
|
|
|
416
551
|
|
|
417
552
|
const hint = this.active
|
|
418
553
|
? "↑↓ select · enter view · esc back"
|
|
419
|
-
: "esc to interrupt ·
|
|
554
|
+
: "esc to interrupt · ↓ to manage";
|
|
420
555
|
const lines: string[] = [];
|
|
421
556
|
lines.push(truncateToWidth(" " + theme.fg("dim", hint), width));
|
|
422
557
|
lines.push(truncateToWidth(` ${this.bullet(0, sel, theme)} main`, width));
|
|
423
558
|
|
|
424
|
-
const window = calculateFleetAgentWindow(agents.length,
|
|
559
|
+
const window = calculateFleetAgentWindow(agents.length, this.viewportStart);
|
|
425
560
|
if (window.hiddenAbove > 0) {
|
|
426
561
|
lines.push(rightAlign("", theme.fg("dim", `↑ ${window.hiddenAbove} more`), width));
|
|
427
562
|
}
|
package/src/write-execution.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createWriteToolDefinition, generateDiffString, generateUnifiedPatch, type ExtensionContext, type ToolDefinition, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { createWriteToolDefinition, type ExtensionContext, generateDiffString, generateUnifiedPatch, type ToolDefinition, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
/** Maximum UTF-8 size of the durable display diff plus unified patch. */
|
|
6
6
|
export const MAX_DURABLE_DIFF_BYTES = 4 * 1024 * 1024;
|
|
@@ -58,7 +58,7 @@ function resolveWritePath(path: string, cwd: string): string {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async function capturePreviousContent(absolutePath: string): Promise<WriteExecutionMeta> {
|
|
61
|
-
let info
|
|
61
|
+
let info: Awaited<ReturnType<typeof lstat>>;
|
|
62
62
|
try {
|
|
63
63
|
info = await lstat(absolutePath);
|
|
64
64
|
} catch (error: any) {
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
6
|
-
buildAgentStatusMenuEntries,
|
|
7
|
-
canOpenActiveAgent,
|
|
8
|
-
canOpenAgentHistory,
|
|
9
|
-
formatAgentHistoryOption,
|
|
10
|
-
isTerminalAgentStatus,
|
|
11
|
-
splitAgentRecords,
|
|
12
|
-
} from "../src/agent-history-list.js";
|
|
13
|
-
|
|
14
|
-
type TestRecord = {
|
|
15
|
-
status: string;
|
|
16
|
-
session?: object;
|
|
17
|
-
transcriptPath?: string;
|
|
18
|
-
type?: string;
|
|
19
|
-
description?: string;
|
|
20
|
-
startedAt?: number;
|
|
21
|
-
completedAt?: number;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const tempDirectories: string[] = [];
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
for (const directory of tempDirectories.splice(0)) {
|
|
28
|
-
rmSync(directory, { recursive: true, force: true });
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
function record(overrides: TestRecord): any {
|
|
33
|
-
return {
|
|
34
|
-
id: "agent-1",
|
|
35
|
-
type: "Explore",
|
|
36
|
-
description: "Inspect the repository",
|
|
37
|
-
status: "completed",
|
|
38
|
-
toolUses: 0,
|
|
39
|
-
startedAt: 1_000,
|
|
40
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
41
|
-
compactionCount: 0,
|
|
42
|
-
...overrides,
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe("agent history list helpers", () => {
|
|
47
|
-
it("recognizes exactly the five terminal statuses", () => {
|
|
48
|
-
for (const status of ["completed", "steered", "stopped", "aborted", "error"]) {
|
|
49
|
-
expect(isTerminalAgentStatus(status)).toBe(true);
|
|
50
|
-
}
|
|
51
|
-
for (const status of ["running", "queued", "unknown", ""]) {
|
|
52
|
-
expect(isTerminalAgentStatus(status)).toBe(false);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("separates active and terminal records without mutating input", () => {
|
|
57
|
-
const done = record({ status: "completed", session: {} });
|
|
58
|
-
const running = record({ status: "running", session: {} });
|
|
59
|
-
const stopped = record({ status: "stopped", session: {} });
|
|
60
|
-
const queued = record({ status: "queued", session: {} });
|
|
61
|
-
const input = [done, running, stopped, queued];
|
|
62
|
-
const before = [...input];
|
|
63
|
-
|
|
64
|
-
const result = splitAgentRecords(input, undefined);
|
|
65
|
-
|
|
66
|
-
expect(result.active).toEqual([running, queued]);
|
|
67
|
-
expect(result.history).toEqual([done, stopped]);
|
|
68
|
-
expect(input).toEqual(before);
|
|
69
|
-
expect(input).toEqual([done, running, stopped, queued]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it("requires a session or existing transcript for history", () => {
|
|
73
|
-
expect(canOpenAgentHistory(record({ session: undefined }), undefined)).toBe(false);
|
|
74
|
-
expect(canOpenAgentHistory(record({ session: {} }), undefined)).toBe(true);
|
|
75
|
-
expect(canOpenAgentHistory(record({ status: "running", session: {} }), undefined)).toBe(false);
|
|
76
|
-
expect(canOpenActiveAgent(record({ status: "running", session: {} }))).toBe(true);
|
|
77
|
-
expect(canOpenActiveAgent(record({ status: "queued", session: undefined }))).toBe(false);
|
|
78
|
-
expect(canOpenActiveAgent(record({ status: "completed", session: {} }))).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("opens history only for an existing project-local transcript", () => {
|
|
82
|
-
const cwd = mkdtempSync(join(tmpdir(), "pi-subagents-history-list-"));
|
|
83
|
-
tempDirectories.push(cwd);
|
|
84
|
-
const transcriptPath = ".pi-subagents/agent-transcripts/agent.jsonl";
|
|
85
|
-
mkdirSync(join(cwd, ".pi-subagents", "agent-transcripts"), { recursive: true });
|
|
86
|
-
const historical = record({ session: undefined, transcriptPath });
|
|
87
|
-
|
|
88
|
-
expect(canOpenAgentHistory(historical, cwd)).toBe(false);
|
|
89
|
-
writeFileSync(join(cwd, transcriptPath), "{}\n");
|
|
90
|
-
expect(canOpenAgentHistory(historical, cwd)).toBe(true);
|
|
91
|
-
expect(canOpenAgentHistory(historical, undefined)).toBe(false);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("builds separate menu entries with openable records only", () => {
|
|
95
|
-
const entries = buildAgentStatusMenuEntries([
|
|
96
|
-
record({ status: "running", session: {} }),
|
|
97
|
-
record({ status: "queued", session: undefined }),
|
|
98
|
-
record({ status: "completed", session: {} }),
|
|
99
|
-
record({ status: "error", session: {} }),
|
|
100
|
-
record({ status: "stopped", session: undefined }),
|
|
101
|
-
], undefined);
|
|
102
|
-
|
|
103
|
-
expect(entries).toEqual(["Running agents (1)", "Agent history (2)"]);
|
|
104
|
-
expect(buildAgentStatusMenuEntries([record({ status: "queued", session: undefined })], undefined)).toEqual([]);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it("formats a bounded history option using the display name and duration", () => {
|
|
108
|
-
const option = formatAgentHistoryOption(record({
|
|
109
|
-
status: "completed",
|
|
110
|
-
type: "Explore",
|
|
111
|
-
description: "A very long description ".repeat(20),
|
|
112
|
-
startedAt: 1_000,
|
|
113
|
-
completedAt: 3_000,
|
|
114
|
-
}), 9_000);
|
|
115
|
-
|
|
116
|
-
expect(option).toContain("[completed]");
|
|
117
|
-
expect(option).toContain("Explore");
|
|
118
|
-
expect(option).toContain("2.0s");
|
|
119
|
-
expect(option.length).toBeLessThanOrEqual(140);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
-
import { AgentManager } from "../src/agent-manager.js";
|
|
3
|
-
|
|
4
|
-
const managers: AgentManager[] = [];
|
|
5
|
-
|
|
6
|
-
afterEach(() => {
|
|
7
|
-
for (const manager of managers.splice(0)) manager.dispose();
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
function makeManager(): AgentManager {
|
|
11
|
-
const manager = new AgentManager();
|
|
12
|
-
managers.push(manager);
|
|
13
|
-
return manager;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function record(overrides: Record<string, unknown> = {}) {
|
|
17
|
-
return {
|
|
18
|
-
id: "agent-1",
|
|
19
|
-
type: "Explore",
|
|
20
|
-
description: "Inspect the repository",
|
|
21
|
-
status: "completed",
|
|
22
|
-
result: "done",
|
|
23
|
-
toolUses: 2,
|
|
24
|
-
startedAt: 1_000,
|
|
25
|
-
completedAt: 2_000,
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("AgentManager.restoreCompleted", () => {
|
|
31
|
-
it("restores all five terminal statuses without sessions", () => {
|
|
32
|
-
const manager = makeManager();
|
|
33
|
-
manager.restoreCompleted(["completed", "steered", "stopped", "aborted", "error"].map((status, i) =>
|
|
34
|
-
record({ id: `agent-${i}`, status }),
|
|
35
|
-
));
|
|
36
|
-
|
|
37
|
-
expect(manager.listAgents()).toHaveLength(5);
|
|
38
|
-
for (const restored of manager.listAgents()) {
|
|
39
|
-
expect(["completed", "steered", "stopped", "aborted", "error"]).toContain(restored.status);
|
|
40
|
-
expect(restored.session).toBeUndefined();
|
|
41
|
-
expect(restored.abortController).toBeUndefined();
|
|
42
|
-
expect(restored.promise).toBeUndefined();
|
|
43
|
-
expect(restored.lifetimeUsage).toEqual({ input: 0, output: 0, cacheWrite: 0 });
|
|
44
|
-
expect(restored.compactionCount).toBe(0);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("ignores running, queued, and invalid records", () => {
|
|
49
|
-
const manager = makeManager();
|
|
50
|
-
manager.restoreCompleted([
|
|
51
|
-
record({ id: "running", status: "running" }),
|
|
52
|
-
record({ id: "queued", status: "queued" }),
|
|
53
|
-
{ id: "invalid" },
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
expect(manager.listAgents()).toEqual([]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("keeps the last valid duplicate input", () => {
|
|
60
|
-
const manager = makeManager();
|
|
61
|
-
manager.restoreCompleted([
|
|
62
|
-
record({ result: "first", startedAt: 1_000, completedAt: 1_100 }),
|
|
63
|
-
record({ result: "last", startedAt: 2_000, completedAt: 2_100 }),
|
|
64
|
-
]);
|
|
65
|
-
|
|
66
|
-
expect(manager.getRecord("agent-1")?.result).toBe("last");
|
|
67
|
-
expect(manager.getRecord("agent-1")?.startedAt).toBe(2_000);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("does not replace a live record and rejects unsafe persisted data", () => {
|
|
71
|
-
const manager = makeManager();
|
|
72
|
-
// Exercise the public map through a restored terminal, then make it live
|
|
73
|
-
// without starting an LLM session.
|
|
74
|
-
manager.restoreCompleted([record({ id: "live", result: "original" })]);
|
|
75
|
-
const liveRecord = manager.getRecord("live")!;
|
|
76
|
-
liveRecord.status = "running";
|
|
77
|
-
|
|
78
|
-
manager.restoreCompleted([
|
|
79
|
-
record({ id: "live", result: "replacement" }),
|
|
80
|
-
record({ id: "bad-time", startedAt: Number.NaN }),
|
|
81
|
-
record({ id: "bad-path", transcriptPath: "../../outside.jsonl" }),
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
|
-
expect(manager.getRecord("live")?.result).toBe("original");
|
|
85
|
-
expect(manager.getRecord("bad-time")).toBeUndefined();
|
|
86
|
-
expect(manager.getRecord("bad-path")).toBeUndefined();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import extension from "../src/index.js";
|
|
6
|
-
import { agentHistoryLocator } from "../src/agent-history.js";
|
|
7
|
-
|
|
8
|
-
const tempDirectories: string[] = [];
|
|
9
|
-
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
for (const directory of tempDirectories.splice(0)) rmSync(directory, { recursive: true, force: true });
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
describe("/agents history navigation", () => {
|
|
15
|
-
it("returns to the Agents menu after closing a history viewer", async () => {
|
|
16
|
-
const cwd = mkdtempSync(join(tmpdir(), "pi-subagents-menu-test-"));
|
|
17
|
-
tempDirectories.push(cwd);
|
|
18
|
-
const transcript = ".pi-subagents/agent-transcripts/agent-1.jsonl";
|
|
19
|
-
mkdirSync(join(cwd, ".pi-subagents/agent-transcripts"), { recursive: true });
|
|
20
|
-
writeFileSync(join(cwd, transcript), JSON.stringify({ message: { role: "user", content: "hello" } }) + "\n");
|
|
21
|
-
|
|
22
|
-
const eventHandlers = new Map<string, Set<(...args: any[]) => any>>();
|
|
23
|
-
const events = {
|
|
24
|
-
on: (name: string, handler: (...args: any[]) => any) => {
|
|
25
|
-
const handlersForEvent = eventHandlers.get(name) ?? new Set<(...args: any[]) => any>();
|
|
26
|
-
handlersForEvent.add(handler);
|
|
27
|
-
eventHandlers.set(name, handlersForEvent);
|
|
28
|
-
return () => handlersForEvent.delete(handler);
|
|
29
|
-
},
|
|
30
|
-
emit: (name: string, ...args: any[]) => {
|
|
31
|
-
for (const handler of eventHandlers.get(name) ?? []) handler(...args);
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
const handlers = new Map<string, (...args: any[]) => any>();
|
|
35
|
-
let command: { handler: (_args: unknown, ctx: any) => Promise<void> } | undefined;
|
|
36
|
-
const pi: any = {
|
|
37
|
-
events,
|
|
38
|
-
registerMessageRenderer: () => {},
|
|
39
|
-
registerTool: () => {},
|
|
40
|
-
registerCommand: (_name: string, definition: typeof command) => { command = definition; },
|
|
41
|
-
on: (name: string, handler: (...args: any[]) => any) => { handlers.set(name, handler); },
|
|
42
|
-
appendEntry: () => {},
|
|
43
|
-
sendMessage: () => {},
|
|
44
|
-
};
|
|
45
|
-
extension(pi);
|
|
46
|
-
|
|
47
|
-
const titles: string[] = [];
|
|
48
|
-
let historySelections = 0;
|
|
49
|
-
const ui = {
|
|
50
|
-
notify: () => {},
|
|
51
|
-
setWidget: () => {},
|
|
52
|
-
setStatus: () => {},
|
|
53
|
-
onTerminalInput: () => () => {},
|
|
54
|
-
select: async (title: string, options: string[]) => {
|
|
55
|
-
titles.push(title);
|
|
56
|
-
if (title === "Agents" && titles.filter(item => item === "Agents").length === 1) {
|
|
57
|
-
return options.find(option => option.startsWith("Agent history ("));
|
|
58
|
-
}
|
|
59
|
-
if (title === "Agent history" && historySelections++ === 0) return options[0];
|
|
60
|
-
return undefined;
|
|
61
|
-
},
|
|
62
|
-
custom: async () => undefined,
|
|
63
|
-
};
|
|
64
|
-
const ctx = {
|
|
65
|
-
cwd,
|
|
66
|
-
ui,
|
|
67
|
-
sessionManager: {
|
|
68
|
-
getSessionId: () => undefined,
|
|
69
|
-
getBranch: () => [{
|
|
70
|
-
type: "custom",
|
|
71
|
-
customType: "subagents:record",
|
|
72
|
-
data: {
|
|
73
|
-
id: "agent-1", type: "Explore", description: "Inspect", status: "completed",
|
|
74
|
-
startedAt: 1, completedAt: 2, transcriptPath: agentHistoryLocator(cwd, join(cwd, transcript)),
|
|
75
|
-
},
|
|
76
|
-
}],
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
await handlers.get("session_start")?.({}, ctx);
|
|
81
|
-
await command?.handler({}, ctx);
|
|
82
|
-
await handlers.get("session_shutdown")?.();
|
|
83
|
-
|
|
84
|
-
expect(titles).toEqual(["Agents", "Agent history", "Agents"]);
|
|
85
|
-
});
|
|
86
|
-
});
|