@esso0428/pi-subagents 0.15.5 → 0.15.7
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 +10 -19
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +19 -0
- package/dist/agent-history-list.d.ts.map +1 -0
- package/dist/agent-history-list.js +69 -0
- package/dist/agent-history-list.js.map +1 -0
- package/dist/agent-history.d.ts +2 -1
- package/dist/agent-history.d.ts.map +1 -0
- package/dist/agent-history.js +32 -34
- package/dist/agent-history.js.map +1 -0
- package/dist/agent-manager.d.ts +24 -8
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +171 -93
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-runner.d.ts +1 -0
- package/dist/agent-runner.d.ts.map +1 -0
- package/dist/agent-runner.js +75 -86
- package/dist/agent-runner.js.map +1 -0
- package/dist/agent-types.d.ts +1 -0
- package/dist/agent-types.d.ts.map +1 -0
- package/dist/agent-types.js +28 -45
- package/dist/agent-types.js.map +1 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +3 -6
- package/dist/context.js.map +1 -0
- package/dist/cross-extension-rpc.d.ts +1 -0
- package/dist/cross-extension-rpc.d.ts.map +1 -0
- package/dist/cross-extension-rpc.js +6 -9
- package/dist/cross-extension-rpc.js.map +1 -0
- package/dist/custom-agents.d.ts +1 -0
- package/dist/custom-agents.d.ts.map +1 -0
- package/dist/custom-agents.js +16 -18
- package/dist/custom-agents.js.map +1 -0
- package/dist/default-agents.d.ts +1 -0
- package/dist/default-agents.d.ts.map +1 -0
- package/dist/default-agents.js +2 -4
- package/dist/default-agents.js.map +1 -0
- package/dist/enabled-models.d.ts +1 -0
- package/dist/enabled-models.d.ts.map +1 -0
- package/dist/enabled-models.js +12 -16
- package/dist/enabled-models.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -4
- package/dist/env.js.map +1 -0
- package/dist/group-join.d.ts +1 -0
- package/dist/group-join.d.ts.map +1 -0
- package/dist/group-join.js +2 -5
- package/dist/group-join.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +306 -292
- package/dist/index.js.map +1 -0
- package/dist/invocation-config.d.ts +1 -0
- package/dist/invocation-config.d.ts.map +1 -0
- package/dist/invocation-config.js +3 -6
- package/dist/invocation-config.js.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +25 -34
- package/dist/memory.js.map +1 -0
- package/dist/model-resolver.d.ts +1 -0
- package/dist/model-resolver.d.ts.map +1 -0
- package/dist/model-resolver.js +2 -4
- package/dist/model-resolver.js.map +1 -0
- package/dist/nico-overrides.d.ts +1 -0
- package/dist/nico-overrides.d.ts.map +1 -0
- package/dist/nico-overrides.js +15 -20
- package/dist/nico-overrides.js.map +1 -0
- package/dist/output-file.d.ts +1 -0
- package/dist/output-file.d.ts.map +1 -0
- package/dist/output-file.js +17 -22
- package/dist/output-file.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +2 -4
- package/dist/prompts.js.map +1 -0
- package/dist/schedule-store.d.ts +1 -0
- package/dist/schedule-store.d.ts.map +1 -0
- package/dist/schedule-store.js +17 -21
- package/dist/schedule-store.js.map +1 -0
- package/dist/schedule.d.ts +1 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +9 -12
- package/dist/schedule.js.map +1 -0
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +16 -23
- package/dist/settings.js.map +1 -0
- package/dist/skill-loader.d.ts +1 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +21 -23
- package/dist/skill-loader.js.map +1 -0
- package/dist/status-note.d.ts +1 -0
- package/dist/status-note.d.ts.map +1 -0
- package/dist/status-note.js +2 -4
- package/dist/status-note.js.map +1 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -4
- package/dist/types.js.map +1 -0
- package/dist/ui/agent-widget.d.ts +2 -5
- package/dist/ui/agent-widget.d.ts.map +1 -0
- package/dist/ui/agent-widget.js +29 -51
- package/dist/ui/agent-widget.js.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.js +25 -0
- package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/types.d.ts +22 -0
- package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/types.js +10 -0
- package/dist/ui/ccstyle/diff/types.js.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.js +58 -0
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
- package/dist/ui/ccstyle/tool-result.d.ts +24 -0
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-result.js +193 -0
- package/dist/ui/ccstyle/tool-result.js.map +1 -0
- package/dist/ui/conversation-blocks.d.ts +46 -0
- package/dist/ui/conversation-blocks.d.ts.map +1 -0
- package/dist/ui/conversation-blocks.js +306 -0
- package/dist/ui/conversation-blocks.js.map +1 -0
- package/dist/ui/conversation-nvim.d.ts +8 -0
- package/dist/ui/conversation-nvim.d.ts.map +1 -0
- package/dist/ui/conversation-nvim.js +117 -0
- package/dist/ui/conversation-nvim.js.map +1 -0
- package/dist/ui/conversation-role.d.ts +13 -0
- package/dist/ui/conversation-role.d.ts.map +1 -0
- package/dist/ui/conversation-role.js +39 -0
- package/dist/ui/conversation-role.js.map +1 -0
- package/dist/ui/conversation-search.d.ts +39 -0
- package/dist/ui/conversation-search.d.ts.map +1 -0
- package/dist/ui/conversation-search.js +124 -0
- package/dist/ui/conversation-search.js.map +1 -0
- package/dist/ui/conversation-timeline.d.ts +62 -0
- package/dist/ui/conversation-timeline.d.ts.map +1 -0
- package/dist/ui/conversation-timeline.js +263 -0
- package/dist/ui/conversation-timeline.js.map +1 -0
- package/dist/ui/conversation-viewer.d.ts +62 -20
- package/dist/ui/conversation-viewer.d.ts.map +1 -0
- package/dist/ui/conversation-viewer.js +449 -224
- package/dist/ui/conversation-viewer.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +24 -11
- package/dist/ui/fleet-list.d.ts.map +1 -0
- package/dist/ui/fleet-list.js +86 -78
- package/dist/ui/fleet-list.js.map +1 -0
- package/dist/ui/schedule-menu.d.ts +1 -0
- package/dist/ui/schedule-menu.d.ts.map +1 -0
- package/dist/ui/schedule-menu.js +2 -4
- package/dist/ui/schedule-menu.js.map +1 -0
- package/dist/ui/viewer-keys.d.ts +1 -0
- package/dist/ui/viewer-keys.d.ts.map +1 -0
- package/dist/ui/viewer-keys.js +8 -10
- package/dist/ui/viewer-keys.js.map +1 -0
- package/dist/usage.d.ts +1 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +5 -10
- package/dist/usage.js.map +1 -0
- package/dist/worktree.d.ts +1 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +27 -31
- package/dist/worktree.js.map +1 -0
- package/dist/write-execution.d.ts +42 -0
- package/dist/write-execution.d.ts.map +1 -0
- package/dist/write-execution.js +138 -0
- package/dist/write-execution.js.map +1 -0
- package/package.json +1 -1
- package/src/agent-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +169 -81
- package/src/agent-runner.ts +12 -13
- package/src/index.ts +117 -98
- package/src/output-file.ts +1 -1
- package/src/types.ts +4 -1
- package/src/ui/agent-widget.ts +3 -9
- package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
- package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
- package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
- package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
- package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
- package/src/ui/ccstyle/diff/render-utils.ts +32 -0
- package/src/ui/ccstyle/diff/types.ts +33 -0
- package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
- package/src/ui/ccstyle/tool-renderer.ts +71 -0
- package/src/ui/ccstyle/tool-result.ts +238 -0
- package/src/ui/conversation-blocks.ts +404 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-role.ts +45 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-timeline.ts +315 -0
- package/src/ui/conversation-viewer.ts +418 -210
- package/src/ui/fleet-list.ts +73 -52
- package/src/write-execution.ts +180 -0
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/output-file-history.test.ts +50 -0
- package/test/ui/conversation-blocks.test.ts +103 -0
- package/test/ui/conversation-diff.test.ts +91 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +182 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/test/write-execution.test.ts +83 -0
- package/tsconfig.json +16 -0
- package/vitest.temp.config.mts +6 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
package/src/ui/fleet-list.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Shows `main` + each running/queued subagent as a navigable list. Pressing ↓ (or
|
|
5
5
|
* ←) 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
|
-
* A viewer stays open when its agent finishes
|
|
7
|
+
* A viewer stays open when its agent finishes so the final output remains readable.
|
|
8
8
|
*
|
|
9
9
|
* Mechanics (see plan): the list is a `belowEditor` widget (render-only), and ALL key
|
|
10
10
|
* handling goes through `onTerminalInput` — which fires before the focused editor and
|
|
@@ -12,22 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { Editor, isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import type { AgentManager } from "../agent-manager.js";
|
|
16
|
-
import {
|
|
17
|
+
import { canOpenActiveAgent } from "../agent-history-list.js";
|
|
17
18
|
import type { AgentRecord } from "../types.js";
|
|
18
19
|
import { getLifetimeTotal } from "../usage.js";
|
|
19
20
|
import { type AgentActivity, getDisplayName, type Theme } from "./agent-widget.js";
|
|
20
|
-
import { ConversationViewer,
|
|
21
|
+
import { ConversationViewer, VIEWPORT_HEIGHT_PCT } from "./conversation-viewer.js";
|
|
21
22
|
|
|
22
23
|
/** Widget key for the below-editor fleet list. */
|
|
23
24
|
const FLEET_KEY = "fleet";
|
|
24
|
-
/**
|
|
25
|
-
const
|
|
25
|
+
/** Maximum total rows contributed by FleetView, including hint and indicators. */
|
|
26
|
+
export const FLEET_MAX_RENDER_ROWS = 6;
|
|
27
|
+
const FLEET_BASE_ROWS = 2; // hint + main
|
|
28
|
+
const FLEET_BODY_ROWS = FLEET_MAX_RENDER_ROWS - FLEET_BASE_ROWS;
|
|
26
29
|
/** Re-render cadence so elapsed/token stats tick while agents run. */
|
|
27
30
|
const TICK_MS = 200;
|
|
28
|
-
/** How long a finished agent lingers in the list before it drops out. */
|
|
29
|
-
const FINISHED_LINGER_MS = 4000;
|
|
30
|
-
|
|
31
31
|
/** Minimal UI surface the FleetView needs from `ctx.ui` (structural subset). */
|
|
32
32
|
export type FleetUICtx = {
|
|
33
33
|
setWidget(
|
|
@@ -62,6 +62,32 @@ export function formatFleetTokens(count: number): string {
|
|
|
62
62
|
return `↓ ${compact} tokens`;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Calculate a bounded agent slice for the complete logical roster.
|
|
67
|
+
* `selectedRosterIndex` includes main at index 0; `end` is exclusive.
|
|
68
|
+
*/
|
|
69
|
+
export function calculateFleetAgentWindow(
|
|
70
|
+
agentCount: number,
|
|
71
|
+
selectedRosterIndex: number,
|
|
72
|
+
): { start: number; end: number; hiddenAbove: number; hiddenBelow: number } {
|
|
73
|
+
const count = Math.max(0, Math.floor(agentCount));
|
|
74
|
+
if (count === 0) return { start: 0, end: 0, hiddenAbove: 0, hiddenBelow: 0 };
|
|
75
|
+
|
|
76
|
+
const selectedAgent = Math.min(
|
|
77
|
+
count - 1,
|
|
78
|
+
Math.max(0, Math.floor(selectedRosterIndex) - 1),
|
|
79
|
+
);
|
|
80
|
+
const reserveAbove = selectedAgent > 0 ? 1 : 0;
|
|
81
|
+
const reserveBelow = selectedAgent < count - 1 ? 1 : 0;
|
|
82
|
+
const visibleCount = Math.max(1, FLEET_BODY_ROWS - reserveAbove - reserveBelow);
|
|
83
|
+
const start = Math.min(
|
|
84
|
+
Math.max(0, selectedAgent - visibleCount + 1),
|
|
85
|
+
Math.max(0, count - visibleCount),
|
|
86
|
+
);
|
|
87
|
+
const end = Math.min(count, start + visibleCount);
|
|
88
|
+
return { start, end, hiddenAbove: start, hiddenBelow: count - end };
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
/**
|
|
66
92
|
* Place `right` flush to `width`, truncating `left` first so the stats survive.
|
|
67
93
|
* The final clamp guarantees the line never exceeds `width` (which would wrap and
|
|
@@ -80,6 +106,7 @@ export class FleetList {
|
|
|
80
106
|
private tui: any | undefined;
|
|
81
107
|
private inputUnsub: (() => void) | undefined;
|
|
82
108
|
private widgetRegistered = false;
|
|
109
|
+
private lastRosterKey: string | undefined;
|
|
83
110
|
private timer: ReturnType<typeof setInterval> | undefined;
|
|
84
111
|
|
|
85
112
|
private enabled = true;
|
|
@@ -95,6 +122,8 @@ export class FleetList {
|
|
|
95
122
|
private manager: AgentManager,
|
|
96
123
|
private agentActivity: Map<string, AgentActivity>,
|
|
97
124
|
private getCwd: () => string | undefined = () => undefined,
|
|
125
|
+
private pi?: ExtensionAPI,
|
|
126
|
+
private getCtx: () => ExtensionContext | undefined = () => undefined,
|
|
98
127
|
) {}
|
|
99
128
|
|
|
100
129
|
// ---- Lifecycle ----
|
|
@@ -112,6 +141,7 @@ export class FleetList {
|
|
|
112
141
|
this.inputUnsub?.();
|
|
113
142
|
this.ui = ui;
|
|
114
143
|
this.widgetRegistered = false;
|
|
144
|
+
this.lastRosterKey = undefined;
|
|
115
145
|
this.tui = undefined;
|
|
116
146
|
this.inputUnsub = ui.onTerminalInput(data => this.handleKey(data));
|
|
117
147
|
}
|
|
@@ -123,7 +153,7 @@ export class FleetList {
|
|
|
123
153
|
|
|
124
154
|
/**
|
|
125
155
|
* Called when an agent finishes. The viewer (if open on it) stays open so the
|
|
126
|
-
* final output remains readable
|
|
156
|
+
* final output remains readable; refresh the active-only roster.
|
|
127
157
|
*/
|
|
128
158
|
onAgentFinished(_id: string): void {
|
|
129
159
|
this.update();
|
|
@@ -137,6 +167,7 @@ export class FleetList {
|
|
|
137
167
|
this.viewingAgentId = undefined;
|
|
138
168
|
if (this.ui && this.widgetRegistered) this.ui.setWidget(FLEET_KEY, undefined);
|
|
139
169
|
this.widgetRegistered = false;
|
|
170
|
+
this.lastRosterKey = undefined;
|
|
140
171
|
this.tui = undefined;
|
|
141
172
|
this.active = false;
|
|
142
173
|
// Null last so a `viewerClose()` microtask above can't re-register the widget.
|
|
@@ -146,7 +177,11 @@ export class FleetList {
|
|
|
146
177
|
/** Re-register/refresh the below-editor widget; clears it when no agents remain. */
|
|
147
178
|
update(): void {
|
|
148
179
|
if (!this.ui) return;
|
|
149
|
-
const
|
|
180
|
+
const records = this.agentRecords();
|
|
181
|
+
const hasAgents = this.enabled && records.length > 0;
|
|
182
|
+
const rosterKey = records.map(record => record.id).join("\0");
|
|
183
|
+
const rosterChanged = rosterKey !== this.lastRosterKey;
|
|
184
|
+
this.lastRosterKey = rosterKey;
|
|
150
185
|
|
|
151
186
|
if (!hasAgents) {
|
|
152
187
|
if (this.widgetRegistered) {
|
|
@@ -157,6 +192,7 @@ export class FleetList {
|
|
|
157
192
|
if (this.timer) { clearInterval(this.timer); this.timer = undefined; }
|
|
158
193
|
this.active = false;
|
|
159
194
|
this.selectedIndex = 0;
|
|
195
|
+
this.lastRosterKey = undefined;
|
|
160
196
|
return;
|
|
161
197
|
}
|
|
162
198
|
|
|
@@ -173,35 +209,21 @@ export class FleetList {
|
|
|
173
209
|
}, { placement: "belowEditor" });
|
|
174
210
|
this.widgetRegistered = true;
|
|
175
211
|
} else {
|
|
176
|
-
this.tui?.requestRender();
|
|
212
|
+
this.tui?.requestRender(rosterChanged);
|
|
177
213
|
}
|
|
178
214
|
}
|
|
179
215
|
|
|
180
216
|
// ---- Roster ----
|
|
181
217
|
|
|
182
218
|
/**
|
|
183
|
-
*
|
|
184
|
-
* started sooner sit at the top.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* Included: running/queued, plus the agent currently being viewed, plus
|
|
188
|
-
* recently-finished ones (they linger briefly before dropping out).
|
|
189
|
-
* Pending agents with no session yet are hidden until they start.
|
|
190
|
-
* (`listAgents()` is newest-first, so we re-sort.)
|
|
219
|
+
* Active agents shown in the list, ordered earliest-launched first so the
|
|
220
|
+
* ones you started sooner sit at the top. Pending agents with no session yet
|
|
221
|
+
* are hidden until they start. (`listAgents()` is newest-first, so we
|
|
222
|
+
* re-sort.)
|
|
191
223
|
*/
|
|
192
224
|
private agentRecords(): AgentRecord[] {
|
|
193
|
-
const now = Date.now();
|
|
194
225
|
return this.manager.listAgents()
|
|
195
|
-
.filter(
|
|
196
|
-
const live = a.session && (
|
|
197
|
-
a.status === "running" || a.status === "queued"
|
|
198
|
-
|| a.id === this.viewingAgentId
|
|
199
|
-
|| (a.completedAt != null && now - a.completedAt < FINISHED_LINGER_MS)
|
|
200
|
-
);
|
|
201
|
-
const cwd = this.getCwd();
|
|
202
|
-
const history = cwd !== undefined && hasAgentHistory(cwd, a.transcriptPath);
|
|
203
|
-
return !!live || history;
|
|
204
|
-
})
|
|
226
|
+
.filter(record => canOpenActiveAgent(record))
|
|
205
227
|
.sort((a, b) => a.startedAt - b.startedAt);
|
|
206
228
|
}
|
|
207
229
|
|
|
@@ -296,18 +318,18 @@ export class FleetList {
|
|
|
296
318
|
}
|
|
297
319
|
const record = entry.record;
|
|
298
320
|
if (!this.ui) return;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
321
|
+
if (!canOpenActiveAgent(record) || !record.session) {
|
|
322
|
+
this.ui.notify(`Agent is ${record.status} — no live session available.`, "info");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const session = record.session;
|
|
326
|
+
const ctx = this.getCtx();
|
|
327
|
+
const pi = this.pi;
|
|
328
|
+
if (!ctx || !pi) {
|
|
329
|
+
this.ui.notify("Agent conversation is unavailable — no active session.", "info");
|
|
307
330
|
return;
|
|
308
331
|
}
|
|
309
332
|
const activity = this.agentActivity.get(record.id);
|
|
310
|
-
const isLive = record.session !== undefined;
|
|
311
333
|
this.viewingAgentId = record.id;
|
|
312
334
|
|
|
313
335
|
void this.ui.custom<undefined>(
|
|
@@ -320,11 +342,12 @@ export class FleetList {
|
|
|
320
342
|
activity,
|
|
321
343
|
theme,
|
|
322
344
|
done,
|
|
323
|
-
|
|
345
|
+
() => {
|
|
324
346
|
if (this.manager.abort(record.id)) this.ui?.notify(`Stopped "${record.description}".`, "info");
|
|
325
|
-
}
|
|
347
|
+
},
|
|
326
348
|
keybindings,
|
|
327
|
-
|
|
349
|
+
(message: string) => this.manager.steer(record.id, message),
|
|
350
|
+
{ pi, ctx, readOnly: false },
|
|
328
351
|
);
|
|
329
352
|
},
|
|
330
353
|
{
|
|
@@ -363,20 +386,18 @@ export class FleetList {
|
|
|
363
386
|
: "esc to interrupt · ← for agents · ↓ to manage";
|
|
364
387
|
const lines: string[] = [];
|
|
365
388
|
lines.push(truncateToWidth(" " + theme.fg("dim", hint), width));
|
|
366
|
-
lines.push("");
|
|
367
389
|
lines.push(truncateToWidth(` ${this.bullet(0, sel, theme)} main`, width));
|
|
368
390
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (start > 0) lines.push(rightAlign("", theme.fg("dim", `↑ ${start} more`), width));
|
|
376
|
-
for (let a = start; a < start + visible; a++) {
|
|
391
|
+
const window = calculateFleetAgentWindow(agents.length, sel);
|
|
392
|
+
if (window.hiddenAbove > 0) {
|
|
393
|
+
lines.push(rightAlign("", theme.fg("dim", `↑ ${window.hiddenAbove} more`), width));
|
|
394
|
+
}
|
|
395
|
+
for (let a = window.start; a < window.end; a++) {
|
|
377
396
|
lines.push(this.renderAgentRow(a + 1, sel, agents[a].record, width, theme));
|
|
378
397
|
}
|
|
379
|
-
if (hiddenBelow > 0)
|
|
398
|
+
if (window.hiddenBelow > 0) {
|
|
399
|
+
lines.push(rightAlign("", theme.fg("dim", `↓ ${window.hiddenBelow} more`), width));
|
|
400
|
+
}
|
|
380
401
|
|
|
381
402
|
return lines;
|
|
382
403
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createWriteToolDefinition, generateDiffString, generateUnifiedPatch, type ExtensionContext, type ToolDefinition, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** Maximum UTF-8 size of the durable display diff plus unified patch. */
|
|
6
|
+
export const MAX_DURABLE_DIFF_BYTES = 4 * 1024 * 1024;
|
|
7
|
+
/** Avoid retaining arbitrarily large previous files in the live-only store. */
|
|
8
|
+
export const MAX_COMPARABLE_WRITE_BYTES = 512_000;
|
|
9
|
+
export const MAX_WRITE_METADATA_ENTRIES = 100;
|
|
10
|
+
|
|
11
|
+
export interface DurableWriteDetails {
|
|
12
|
+
diff?: string;
|
|
13
|
+
patch?: string;
|
|
14
|
+
firstChangedLine?: number;
|
|
15
|
+
diffUnavailableReason?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Ephemeral execution metadata used by the live renderer. Never persisted as a before/after pair. */
|
|
19
|
+
export interface WriteExecutionMeta extends DurableWriteDetails {
|
|
20
|
+
fileExistedBeforeWrite: boolean;
|
|
21
|
+
previousContent?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class WriteExecutionMetadataStore {
|
|
25
|
+
readonly entries = new Map<string, WriteExecutionMeta>();
|
|
26
|
+
|
|
27
|
+
set(toolCallId: string, metadata: WriteExecutionMeta): void {
|
|
28
|
+
this.entries.delete(toolCallId);
|
|
29
|
+
this.entries.set(toolCallId, metadata);
|
|
30
|
+
while (this.entries.size > MAX_WRITE_METADATA_ENTRIES) {
|
|
31
|
+
const oldest = this.entries.keys().next().value;
|
|
32
|
+
if (oldest === undefined) break;
|
|
33
|
+
this.entries.delete(oldest);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get(toolCallId: unknown): WriteExecutionMeta | undefined {
|
|
38
|
+
return typeof toolCallId === "string" ? this.entries.get(toolCallId) : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
delete(toolCallId: string): void {
|
|
42
|
+
this.entries.delete(toolCallId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear(): void {
|
|
46
|
+
this.entries.clear();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type WriteParams = { path: string; content: string };
|
|
51
|
+
|
|
52
|
+
function resolveWritePath(path: string, cwd: string): string {
|
|
53
|
+
if (path === "~") return process.env.HOME ?? cwd;
|
|
54
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) {
|
|
55
|
+
return resolve(process.env.HOME ?? cwd, path.slice(2));
|
|
56
|
+
}
|
|
57
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function capturePreviousContent(absolutePath: string): Promise<WriteExecutionMeta> {
|
|
61
|
+
let info;
|
|
62
|
+
try {
|
|
63
|
+
info = await lstat(absolutePath);
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
if (error?.code === "ENOENT") return { fileExistedBeforeWrite: false, previousContent: "" };
|
|
66
|
+
return {
|
|
67
|
+
fileExistedBeforeWrite: true,
|
|
68
|
+
diffUnavailableReason: "unable to inspect the previous file",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!info.isFile()) {
|
|
73
|
+
return {
|
|
74
|
+
fileExistedBeforeWrite: true,
|
|
75
|
+
diffUnavailableReason: "previous path is not a regular file",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (info.size > MAX_COMPARABLE_WRITE_BYTES) {
|
|
79
|
+
return {
|
|
80
|
+
fileExistedBeforeWrite: true,
|
|
81
|
+
diffUnavailableReason: `previous file exceeds ${MAX_COMPARABLE_WRITE_BYTES} bytes`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const bytes = await readFile(absolutePath);
|
|
87
|
+
if (bytes.byteLength > MAX_COMPARABLE_WRITE_BYTES) {
|
|
88
|
+
return {
|
|
89
|
+
fileExistedBeforeWrite: true,
|
|
90
|
+
diffUnavailableReason: `previous file exceeds ${MAX_COMPARABLE_WRITE_BYTES} bytes`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const previousContent = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
94
|
+
return { fileExistedBeforeWrite: true, previousContent };
|
|
95
|
+
} catch {
|
|
96
|
+
return {
|
|
97
|
+
fileExistedBeforeWrite: true,
|
|
98
|
+
diffUnavailableReason: "previous file is not comparable UTF-8 text",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function durableDetails(
|
|
104
|
+
metadata: WriteExecutionMeta,
|
|
105
|
+
path: string,
|
|
106
|
+
content: string,
|
|
107
|
+
): DurableWriteDetails {
|
|
108
|
+
if (metadata.diffUnavailableReason) {
|
|
109
|
+
return { diffUnavailableReason: metadata.diffUnavailableReason };
|
|
110
|
+
}
|
|
111
|
+
const previousContent = metadata.previousContent ?? "";
|
|
112
|
+
const diffResult = generateDiffString(previousContent, content);
|
|
113
|
+
const patch = generateUnifiedPatch(path, previousContent, content);
|
|
114
|
+
const totalBytes = Buffer.byteLength(diffResult.diff, "utf8") + Buffer.byteLength(patch, "utf8");
|
|
115
|
+
if (totalBytes > MAX_DURABLE_DIFF_BYTES) {
|
|
116
|
+
return { diffUnavailableReason: `durable diff exceeds ${MAX_DURABLE_DIFF_BYTES} bytes` };
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
diff: diffResult.diff,
|
|
120
|
+
patch,
|
|
121
|
+
firstChangedLine: diffResult.firstChangedLine,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function executeWriteWithMetadata(
|
|
126
|
+
store: WriteExecutionMetadataStore,
|
|
127
|
+
toolCallId: string,
|
|
128
|
+
params: WriteParams,
|
|
129
|
+
signal: AbortSignal | undefined,
|
|
130
|
+
cwd: string,
|
|
131
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; details?: DurableWriteDetails }> {
|
|
132
|
+
const absolutePath = resolveWritePath(params.path, cwd);
|
|
133
|
+
store.delete(toolCallId);
|
|
134
|
+
|
|
135
|
+
return withFileMutationQueue(absolutePath, async () => {
|
|
136
|
+
const throwIfAborted = () => {
|
|
137
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
138
|
+
};
|
|
139
|
+
throwIfAborted();
|
|
140
|
+
const metadata = await capturePreviousContent(absolutePath);
|
|
141
|
+
throwIfAborted();
|
|
142
|
+
await mkdir(dirname(absolutePath), { recursive: true });
|
|
143
|
+
throwIfAborted();
|
|
144
|
+
await writeFile(absolutePath, params.content, "utf8");
|
|
145
|
+
throwIfAborted();
|
|
146
|
+
|
|
147
|
+
const details = durableDetails(metadata, params.path, params.content);
|
|
148
|
+
store.set(toolCallId, { ...metadata, ...details });
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text" as const, text: `Successfully wrote to ${params.path}` }],
|
|
151
|
+
details,
|
|
152
|
+
};
|
|
153
|
+
}).catch((error) => {
|
|
154
|
+
store.delete(toolCallId);
|
|
155
|
+
throw error;
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A session-local write definition. It retains Pi's public schema/description
|
|
161
|
+
* and replaces only execution so the parent registry is never mutated.
|
|
162
|
+
*/
|
|
163
|
+
export function createTrackedWriteTool(
|
|
164
|
+
cwd: string,
|
|
165
|
+
store = new WriteExecutionMetadataStore(),
|
|
166
|
+
): ToolDefinition<any, DurableWriteDetails> {
|
|
167
|
+
const nativeWrite = createWriteToolDefinition(cwd);
|
|
168
|
+
return {
|
|
169
|
+
...nativeWrite,
|
|
170
|
+
async execute(
|
|
171
|
+
toolCallId: string,
|
|
172
|
+
params: WriteParams,
|
|
173
|
+
signal: AbortSignal | undefined,
|
|
174
|
+
_onUpdate: unknown,
|
|
175
|
+
ctx: ExtensionContext,
|
|
176
|
+
) {
|
|
177
|
+
return executeWriteWithMetadata(store, toolCallId, params, signal, ctx.cwd || cwd);
|
|
178
|
+
},
|
|
179
|
+
} as ToolDefinition<any, DurableWriteDetails>;
|
|
180
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
});
|