@esso0428/pi-subagents 0.15.14 → 0.15.16
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 +9 -32
- package/dist/ui/agent-widget.d.ts +8 -0
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +40 -12
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +2 -4
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-renderer.js +4 -10
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-result.d.ts +9 -16
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-result.js +71 -121
- package/dist/ui/ccstyle/tool-result.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +14 -9
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +60 -72
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +4 -1
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +95 -94
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/write-execution.d.ts +0 -2
- package/dist/write-execution.d.ts.map +1 -1
- package/dist/write-execution.js +1 -5
- package/dist/write-execution.js.map +1 -1
- package/package.json +1 -1
- package/src/ui/agent-widget.ts +106 -20
- package/src/ui/ccstyle/diff/diff-renderer.ts +2 -7
- package/src/ui/ccstyle/tool-renderer.ts +9 -22
- package/src/ui/ccstyle/tool-result.ts +82 -186
- package/src/ui/conversation-blocks.ts +13 -11
- package/src/ui/conversation-timeline.ts +211 -122
- package/src/ui/conversation-viewer.ts +149 -125
- package/src/ui/fleet-list.ts +34 -1
- package/src/write-execution.ts +1 -7
- package/test/ui/agent-widget.test.ts +161 -0
- package/test/ui/conversation-diff.test.ts +1 -47
- package/test/ui/conversation-viewer.test.ts +181 -112
- package/test/ui/fleet-list.test.ts +5 -1
- package/test/write-execution.test.ts +0 -2
- package/.pi/tasks/tasks-01a09263-0ac6-77be-97f5-664b3ffea76b.json +0 -53
- package/.pi/workflows/index.json +0 -5
|
@@ -29,10 +29,15 @@ export interface TimelineRenderLine {
|
|
|
29
29
|
railable?: boolean;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export interface
|
|
33
|
-
|
|
32
|
+
export interface TimelineBlockSpan {
|
|
33
|
+
/** Index into the snapshot's messageBlocks array, not the source block array. */
|
|
34
|
+
blockIndex: number;
|
|
34
35
|
startLine: number;
|
|
35
36
|
height: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface TimelineToolSpan extends TimelineBlockSpan {
|
|
40
|
+
toolCallId: string;
|
|
36
41
|
/** The only row that opens a collapsed card with a single click. */
|
|
37
42
|
clickStartLine?: number;
|
|
38
43
|
clickHeight?: number;
|
|
@@ -43,13 +48,32 @@ export interface ConversationTimelineSnapshot {
|
|
|
43
48
|
messageStarts: number[];
|
|
44
49
|
messageBlocks: ConversationBlock[];
|
|
45
50
|
interactiveToolIds: string[];
|
|
51
|
+
blockSpans: TimelineBlockSpan[];
|
|
46
52
|
toolSpans: TimelineToolSpan[];
|
|
47
53
|
}
|
|
48
54
|
|
|
55
|
+
export interface ConversationTimelineMouseResult {
|
|
56
|
+
handled?: boolean;
|
|
57
|
+
capture?: boolean;
|
|
58
|
+
render?: boolean;
|
|
59
|
+
focusedBlockIndex?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ConversationTimelineLineRange {
|
|
63
|
+
startLine: number;
|
|
64
|
+
height: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ConversationTimelineChange {
|
|
68
|
+
kind: "content" | "interaction";
|
|
69
|
+
/** Lines patched in-place for an interaction-only change such as hover. */
|
|
70
|
+
changedRanges?: readonly ConversationTimelineLineRange[];
|
|
71
|
+
}
|
|
72
|
+
|
|
49
73
|
export interface ConversationTimelineOptions {
|
|
50
74
|
cwd?: string;
|
|
51
75
|
record?: AgentRecord;
|
|
52
|
-
onChange?: () => void;
|
|
76
|
+
onChange?: (change: ConversationTimelineChange) => void;
|
|
53
77
|
}
|
|
54
78
|
|
|
55
79
|
function safeWidth(width: number): number {
|
|
@@ -81,7 +105,7 @@ function fallbackToolLines(block: ConversationBlock, width: number, expanded = f
|
|
|
81
105
|
const clipped = truncateToWidth(line, width, "");
|
|
82
106
|
if (!hovered || expanded || !theme) return clipped;
|
|
83
107
|
const plain = stripAnsi(clipped);
|
|
84
|
-
const marker = plain.includes(showMoreHintText()) ? showMoreHintText() :
|
|
108
|
+
const marker = plain.includes(showMoreHintText()) ? showMoreHintText() : "";
|
|
85
109
|
if (!marker) return clipped;
|
|
86
110
|
const markerIndex = plain.indexOf(marker);
|
|
87
111
|
return theme.fg("dim", plain.slice(0, markerIndex)) + theme.fg("text", marker) + theme.fg("dim", plain.slice(markerIndex + marker.length));
|
|
@@ -144,8 +168,8 @@ export class ConversationTimeline implements Component {
|
|
|
144
168
|
private expandedTools = new Set<string>();
|
|
145
169
|
private hoveredToolCallId: string | undefined;
|
|
146
170
|
private hoveredIoView: any;
|
|
171
|
+
private hoveredIoToolCallId: string | undefined;
|
|
147
172
|
private hoveredIoSection: "input" | "output" | null = null;
|
|
148
|
-
/** The last tool clicked; used to make only clickCount=2 collapse it. */
|
|
149
173
|
private lastClickedToolCallId: string | undefined;
|
|
150
174
|
private lastClickedPoint: { x: number; y: number } | undefined;
|
|
151
175
|
private toolComponents = new Map<string, Component & { handleMouse?: (event: TuiMouseEvent) => unknown }>();
|
|
@@ -154,6 +178,7 @@ export class ConversationTimeline implements Component {
|
|
|
154
178
|
messageStarts: [],
|
|
155
179
|
messageBlocks: [],
|
|
156
180
|
interactiveToolIds: [],
|
|
181
|
+
blockSpans: [],
|
|
157
182
|
toolSpans: [],
|
|
158
183
|
};
|
|
159
184
|
private lastWidth = 0;
|
|
@@ -169,15 +194,12 @@ export class ConversationTimeline implements Component {
|
|
|
169
194
|
const ids = new Set(this.blocks.filter((block) => block.kind === "tool" && block.toolCallId).map((block) => block.toolCallId!));
|
|
170
195
|
if (this.focusedToolCallId && !ids.has(this.focusedToolCallId)) this.focusedToolCallId = undefined;
|
|
171
196
|
if (this.hoveredToolCallId && !ids.has(this.hoveredToolCallId)) this.hoveredToolCallId = undefined;
|
|
172
|
-
if (this.
|
|
173
|
-
this.hoveredIoView
|
|
197
|
+
if (this.hoveredIoToolCallId && !ids.has(this.hoveredIoToolCallId)) {
|
|
198
|
+
this.hoveredIoView?.setHoveredSection?.(null);
|
|
174
199
|
this.hoveredIoView = undefined;
|
|
200
|
+
this.hoveredIoToolCallId = undefined;
|
|
175
201
|
this.hoveredIoSection = null;
|
|
176
202
|
}
|
|
177
|
-
if (this.lastClickedToolCallId && !ids.has(this.lastClickedToolCallId)) {
|
|
178
|
-
this.lastClickedToolCallId = undefined;
|
|
179
|
-
this.lastClickedPoint = undefined;
|
|
180
|
-
}
|
|
181
203
|
for (const id of this.expandedTools) if (!ids.has(id)) this.expandedTools.delete(id);
|
|
182
204
|
for (const id of this.toolComponents.keys()) if (!ids.has(id)) this.toolComponents.delete(id);
|
|
183
205
|
this.invalidate();
|
|
@@ -189,13 +211,10 @@ export class ConversationTimeline implements Component {
|
|
|
189
211
|
if (!show) {
|
|
190
212
|
this.focusedToolCallId = undefined;
|
|
191
213
|
this.hoveredToolCallId = undefined;
|
|
192
|
-
|
|
193
|
-
this.hoveredIoView.setHoveredSection(null);
|
|
194
|
-
}
|
|
214
|
+
this.hoveredIoView?.setHoveredSection?.(null);
|
|
195
215
|
this.hoveredIoView = undefined;
|
|
216
|
+
this.hoveredIoToolCallId = undefined;
|
|
196
217
|
this.hoveredIoSection = null;
|
|
197
|
-
this.lastClickedToolCallId = undefined;
|
|
198
|
-
this.lastClickedPoint = undefined;
|
|
199
218
|
}
|
|
200
219
|
this.invalidate();
|
|
201
220
|
}
|
|
@@ -205,18 +224,16 @@ export class ConversationTimeline implements Component {
|
|
|
205
224
|
}
|
|
206
225
|
|
|
207
226
|
setFocusedToolCallId(id: string | undefined): void {
|
|
227
|
+
// Focus changes are view state. The rendered rows do not change, so keep
|
|
228
|
+
// the snapshot and its Markdown/tool output cache intact.
|
|
229
|
+
if (this.focusedToolCallId === id) return;
|
|
208
230
|
this.focusedToolCallId = id;
|
|
209
|
-
this.invalidate();
|
|
210
231
|
}
|
|
211
232
|
|
|
212
233
|
getFocusedToolCallId(): string | undefined {
|
|
213
234
|
return this.focusedToolCallId;
|
|
214
235
|
}
|
|
215
236
|
|
|
216
|
-
getHoveredToolCallId(): string | undefined {
|
|
217
|
-
return this.hoveredToolCallId;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
237
|
getSnapshot(): ConversationTimelineSnapshot {
|
|
221
238
|
if (this.lastWidth <= 0) this.render(80);
|
|
222
239
|
return this.snapshot;
|
|
@@ -230,8 +247,7 @@ export class ConversationTimeline implements Component {
|
|
|
230
247
|
? direction > 0 ? 0 : ids.length - 1
|
|
231
248
|
: (current + direction + ids.length) % ids.length;
|
|
232
249
|
this.focusedToolCallId = ids[next];
|
|
233
|
-
this.
|
|
234
|
-
this.options.onChange?.();
|
|
250
|
+
this.options.onChange?.({ kind: "interaction" });
|
|
235
251
|
return true;
|
|
236
252
|
}
|
|
237
253
|
|
|
@@ -241,7 +257,7 @@ export class ConversationTimeline implements Component {
|
|
|
241
257
|
if (this.expandedTools.has(id)) this.expandedTools.delete(id);
|
|
242
258
|
else this.expandedTools.add(id);
|
|
243
259
|
this.invalidate();
|
|
244
|
-
this.options.onChange?.();
|
|
260
|
+
this.options.onChange?.({ kind: "content" });
|
|
245
261
|
return true;
|
|
246
262
|
}
|
|
247
263
|
|
|
@@ -251,20 +267,24 @@ export class ConversationTimeline implements Component {
|
|
|
251
267
|
|
|
252
268
|
render(width: number): string[] {
|
|
253
269
|
const requestedWidth = safeWidth(width);
|
|
270
|
+
if (this.lastWidth === requestedWidth) return this.snapshot.lines.map((line) => line.text);
|
|
271
|
+
|
|
254
272
|
const lines: TimelineRenderLine[] = [];
|
|
255
273
|
const messageStarts: number[] = [];
|
|
256
274
|
const messageBlocks: ConversationBlock[] = [];
|
|
257
275
|
const interactiveToolIds: string[] = [];
|
|
276
|
+
const blockSpans: TimelineBlockSpan[] = [];
|
|
258
277
|
const toolSpans: TimelineToolSpan[] = [];
|
|
259
|
-
const visibleBlocks = this.blocks.filter((block) => this.showTools || block.kind !== "tool");
|
|
260
278
|
|
|
261
|
-
if (
|
|
279
|
+
if (this.blocks.length === 0 || (!this.showTools && this.blocks.every((block) => block.kind === "tool"))) {
|
|
262
280
|
const text = this.blocks.length === 0 ? "(waiting for first message...)" : "(tools hidden)";
|
|
263
281
|
lines.push({ text: this.theme.fg("dim", text), plain: text, blockIndex: -1 });
|
|
264
282
|
}
|
|
265
283
|
|
|
266
|
-
for (
|
|
267
|
-
const
|
|
284
|
+
for (let sourceBlockIndex = 0; sourceBlockIndex < this.blocks.length; sourceBlockIndex++) {
|
|
285
|
+
const block = this.blocks[sourceBlockIndex]!;
|
|
286
|
+
if (!this.showTools && block.kind === "tool") continue;
|
|
287
|
+
const blockIndex = sourceBlockIndex;
|
|
268
288
|
if (lines.length > 0) {
|
|
269
289
|
const separator = this.theme.fg("border", " " + "─ ".repeat(Math.max(1, Math.floor((requestedWidth - 6) / 2))).trimEnd());
|
|
270
290
|
lines.push({ text: truncateToWidth(separator, requestedWidth), plain: stripAnsi(separator), blockIndex });
|
|
@@ -272,6 +292,7 @@ export class ConversationTimeline implements Component {
|
|
|
272
292
|
|
|
273
293
|
const hasRoleHeader = block.role === "user" || block.role === "assistant";
|
|
274
294
|
const blockStartLine = lines.length;
|
|
295
|
+
const snapshotBlockIndex = messageBlocks.length;
|
|
275
296
|
messageStarts.push(blockStartLine);
|
|
276
297
|
messageBlocks.push(block);
|
|
277
298
|
if (hasRoleHeader) {
|
|
@@ -282,60 +303,22 @@ export class ConversationTimeline implements Component {
|
|
|
282
303
|
if (block.kind === "tool") {
|
|
283
304
|
const id = block.toolCallId;
|
|
284
305
|
if (id) interactiveToolIds.push(id);
|
|
285
|
-
|
|
286
|
-
if (id) {
|
|
287
|
-
const existing = this.toolComponents.get(id);
|
|
288
|
-
if (existing) {
|
|
289
|
-
applyToolComponentState(existing, block, this.expandedTools.has(id));
|
|
290
|
-
component = existing;
|
|
291
|
-
} else {
|
|
292
|
-
component = createToolComponent(
|
|
293
|
-
block,
|
|
294
|
-
this.tui,
|
|
295
|
-
this.options.cwd ?? process.cwd(),
|
|
296
|
-
this.expandedTools.has(id),
|
|
297
|
-
() => this.hoveredToolCallId === id,
|
|
298
|
-
) as (Component & { handleMouse?: (event: TuiMouseEvent) => unknown }) | undefined;
|
|
299
|
-
if (component) this.toolComponents.set(id, component);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
let rendered: string[];
|
|
303
|
-
try {
|
|
304
|
-
rendered = component?.render(requestedWidth) ?? fallbackToolLines(
|
|
305
|
-
block,
|
|
306
|
-
requestedWidth,
|
|
307
|
-
id ? this.expandedTools.has(id) : false,
|
|
308
|
-
id ? this.hoveredToolCallId === id : false,
|
|
309
|
-
this.theme,
|
|
310
|
-
);
|
|
311
|
-
} catch {
|
|
312
|
-
// Pi's interactive theme is a capability, not a viewer dependency;
|
|
313
|
-
// test hosts and older Pi versions can lack its initialized theme.
|
|
314
|
-
rendered = fallbackToolLines(
|
|
315
|
-
block,
|
|
316
|
-
requestedWidth,
|
|
317
|
-
id ? this.expandedTools.has(id) : false,
|
|
318
|
-
id ? this.hoveredToolCallId === id : false,
|
|
319
|
-
this.theme,
|
|
320
|
-
);
|
|
321
|
-
}
|
|
306
|
+
const { rendered, hintLine } = this.renderToolLines(block, requestedWidth);
|
|
322
307
|
for (const line of rendered) {
|
|
323
308
|
lines.push({ text: truncateToWidth(line, requestedWidth), plain: stripAnsi(line), blockIndex, railable: true });
|
|
324
309
|
}
|
|
325
310
|
if (id) {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
? rendered.findIndex((line) => {
|
|
329
|
-
const plain = stripAnsi(line);
|
|
330
|
-
return plain.includes(showMoreHintText()) || plain.includes("Enter to expand");
|
|
331
|
-
})
|
|
332
|
-
: -1;
|
|
311
|
+
const blockHeight = lines.length - blockStartLine;
|
|
312
|
+
blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: blockHeight });
|
|
333
313
|
toolSpans.push({
|
|
314
|
+
blockIndex: snapshotBlockIndex,
|
|
334
315
|
toolCallId: id,
|
|
335
|
-
startLine,
|
|
336
|
-
height:
|
|
337
|
-
...(hintLine >= 0 ? { clickStartLine:
|
|
316
|
+
startLine: blockStartLine,
|
|
317
|
+
height: blockHeight,
|
|
318
|
+
...(hintLine >= 0 ? { clickStartLine: blockStartLine + (hasRoleHeader ? 1 : 0) + hintLine, clickHeight: 1 } : {}),
|
|
338
319
|
});
|
|
320
|
+
} else {
|
|
321
|
+
blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: lines.length - blockStartLine });
|
|
339
322
|
}
|
|
340
323
|
continue;
|
|
341
324
|
}
|
|
@@ -358,14 +341,88 @@ export class ConversationTimeline implements Component {
|
|
|
358
341
|
}
|
|
359
342
|
}
|
|
360
343
|
}
|
|
344
|
+
blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: lines.length - blockStartLine });
|
|
361
345
|
}
|
|
362
346
|
|
|
363
347
|
this.lastWidth = requestedWidth;
|
|
364
|
-
this.snapshot = { lines, messageStarts, messageBlocks, interactiveToolIds, toolSpans };
|
|
348
|
+
this.snapshot = { lines, messageStarts, messageBlocks, interactiveToolIds, blockSpans, toolSpans };
|
|
365
349
|
return lines.map((line) => line.text);
|
|
366
350
|
}
|
|
367
351
|
|
|
368
|
-
|
|
352
|
+
private renderToolLines(block: ConversationBlock, width: number): { rendered: string[]; hintLine: number } {
|
|
353
|
+
const id = block.toolCallId;
|
|
354
|
+
let component: (Component & { handleMouse?: (event: TuiMouseEvent) => unknown }) | undefined;
|
|
355
|
+
if (id) {
|
|
356
|
+
const existing = this.toolComponents.get(id);
|
|
357
|
+
if (existing) {
|
|
358
|
+
applyToolComponentState(existing, block, this.expandedTools.has(id));
|
|
359
|
+
component = existing;
|
|
360
|
+
} else {
|
|
361
|
+
component = createToolComponent(
|
|
362
|
+
block,
|
|
363
|
+
this.tui,
|
|
364
|
+
this.options.cwd ?? process.cwd(),
|
|
365
|
+
this.expandedTools.has(id),
|
|
366
|
+
() => this.hoveredToolCallId === id,
|
|
367
|
+
) as (Component & { handleMouse?: (event: TuiMouseEvent) => unknown }) | undefined;
|
|
368
|
+
if (component) this.toolComponents.set(id, component);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
let rendered: string[];
|
|
373
|
+
try {
|
|
374
|
+
rendered = component?.render(width) ?? fallbackToolLines(
|
|
375
|
+
block,
|
|
376
|
+
width,
|
|
377
|
+
id ? this.expandedTools.has(id) : false,
|
|
378
|
+
id ? this.hoveredToolCallId === id : false,
|
|
379
|
+
this.theme,
|
|
380
|
+
);
|
|
381
|
+
} catch {
|
|
382
|
+
rendered = fallbackToolLines(
|
|
383
|
+
block,
|
|
384
|
+
width,
|
|
385
|
+
id ? this.expandedTools.has(id) : false,
|
|
386
|
+
id ? this.hoveredToolCallId === id : false,
|
|
387
|
+
this.theme,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const hintLine = id && !this.expandedTools.has(id)
|
|
391
|
+
? rendered.findIndex((line) => {
|
|
392
|
+
const plain = stripAnsi(line);
|
|
393
|
+
return plain.includes(showMoreHintText()) || plain.includes("Enter to expand");
|
|
394
|
+
})
|
|
395
|
+
: -1;
|
|
396
|
+
return { rendered, hintLine };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Hover only changes styling in a tool card. Re-render those rows in place
|
|
401
|
+
* and leave the Markdown rows and their spans untouched.
|
|
402
|
+
*/
|
|
403
|
+
private refreshToolSpan(span: TimelineToolSpan): boolean {
|
|
404
|
+
const block = this.snapshot.messageBlocks[span.blockIndex];
|
|
405
|
+
if (!block || block.kind !== "tool") return false;
|
|
406
|
+
const { rendered, hintLine } = this.renderToolLines(block, this.lastWidth);
|
|
407
|
+
if (rendered.length + (block.role === "user" || block.role === "assistant" ? 1 : 0) !== span.height) return false;
|
|
408
|
+
|
|
409
|
+
const headerOffset = block.role === "user" || block.role === "assistant" ? 1 : 0;
|
|
410
|
+
const sourceBlockIndex = this.snapshot.lines[span.startLine]?.blockIndex ?? -1;
|
|
411
|
+
for (let index = 0; index < rendered.length; index++) {
|
|
412
|
+
const line = rendered[index];
|
|
413
|
+
this.snapshot.lines[span.startLine + headerOffset + index] = {
|
|
414
|
+
text: truncateToWidth(line, this.lastWidth),
|
|
415
|
+
plain: stripAnsi(line),
|
|
416
|
+
blockIndex: sourceBlockIndex,
|
|
417
|
+
railable: true,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
span.clickStartLine = hintLine >= 0 ? span.startLine + headerOffset + hintLine : undefined;
|
|
421
|
+
span.clickHeight = hintLine >= 0 ? 1 : undefined;
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
handleMouse(event: TuiMouseEvent): ConversationTimelineMouseResult | undefined {
|
|
369
426
|
if (event.type === "move") {
|
|
370
427
|
const hovered = this.snapshot.toolSpans.find((span) =>
|
|
371
428
|
span.clickStartLine !== undefined &&
|
|
@@ -389,78 +446,110 @@ export class ConversationTimeline implements Component {
|
|
|
389
446
|
}
|
|
390
447
|
}
|
|
391
448
|
}
|
|
392
|
-
const unchanged = hovered === this.hoveredToolCallId &&
|
|
393
|
-
nextIoView === this.hoveredIoView && nextIoSection === this.hoveredIoSection;
|
|
449
|
+
const unchanged = hovered === this.hoveredToolCallId && nextIoView === this.hoveredIoView && nextIoSection === this.hoveredIoSection;
|
|
394
450
|
if (unchanged) return undefined;
|
|
395
451
|
if (this.hoveredIoView && this.hoveredIoView !== nextIoView && typeof this.hoveredIoView.setHoveredSection === "function") {
|
|
396
452
|
this.hoveredIoView.setHoveredSection(null);
|
|
397
453
|
}
|
|
454
|
+
const previousHoveredToolCallId = this.hoveredToolCallId;
|
|
455
|
+
const previousIoToolCallId = this.hoveredIoToolCallId;
|
|
456
|
+
const nextIoToolCallId = nextIoView ? fullSpan?.toolCallId : undefined;
|
|
398
457
|
this.hoveredToolCallId = hovered;
|
|
399
458
|
this.hoveredIoView = nextIoView;
|
|
459
|
+
this.hoveredIoToolCallId = nextIoToolCallId;
|
|
400
460
|
this.hoveredIoSection = nextIoSection;
|
|
401
461
|
if (nextIoView) nextIoView.setHoveredSection(nextIoSection);
|
|
402
|
-
|
|
403
|
-
|
|
462
|
+
|
|
463
|
+
const changedIds = new Set(
|
|
464
|
+
[previousHoveredToolCallId, hovered, previousIoToolCallId, nextIoToolCallId]
|
|
465
|
+
.filter((id): id is string => id !== undefined),
|
|
466
|
+
);
|
|
467
|
+
const changedRanges: ConversationTimelineLineRange[] = [];
|
|
468
|
+
let patched = true;
|
|
469
|
+
for (const id of changedIds) {
|
|
470
|
+
const span = this.snapshot.toolSpans.find((candidate) => candidate.toolCallId === id);
|
|
471
|
+
if (!span || !this.refreshToolSpan(span)) {
|
|
472
|
+
patched = false;
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
changedRanges.push({ startLine: span.startLine, height: span.height });
|
|
476
|
+
}
|
|
477
|
+
if (!patched) {
|
|
478
|
+
this.invalidate();
|
|
479
|
+
this.options.onChange?.({ kind: "content" });
|
|
480
|
+
} else {
|
|
481
|
+
this.options.onChange?.({ kind: "interaction", changedRanges });
|
|
482
|
+
}
|
|
404
483
|
return { handled: true, render: true };
|
|
405
484
|
}
|
|
406
485
|
|
|
407
|
-
// The TUI sends a click only when a component claims the preceding press.
|
|
408
|
-
// Claim only the collapsed hint (or the last expanded card's hit area for
|
|
409
|
-
// a possible second click); never change expansion on press or release.
|
|
410
486
|
if (event.type !== "click" && event.type !== "press") return undefined;
|
|
411
487
|
if (event.button !== "left") return undefined;
|
|
412
|
-
|
|
488
|
+
|
|
489
|
+
// Keep the hint ahead of the full tool span. Its row has a distinct action.
|
|
413
490
|
const hintSpan = this.snapshot.toolSpans.find((candidate) =>
|
|
414
491
|
candidate.clickStartLine !== undefined &&
|
|
415
492
|
event.y >= candidate.clickStartLine &&
|
|
416
493
|
event.y < candidate.clickStartLine + (candidate.clickHeight ?? 1),
|
|
417
494
|
);
|
|
418
495
|
const fullSpan = this.snapshot.toolSpans.find((candidate) =>
|
|
419
|
-
event.y >= candidate.startLine &&
|
|
420
|
-
event.y < candidate.startLine + candidate.height,
|
|
496
|
+
event.y >= candidate.startLine && event.y < candidate.startLine + candidate.height,
|
|
421
497
|
);
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
&& event.screenY === this.lastClickedPoint.y;
|
|
428
|
-
const span = event.type === "click" && clickCount === 2 && lastSpan
|
|
429
|
-
? lastSpan
|
|
430
|
-
: hintSpan ?? (sameLastPoint ? lastSpan : undefined);
|
|
431
|
-
if (!span) return undefined;
|
|
498
|
+
const blockSpan = this.snapshot.blockSpans.find((candidate) =>
|
|
499
|
+
event.y >= candidate.startLine && event.y < candidate.startLine + candidate.height,
|
|
500
|
+
);
|
|
501
|
+
const target = hintSpan ?? fullSpan ?? blockSpan;
|
|
502
|
+
if (!target) return undefined;
|
|
432
503
|
if (event.type === "press") return { handled: true, capture: true, render: false };
|
|
433
504
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
this.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
505
|
+
if (hintSpan || fullSpan) {
|
|
506
|
+
const span = hintSpan ?? fullSpan!;
|
|
507
|
+
const clickCount = event.clickCount ?? 1;
|
|
508
|
+
const previousClickedToolCallId = this.lastClickedToolCallId;
|
|
509
|
+
const lastSpan = this.lastClickedToolCallId
|
|
510
|
+
? this.snapshot.toolSpans.find((candidate) => candidate.toolCallId === this.lastClickedToolCallId)
|
|
511
|
+
: undefined;
|
|
512
|
+
const sameLastPoint = this.lastClickedPoint !== undefined &&
|
|
513
|
+
event.screenX === this.lastClickedPoint.x && event.screenY === this.lastClickedPoint.y;
|
|
514
|
+
const sameLastSpan = sameLastPoint && span.toolCallId === lastSpan?.toolCallId;
|
|
515
|
+
const isExpanded = this.expandedTools.has(span.toolCallId);
|
|
516
|
+
const shouldExpand = !isExpanded && hintSpan?.toolCallId === span.toolCallId;
|
|
517
|
+
const shouldCollapse = isExpanded && clickCount === 2 && sameLastSpan && span.toolCallId === previousClickedToolCallId;
|
|
518
|
+
|
|
519
|
+
this.focusedToolCallId = span.toolCallId;
|
|
520
|
+
this.lastClickedToolCallId = span.toolCallId;
|
|
521
|
+
this.lastClickedPoint = { x: event.screenX, y: event.screenY };
|
|
522
|
+
if (shouldExpand) this.expandedTools.add(span.toolCallId);
|
|
523
|
+
else if (shouldCollapse) this.expandedTools.delete(span.toolCallId);
|
|
524
|
+
|
|
525
|
+
if (shouldExpand || shouldCollapse) {
|
|
526
|
+
const component = this.toolComponents.get(span.toolCallId);
|
|
527
|
+
try {
|
|
528
|
+
component?.handleMouse?.({ ...event, y: event.y - span.startLine, height: span.height });
|
|
529
|
+
} catch {
|
|
530
|
+
// A missing/older Pi mouse implementation must not break the viewer.
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (shouldExpand || shouldCollapse) {
|
|
534
|
+
this.invalidate();
|
|
535
|
+
this.options.onChange?.({ kind: "content" });
|
|
536
|
+
} else {
|
|
537
|
+
this.options.onChange?.({ kind: "interaction" });
|
|
538
|
+
}
|
|
539
|
+
return { handled: true, render: shouldExpand || shouldCollapse, focusedBlockIndex: span.blockIndex };
|
|
446
540
|
}
|
|
447
541
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
// A missing/older Pi mouse implementation must not break the viewer.
|
|
456
|
-
}
|
|
457
|
-
this.invalidate();
|
|
458
|
-
this.options.onChange?.();
|
|
459
|
-
return { handled: true, render: true };
|
|
542
|
+
// A message click intentionally drops any stale tool focus so Enter cannot
|
|
543
|
+
// toggle a tool that is no longer the selected block.
|
|
544
|
+
this.focusedToolCallId = undefined;
|
|
545
|
+
this.lastClickedToolCallId = undefined;
|
|
546
|
+
this.lastClickedPoint = undefined;
|
|
547
|
+
this.options.onChange?.({ kind: "interaction" });
|
|
548
|
+
return { handled: true, render: true, focusedBlockIndex: target.blockIndex };
|
|
460
549
|
}
|
|
461
550
|
|
|
462
551
|
invalidate(): void {
|
|
463
552
|
this.lastWidth = 0;
|
|
464
|
-
this.snapshot = { lines: [], messageStarts: [], messageBlocks: [], interactiveToolIds: [], toolSpans: [] };
|
|
553
|
+
this.snapshot = { lines: [], messageStarts: [], messageBlocks: [], interactiveToolIds: [], blockSpans: [], toolSpans: [] };
|
|
465
554
|
}
|
|
466
555
|
}
|