@esso0428/pi-subagents 0.15.9 → 0.15.10
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/.pi/tasks/tasks-01a09263-0ac6-77be-97f5-664b3ffea76b.json +53 -0
- package/.pi/workflows/index.json +5 -0
- package/CHANGELOG.md +10 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +4 -2
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
- 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 +12 -2
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-result.js +14 -3
- package/dist/ui/ccstyle/tool-result.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +8 -0
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +101 -20
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +3 -0
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +63 -17
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/write-execution.d.ts +2 -0
- package/dist/write-execution.d.ts.map +1 -1
- package/dist/write-execution.js +5 -1
- package/dist/write-execution.js.map +1 -1
- package/package.json +1 -1
- package/src/ui/ccstyle/diff/diff-renderer.ts +7 -2
- package/src/ui/ccstyle/tool-renderer.ts +18 -2
- package/src/ui/ccstyle/tool-result.ts +14 -3
- package/src/ui/conversation-timeline.ts +126 -18
- package/src/ui/conversation-viewer.ts +62 -17
- package/src/write-execution.ts +7 -1
- package/test/ui/conversation-diff.test.ts +13 -0
- package/test/ui/conversation-viewer.test.ts +54 -6
- package/test/write-execution.test.ts +2 -0
|
@@ -62,6 +62,7 @@ export class ConversationViewer implements Component {
|
|
|
62
62
|
private lastInnerW = 0;
|
|
63
63
|
private closed = false;
|
|
64
64
|
private stopArmed = false;
|
|
65
|
+
private hoveredClose = false;
|
|
65
66
|
private keys: ViewerKeys;
|
|
66
67
|
private composer: Input | undefined;
|
|
67
68
|
private searchMode = false;
|
|
@@ -135,11 +136,11 @@ export class ConversationViewer implements Component {
|
|
|
135
136
|
this.timeline.toggleFocusedTool();
|
|
136
137
|
return;
|
|
137
138
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
}
|
|
140
|
+
if (data === "e" && this.canSteer()) {
|
|
141
|
+
this.stopArmed = false;
|
|
142
|
+
this.openComposer();
|
|
143
|
+
return;
|
|
143
144
|
}
|
|
144
145
|
if (matchesKey(data, "x")) {
|
|
145
146
|
if (this.isStoppable()) {
|
|
@@ -210,9 +211,35 @@ export class ConversationViewer implements Component {
|
|
|
210
211
|
void maxScroll;
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
handleMouse(event: TuiMouseEvent): { handled?: boolean; render?: boolean } | undefined {
|
|
214
|
-
if (this.composer || this.searchMode) return undefined;
|
|
214
|
+
handleMouse(event: TuiMouseEvent): { handled?: boolean; capture?: boolean; render?: boolean } | undefined {
|
|
215
215
|
const innerW = Math.max(1, this.lastInnerW || event.width - 4);
|
|
216
|
+
const closeHit = this.isCloseButtonHit(event.x, event.y, innerW);
|
|
217
|
+
if (event.type === "move") {
|
|
218
|
+
const closeChanged = closeHit !== this.hoveredClose;
|
|
219
|
+
this.hoveredClose = closeHit;
|
|
220
|
+
if (this.composer || this.searchMode) {
|
|
221
|
+
return closeChanged ? { handled: true, render: true } : undefined;
|
|
222
|
+
}
|
|
223
|
+
const contentLines = this.buildContentLines(innerW);
|
|
224
|
+
const viewportHeight = this.viewportHeight();
|
|
225
|
+
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
226
|
+
const visibleStart = Math.min(this.scrollOffset, maxScroll);
|
|
227
|
+
const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
|
|
228
|
+
const contentY = event.y - contentOrigin + visibleStart;
|
|
229
|
+
// Forward moves outside the content too so a previously hovered tool
|
|
230
|
+
// hint is cleared when the pointer returns to the header/footer.
|
|
231
|
+
const timelineResult = this.timeline.handleMouse({ ...event, y: contentY });
|
|
232
|
+
return timelineResult ?? (closeChanged ? { handled: true, render: true } : undefined);
|
|
233
|
+
}
|
|
234
|
+
if (closeHit && event.button === "left") {
|
|
235
|
+
if (event.type === "press") return { handled: true, capture: true, render: false };
|
|
236
|
+
if (event.type === "click") {
|
|
237
|
+
this.closed = true;
|
|
238
|
+
this.done(undefined);
|
|
239
|
+
return { handled: true, render: false };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (this.composer || this.searchMode) return undefined;
|
|
216
243
|
const contentLines = this.buildContentLines(innerW);
|
|
217
244
|
const viewportHeight = this.viewportHeight();
|
|
218
245
|
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
@@ -231,13 +258,16 @@ export class ConversationViewer implements Component {
|
|
|
231
258
|
if (contentY < 0 || contentY >= contentLines.length) return undefined;
|
|
232
259
|
const result = this.timeline.handleMouse({ ...event, y: contentY });
|
|
233
260
|
if (result?.handled) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
261
|
+
if (event.type === "click") {
|
|
262
|
+
const focusedTool = this.timeline.getFocusedToolCallId();
|
|
263
|
+
const refreshed = this.timeline.getSnapshot();
|
|
264
|
+
const selected = refreshed.messageBlocks.findIndex((block) => block.toolCallId === focusedTool);
|
|
265
|
+
if (selected >= 0) this.selectedMessageIdx = selected;
|
|
266
|
+
this.cacheVersion++;
|
|
267
|
+
this.invalidate();
|
|
268
|
+
this.tui.requestRender();
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
241
271
|
}
|
|
242
272
|
return result;
|
|
243
273
|
}
|
|
@@ -268,7 +298,12 @@ export class ConversationViewer implements Component {
|
|
|
268
298
|
const percent = getSessionContextPercent(this.activity?.session);
|
|
269
299
|
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
270
300
|
}
|
|
271
|
-
|
|
301
|
+
const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
|
|
302
|
+
const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[esc]")) : th.fg("dim", "[esc]");
|
|
303
|
+
const closeWidth = visibleWidth(closeLabel);
|
|
304
|
+
const headerLeft = truncateToWidth(headerText, Math.max(0, innerW - closeWidth - 1), "", true);
|
|
305
|
+
const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) - closeWidth);
|
|
306
|
+
lines.push(row(headerLeft + " ".repeat(headerGap) + closeLabel));
|
|
272
307
|
const invocationLine = this.invocationLine();
|
|
273
308
|
if (invocationLine) lines.push(row(invocationLine));
|
|
274
309
|
lines.push(hrMid);
|
|
@@ -322,7 +357,7 @@ export class ConversationViewer implements Component {
|
|
|
322
357
|
lines.push(row(truncateToWidth(th.fg("accent", query) + th.fg("dim", matchCount) + hints, innerW, "...", true)));
|
|
323
358
|
} else {
|
|
324
359
|
const actions: string[] = [];
|
|
325
|
-
if (this.canSteer()) actions.push(th.fg("dim", "
|
|
360
|
+
if (this.canSteer()) actions.push(th.fg("dim", "e steer"));
|
|
326
361
|
if (this.isStoppable()) actions.push(this.stopArmed ? th.fg("error", "x again to STOP") : th.fg("dim", "x stop"));
|
|
327
362
|
const editHint = this.isLive() ? " O" : "";
|
|
328
363
|
const shortcutHints = [
|
|
@@ -354,6 +389,13 @@ export class ConversationViewer implements Component {
|
|
|
354
389
|
return lines;
|
|
355
390
|
}
|
|
356
391
|
|
|
392
|
+
private isCloseButtonHit(x: number, y: number, innerW: number): boolean {
|
|
393
|
+
if (y !== 1) return false;
|
|
394
|
+
const labelWidth = visibleWidth("[esc]");
|
|
395
|
+
const labelStart = 2 + Math.max(0, innerW - labelWidth);
|
|
396
|
+
return x >= labelStart && x < 2 + innerW;
|
|
397
|
+
}
|
|
398
|
+
|
|
357
399
|
invalidate(): void {
|
|
358
400
|
this.cachedWidth = 0;
|
|
359
401
|
this.cachedLines = [];
|
|
@@ -429,7 +471,10 @@ export class ConversationViewer implements Component {
|
|
|
429
471
|
}
|
|
430
472
|
|
|
431
473
|
private activeHeaderLine(): number {
|
|
432
|
-
|
|
474
|
+
const block = this.cachedMessageBlocks[this.currentMessageIndex()];
|
|
475
|
+
return block?.role === "user" || block?.role === "assistant"
|
|
476
|
+
? this.cachedMessageStarts[this.currentMessageIndex()] ?? -1
|
|
477
|
+
: -1;
|
|
433
478
|
}
|
|
434
479
|
|
|
435
480
|
private currentBlock(): ConversationBlock | undefined {
|
package/src/write-execution.ts
CHANGED
|
@@ -9,6 +9,8 @@ export const MAX_COMPARABLE_WRITE_BYTES = 512_000;
|
|
|
9
9
|
export const MAX_WRITE_METADATA_ENTRIES = 100;
|
|
10
10
|
|
|
11
11
|
export interface DurableWriteDetails {
|
|
12
|
+
/** Persisted so history can distinguish create vs overwrite without file access. */
|
|
13
|
+
fileExistedBeforeWrite?: boolean;
|
|
12
14
|
diff?: string;
|
|
13
15
|
patch?: string;
|
|
14
16
|
firstChangedLine?: number;
|
|
@@ -106,7 +108,10 @@ function durableDetails(
|
|
|
106
108
|
content: string,
|
|
107
109
|
): DurableWriteDetails {
|
|
108
110
|
if (metadata.diffUnavailableReason) {
|
|
109
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
fileExistedBeforeWrite: metadata.fileExistedBeforeWrite,
|
|
113
|
+
diffUnavailableReason: metadata.diffUnavailableReason,
|
|
114
|
+
};
|
|
110
115
|
}
|
|
111
116
|
const previousContent = metadata.previousContent ?? "";
|
|
112
117
|
const diffResult = generateDiffString(previousContent, content);
|
|
@@ -116,6 +121,7 @@ function durableDetails(
|
|
|
116
121
|
return { diffUnavailableReason: `durable diff exceeds ${MAX_DURABLE_DIFF_BYTES} bytes` };
|
|
117
122
|
}
|
|
118
123
|
return {
|
|
124
|
+
fileExistedBeforeWrite: metadata.fileExistedBeforeWrite,
|
|
119
125
|
diff: diffResult.diff,
|
|
120
126
|
patch,
|
|
121
127
|
firstChangedLine: diffResult.firstChangedLine,
|
|
@@ -66,6 +66,19 @@ describe("viewer-local ccstyle diff renderer", () => {
|
|
|
66
66
|
expect(rendered).not.toContain("later");
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
it("labels persisted overwrites correctly without reading the current file", () => {
|
|
70
|
+
const component = renderRichToolResult("write", {
|
|
71
|
+
content: [{ type: "text", text: "Successfully wrote" }],
|
|
72
|
+
details: {
|
|
73
|
+
diff: "-1 old\n+1 new",
|
|
74
|
+
patch: "--- file.txt\n+++ file.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n",
|
|
75
|
+
},
|
|
76
|
+
}, { expanded: false }, theme, { args: { path: "file.txt", content: "new\n" } });
|
|
77
|
+
|
|
78
|
+
expect(component?.render(100).join("\\n")).toContain("overwritten");
|
|
79
|
+
expect(component?.render(100).join("\\n")).toContain("click to show more");
|
|
80
|
+
});
|
|
81
|
+
|
|
69
82
|
it("returns an explicit unavailable row instead of an empty success card", () => {
|
|
70
83
|
const component = renderRichToolResult("write", {
|
|
71
84
|
content: [{ type: "text", text: "Successfully wrote" }],
|
|
@@ -83,8 +83,13 @@ describe("ConversationViewer", () => {
|
|
|
83
83
|
);
|
|
84
84
|
const liveFooter = live.render(120).join("\n");
|
|
85
85
|
expect(liveFooter).toContain("j/k scroll · J/K messages · g/G t M o O /n N q");
|
|
86
|
-
expect(liveFooter).toContain("
|
|
86
|
+
expect(liveFooter).toContain("e steer");
|
|
87
87
|
expect(liveFooter).toContain("x stop");
|
|
88
|
+
live.handleInput("\r");
|
|
89
|
+
expect(live.render(120).join("\n")).not.toContain("Enter send");
|
|
90
|
+
live.handleInput("e");
|
|
91
|
+
expect(live.render(120).join("\n")).toContain("Enter send");
|
|
92
|
+
live.handleInput("\x1b");
|
|
88
93
|
|
|
89
94
|
const history = new ConversationViewer(
|
|
90
95
|
tui(),
|
|
@@ -147,7 +152,9 @@ describe("ConversationViewer", () => {
|
|
|
147
152
|
vi.fn(),
|
|
148
153
|
);
|
|
149
154
|
|
|
150
|
-
|
|
155
|
+
const initial = viewer.render(120).join("\n");
|
|
156
|
+
expect(initial).not.toContain("six");
|
|
157
|
+
expect(initial).not.toContain("READ");
|
|
151
158
|
viewer.handleInput("\r");
|
|
152
159
|
expect(viewer.render(120).join("\n")).toContain("six");
|
|
153
160
|
});
|
|
@@ -209,7 +216,7 @@ describe("ConversationViewer", () => {
|
|
|
209
216
|
expect((viewer as any).scrollOffset).toBeLessThan(before);
|
|
210
217
|
});
|
|
211
218
|
|
|
212
|
-
it("
|
|
219
|
+
it("expands on the first click and collapses only on the second click", () => {
|
|
213
220
|
const viewer = new ConversationViewer(
|
|
214
221
|
tui(),
|
|
215
222
|
createStaticConversationSource([
|
|
@@ -222,11 +229,52 @@ describe("ConversationViewer", () => {
|
|
|
222
229
|
vi.fn(),
|
|
223
230
|
);
|
|
224
231
|
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
225
|
-
const
|
|
226
|
-
expect(
|
|
232
|
+
const span = (viewer as any).timeline.getSnapshot().toolSpans[0];
|
|
233
|
+
expect(span.clickStartLine).toEqual(expect.any(Number));
|
|
234
|
+
const hintY = 4 + span.clickStartLine;
|
|
235
|
+
const mouse = { button: "left", x: 1, screenX: 1, screenY: hintY, width: 120, height: 30, shift: false, alt: false, ctrl: false };
|
|
236
|
+
expect(viewer.handleMouse({ ...mouse, type: "press", y: hintY } as any)?.handled).toBe(true);
|
|
227
237
|
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
228
|
-
viewer.handleMouse({ ...mouse, type: "click", y:
|
|
238
|
+
viewer.handleMouse({ ...mouse, type: "click", y: hintY, clickCount: 1 } as any);
|
|
229
239
|
expect(viewer.render(120).join("\n")).toContain("six");
|
|
240
|
+
viewer.handleMouse({ ...mouse, type: "click", y: hintY, clickCount: 1 } as any);
|
|
241
|
+
expect(viewer.render(120).join("\n")).toContain("six");
|
|
242
|
+
viewer.handleMouse({ ...mouse, type: "click", y: hintY, clickCount: 2 } as any);
|
|
243
|
+
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("highlights the tool hint on hover and closes from the viewer [esc] button", () => {
|
|
247
|
+
const semanticTheme = {
|
|
248
|
+
fg: (name: string, text: string) => `<${name}>${text}</${name}>`,
|
|
249
|
+
bold: (text: string) => `<bold>${text}</bold>`,
|
|
250
|
+
};
|
|
251
|
+
const done = vi.fn();
|
|
252
|
+
const viewer = new ConversationViewer(
|
|
253
|
+
tui(),
|
|
254
|
+
createStaticConversationSource([
|
|
255
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "x" } }] },
|
|
256
|
+
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "one\ntwo\nthree\nfour\nfive\nsix" }] },
|
|
257
|
+
] as any),
|
|
258
|
+
record(),
|
|
259
|
+
undefined,
|
|
260
|
+
semanticTheme,
|
|
261
|
+
done,
|
|
262
|
+
);
|
|
263
|
+
const initial = viewer.render(120).join("\n");
|
|
264
|
+
const span = (viewer as any).timeline.getSnapshot().toolSpans[0];
|
|
265
|
+
expect(span.clickStartLine).toEqual(expect.any(Number));
|
|
266
|
+
const hintY = 4 + span.clickStartLine;
|
|
267
|
+
expect(initial).not.toContain("<text>click to show more</text>");
|
|
268
|
+
viewer.handleMouse({ type: "move", button: "none", x: 2, y: hintY, screenX: 2, screenY: hintY, width: 120, height: 30, shift: false, alt: false, ctrl: false } as any);
|
|
269
|
+
expect(viewer.render(120).join("\n")).toContain("<text>click to show more</text>");
|
|
270
|
+
|
|
271
|
+
expect(viewer.render(120).join("\n")).toContain("[esc]");
|
|
272
|
+
viewer.handleMouse({ type: "move", button: "none", x: 114, y: 1, screenX: 114, screenY: 1, width: 120, height: 30, shift: false, alt: false, ctrl: false } as any);
|
|
273
|
+
expect(viewer.render(120).join("\n")).toContain("<text><bold>[esc]</bold></text>");
|
|
274
|
+
const close = { button: "left", x: 114, screenX: 114, screenY: 1, y: 1, width: 120, height: 30, shift: false, alt: false, ctrl: false };
|
|
275
|
+
expect(viewer.handleMouse({ ...close, type: "press" } as any)?.handled).toBe(true);
|
|
276
|
+
expect(viewer.handleMouse({ ...close, type: "click", clickCount: 1 } as any)?.handled).toBe(true);
|
|
277
|
+
expect(done).toHaveBeenCalledTimes(1);
|
|
230
278
|
});
|
|
231
279
|
|
|
232
280
|
it("keeps historical viewers read-only and closes on Escape", () => {
|
|
@@ -31,6 +31,7 @@ describe("executeWriteWithMetadata", () => {
|
|
|
31
31
|
|
|
32
32
|
expect(await readFile(join(cwd, "nested/file.txt"), "utf8")).toBe("new\n");
|
|
33
33
|
expect(result.content[0]?.text).toContain("Successfully wrote to nested/file.txt");
|
|
34
|
+
expect(result.details?.fileExistedBeforeWrite).toBe(false);
|
|
34
35
|
expect(result.details?.diff).toContain("+1 new");
|
|
35
36
|
expect(result.details?.patch).toContain("+new");
|
|
36
37
|
expect(store.get("call-new")?.diff).toBe(result.details?.diff);
|
|
@@ -48,6 +49,7 @@ describe("executeWriteWithMetadata", () => {
|
|
|
48
49
|
|
|
49
50
|
await writeFile(join(cwd, "file.txt"), "later\n", "utf8");
|
|
50
51
|
|
|
52
|
+
expect(persisted.fileExistedBeforeWrite).toBe(true);
|
|
51
53
|
expect(persisted.diff).toContain("-1 old");
|
|
52
54
|
expect(persisted.diff).toContain("+1 new");
|
|
53
55
|
expect(persisted.patch).toContain("-old");
|