@esso0428/pi-subagents 0.15.17 → 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 +12 -0
- 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 +3 -3
- package/src/output-file.ts +3 -3
- 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/fleet-list.ts +3 -4
- 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 -172
- 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/deferred-ui-refresh.test.ts +0 -55
- package/test/ui/fleet-list.test.ts +0 -389
- package/test/write-execution.test.ts +0 -83
- package/tsconfig.json +0 -16
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
calculateFleetAgentWindow,
|
|
4
|
-
ensureFleetSelectionVisible,
|
|
5
|
-
FLEET_MAX_RENDER_ROWS,
|
|
6
|
-
FleetList,
|
|
7
|
-
} from "../../src/ui/fleet-list.js";
|
|
8
|
-
import { canOpenActiveAgent } from "../../src/agent-history-list.js";
|
|
9
|
-
|
|
10
|
-
const theme = {
|
|
11
|
-
fg: (_color: string, text: string) => text,
|
|
12
|
-
bold: (text: string) => text,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
function makeAgents(count: number): any[] {
|
|
16
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
17
|
-
id: `agent-${i}`,
|
|
18
|
-
type: "Explore",
|
|
19
|
-
description: `agent ${i}`,
|
|
20
|
-
status: "running",
|
|
21
|
-
session: { messages: [], subscribe: () => () => {} },
|
|
22
|
-
toolUses: 0,
|
|
23
|
-
startedAt: i + 1,
|
|
24
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
25
|
-
compactionCount: 0,
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function createFleetHarness(records: any[]) {
|
|
30
|
-
const manager: any = {
|
|
31
|
-
records,
|
|
32
|
-
listAgents() { return this.records; },
|
|
33
|
-
abort: vi.fn(() => false),
|
|
34
|
-
steer: vi.fn(() => false),
|
|
35
|
-
};
|
|
36
|
-
const tui = { terminal: { rows: 24 }, requestRender: vi.fn() } as any;
|
|
37
|
-
let terminalInput: ((data: string) => { consume?: boolean } | undefined) | undefined;
|
|
38
|
-
let factory: any;
|
|
39
|
-
let widget: any;
|
|
40
|
-
const ui: any = {
|
|
41
|
-
setWidget: vi.fn((_key: string, content: any) => { factory = content; }),
|
|
42
|
-
onTerminalInput: (handler: typeof terminalInput) => {
|
|
43
|
-
terminalInput = handler;
|
|
44
|
-
return () => { terminalInput = undefined; };
|
|
45
|
-
},
|
|
46
|
-
getEditorText: () => "",
|
|
47
|
-
notify: vi.fn(),
|
|
48
|
-
custom: vi.fn(async () => undefined),
|
|
49
|
-
};
|
|
50
|
-
const fleet = new FleetList(manager, new Map());
|
|
51
|
-
fleet.setUICtx(ui);
|
|
52
|
-
fleet.update();
|
|
53
|
-
const render = (width = 120) => {
|
|
54
|
-
widget = factory ? factory(tui, theme) : undefined;
|
|
55
|
-
return widget?.render(width) ?? [];
|
|
56
|
-
};
|
|
57
|
-
const input = (data: string) => terminalInput?.(data);
|
|
58
|
-
const mouse = (wheelDelta: number) => widget?.handleMouse({ type: "wheel", wheelDelta });
|
|
59
|
-
render(); // The real TUI invokes the widget factory before keyboard input arrives.
|
|
60
|
-
return { manager, fleet, tui, ui, render, input, mouse, getWidget: () => widget };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
describe("canOpenActiveAgent", () => {
|
|
64
|
-
it("does not treat terminal records as active FleetView rows", () => {
|
|
65
|
-
expect(canOpenActiveAgent({ status: "completed", session: {} } as any)).toBe(false);
|
|
66
|
-
expect(canOpenActiveAgent({ status: "error", session: {} } as any)).toBe(false);
|
|
67
|
-
expect(canOpenActiveAgent({ status: "running", session: {} } as any)).toBe(true);
|
|
68
|
-
expect(canOpenActiveAgent({ status: "queued", session: undefined } as any)).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
describe("FleetView focus-preserving window", () => {
|
|
73
|
-
it("does not move the window while the selected agent is visible", () => {
|
|
74
|
-
expect(calculateFleetAgentWindow(12, 0)).toMatchObject({
|
|
75
|
-
start: 0,
|
|
76
|
-
end: 3,
|
|
77
|
-
hiddenAbove: 0,
|
|
78
|
-
hiddenBelow: 9,
|
|
79
|
-
});
|
|
80
|
-
expect(ensureFleetSelectionVisible(12, 0, 0)).toBe(0);
|
|
81
|
-
expect(ensureFleetSelectionVisible(12, 1, 0)).toBe(0);
|
|
82
|
-
expect(ensureFleetSelectionVisible(12, 2, 0)).toBe(0);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("moves only enough to show a selected agent below the window", () => {
|
|
86
|
-
const nextStart = ensureFleetSelectionVisible(12, 3, 0);
|
|
87
|
-
expect(nextStart).toBe(2);
|
|
88
|
-
const window = calculateFleetAgentWindow(12, nextStart);
|
|
89
|
-
expect(3).toBeGreaterThanOrEqual(window.start);
|
|
90
|
-
expect(3).toBeLessThan(window.end);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("moves only enough to show a selected agent above the window", () => {
|
|
94
|
-
const nextStart = ensureFleetSelectionVisible(12, 3, 4);
|
|
95
|
-
expect(nextStart).toBe(3);
|
|
96
|
-
const window = calculateFleetAgentWindow(12, nextStart);
|
|
97
|
-
expect(3).toBeGreaterThanOrEqual(window.start);
|
|
98
|
-
expect(3).toBeLessThan(window.end);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("preserves the marker row budget", () => {
|
|
102
|
-
for (const count of [0, 1, 2, 4, 5, 12]) {
|
|
103
|
-
for (let start = 0; start < Math.max(1, count); start++) {
|
|
104
|
-
const window = calculateFleetAgentWindow(count, start);
|
|
105
|
-
const rows = 2
|
|
106
|
-
+ (window.hiddenAbove > 0 ? 1 : 0)
|
|
107
|
-
+ (window.end - window.start)
|
|
108
|
-
+ (window.hiddenBelow > 0 ? 1 : 0);
|
|
109
|
-
expect(rows).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("clamps empty, negative, and oversized inputs", () => {
|
|
115
|
-
expect(calculateFleetAgentWindow(0, 99)).toEqual({
|
|
116
|
-
start: 0,
|
|
117
|
-
end: 0,
|
|
118
|
-
hiddenAbove: 0,
|
|
119
|
-
hiddenBelow: 0,
|
|
120
|
-
});
|
|
121
|
-
expect(ensureFleetSelectionVisible(12, -1, -4)).toBe(0);
|
|
122
|
-
expect(ensureFleetSelectionVisible(12, 999, 999)).toBe(9);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
describe("FleetList bounded window", () => {
|
|
127
|
-
it("keeps every FleetView viewport within six total rows", () => {
|
|
128
|
-
for (const agentCount of [0, 1, 2, 4, 5, 12]) {
|
|
129
|
-
for (let selected = 0; selected <= agentCount; selected++) {
|
|
130
|
-
const viewportStart = agentCount > 0
|
|
131
|
-
? ensureFleetSelectionVisible(agentCount, Math.max(0, selected - 1), 0)
|
|
132
|
-
: 0;
|
|
133
|
-
const window = calculateFleetAgentWindow(agentCount, viewportStart);
|
|
134
|
-
const rows = 2 + (window.hiddenAbove > 0 ? 1 : 0)
|
|
135
|
-
+ (window.end - window.start)
|
|
136
|
-
+ (window.hiddenBelow > 0 ? 1 : 0);
|
|
137
|
-
expect(rows).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
138
|
-
if (agentCount > 0 && selected > 0) {
|
|
139
|
-
expect(selected - 1).toBeGreaterThanOrEqual(window.start);
|
|
140
|
-
expect(selected - 1).toBeLessThan(window.end);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("reserves indicator rows while keeping the selected middle agent visible", () => {
|
|
147
|
-
const viewportStart = ensureFleetSelectionVisible(12, 5, 0);
|
|
148
|
-
expect(viewportStart).toBe(4);
|
|
149
|
-
const window = calculateFleetAgentWindow(12, viewportStart);
|
|
150
|
-
expect(window.hiddenAbove).toBeGreaterThan(0);
|
|
151
|
-
expect(window.hiddenBelow).toBeGreaterThan(0);
|
|
152
|
-
expect(5).toBeGreaterThanOrEqual(window.start);
|
|
153
|
-
expect(5).toBeLessThan(window.end);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("keeps the rendered window stable while moving inside it", () => {
|
|
157
|
-
const { render, input } = createFleetHarness(makeAgents(12));
|
|
158
|
-
input("\u001b[B"); // activate, select main
|
|
159
|
-
const before = render().join("\n");
|
|
160
|
-
|
|
161
|
-
input("\u001b[B"); // select agent 0, still in the first window
|
|
162
|
-
const afterFirst = render().join("\n");
|
|
163
|
-
input("\u001b[B"); // select agent 1, still in the first window
|
|
164
|
-
const afterSecond = render().join("\n");
|
|
165
|
-
|
|
166
|
-
expect(afterFirst).toContain("agent 0");
|
|
167
|
-
expect(afterSecond).toContain("agent 1");
|
|
168
|
-
expect(afterSecond).toContain("agent 0");
|
|
169
|
-
expect(afterSecond).toContain("↓ 9 more");
|
|
170
|
-
expect(afterSecond).not.toContain("↑");
|
|
171
|
-
expect(before).toContain("↓ 9 more");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("moves the window only when the selected row leaves it", () => {
|
|
175
|
-
const { render, input } = createFleetHarness(makeAgents(12));
|
|
176
|
-
input("\u001b[B");
|
|
177
|
-
input("\u001b[B");
|
|
178
|
-
input("\u001b[B");
|
|
179
|
-
const beforeBoundary = render().join("\n");
|
|
180
|
-
input("\u001b[B");
|
|
181
|
-
const afterBoundary = render().join("\n");
|
|
182
|
-
|
|
183
|
-
expect(beforeBoundary).toContain("agent 1");
|
|
184
|
-
expect(afterBoundary).toContain("agent 3");
|
|
185
|
-
expect(afterBoundary).toContain("↑");
|
|
186
|
-
expect(afterBoundary).not.toEqual(beforeBoundary);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("treats vim and arrow keys as equivalent directional input", () => {
|
|
190
|
-
const arrows = createFleetHarness(makeAgents(12));
|
|
191
|
-
const vim = createFleetHarness(makeAgents(12));
|
|
192
|
-
arrows.input("\u001b[B");
|
|
193
|
-
vim.input("\u001b[B");
|
|
194
|
-
|
|
195
|
-
for (let i = 0; i < 4; i++) {
|
|
196
|
-
arrows.input("\u001b[B");
|
|
197
|
-
vim.input("j");
|
|
198
|
-
expect(vim.render()).toEqual(arrows.render());
|
|
199
|
-
}
|
|
200
|
-
for (let i = 0; i < 2; i++) {
|
|
201
|
-
arrows.input("\u001b[A");
|
|
202
|
-
vim.input("k");
|
|
203
|
-
expect(vim.render()).toEqual(arrows.render());
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it("activates inactive FleetView equivalently for j, down, and wheel-down", () => {
|
|
208
|
-
const arrows = createFleetHarness(makeAgents(2));
|
|
209
|
-
const vim = createFleetHarness(makeAgents(2));
|
|
210
|
-
const wheel = createFleetHarness(makeAgents(2));
|
|
211
|
-
|
|
212
|
-
expect(arrows.input("\u001b[B")).toEqual({ consume: true });
|
|
213
|
-
expect(vim.input("j")).toEqual({ consume: true });
|
|
214
|
-
expect(wheel.mouse(1)).toEqual({ handled: true, render: true });
|
|
215
|
-
expect(vim.render()).toEqual(arrows.render());
|
|
216
|
-
expect(wheel.render()).toEqual(arrows.render());
|
|
217
|
-
expect(arrows.render()[0]).toContain("↑↓ select");
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("activates inactive FleetView with left at an empty editor", () => {
|
|
221
|
-
const { render, input } = createFleetHarness(makeAgents(1));
|
|
222
|
-
|
|
223
|
-
expect(input("\u001b[D")).toEqual({ consume: true });
|
|
224
|
-
expect(render()[0]).toContain("↑↓ select");
|
|
225
|
-
expect(render()[1]).toContain("● main");
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("keeps left activation consistent and does not become down when active", () => {
|
|
229
|
-
const left = createFleetHarness(makeAgents(1));
|
|
230
|
-
const down = createFleetHarness(makeAgents(1));
|
|
231
|
-
|
|
232
|
-
expect(left.render()[0]).toContain("↓ to manage");
|
|
233
|
-
expect(left.input("\u001b[D")).toEqual({ consume: true });
|
|
234
|
-
expect(down.input("\u001b[B")).toEqual({ consume: true });
|
|
235
|
-
expect(left.render()).toEqual(down.render());
|
|
236
|
-
expect(left.render()[1]).toContain("● main");
|
|
237
|
-
|
|
238
|
-
const active = left.render();
|
|
239
|
-
left.input("\u001b[D");
|
|
240
|
-
expect(left.render()).toEqual(active);
|
|
241
|
-
expect(left.render()[1]).toContain("● main");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("keeps the existing left activation contract", () => {
|
|
245
|
-
const { render, input } = createFleetHarness(makeAgents(2));
|
|
246
|
-
|
|
247
|
-
expect(input("\u001b[D")).toEqual({ consume: true });
|
|
248
|
-
expect(render()[0]).toContain("↑↓ select");
|
|
249
|
-
expect(render()[1]).toContain("● main");
|
|
250
|
-
|
|
251
|
-
input("\u001b[D");
|
|
252
|
-
expect(render()[1]).toContain("● main");
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("exposes normalized wheel input and maps its direction like arrows", () => {
|
|
256
|
-
const arrows = createFleetHarness(makeAgents(12));
|
|
257
|
-
const wheel = createFleetHarness(makeAgents(12));
|
|
258
|
-
expect(typeof wheel.getWidget().handleMouse).toBe("function");
|
|
259
|
-
arrows.input("\u001b[B");
|
|
260
|
-
wheel.input("\u001b[B");
|
|
261
|
-
|
|
262
|
-
arrows.input("\u001b[B");
|
|
263
|
-
expect(wheel.mouse(1)).toEqual({ handled: true, render: true });
|
|
264
|
-
expect(wheel.render()).toEqual(arrows.render());
|
|
265
|
-
arrows.input("\u001b[B");
|
|
266
|
-
expect(wheel.mouse(2)).toEqual({ handled: true, render: true });
|
|
267
|
-
expect(wheel.render()).toEqual(arrows.render());
|
|
268
|
-
|
|
269
|
-
arrows.input("\u001b[A");
|
|
270
|
-
expect(wheel.mouse(-1)).toEqual({ handled: true, render: true });
|
|
271
|
-
expect(wheel.render()).toEqual(arrows.render());
|
|
272
|
-
expect(wheel.getWidget().handleMouse({ type: "wheel", wheelDelta: 0 })).toBeUndefined();
|
|
273
|
-
expect(wheel.getWidget().handleMouse({ type: "wheel" })).toBeUndefined();
|
|
274
|
-
expect(wheel.getWidget().handleMouse({ type: "mousemove", wheelDelta: 1 })).toBeUndefined();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it("activates on downward wheel only when inactive at an empty editor", () => {
|
|
278
|
-
const { render, mouse } = createFleetHarness(makeAgents(2));
|
|
279
|
-
|
|
280
|
-
expect(render()[0]).toContain("↓ to manage");
|
|
281
|
-
expect(mouse(-1)).toBeUndefined();
|
|
282
|
-
expect(render()[0]).toContain("↓ to manage");
|
|
283
|
-
|
|
284
|
-
expect(mouse(1)).toEqual({ handled: true, render: true });
|
|
285
|
-
expect(render()[0]).toContain("↑↓ select");
|
|
286
|
-
expect(render()[1]).toContain("● main");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("keeps vim, arrows, and wheel navigation at roster boundaries", () => {
|
|
290
|
-
const { render, input, mouse } = createFleetHarness(makeAgents(2));
|
|
291
|
-
input("\u001b[B"); // activate on main
|
|
292
|
-
expect(mouse(-1)).toEqual({ handled: true, render: true }); // up from main exits, rather than moving past the top
|
|
293
|
-
expect(render()[0]).toContain("← for agents");
|
|
294
|
-
|
|
295
|
-
input("\u001b[B");
|
|
296
|
-
input("j");
|
|
297
|
-
input("j"); // bottom agent
|
|
298
|
-
const atBottom = render().join("\n");
|
|
299
|
-
input("\u001b[B");
|
|
300
|
-
expect(render().join("\n")).toEqual(atBottom);
|
|
301
|
-
expect(mouse(1)).toEqual({ handled: true, render: true });
|
|
302
|
-
expect(render().join("\n")).toEqual(atBottom);
|
|
303
|
-
|
|
304
|
-
input("k");
|
|
305
|
-
input("k");
|
|
306
|
-
expect(render()[1]).toContain("● main");
|
|
307
|
-
input("\u001b[A"); // up from main deactivates at the top boundary
|
|
308
|
-
expect(render()[0]).toContain("↓ to manage");
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("keeps keyboard and wheel directions equivalent at active boundaries", () => {
|
|
312
|
-
const arrows = createFleetHarness(makeAgents(2));
|
|
313
|
-
const vim = createFleetHarness(makeAgents(2));
|
|
314
|
-
const wheel = createFleetHarness(makeAgents(2));
|
|
315
|
-
const compare = (arrow: () => unknown, vi: () => unknown, mouse: () => unknown) => {
|
|
316
|
-
expect(vi()).toEqual(arrow());
|
|
317
|
-
expect(mouse()).toEqual({ handled: true, render: true });
|
|
318
|
-
expect(vim.render()).toEqual(arrows.render());
|
|
319
|
-
expect(wheel.render()).toEqual(arrows.render());
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
compare(() => arrows.input("\u001b[B"), () => vim.input("j"), () => wheel.mouse(1));
|
|
323
|
-
compare(() => arrows.input("\u001b[B"), () => vim.input("j"), () => wheel.mouse(1));
|
|
324
|
-
compare(() => arrows.input("\u001b[B"), () => vim.input("j"), () => wheel.mouse(1));
|
|
325
|
-
|
|
326
|
-
// Down at the bottom clamps identically, while up eventually deactivates
|
|
327
|
-
// all three controls at the top boundary.
|
|
328
|
-
compare(() => arrows.input("\u001b[B"), () => vim.input("j"), () => wheel.mouse(1));
|
|
329
|
-
compare(() => arrows.input("\u001b[A"), () => vim.input("k"), () => wheel.mouse(-1));
|
|
330
|
-
compare(() => arrows.input("\u001b[A"), () => vim.input("k"), () => wheel.mouse(-1));
|
|
331
|
-
compare(() => arrows.input("\u001b[A"), () => vim.input("k"), () => wheel.mouse(-1));
|
|
332
|
-
expect(arrows.render()[0]).toContain("↓ to manage");
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it("slides the visible window while navigating the full roster", () => {
|
|
336
|
-
const { render, input } = createFleetHarness(makeAgents(12));
|
|
337
|
-
input("\u001b[B");
|
|
338
|
-
for (let i = 0; i < 6; i++) input("\u001b[B");
|
|
339
|
-
const middle = render();
|
|
340
|
-
expect(middle.length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
341
|
-
expect(middle.some(line => /↑ \d+ more/.test(line))).toBe(true);
|
|
342
|
-
expect(middle.some(line => /↓ \d+ more/.test(line))).toBe(true);
|
|
343
|
-
expect(middle.some(line => line.includes("●"))).toBe(true);
|
|
344
|
-
|
|
345
|
-
input("\u001b[A");
|
|
346
|
-
const afterUp = render();
|
|
347
|
-
expect(afterUp.length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
348
|
-
expect(afterUp.some(line => line.includes("●"))).toBe(true);
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("keeps the first and last windows bounded", () => {
|
|
352
|
-
const { render, input } = createFleetHarness(makeAgents(12));
|
|
353
|
-
expect(render().length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
354
|
-
input("\u001b[B");
|
|
355
|
-
for (let i = 0; i < 12; i++) input("\u001b[B");
|
|
356
|
-
const last = render();
|
|
357
|
-
expect(last.length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
358
|
-
expect(last.some(line => /↓ \d+ more/.test(line))).toBe(false);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it("skips unchanged timer updates but redraws once when the active roster changes", () => {
|
|
362
|
-
const { manager, fleet, tui } = createFleetHarness(makeAgents(5));
|
|
363
|
-
tui.requestRender.mockClear();
|
|
364
|
-
fleet.update();
|
|
365
|
-
expect(tui.requestRender).not.toHaveBeenCalled();
|
|
366
|
-
|
|
367
|
-
manager.records.pop();
|
|
368
|
-
fleet.update();
|
|
369
|
-
expect(tui.requestRender).toHaveBeenCalledWith(true);
|
|
370
|
-
fleet.dispose();
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it("clears the widget when the active roster becomes empty", () => {
|
|
374
|
-
const { manager, fleet, ui } = createFleetHarness(makeAgents(1));
|
|
375
|
-
manager.records.splice(0);
|
|
376
|
-
fleet.update();
|
|
377
|
-
expect(ui.setWidget).toHaveBeenLastCalledWith("fleet", undefined);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it("refreshes the current TUI without handling keyboard input", () => {
|
|
381
|
-
const { fleet, tui } = createFleetHarness(makeAgents(1));
|
|
382
|
-
tui.requestRender.mockClear();
|
|
383
|
-
|
|
384
|
-
expect((fleet as any).requestUiRefresh(true)).toBe(true);
|
|
385
|
-
expect(tui.requestRender).toHaveBeenCalledWith(true);
|
|
386
|
-
fleet.dispose();
|
|
387
|
-
expect((fleet as any).requestUiRefresh(true)).toBe(false);
|
|
388
|
-
});
|
|
389
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
6
|
-
executeWriteWithMetadata,
|
|
7
|
-
MAX_DURABLE_DIFF_BYTES,
|
|
8
|
-
WriteExecutionMetadataStore,
|
|
9
|
-
} from "../src/write-execution.js";
|
|
10
|
-
|
|
11
|
-
const tempDirs: string[] = [];
|
|
12
|
-
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
async function tempDir(): Promise<string> {
|
|
18
|
-
const dir = await mkdtemp(join(tmpdir(), "pi-subagents-write-test-"));
|
|
19
|
-
tempDirs.push(dir);
|
|
20
|
-
return dir;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("executeWriteWithMetadata", () => {
|
|
24
|
-
it("writes a new file and returns a durable display diff and patch", async () => {
|
|
25
|
-
const cwd = await tempDir();
|
|
26
|
-
const store = new WriteExecutionMetadataStore();
|
|
27
|
-
const result = await executeWriteWithMetadata(store, "call-new", {
|
|
28
|
-
path: "nested/file.txt",
|
|
29
|
-
content: "new\n",
|
|
30
|
-
}, undefined, cwd);
|
|
31
|
-
|
|
32
|
-
expect(await readFile(join(cwd, "nested/file.txt"), "utf8")).toBe("new\n");
|
|
33
|
-
expect(result.content[0]?.text).toContain("Successfully wrote to nested/file.txt");
|
|
34
|
-
expect(result.details?.diff).toContain("+1 new");
|
|
35
|
-
expect(result.details?.patch).toContain("+new");
|
|
36
|
-
expect(store.get("call-new")?.diff).toBe(result.details?.diff);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("returns durable details that remain independent of later file changes", async () => {
|
|
40
|
-
const cwd = await tempDir();
|
|
41
|
-
await writeFile(join(cwd, "file.txt"), "old\n", "utf8");
|
|
42
|
-
const store = new WriteExecutionMetadataStore();
|
|
43
|
-
const result = await executeWriteWithMetadata(store, "call-existing", {
|
|
44
|
-
path: "file.txt",
|
|
45
|
-
content: "new\n",
|
|
46
|
-
}, undefined, cwd);
|
|
47
|
-
const persisted = { ...result.details };
|
|
48
|
-
|
|
49
|
-
await writeFile(join(cwd, "file.txt"), "later\n", "utf8");
|
|
50
|
-
|
|
51
|
-
expect(persisted.diff).toContain("-1 old");
|
|
52
|
-
expect(persisted.diff).toContain("+1 new");
|
|
53
|
-
expect(persisted.patch).toContain("-old");
|
|
54
|
-
expect(persisted.patch).toContain("+new");
|
|
55
|
-
expect(persisted).not.toHaveProperty("previousContent");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("writes successfully but marks an oversized durable diff unavailable", async () => {
|
|
59
|
-
const cwd = await tempDir();
|
|
60
|
-
const store = new WriteExecutionMetadataStore();
|
|
61
|
-
const content = "x".repeat(MAX_DURABLE_DIFF_BYTES + 1);
|
|
62
|
-
const result = await executeWriteWithMetadata(store, "call-large", {
|
|
63
|
-
path: "large.txt",
|
|
64
|
-
content,
|
|
65
|
-
}, undefined, cwd);
|
|
66
|
-
|
|
67
|
-
expect((await readFile(join(cwd, "large.txt"), "utf8")).length).toBe(content.length);
|
|
68
|
-
expect(result.details?.diffUnavailableReason).toContain("durable diff exceeds");
|
|
69
|
-
expect(result.details).not.toHaveProperty("diff");
|
|
70
|
-
expect(store.get("call-large")?.previousContent).toBe("");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("does not fabricate metadata when the write fails", async () => {
|
|
74
|
-
const cwd = await tempDir();
|
|
75
|
-
const store = new WriteExecutionMetadataStore();
|
|
76
|
-
|
|
77
|
-
await expect(executeWriteWithMetadata(store, "call-fail", {
|
|
78
|
-
path: "/dev/null/pi-subagents-impossible/file.txt",
|
|
79
|
-
content: "nope",
|
|
80
|
-
}, undefined, cwd)).rejects.toBeTruthy();
|
|
81
|
-
expect(store.get("call-fail")).toBeUndefined();
|
|
82
|
-
});
|
|
83
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "Bundler",
|
|
6
|
-
"strict": true,
|
|
7
|
-
"declaration": true,
|
|
8
|
-
"declarationMap": true,
|
|
9
|
-
"sourceMap": true,
|
|
10
|
-
"rootDir": "src",
|
|
11
|
-
"outDir": "dist",
|
|
12
|
-
"skipLibCheck": true,
|
|
13
|
-
"noEmitOnError": true
|
|
14
|
-
},
|
|
15
|
-
"include": ["src/**/*.ts"]
|
|
16
|
-
}
|