@esso0428/pi-subagents 0.15.13 → 0.15.14

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.
@@ -143,6 +143,8 @@ export class ConversationTimeline implements Component {
143
143
  private focusedToolCallId: string | undefined;
144
144
  private expandedTools = new Set<string>();
145
145
  private hoveredToolCallId: string | undefined;
146
+ private hoveredIoView: any;
147
+ private hoveredIoSection: "input" | "output" | null = null;
146
148
  /** The last tool clicked; used to make only clickCount=2 collapse it. */
147
149
  private lastClickedToolCallId: string | undefined;
148
150
  private lastClickedPoint: { x: number; y: number } | undefined;
@@ -167,6 +169,11 @@ export class ConversationTimeline implements Component {
167
169
  const ids = new Set(this.blocks.filter((block) => block.kind === "tool" && block.toolCallId).map((block) => block.toolCallId!));
168
170
  if (this.focusedToolCallId && !ids.has(this.focusedToolCallId)) this.focusedToolCallId = undefined;
169
171
  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
+ }
170
177
  if (this.lastClickedToolCallId && !ids.has(this.lastClickedToolCallId)) {
171
178
  this.lastClickedToolCallId = undefined;
172
179
  this.lastClickedPoint = undefined;
@@ -182,6 +189,11 @@ export class ConversationTimeline implements Component {
182
189
  if (!show) {
183
190
  this.focusedToolCallId = undefined;
184
191
  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;
185
197
  this.lastClickedToolCallId = undefined;
186
198
  this.lastClickedPoint = undefined;
187
199
  }
@@ -360,8 +372,33 @@ export class ConversationTimeline implements Component {
360
372
  event.y >= span.clickStartLine &&
361
373
  event.y < span.clickStartLine + (span.clickHeight ?? 1),
362
374
  )?.toolCallId;
363
- if (hovered === this.hoveredToolCallId) return undefined;
375
+ const fullSpan = this.snapshot.toolSpans.find((span) =>
376
+ event.y >= span.startLine && event.y < span.startLine + span.height,
377
+ );
378
+ let nextIoView: any;
379
+ let nextIoSection: "input" | "output" | null = null;
380
+ if (fullSpan) {
381
+ const component = this.toolComponents.get(fullSpan.toolCallId) as any;
382
+ const view = component?.resultRendererComponent;
383
+ const localRow = event.y - fullSpan.startLine;
384
+ if (view && typeof view.sectionAtLine === "function" && typeof view.showMoreLine === "function" && typeof view.setHoveredSection === "function") {
385
+ const section = view.sectionAtLine(localRow);
386
+ if (section && view.showMoreLine(section) === localRow) {
387
+ nextIoView = view;
388
+ nextIoSection = section;
389
+ }
390
+ }
391
+ }
392
+ const unchanged = hovered === this.hoveredToolCallId &&
393
+ nextIoView === this.hoveredIoView && nextIoSection === this.hoveredIoSection;
394
+ if (unchanged) return undefined;
395
+ if (this.hoveredIoView && this.hoveredIoView !== nextIoView && typeof this.hoveredIoView.setHoveredSection === "function") {
396
+ this.hoveredIoView.setHoveredSection(null);
397
+ }
364
398
  this.hoveredToolCallId = hovered;
399
+ this.hoveredIoView = nextIoView;
400
+ this.hoveredIoSection = nextIoSection;
401
+ if (nextIoView) nextIoView.setHoveredSection(nextIoSection);
365
402
  this.invalidate();
366
403
  this.options.onChange?.();
367
404
  return { handled: true, render: true };
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ExtensionAPI, ExtensionContext, AgentSession } from "@earendil-works/pi-coding-agent";
9
9
  import { copyToClipboard } from "@earendil-works/pi-coding-agent";
10
- import { Text, type Component, Input, Key, matchesKey, type TUI, type TuiMouseEvent, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
+ import { type Component, Input, Key, matchesKey, type TUI, type TuiMouseEvent, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
11
11
  import type { AgentRecord } from "../types.js";
12
12
  import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
13
13
  import type { Theme } from "./agent-widget.js";
@@ -18,14 +18,14 @@ import { renderConversationRoleHeader } from "./conversation-role.js";
18
18
  import { editLiveAssistantBlockInNvim, viewConversationBlockInNvim } from "./conversation-nvim.js";
19
19
  import { findConversationMatches, highlightConversationLine, nearestConversationMatchAfter, nearestConversationMatchBefore, stripAnsi, type ConversationMatch } from "./conversation-search.js";
20
20
  import { createViewerKeys, type ViewerKeybindings, type ViewerKeys } from "./viewer-keys.js";
21
- import { formatToolInputArgs, textFromResult } from "./ccstyle/tool-result.js";
21
+ import { createViewerCcstyleResult } from "./ccstyle/tool-result.js";
22
22
 
23
23
  /** Base lines consumed by the overlay: borders, header, separator and footer. */
24
24
  const CHROME_LINES_BASE = 6;
25
25
  const FULL_TOOL_PREVIEW_MAX_CHARS = 500_000;
26
26
 
27
27
  class FullToolPreview implements Component {
28
- private readonly text: Text;
28
+ private readonly body: Component;
29
29
  private scrollOffset = 0;
30
30
  private pageSize = 1;
31
31
  private totalLines = 1;
@@ -35,10 +35,10 @@ class FullToolPreview implements Component {
35
35
  private readonly tui: TUI,
36
36
  private readonly theme: Theme,
37
37
  private readonly title: string,
38
- content: string,
38
+ body: Component,
39
39
  private readonly done: (result: undefined) => void,
40
40
  ) {
41
- this.text = new Text(content || "(empty)", 0, 0);
41
+ this.body = body;
42
42
  }
43
43
 
44
44
  handleInput(data: string): void {
@@ -81,7 +81,7 @@ class FullToolPreview implements Component {
81
81
  const bodyWidth = Math.max(1, inner - 1);
82
82
  const terminalRows = Math.max(1, this.tui.terminal.rows);
83
83
  const viewport = Math.max(1, Math.min(30, Math.floor(terminalRows * 0.8), terminalRows - 6));
84
- const wrapped = this.text.render(bodyWidth);
84
+ const wrapped = this.body.render(bodyWidth);
85
85
  this.totalLines = wrapped.length;
86
86
  this.pageSize = viewport;
87
87
  this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, this.totalLines - this.pageSize));
@@ -106,7 +106,7 @@ class FullToolPreview implements Component {
106
106
  }
107
107
 
108
108
  invalidate(): void {
109
- this.text.invalidate();
109
+ this.body.invalidate();
110
110
  }
111
111
 
112
112
  private scrollTo(offset: number): void {
@@ -676,24 +676,25 @@ export class ConversationViewer implements Component {
676
676
  const custom = this.operations?.ctx?.ui?.custom;
677
677
  if (block?.kind !== "tool" || typeof custom !== "function") return;
678
678
 
679
- const input = formatToolInputArgs(block.args ?? block.toolArguments, FULL_TOOL_PREVIEW_MAX_CHARS);
680
- const result = block.toolResult ?? block.result;
681
- const output = result
682
- ? textFromResult(result, true, FULL_TOOL_PREVIEW_MAX_CHARS)
683
- : "(no result yet)";
684
- const content = [
685
- "Input",
686
- "=====",
687
- input || "(empty)",
688
- "",
689
- "Output",
690
- "======",
691
- output || "(empty)",
692
- ].join("\n");
693
- const title = `${block.toolName || "Tool"} · full preview`;
694
-
695
- void custom.call(this.operations!.ctx.ui, (tui, overlayTheme, _keybindings, done) =>
696
- new FullToolPreview(tui, overlayTheme as Theme, title, content, done), {
679
+ const toolName = block.toolName || "tool";
680
+ const args = block.args ?? block.toolArguments;
681
+ const result = block.toolResult ?? block.result ?? { content: [] };
682
+ const title = `${toolName} · full preview`;
683
+
684
+ void custom.call(this.operations!.ctx.ui, (tui, overlayTheme, _keybindings, done) => {
685
+ const body = createViewerCcstyleResult(
686
+ toolName,
687
+ result,
688
+ { expanded: true, isError: result.isError === true },
689
+ overlayTheme,
690
+ {
691
+ args,
692
+ maxChars: FULL_TOOL_PREVIEW_MAX_CHARS,
693
+ diffConfig: { expandedPreviewMaxLines: Number.MAX_SAFE_INTEGER },
694
+ },
695
+ );
696
+ return new FullToolPreview(tui, overlayTheme as Theme, title, body, done);
697
+ }, {
697
698
  overlay: true,
698
699
  overlayOptions: {
699
700
  anchor: "center",
@@ -66,6 +66,25 @@ describe("viewer-local ccstyle diff renderer", () => {
66
66
  expect(rendered).not.toContain("later");
67
67
  });
68
68
 
69
+ it("matches the pi-cc Input/Output tree style and hover affordance", () => {
70
+ const component = createViewerCcstyleResult("read", {
71
+ content: [{ type: "text", text: "one\ntwo\nthree" }],
72
+ }, { expanded: true }, theme, {
73
+ args: { path: "file.txt", offset: 2 },
74
+ diffConfig: { expandedPreviewMaxLines: 2 },
75
+ }) as any;
76
+
77
+ const initial = component.render(100).join("\n");
78
+ expect(initial).toContain("<accent><bold>Input</bold></accent>");
79
+ expect(initial).toContain("<dim>path: </dim><muted>file.txt</muted>");
80
+ expect(initial).toContain("<accent><bold>Output</bold></accent>");
81
+ expect(initial).toContain("click to show more");
82
+ const outputHintLine = component.showMoreLine("output");
83
+ expect(outputHintLine).toEqual(expect.any(Number));
84
+ component.setHoveredSection("output");
85
+ expect(component.render(100).join("\n")).toContain("<text> click to show more</text>");
86
+ });
87
+
69
88
  it("reads tool-hint hover state at render time", () => {
70
89
  let hovered = false;
71
90
  const component = createViewerCcstyleResult("read", {