@esso0428/pi-subagents 0.15.14 → 0.15.15

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +6 -37
  2. package/dist/ui/agent-widget.d.ts +8 -0
  3. package/dist/ui/agent-widget.d.ts.map +1 -1
  4. package/dist/ui/agent-widget.js +40 -12
  5. package/dist/ui/agent-widget.js.map +1 -1
  6. package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
  7. package/dist/ui/ccstyle/diff/diff-renderer.js +2 -4
  8. package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
  9. package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
  10. package/dist/ui/ccstyle/tool-renderer.js +4 -10
  11. package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
  12. package/dist/ui/ccstyle/tool-result.d.ts +9 -16
  13. package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
  14. package/dist/ui/ccstyle/tool-result.js +71 -121
  15. package/dist/ui/ccstyle/tool-result.js.map +1 -1
  16. package/dist/ui/conversation-timeline.d.ts +14 -9
  17. package/dist/ui/conversation-timeline.d.ts.map +1 -1
  18. package/dist/ui/conversation-timeline.js +60 -72
  19. package/dist/ui/conversation-timeline.js.map +1 -1
  20. package/dist/ui/conversation-viewer.d.ts +4 -1
  21. package/dist/ui/conversation-viewer.d.ts.map +1 -1
  22. package/dist/ui/conversation-viewer.js +95 -94
  23. package/dist/ui/conversation-viewer.js.map +1 -1
  24. package/dist/write-execution.d.ts +0 -2
  25. package/dist/write-execution.d.ts.map +1 -1
  26. package/dist/write-execution.js +1 -5
  27. package/dist/write-execution.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/ui/agent-widget.ts +38 -12
  30. package/src/ui/ccstyle/diff/diff-renderer.ts +2 -7
  31. package/src/ui/ccstyle/tool-renderer.ts +9 -22
  32. package/src/ui/ccstyle/tool-result.ts +82 -186
  33. package/src/ui/conversation-timeline.ts +77 -73
  34. package/src/ui/conversation-viewer.ts +102 -108
  35. package/src/write-execution.ts +1 -7
  36. package/test/ui/agent-widget.test.ts +123 -0
  37. package/test/ui/conversation-diff.test.ts +1 -47
  38. package/test/ui/conversation-viewer.test.ts +181 -112
  39. package/test/write-execution.test.ts +0 -2
  40. package/.pi/tasks/tasks-01a09263-0ac6-77be-97f5-664b3ffea76b.json +0 -53
  41. 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 TimelineToolSpan {
33
- toolCallId: string;
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,9 +48,17 @@ 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
+
49
62
  export interface ConversationTimelineOptions {
50
63
  cwd?: string;
51
64
  record?: AgentRecord;
@@ -81,7 +94,7 @@ function fallbackToolLines(block: ConversationBlock, width: number, expanded = f
81
94
  const clipped = truncateToWidth(line, width, "");
82
95
  if (!hovered || expanded || !theme) return clipped;
83
96
  const plain = stripAnsi(clipped);
84
- const marker = plain.includes(showMoreHintText()) ? showMoreHintText() : plain.includes("Enter to expand") ? "Enter to expand" : "";
97
+ const marker = plain.includes(showMoreHintText()) ? showMoreHintText() : "";
85
98
  if (!marker) return clipped;
86
99
  const markerIndex = plain.indexOf(marker);
87
100
  return theme.fg("dim", plain.slice(0, markerIndex)) + theme.fg("text", marker) + theme.fg("dim", plain.slice(markerIndex + marker.length));
@@ -145,7 +158,6 @@ export class ConversationTimeline implements Component {
145
158
  private hoveredToolCallId: string | undefined;
146
159
  private hoveredIoView: any;
147
160
  private hoveredIoSection: "input" | "output" | null = null;
148
- /** The last tool clicked; used to make only clickCount=2 collapse it. */
149
161
  private lastClickedToolCallId: string | undefined;
150
162
  private lastClickedPoint: { x: number; y: number } | undefined;
151
163
  private toolComponents = new Map<string, Component & { handleMouse?: (event: TuiMouseEvent) => unknown }>();
@@ -154,6 +166,7 @@ export class ConversationTimeline implements Component {
154
166
  messageStarts: [],
155
167
  messageBlocks: [],
156
168
  interactiveToolIds: [],
169
+ blockSpans: [],
157
170
  toolSpans: [],
158
171
  };
159
172
  private lastWidth = 0;
@@ -169,15 +182,6 @@ export class ConversationTimeline implements Component {
169
182
  const ids = new Set(this.blocks.filter((block) => block.kind === "tool" && block.toolCallId).map((block) => block.toolCallId!));
170
183
  if (this.focusedToolCallId && !ids.has(this.focusedToolCallId)) this.focusedToolCallId = undefined;
171
184
  if (this.hoveredToolCallId && !ids.has(this.hoveredToolCallId)) this.hoveredToolCallId = undefined;
172
- if (this.hoveredIoView && typeof this.hoveredIoView.setHoveredSection === "function") {
173
- this.hoveredIoView.setHoveredSection(null);
174
- this.hoveredIoView = undefined;
175
- this.hoveredIoSection = null;
176
- }
177
- if (this.lastClickedToolCallId && !ids.has(this.lastClickedToolCallId)) {
178
- this.lastClickedToolCallId = undefined;
179
- this.lastClickedPoint = undefined;
180
- }
181
185
  for (const id of this.expandedTools) if (!ids.has(id)) this.expandedTools.delete(id);
182
186
  for (const id of this.toolComponents.keys()) if (!ids.has(id)) this.toolComponents.delete(id);
183
187
  this.invalidate();
@@ -189,13 +193,6 @@ export class ConversationTimeline implements Component {
189
193
  if (!show) {
190
194
  this.focusedToolCallId = undefined;
191
195
  this.hoveredToolCallId = undefined;
192
- if (this.hoveredIoView && typeof this.hoveredIoView.setHoveredSection === "function") {
193
- this.hoveredIoView.setHoveredSection(null);
194
- }
195
- this.hoveredIoView = undefined;
196
- this.hoveredIoSection = null;
197
- this.lastClickedToolCallId = undefined;
198
- this.lastClickedPoint = undefined;
199
196
  }
200
197
  this.invalidate();
201
198
  }
@@ -213,10 +210,6 @@ export class ConversationTimeline implements Component {
213
210
  return this.focusedToolCallId;
214
211
  }
215
212
 
216
- getHoveredToolCallId(): string | undefined {
217
- return this.hoveredToolCallId;
218
- }
219
-
220
213
  getSnapshot(): ConversationTimelineSnapshot {
221
214
  if (this.lastWidth <= 0) this.render(80);
222
215
  return this.snapshot;
@@ -255,6 +248,7 @@ export class ConversationTimeline implements Component {
255
248
  const messageStarts: number[] = [];
256
249
  const messageBlocks: ConversationBlock[] = [];
257
250
  const interactiveToolIds: string[] = [];
251
+ const blockSpans: TimelineBlockSpan[] = [];
258
252
  const toolSpans: TimelineToolSpan[] = [];
259
253
  const visibleBlocks = this.blocks.filter((block) => this.showTools || block.kind !== "tool");
260
254
 
@@ -272,6 +266,7 @@ export class ConversationTimeline implements Component {
272
266
 
273
267
  const hasRoleHeader = block.role === "user" || block.role === "assistant";
274
268
  const blockStartLine = lines.length;
269
+ const snapshotBlockIndex = messageBlocks.length;
275
270
  messageStarts.push(blockStartLine);
276
271
  messageBlocks.push(block);
277
272
  if (hasRoleHeader) {
@@ -309,8 +304,6 @@ export class ConversationTimeline implements Component {
309
304
  this.theme,
310
305
  );
311
306
  } 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
307
  rendered = fallbackToolLines(
315
308
  block,
316
309
  requestedWidth,
@@ -323,19 +316,23 @@ export class ConversationTimeline implements Component {
323
316
  lines.push({ text: truncateToWidth(line, requestedWidth), plain: stripAnsi(line), blockIndex, railable: true });
324
317
  }
325
318
  if (id) {
326
- const startLine = blockStartLine;
327
319
  const hintLine = !this.expandedTools.has(id)
328
320
  ? rendered.findIndex((line) => {
329
321
  const plain = stripAnsi(line);
330
322
  return plain.includes(showMoreHintText()) || plain.includes("Enter to expand");
331
323
  })
332
324
  : -1;
325
+ const blockHeight = lines.length - blockStartLine;
326
+ blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: blockHeight });
333
327
  toolSpans.push({
328
+ blockIndex: snapshotBlockIndex,
334
329
  toolCallId: id,
335
- startLine,
336
- height: lines.length - startLine,
337
- ...(hintLine >= 0 ? { clickStartLine: startLine + (hasRoleHeader ? 1 : 0) + hintLine, clickHeight: 1 } : {}),
330
+ startLine: blockStartLine,
331
+ height: blockHeight,
332
+ ...(hintLine >= 0 ? { clickStartLine: blockStartLine + (hasRoleHeader ? 1 : 0) + hintLine, clickHeight: 1 } : {}),
338
333
  });
334
+ } else {
335
+ blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: lines.length - blockStartLine });
339
336
  }
340
337
  continue;
341
338
  }
@@ -358,14 +355,15 @@ export class ConversationTimeline implements Component {
358
355
  }
359
356
  }
360
357
  }
358
+ blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: lines.length - blockStartLine });
361
359
  }
362
360
 
363
361
  this.lastWidth = requestedWidth;
364
- this.snapshot = { lines, messageStarts, messageBlocks, interactiveToolIds, toolSpans };
362
+ this.snapshot = { lines, messageStarts, messageBlocks, interactiveToolIds, blockSpans, toolSpans };
365
363
  return lines.map((line) => line.text);
366
364
  }
367
365
 
368
- handleMouse(event: TuiMouseEvent): { handled?: boolean; capture?: boolean; render?: boolean } | undefined {
366
+ handleMouse(event: TuiMouseEvent): ConversationTimelineMouseResult | undefined {
369
367
  if (event.type === "move") {
370
368
  const hovered = this.snapshot.toolSpans.find((span) =>
371
369
  span.clickStartLine !== undefined &&
@@ -389,8 +387,7 @@ export class ConversationTimeline implements Component {
389
387
  }
390
388
  }
391
389
  }
392
- const unchanged = hovered === this.hoveredToolCallId &&
393
- nextIoView === this.hoveredIoView && nextIoSection === this.hoveredIoSection;
390
+ const unchanged = hovered === this.hoveredToolCallId && nextIoView === this.hoveredIoView && nextIoSection === this.hoveredIoSection;
394
391
  if (unchanged) return undefined;
395
392
  if (this.hoveredIoView && this.hoveredIoView !== nextIoView && typeof this.hoveredIoView.setHoveredSection === "function") {
396
393
  this.hoveredIoView.setHoveredSection(null);
@@ -404,63 +401,70 @@ export class ConversationTimeline implements Component {
404
401
  return { handled: true, render: true };
405
402
  }
406
403
 
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
404
  if (event.type !== "click" && event.type !== "press") return undefined;
411
405
  if (event.button !== "left") return undefined;
412
- const clickCount = event.clickCount ?? 1;
406
+
407
+ // Keep the hint ahead of the full tool span. Its row has a distinct action.
413
408
  const hintSpan = this.snapshot.toolSpans.find((candidate) =>
414
409
  candidate.clickStartLine !== undefined &&
415
410
  event.y >= candidate.clickStartLine &&
416
411
  event.y < candidate.clickStartLine + (candidate.clickHeight ?? 1),
417
412
  );
418
413
  const fullSpan = this.snapshot.toolSpans.find((candidate) =>
419
- event.y >= candidate.startLine &&
420
- event.y < candidate.startLine + candidate.height,
414
+ event.y >= candidate.startLine && event.y < candidate.startLine + candidate.height,
421
415
  );
422
- const lastSpan = this.lastClickedToolCallId
423
- ? this.snapshot.toolSpans.find((candidate) => candidate.toolCallId === this.lastClickedToolCallId)
424
- : undefined;
425
- const sameLastPoint = this.lastClickedPoint !== undefined
426
- && event.screenX === this.lastClickedPoint.x
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;
416
+ const blockSpan = this.snapshot.blockSpans.find((candidate) =>
417
+ event.y >= candidate.startLine && event.y < candidate.startLine + candidate.height,
418
+ );
419
+ const target = hintSpan ?? fullSpan ?? blockSpan;
420
+ if (!target) return undefined;
432
421
  if (event.type === "press") return { handled: true, capture: true, render: false };
433
422
 
434
- const previousClickedToolCallId = this.lastClickedToolCallId;
435
- this.focusedToolCallId = span.toolCallId;
436
- this.lastClickedToolCallId = span.toolCallId;
437
- this.lastClickedPoint = { x: event.screenX, y: event.screenY };
438
- const isExpanded = this.expandedTools.has(span.toolCallId);
439
- const shouldCollapse = isExpanded && clickCount === 2 && span.toolCallId === previousClickedToolCallId;
440
- if (!isExpanded && hintSpan?.toolCallId === span.toolCallId) {
441
- this.expandedTools.add(span.toolCallId);
442
- } else if (shouldCollapse) {
443
- this.expandedTools.delete(span.toolCallId);
444
- } else if (!isExpanded) {
445
- return { handled: true, render: false };
423
+ if (hintSpan || fullSpan) {
424
+ const span = hintSpan ?? fullSpan!;
425
+ const clickCount = event.clickCount ?? 1;
426
+ const previousClickedToolCallId = this.lastClickedToolCallId;
427
+ const lastSpan = this.lastClickedToolCallId
428
+ ? this.snapshot.toolSpans.find((candidate) => candidate.toolCallId === this.lastClickedToolCallId)
429
+ : undefined;
430
+ const sameLastPoint = this.lastClickedPoint !== undefined &&
431
+ event.screenX === this.lastClickedPoint.x && event.screenY === this.lastClickedPoint.y;
432
+ const sameLastSpan = sameLastPoint && span.toolCallId === lastSpan?.toolCallId;
433
+ const isExpanded = this.expandedTools.has(span.toolCallId);
434
+ const shouldExpand = !isExpanded && hintSpan?.toolCallId === span.toolCallId;
435
+ const shouldCollapse = isExpanded && clickCount === 2 && sameLastSpan && span.toolCallId === previousClickedToolCallId;
436
+
437
+ this.focusedToolCallId = span.toolCallId;
438
+ this.lastClickedToolCallId = span.toolCallId;
439
+ this.lastClickedPoint = { x: event.screenX, y: event.screenY };
440
+ if (shouldExpand) this.expandedTools.add(span.toolCallId);
441
+ else if (shouldCollapse) this.expandedTools.delete(span.toolCallId);
442
+
443
+ if (shouldExpand || shouldCollapse) {
444
+ const component = this.toolComponents.get(span.toolCallId);
445
+ try {
446
+ component?.handleMouse?.({ ...event, y: event.y - span.startLine, height: span.height });
447
+ } catch {
448
+ // A missing/older Pi mouse implementation must not break the viewer.
449
+ }
450
+ }
451
+ this.invalidate();
452
+ this.options.onChange?.();
453
+ return { handled: true, render: shouldExpand || shouldCollapse, focusedBlockIndex: span.blockIndex };
446
454
  }
447
455
 
448
- const component = this.toolComponents.get(span.toolCallId);
449
- try {
450
- // Keep Pi's normalized mouse contract in the component tree. The local
451
- // role header occupies the first row; ToolExecutionComponent receives
452
- // the remaining card-relative coordinate.
453
- component?.handleMouse?.({ ...event, y: event.y - span.startLine, height: span.height });
454
- } catch {
455
- // A missing/older Pi mouse implementation must not break the viewer.
456
- }
456
+ // A message click intentionally drops any stale tool focus so Enter cannot
457
+ // toggle a tool that is no longer the selected block.
458
+ this.focusedToolCallId = undefined;
459
+ this.lastClickedToolCallId = undefined;
460
+ this.lastClickedPoint = undefined;
457
461
  this.invalidate();
458
462
  this.options.onChange?.();
459
- return { handled: true, render: true };
463
+ return { handled: true, render: true, focusedBlockIndex: target.blockIndex };
460
464
  }
461
465
 
462
466
  invalidate(): void {
463
467
  this.lastWidth = 0;
464
- this.snapshot = { lines: [], messageStarts: [], messageBlocks: [], interactiveToolIds: [], toolSpans: [] };
468
+ this.snapshot = { lines: [], messageStarts: [], messageBlocks: [], interactiveToolIds: [], blockSpans: [], toolSpans: [] };
465
469
  }
466
470
  }