@esso0428/pi-subagents 0.17.2 → 0.17.4
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 +17 -0
- package/README.md +3 -3
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +55 -9
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +1 -1
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +10 -0
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +127 -22
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/package.json +1 -1
- package/src/ui/agent-widget.ts +58 -9
- package/src/ui/conversation-timeline.ts +4 -1
- package/src/ui/conversation-viewer.ts +145 -21
|
@@ -29,6 +29,44 @@ const MATCH_BG = "\x1b[48;5;238m";
|
|
|
29
29
|
const MATCH_BG_CURRENT = "\x1b[48;5;220m\x1b[30m";
|
|
30
30
|
const BG_RESET = "\x1b[49m\x1b[39m";
|
|
31
31
|
|
|
32
|
+
function renderScrollbarCell(
|
|
33
|
+
theme: Theme,
|
|
34
|
+
totalLines: number,
|
|
35
|
+
viewportHeight: number,
|
|
36
|
+
scrollOffset: number,
|
|
37
|
+
lineIndex: number,
|
|
38
|
+
active: boolean,
|
|
39
|
+
): string {
|
|
40
|
+
const total = Math.max(0, Math.floor(totalLines));
|
|
41
|
+
const viewport = Math.max(1, Math.floor(viewportHeight));
|
|
42
|
+
if (total <= viewport) return theme.fg("scrollbarTrack", " ");
|
|
43
|
+
const maxOffset = Math.max(0, total - viewport);
|
|
44
|
+
const offset = Math.max(0, Math.min(maxOffset, Math.floor(scrollOffset)));
|
|
45
|
+
const thumbHeight = Math.max(1, Math.round((viewport * viewport) / total));
|
|
46
|
+
const thumbStart = Math.round(((viewport - thumbHeight) * offset) / maxOffset);
|
|
47
|
+
const inThumb = lineIndex >= thumbStart && lineIndex < thumbStart + thumbHeight;
|
|
48
|
+
return theme.fg(
|
|
49
|
+
inThumb ? "scrollbarThumb" : "scrollbarTrack",
|
|
50
|
+
inThumb ? (active ? "█" : "┃") : "│",
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function renderScrollMoreRule(
|
|
55
|
+
theme: Theme,
|
|
56
|
+
width: number,
|
|
57
|
+
direction: "top" | "bottom",
|
|
58
|
+
hiddenLines: number,
|
|
59
|
+
): string {
|
|
60
|
+
const safeWidth = Math.max(1, Math.floor(width));
|
|
61
|
+
if (hiddenLines <= 0) return theme.fg("border", `├${"─".repeat(safeWidth)}┤`);
|
|
62
|
+
const label = `${direction === "top" ? "↑" : "↓"} ${hiddenLines} more`;
|
|
63
|
+
const labelWidth = visibleWidth(label);
|
|
64
|
+
if (labelWidth + 2 > safeWidth) return theme.fg("border", `├${"─".repeat(safeWidth)}┤`);
|
|
65
|
+
const left = Math.floor((safeWidth - labelWidth) / 2);
|
|
66
|
+
const right = Math.max(0, safeWidth - left - labelWidth);
|
|
67
|
+
return `${theme.fg("border", `├${"─".repeat(left)}`)}${theme.fg("dim", label)}${theme.fg("border", `${"─".repeat(right)}┤`)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
32
70
|
/** The live fields needed by the viewer; historical viewers use a static source. */
|
|
33
71
|
export type ConversationSource = Pick<AgentSession, "messages" | "subscribe">;
|
|
34
72
|
|
|
@@ -48,6 +86,7 @@ class FullToolPreview implements Component {
|
|
|
48
86
|
private totalLines = 1;
|
|
49
87
|
private pendingTop = false;
|
|
50
88
|
private hoveredClose = false;
|
|
89
|
+
private scrollbarActive = false;
|
|
51
90
|
|
|
52
91
|
constructor(
|
|
53
92
|
private readonly tui: TUI,
|
|
@@ -99,10 +138,12 @@ class FullToolPreview implements Component {
|
|
|
99
138
|
render(width: number): string[] {
|
|
100
139
|
const safeWidth = Math.max(12, Math.floor(Number.isFinite(width) ? width : 12));
|
|
101
140
|
const inner = Math.max(1, safeWidth - 2);
|
|
141
|
+
// Reserve the rightmost inner cell for the same track/thumb rail used by
|
|
142
|
+
// pi-tui's native ScrollView layout.
|
|
102
143
|
const bodyWidth = Math.max(1, inner - 1);
|
|
103
144
|
const terminalRows = Math.max(1, this.tui.terminal.rows);
|
|
104
145
|
const viewport = Math.max(1, Math.min(30, Math.floor(terminalRows * 0.8), terminalRows - 6));
|
|
105
|
-
const wrapped = this.body.render(bodyWidth);
|
|
146
|
+
const wrapped = this.body.render(Math.max(1, bodyWidth - 1));
|
|
106
147
|
this.totalLines = wrapped.length;
|
|
107
148
|
this.pageSize = viewport;
|
|
108
149
|
this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, this.totalLines - this.pageSize));
|
|
@@ -112,15 +153,28 @@ class FullToolPreview implements Component {
|
|
|
112
153
|
const clipped = truncateToWidth(text, rowWidth, "…", true);
|
|
113
154
|
return clipped + " ".repeat(Math.max(0, rowWidth - visibleWidth(clipped)));
|
|
114
155
|
};
|
|
156
|
+
const hiddenAbove = this.scrollOffset;
|
|
157
|
+
const hiddenBelow = Math.max(0, this.totalLines - this.scrollOffset - this.pageSize);
|
|
115
158
|
const close = this.theme.fg(this.hoveredClose ? "text" : "dim", "[esc]");
|
|
116
159
|
const header = this.theme.bold(this.theme.fg("accent", this.title));
|
|
117
160
|
const status = `${this.scrollOffset + 1}-${Math.min(this.totalLines, this.scrollOffset + this.pageSize)} / ${this.totalLines} lines · j/k gg/G ↑↓ PgUp/PgDn · [esc] close`;
|
|
118
161
|
return [
|
|
119
162
|
border(`╭${"─".repeat(inner)}╮`),
|
|
120
163
|
`${border("│")}${pad(` ${header}`, inner - visibleWidth(close))}${close}${border("│")}`,
|
|
121
|
-
|
|
122
|
-
...Array.from({ length: viewport }, (_, index) =>
|
|
123
|
-
|
|
164
|
+
renderScrollMoreRule(this.theme, inner, "top", hiddenAbove),
|
|
165
|
+
...Array.from({ length: viewport }, (_, index) => {
|
|
166
|
+
const content = pad(` ${visible[index] ?? ""}`, bodyWidth);
|
|
167
|
+
const rail = renderScrollbarCell(
|
|
168
|
+
this.theme,
|
|
169
|
+
this.totalLines,
|
|
170
|
+
this.pageSize,
|
|
171
|
+
this.scrollOffset,
|
|
172
|
+
index,
|
|
173
|
+
this.scrollbarActive,
|
|
174
|
+
);
|
|
175
|
+
return `${border("│")}${content}${rail}${border("│")}`;
|
|
176
|
+
}),
|
|
177
|
+
renderScrollMoreRule(this.theme, inner, "bottom", hiddenBelow),
|
|
124
178
|
`${border("│")}${pad(this.theme.fg("dim", ` ${status}`))}${border("│")}`,
|
|
125
179
|
border(`╰${"─".repeat(inner)}╯`),
|
|
126
180
|
];
|
|
@@ -134,6 +188,7 @@ class FullToolPreview implements Component {
|
|
|
134
188
|
const next = Math.max(0, Math.min(Math.max(0, this.totalLines - this.pageSize), offset));
|
|
135
189
|
if (next === this.scrollOffset) return;
|
|
136
190
|
this.scrollOffset = next;
|
|
191
|
+
this.scrollbarActive = true;
|
|
137
192
|
this.tui.requestRender();
|
|
138
193
|
}
|
|
139
194
|
}
|
|
@@ -164,6 +219,7 @@ export class ConversationViewer implements Component {
|
|
|
164
219
|
private lastInnerW = 0;
|
|
165
220
|
private closed = false;
|
|
166
221
|
private stopArmed = false;
|
|
222
|
+
private hoveredPreview = false;
|
|
167
223
|
private hoveredClose = false;
|
|
168
224
|
private keys: ViewerKeys;
|
|
169
225
|
private composer: Input | undefined;
|
|
@@ -171,6 +227,10 @@ export class ConversationViewer implements Component {
|
|
|
171
227
|
private searchQuery = "";
|
|
172
228
|
private searchMatches: ConversationMatch[] = [];
|
|
173
229
|
private currentMatchIdx = -1;
|
|
230
|
+
/** Follow-up drafts, newest first, matching pi editor history semantics. */
|
|
231
|
+
private steerHistory: string[] = [];
|
|
232
|
+
private steerHistoryIndex = -1;
|
|
233
|
+
private steerHistoryDraft = "";
|
|
174
234
|
private timeline: ConversationTimeline;
|
|
175
235
|
|
|
176
236
|
constructor(
|
|
@@ -202,7 +262,16 @@ export class ConversationViewer implements Component {
|
|
|
202
262
|
|
|
203
263
|
handleInput(data: string): void {
|
|
204
264
|
if (this.composer) {
|
|
205
|
-
|
|
265
|
+
if (matchesKey(data, Key.alt("up")) || data === "a-up" || data === "alt+up") {
|
|
266
|
+
this.recallSteerDraft();
|
|
267
|
+
} else {
|
|
268
|
+
const before = this.composer.getValue();
|
|
269
|
+
this.composer.handleInput(data);
|
|
270
|
+
if (this.composer && before !== this.composer.getValue()) {
|
|
271
|
+
this.steerHistoryIndex = -1;
|
|
272
|
+
this.steerHistoryDraft = "";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
206
275
|
this.tui.requestRender();
|
|
207
276
|
return;
|
|
208
277
|
}
|
|
@@ -305,11 +374,16 @@ export class ConversationViewer implements Component {
|
|
|
305
374
|
|
|
306
375
|
handleMouse(event: TuiMouseEvent): { handled?: boolean; capture?: boolean; render?: boolean } | undefined {
|
|
307
376
|
const innerW = Math.max(1, this.lastInnerW || event.width - 4);
|
|
377
|
+
const previewHit = this.isPreviewButtonHit(event.x, event.y, innerW);
|
|
308
378
|
const closeHit = this.isCloseButtonHit(event.x, event.y, innerW);
|
|
309
379
|
if (event.type === "move") {
|
|
380
|
+
const previewChanged = previewHit !== this.hoveredPreview;
|
|
310
381
|
const closeChanged = closeHit !== this.hoveredClose;
|
|
382
|
+
this.hoveredPreview = previewHit;
|
|
311
383
|
this.hoveredClose = closeHit;
|
|
312
|
-
if (this.composer || this.searchMode)
|
|
384
|
+
if (this.composer || this.searchMode) {
|
|
385
|
+
return previewChanged || closeChanged ? { handled: true, render: true } : undefined;
|
|
386
|
+
}
|
|
313
387
|
const contentLines = this.buildContentLines(innerW);
|
|
314
388
|
const viewportHeight = this.viewportHeight();
|
|
315
389
|
const maxScroll = Math.max(0, contentLines.length - viewportHeight);
|
|
@@ -318,7 +392,14 @@ export class ConversationViewer implements Component {
|
|
|
318
392
|
const contentOrigin = 3 + (this.invocationLine() ? 1 : 0);
|
|
319
393
|
const contentY = event.y - contentOrigin + visibleStart;
|
|
320
394
|
const timelineResult = this.timeline.handleMouse({ ...event, y: contentY });
|
|
321
|
-
return timelineResult ?? (closeChanged ? { handled: true, render: true } : undefined);
|
|
395
|
+
return timelineResult ?? (previewChanged || closeChanged ? { handled: true, render: true } : undefined);
|
|
396
|
+
}
|
|
397
|
+
if (previewHit && event.button === "left") {
|
|
398
|
+
if (event.type === "press") return { handled: true, capture: true, render: false };
|
|
399
|
+
if (event.type === "click") {
|
|
400
|
+
this.openFocusedToolPreview();
|
|
401
|
+
return { handled: true, render: false };
|
|
402
|
+
}
|
|
322
403
|
}
|
|
323
404
|
if (closeHit && event.button === "left") {
|
|
324
405
|
if (event.type === "press") return { handled: true, capture: true, render: false };
|
|
@@ -362,11 +443,24 @@ export class ConversationViewer implements Component {
|
|
|
362
443
|
return result;
|
|
363
444
|
}
|
|
364
445
|
|
|
446
|
+
private headerActionGeometry(innerW: number): { previewStart: number; previewEnd: number; closeStart: number; closeEnd: number } {
|
|
447
|
+
const closeEnd = 2 + innerW;
|
|
448
|
+
const closeStart = closeEnd - visibleWidth("[esc]");
|
|
449
|
+
const previewEnd = closeStart - 1;
|
|
450
|
+
const previewStart = previewEnd - visibleWidth("[preview]");
|
|
451
|
+
return { previewStart, previewEnd, closeStart, closeEnd };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private isPreviewButtonHit(x: number, y: number, innerW: number): boolean {
|
|
455
|
+
if (y !== 1) return false;
|
|
456
|
+
const { previewStart, previewEnd } = this.headerActionGeometry(innerW);
|
|
457
|
+
return x >= previewStart && x < previewEnd;
|
|
458
|
+
}
|
|
459
|
+
|
|
365
460
|
private isCloseButtonHit(x: number, y: number, innerW: number): boolean {
|
|
366
461
|
if (y !== 1) return false;
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
return x >= labelStart && x < 2 + innerW;
|
|
462
|
+
const { closeStart, closeEnd } = this.headerActionGeometry(innerW);
|
|
463
|
+
return x >= closeStart && x < closeEnd;
|
|
370
464
|
}
|
|
371
465
|
|
|
372
466
|
render(width: number): string[] {
|
|
@@ -396,11 +490,15 @@ export class ConversationViewer implements Component {
|
|
|
396
490
|
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
397
491
|
}
|
|
398
492
|
const headerText = `${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${fgPreservingNestedStyles(th, "dim", headerParts.join(" · "))}`;
|
|
493
|
+
const previewAvailable = this.focusedToolBlock() !== undefined;
|
|
494
|
+
const previewLabel = previewAvailable && this.hoveredPreview
|
|
495
|
+
? th.fg("text", th.bold("[preview]"))
|
|
496
|
+
: th.fg("dim", "[preview]");
|
|
399
497
|
const closeLabel = this.hoveredClose ? th.fg("text", th.bold("[esc]")) : th.fg("dim", "[esc]");
|
|
400
|
-
const
|
|
401
|
-
const headerLeft = truncateToWidth(headerText, Math.max(0, innerW -
|
|
402
|
-
const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) -
|
|
403
|
-
lines.push(row(headerLeft + " ".repeat(headerGap) + closeLabel));
|
|
498
|
+
const actionsWidth = visibleWidth(previewLabel) + 1 + visibleWidth(closeLabel);
|
|
499
|
+
const headerLeft = truncateToWidth(headerText, Math.max(0, innerW - actionsWidth - 1), "", true);
|
|
500
|
+
const headerGap = Math.max(1, innerW - visibleWidth(headerLeft) - actionsWidth);
|
|
501
|
+
lines.push(row(headerLeft + " ".repeat(headerGap) + previewLabel + " " + closeLabel));
|
|
404
502
|
const invocationLine = this.invocationLine();
|
|
405
503
|
if (invocationLine) lines.push(row(invocationLine));
|
|
406
504
|
lines.push(hrMid);
|
|
@@ -442,7 +540,7 @@ export class ConversationViewer implements Component {
|
|
|
442
540
|
lines.push(hrMid);
|
|
443
541
|
if (this.composer) {
|
|
444
542
|
lines.push(row(this.composer.render(innerW)[0] ?? ""));
|
|
445
|
-
const hint = th.fg("dim", "Enter send · Esc cancel");
|
|
543
|
+
const hint = th.fg("dim", "Enter send · Esc cancel · Alt+Up recall");
|
|
446
544
|
const label = th.fg("accent", "✎ steer");
|
|
447
545
|
lines.push(row(label + " ".repeat(Math.max(1, innerW - visibleWidth(label) - visibleWidth(hint))) + hint));
|
|
448
546
|
} else if (this.searchMode) {
|
|
@@ -710,14 +808,18 @@ export class ConversationViewer implements Component {
|
|
|
710
808
|
this.scrollOffset = Math.max(0, Math.min(maxScroll, this.scrollOffset));
|
|
711
809
|
}
|
|
712
810
|
|
|
713
|
-
private
|
|
811
|
+
private focusedToolBlock(): ConversationBlock | undefined {
|
|
714
812
|
this.buildContentLines(this.lastInnerW || 80);
|
|
715
813
|
const focusedToolCallId = this.timeline.getFocusedToolCallId();
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
814
|
+
if (!focusedToolCallId) return undefined;
|
|
815
|
+
const block = this.cachedMessageBlocks.find((candidate) => candidate.toolCallId === focusedToolCallId);
|
|
816
|
+
return block?.kind === "tool" ? block : undefined;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private openFocusedToolPreview(): void {
|
|
820
|
+
const block = this.focusedToolBlock();
|
|
719
821
|
const custom = this.operations?.ctx?.ui?.custom;
|
|
720
|
-
if (block
|
|
822
|
+
if (!block || typeof custom !== "function") return;
|
|
721
823
|
|
|
722
824
|
const toolName = block.toolName || "tool";
|
|
723
825
|
const args = block.args ?? block.toolArguments;
|
|
@@ -865,17 +967,39 @@ export class ConversationViewer implements Component {
|
|
|
865
967
|
private openComposer(): void {
|
|
866
968
|
const input = new Input();
|
|
867
969
|
input.focused = true;
|
|
970
|
+
this.steerHistoryIndex = -1;
|
|
971
|
+
this.steerHistoryDraft = "";
|
|
868
972
|
input.onSubmit = (value: string) => {
|
|
869
973
|
const message = value.trim();
|
|
870
974
|
this.composer = undefined;
|
|
871
|
-
|
|
975
|
+
this.steerHistoryIndex = -1;
|
|
976
|
+
this.steerHistoryDraft = "";
|
|
977
|
+
if (message) {
|
|
978
|
+
if (this.steerHistory[0] !== message) this.steerHistory.unshift(message);
|
|
979
|
+
if (this.steerHistory.length > 100) this.steerHistory.pop();
|
|
980
|
+
this.onSteer?.(message);
|
|
981
|
+
}
|
|
872
982
|
this.tui.requestRender();
|
|
873
983
|
};
|
|
874
984
|
input.onEscape = () => {
|
|
875
985
|
this.composer = undefined;
|
|
986
|
+
this.steerHistoryIndex = -1;
|
|
987
|
+
this.steerHistoryDraft = "";
|
|
876
988
|
this.tui.requestRender();
|
|
877
989
|
};
|
|
878
990
|
this.composer = input;
|
|
879
991
|
this.tui.requestRender();
|
|
880
992
|
}
|
|
993
|
+
|
|
994
|
+
/** Recall the newest prior steer, preserving the draft for future editing. */
|
|
995
|
+
private recallSteerDraft(): void {
|
|
996
|
+
if (!this.composer || this.steerHistory.length === 0) return;
|
|
997
|
+
if (this.steerHistoryIndex < 0) {
|
|
998
|
+
this.steerHistoryDraft = this.composer.getValue();
|
|
999
|
+
this.steerHistoryIndex = 0;
|
|
1000
|
+
} else if (this.steerHistoryIndex < this.steerHistory.length - 1) {
|
|
1001
|
+
this.steerHistoryIndex++;
|
|
1002
|
+
}
|
|
1003
|
+
this.composer.setValue(this.steerHistory[this.steerHistoryIndex] ?? this.steerHistoryDraft);
|
|
1004
|
+
}
|
|
881
1005
|
}
|