@esso0428/pi-subagents 0.15.13 → 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 -31
  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 +17 -11
  11. package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
  12. package/dist/ui/ccstyle/tool-result.d.ts +22 -10
  13. package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
  14. package/dist/ui/ccstyle/tool-result.js +160 -73
  15. package/dist/ui/ccstyle/tool-result.js.map +1 -1
  16. package/dist/ui/conversation-timeline.d.ts +16 -9
  17. package/dist/ui/conversation-timeline.d.ts.map +1 -1
  18. package/dist/ui/conversation-timeline.js +85 -61
  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 +107 -110
  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 +23 -24
  32. package/src/ui/ccstyle/tool-result.ts +167 -107
  33. package/src/ui/conversation-timeline.ts +103 -62
  34. package/src/ui/conversation-viewer.ts +119 -124
  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 -28
  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));
@@ -143,7 +156,8 @@ export class ConversationTimeline implements Component {
143
156
  private focusedToolCallId: string | undefined;
144
157
  private expandedTools = new Set<string>();
145
158
  private hoveredToolCallId: string | undefined;
146
- /** The last tool clicked; used to make only clickCount=2 collapse it. */
159
+ private hoveredIoView: any;
160
+ private hoveredIoSection: "input" | "output" | null = null;
147
161
  private lastClickedToolCallId: string | undefined;
148
162
  private lastClickedPoint: { x: number; y: number } | undefined;
149
163
  private toolComponents = new Map<string, Component & { handleMouse?: (event: TuiMouseEvent) => unknown }>();
@@ -152,6 +166,7 @@ export class ConversationTimeline implements Component {
152
166
  messageStarts: [],
153
167
  messageBlocks: [],
154
168
  interactiveToolIds: [],
169
+ blockSpans: [],
155
170
  toolSpans: [],
156
171
  };
157
172
  private lastWidth = 0;
@@ -167,10 +182,6 @@ export class ConversationTimeline implements Component {
167
182
  const ids = new Set(this.blocks.filter((block) => block.kind === "tool" && block.toolCallId).map((block) => block.toolCallId!));
168
183
  if (this.focusedToolCallId && !ids.has(this.focusedToolCallId)) this.focusedToolCallId = undefined;
169
184
  if (this.hoveredToolCallId && !ids.has(this.hoveredToolCallId)) this.hoveredToolCallId = undefined;
170
- if (this.lastClickedToolCallId && !ids.has(this.lastClickedToolCallId)) {
171
- this.lastClickedToolCallId = undefined;
172
- this.lastClickedPoint = undefined;
173
- }
174
185
  for (const id of this.expandedTools) if (!ids.has(id)) this.expandedTools.delete(id);
175
186
  for (const id of this.toolComponents.keys()) if (!ids.has(id)) this.toolComponents.delete(id);
176
187
  this.invalidate();
@@ -182,8 +193,6 @@ export class ConversationTimeline implements Component {
182
193
  if (!show) {
183
194
  this.focusedToolCallId = undefined;
184
195
  this.hoveredToolCallId = undefined;
185
- this.lastClickedToolCallId = undefined;
186
- this.lastClickedPoint = undefined;
187
196
  }
188
197
  this.invalidate();
189
198
  }
@@ -201,10 +210,6 @@ export class ConversationTimeline implements Component {
201
210
  return this.focusedToolCallId;
202
211
  }
203
212
 
204
- getHoveredToolCallId(): string | undefined {
205
- return this.hoveredToolCallId;
206
- }
207
-
208
213
  getSnapshot(): ConversationTimelineSnapshot {
209
214
  if (this.lastWidth <= 0) this.render(80);
210
215
  return this.snapshot;
@@ -243,6 +248,7 @@ export class ConversationTimeline implements Component {
243
248
  const messageStarts: number[] = [];
244
249
  const messageBlocks: ConversationBlock[] = [];
245
250
  const interactiveToolIds: string[] = [];
251
+ const blockSpans: TimelineBlockSpan[] = [];
246
252
  const toolSpans: TimelineToolSpan[] = [];
247
253
  const visibleBlocks = this.blocks.filter((block) => this.showTools || block.kind !== "tool");
248
254
 
@@ -260,6 +266,7 @@ export class ConversationTimeline implements Component {
260
266
 
261
267
  const hasRoleHeader = block.role === "user" || block.role === "assistant";
262
268
  const blockStartLine = lines.length;
269
+ const snapshotBlockIndex = messageBlocks.length;
263
270
  messageStarts.push(blockStartLine);
264
271
  messageBlocks.push(block);
265
272
  if (hasRoleHeader) {
@@ -297,8 +304,6 @@ export class ConversationTimeline implements Component {
297
304
  this.theme,
298
305
  );
299
306
  } catch {
300
- // Pi's interactive theme is a capability, not a viewer dependency;
301
- // test hosts and older Pi versions can lack its initialized theme.
302
307
  rendered = fallbackToolLines(
303
308
  block,
304
309
  requestedWidth,
@@ -311,19 +316,23 @@ export class ConversationTimeline implements Component {
311
316
  lines.push({ text: truncateToWidth(line, requestedWidth), plain: stripAnsi(line), blockIndex, railable: true });
312
317
  }
313
318
  if (id) {
314
- const startLine = blockStartLine;
315
319
  const hintLine = !this.expandedTools.has(id)
316
320
  ? rendered.findIndex((line) => {
317
321
  const plain = stripAnsi(line);
318
322
  return plain.includes(showMoreHintText()) || plain.includes("Enter to expand");
319
323
  })
320
324
  : -1;
325
+ const blockHeight = lines.length - blockStartLine;
326
+ blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: blockHeight });
321
327
  toolSpans.push({
328
+ blockIndex: snapshotBlockIndex,
322
329
  toolCallId: id,
323
- startLine,
324
- height: lines.length - startLine,
325
- ...(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 } : {}),
326
333
  });
334
+ } else {
335
+ blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: lines.length - blockStartLine });
327
336
  }
328
337
  continue;
329
338
  }
@@ -346,84 +355,116 @@ export class ConversationTimeline implements Component {
346
355
  }
347
356
  }
348
357
  }
358
+ blockSpans.push({ blockIndex: snapshotBlockIndex, startLine: blockStartLine, height: lines.length - blockStartLine });
349
359
  }
350
360
 
351
361
  this.lastWidth = requestedWidth;
352
- this.snapshot = { lines, messageStarts, messageBlocks, interactiveToolIds, toolSpans };
362
+ this.snapshot = { lines, messageStarts, messageBlocks, interactiveToolIds, blockSpans, toolSpans };
353
363
  return lines.map((line) => line.text);
354
364
  }
355
365
 
356
- handleMouse(event: TuiMouseEvent): { handled?: boolean; capture?: boolean; render?: boolean } | undefined {
366
+ handleMouse(event: TuiMouseEvent): ConversationTimelineMouseResult | undefined {
357
367
  if (event.type === "move") {
358
368
  const hovered = this.snapshot.toolSpans.find((span) =>
359
369
  span.clickStartLine !== undefined &&
360
370
  event.y >= span.clickStartLine &&
361
371
  event.y < span.clickStartLine + (span.clickHeight ?? 1),
362
372
  )?.toolCallId;
363
- if (hovered === this.hoveredToolCallId) return undefined;
373
+ const fullSpan = this.snapshot.toolSpans.find((span) =>
374
+ event.y >= span.startLine && event.y < span.startLine + span.height,
375
+ );
376
+ let nextIoView: any;
377
+ let nextIoSection: "input" | "output" | null = null;
378
+ if (fullSpan) {
379
+ const component = this.toolComponents.get(fullSpan.toolCallId) as any;
380
+ const view = component?.resultRendererComponent;
381
+ const localRow = event.y - fullSpan.startLine;
382
+ if (view && typeof view.sectionAtLine === "function" && typeof view.showMoreLine === "function" && typeof view.setHoveredSection === "function") {
383
+ const section = view.sectionAtLine(localRow);
384
+ if (section && view.showMoreLine(section) === localRow) {
385
+ nextIoView = view;
386
+ nextIoSection = section;
387
+ }
388
+ }
389
+ }
390
+ const unchanged = hovered === this.hoveredToolCallId && nextIoView === this.hoveredIoView && nextIoSection === this.hoveredIoSection;
391
+ if (unchanged) return undefined;
392
+ if (this.hoveredIoView && this.hoveredIoView !== nextIoView && typeof this.hoveredIoView.setHoveredSection === "function") {
393
+ this.hoveredIoView.setHoveredSection(null);
394
+ }
364
395
  this.hoveredToolCallId = hovered;
396
+ this.hoveredIoView = nextIoView;
397
+ this.hoveredIoSection = nextIoSection;
398
+ if (nextIoView) nextIoView.setHoveredSection(nextIoSection);
365
399
  this.invalidate();
366
400
  this.options.onChange?.();
367
401
  return { handled: true, render: true };
368
402
  }
369
403
 
370
- // The TUI sends a click only when a component claims the preceding press.
371
- // Claim only the collapsed hint (or the last expanded card's hit area for
372
- // a possible second click); never change expansion on press or release.
373
404
  if (event.type !== "click" && event.type !== "press") return undefined;
374
405
  if (event.button !== "left") return undefined;
375
- const clickCount = event.clickCount ?? 1;
406
+
407
+ // Keep the hint ahead of the full tool span. Its row has a distinct action.
376
408
  const hintSpan = this.snapshot.toolSpans.find((candidate) =>
377
409
  candidate.clickStartLine !== undefined &&
378
410
  event.y >= candidate.clickStartLine &&
379
411
  event.y < candidate.clickStartLine + (candidate.clickHeight ?? 1),
380
412
  );
381
413
  const fullSpan = this.snapshot.toolSpans.find((candidate) =>
382
- event.y >= candidate.startLine &&
383
- event.y < candidate.startLine + candidate.height,
414
+ event.y >= candidate.startLine && event.y < candidate.startLine + candidate.height,
384
415
  );
385
- const lastSpan = this.lastClickedToolCallId
386
- ? this.snapshot.toolSpans.find((candidate) => candidate.toolCallId === this.lastClickedToolCallId)
387
- : undefined;
388
- const sameLastPoint = this.lastClickedPoint !== undefined
389
- && event.screenX === this.lastClickedPoint.x
390
- && event.screenY === this.lastClickedPoint.y;
391
- const span = event.type === "click" && clickCount === 2 && lastSpan
392
- ? lastSpan
393
- : hintSpan ?? (sameLastPoint ? lastSpan : undefined);
394
- 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;
395
421
  if (event.type === "press") return { handled: true, capture: true, render: false };
396
422
 
397
- const previousClickedToolCallId = this.lastClickedToolCallId;
398
- this.focusedToolCallId = span.toolCallId;
399
- this.lastClickedToolCallId = span.toolCallId;
400
- this.lastClickedPoint = { x: event.screenX, y: event.screenY };
401
- const isExpanded = this.expandedTools.has(span.toolCallId);
402
- const shouldCollapse = isExpanded && clickCount === 2 && span.toolCallId === previousClickedToolCallId;
403
- if (!isExpanded && hintSpan?.toolCallId === span.toolCallId) {
404
- this.expandedTools.add(span.toolCallId);
405
- } else if (shouldCollapse) {
406
- this.expandedTools.delete(span.toolCallId);
407
- } else if (!isExpanded) {
408
- 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 };
409
454
  }
410
455
 
411
- const component = this.toolComponents.get(span.toolCallId);
412
- try {
413
- // Keep Pi's normalized mouse contract in the component tree. The local
414
- // role header occupies the first row; ToolExecutionComponent receives
415
- // the remaining card-relative coordinate.
416
- component?.handleMouse?.({ ...event, y: event.y - span.startLine, height: span.height });
417
- } catch {
418
- // A missing/older Pi mouse implementation must not break the viewer.
419
- }
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;
420
461
  this.invalidate();
421
462
  this.options.onChange?.();
422
- return { handled: true, render: true };
463
+ return { handled: true, render: true, focusedBlockIndex: target.blockIndex };
423
464
  }
424
465
 
425
466
  invalidate(): void {
426
467
  this.lastWidth = 0;
427
- this.snapshot = { lines: [], messageStarts: [], messageBlocks: [], interactiveToolIds: [], toolSpans: [] };
468
+ this.snapshot = { lines: [], messageStarts: [], messageBlocks: [], interactiveToolIds: [], blockSpans: [], toolSpans: [] };
428
469
  }
429
470
  }