@oh-my-pi/pi-tui 18.4.4 → 18.4.6

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 (63) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/types/apps/git/sidebar.d.ts +2 -2
  3. package/dist/types/chat/recap-notice.d.ts +13 -0
  4. package/dist/types/components/editor.d.ts +15 -3
  5. package/dist/types/components/input.d.ts +11 -1
  6. package/dist/types/native/blobs.d.ts +1 -1
  7. package/dist/types/native/node.d.ts +63 -1
  8. package/dist/types/overlays/agent-hub-projection.d.ts +9 -0
  9. package/dist/types/overlays/ask-dialog.d.ts +9 -5
  10. package/dist/types/overlays/copy-selector.d.ts +0 -23
  11. package/dist/types/overlays/error-banner.d.ts +5 -2
  12. package/dist/types/overlays/hook-editor.d.ts +12 -0
  13. package/dist/types/overlays/jobs-panel.d.ts +25 -1
  14. package/dist/types/overlays/model-hub.d.ts +2 -0
  15. package/dist/types/overlays/rewind-selector.d.ts +17 -4
  16. package/dist/types/prompt/custom-editor.d.ts +16 -8
  17. package/dist/types/prompt/welcome.d.ts +3 -3
  18. package/dist/types/render/render-utils.d.ts +5 -0
  19. package/dist/types/status-line/component.d.ts +2 -0
  20. package/dist/types/status-line/host.d.ts +2 -0
  21. package/dist/types/status-line/metrics.d.ts +4 -0
  22. package/dist/types/status-line/types.d.ts +13 -2
  23. package/dist/types/tools/edit.d.ts +2 -0
  24. package/dist/types/tools/task.d.ts +7 -0
  25. package/dist/types/tui.d.ts +8 -1
  26. package/package.json +9 -9
  27. package/src/apps/git/sidebar.ts +15 -2
  28. package/src/chat/assistant-message.ts +2 -2
  29. package/src/chat/recap-notice.ts +30 -0
  30. package/src/chat/tool-execution.ts +19 -3
  31. package/src/chat/user-message.ts +2 -1
  32. package/src/components/editor.ts +115 -16
  33. package/src/components/input.ts +57 -1
  34. package/src/native/backend.ts +28 -10
  35. package/src/native/blobs.ts +1 -1
  36. package/src/native/encode.ts +1 -0
  37. package/src/native/node.ts +108 -1
  38. package/src/native/reconcile.ts +5 -4
  39. package/src/overlays/agent-hub-projection.ts +25 -0
  40. package/src/overlays/agent-hub.ts +4 -15
  41. package/src/overlays/ask-dialog.ts +20 -32
  42. package/src/overlays/copy-selector.ts +6 -6
  43. package/src/overlays/error-banner.ts +14 -3
  44. package/src/overlays/hook-editor.ts +59 -6
  45. package/src/overlays/jobs-panel.ts +132 -49
  46. package/src/overlays/model-browser.ts +56 -12
  47. package/src/overlays/model-hub.ts +82 -29
  48. package/src/overlays/rewind-selector.ts +235 -332
  49. package/src/overlays/session-selector.ts +19 -20
  50. package/src/prompt/custom-editor.ts +119 -30
  51. package/src/prompt/welcome.ts +4 -33
  52. package/src/render/render-utils.ts +51 -3
  53. package/src/status-line/component.ts +61 -30
  54. package/src/status-line/host.ts +2 -0
  55. package/src/status-line/metrics.ts +36 -5
  56. package/src/status-line/segments.ts +9 -1
  57. package/src/status-line/types.ts +13 -2
  58. package/src/terminal.ts +27 -6
  59. package/src/tools/edit.ts +23 -0
  60. package/src/tools/eval.ts +43 -11
  61. package/src/tools/grouped-file-output.ts +6 -0
  62. package/src/tools/task.ts +29 -4
  63. package/src/tui.ts +9 -1
@@ -45,6 +45,8 @@ export interface StatusLineSession {
45
45
  orchestrationCacheRead: number;
46
46
  premiumRequests: number;
47
47
  cost: number;
48
+ /** Portion of `cost` carried by completed subagent task results. */
49
+ subagentCost?: number;
48
50
  };
49
51
  };
50
52
  modelRegistry: {
@@ -10,14 +10,26 @@ export interface BillingSummaryOptions {
10
10
  readonly premiumRequests: number;
11
11
  readonly fractionDigits: number;
12
12
  readonly pricingPeriod?: "peak" | "off-peak";
13
+ /** Subagent-tree spend, rendered `(+…)` after the session's own spend; `cost` excludes it. */
14
+ readonly subagentCost?: number;
13
15
  readonly advisor?: {
14
16
  readonly cost: number;
15
17
  readonly usingSubscription: boolean;
16
18
  };
17
19
  }
18
20
 
19
- function formatSpend(amount: number, usingSubscription: boolean, fractionDigits: number, uiTheme: Theme): string {
21
+ type BillingUnit = "metered" | "subscription";
22
+
23
+ /** `showUnit: false` omits the `$`/subscription symbol when the summary already printed it. */
24
+ function formatSpend(
25
+ amount: number,
26
+ usingSubscription: boolean,
27
+ fractionDigits: number,
28
+ uiTheme: Theme,
29
+ showUnit: boolean,
30
+ ): string {
20
31
  const formatted = amount.toFixed(fractionDigits);
32
+ if (!showUnit) return formatted;
21
33
  if (!usingSubscription) return `$${formatted}`;
22
34
  if (uiTheme.getSymbolPreset() === "nerd") {
23
35
  const icon = uiTheme.icon.subscription;
@@ -31,8 +43,9 @@ function formatAdvisorSpend(
31
43
  usingSubscription: boolean,
32
44
  fractionDigits: number,
33
45
  uiTheme: Theme,
46
+ showUnit: boolean,
34
47
  ): string {
35
- const spend = formatSpend(amount, usingSubscription, fractionDigits, uiTheme);
48
+ const spend = formatSpend(amount, usingSubscription, fractionDigits, uiTheme, showUnit);
36
49
  const icon = uiTheme.icon.advisor;
37
50
  return icon && icon !== "(adv)" ? `${icon} ${spend}` : `${spend} (adv)`;
38
51
  }
@@ -40,32 +53,50 @@ function formatAdvisorSpend(
40
53
  /**
41
54
  * Shared billing metric presentation. Callers select precision explicitly so
42
55
  * the legacy footer keeps three decimals while current status segments keep two.
56
+ * A unit symbol (`$` or the subscription mark) is printed at most once; later
57
+ * amounts billed the same way render bare.
43
58
  */
44
59
  export function formatBillingSummary(options: BillingSummaryOptions, uiTheme: Theme): string | undefined {
45
60
  const premiumRequests = normalizePremiumRequests(options.premiumRequests);
46
61
  const advisorCost = options.advisor?.cost ?? 0;
47
- if (!options.cost && !advisorCost && !options.usingSubscription && !premiumRequests && !options.pricingPeriod) {
62
+ const subagentCost = options.subagentCost ?? 0;
63
+ if (
64
+ !options.cost &&
65
+ !subagentCost &&
66
+ !advisorCost &&
67
+ !options.usingSubscription &&
68
+ !premiumRequests &&
69
+ !options.pricingPeriod
70
+ ) {
48
71
  return undefined;
49
72
  }
50
73
 
51
74
  const parts: string[] = [];
52
- if (options.cost || options.pricingPeriod) {
53
- parts.push(formatSpend(options.cost, options.usingSubscription, options.fractionDigits, uiTheme));
75
+ let shownUnit: BillingUnit | undefined;
76
+ const primaryUnit: BillingUnit = options.usingSubscription ? "subscription" : "metered";
77
+ if (options.cost || options.pricingPeriod || subagentCost) {
78
+ parts.push(formatSpend(options.cost, options.usingSubscription, options.fractionDigits, uiTheme, true));
79
+ shownUnit = primaryUnit;
54
80
  } else if (options.usingSubscription) {
55
81
  parts.push(
56
82
  uiTheme.getSymbolPreset() === "nerd" && uiTheme.icon.subscription ? uiTheme.icon.subscription : "(sub)",
57
83
  );
84
+ shownUnit = primaryUnit;
58
85
  }
86
+ // Always follows the primary spend, which already carries the unit.
87
+ if (subagentCost) parts.push(`(+${subagentCost.toFixed(options.fractionDigits)})`);
59
88
  if (options.pricingPeriod) parts.push(options.pricingPeriod === "peak" ? "↑" : "↓");
60
89
  if (premiumRequests) parts.push(`★ ${formatNumber(premiumRequests)}`);
61
90
  if (advisorCost && options.advisor) {
62
91
  const prefix = parts.length > 0 ? "+ " : "";
92
+ const advisorUnit: BillingUnit = options.advisor.usingSubscription ? "subscription" : "metered";
63
93
  parts.push(
64
94
  `${prefix}${formatAdvisorSpend(
65
95
  advisorCost,
66
96
  options.advisor.usingSubscription,
67
97
  options.fractionDigits,
68
98
  uiTheme,
99
+ shownUnit !== advisorUnit,
69
100
  )}`,
70
101
  );
71
102
  }
@@ -782,6 +782,13 @@ const tokenRateSegment: StatusLineSegment = {
782
782
  /** Billing summary for the `cost` segment, or undefined when there is nothing to bill. */
783
783
  function costSummary(ctx: SegmentContext): string | undefined {
784
784
  const { cost, premiumRequests } = ctx.usageStats;
785
+ // `cost` folds in completed task results; show the session's own spend and
786
+ // the subagent tree separately. The hub-projected tree total also covers
787
+ // grandchildren and running/async agents, but it lags persisted-roster
788
+ // hydration after resume, so the task-result sum is its floor.
789
+ const taskResultCost = ctx.usageStats.subagentCost ?? 0;
790
+ const ownCost = Math.max(0, cost - taskResultCost);
791
+ const subagentCost = Math.max(ctx.subagentTreeCost ?? 0, taskResultCost);
785
792
  const advisorCost = ctx.session.getAdvisorCost?.() ?? 0;
786
793
  const state = ctx.session.state;
787
794
  const pricingPeriod = state.model?.cost
@@ -794,7 +801,8 @@ function costSummary(ctx: SegmentContext): string | undefined {
794
801
  // working-spinner cadence, so an eager per-frame probe pinned CPU (#10129).
795
802
  return formatBillingSummary(
796
803
  {
797
- cost,
804
+ cost: ownCost,
805
+ subagentCost,
798
806
  usingSubscription,
799
807
  premiumRequests,
800
808
  fractionDigits: 2,
@@ -138,6 +138,8 @@ export interface SegmentContext {
138
138
  orchestrationCacheRead: number;
139
139
  premiumRequests: number;
140
140
  cost: number;
141
+ /** Portion of `cost` carried by completed subagent task results. */
142
+ subagentCost?: number;
141
143
  tokensPerSecond: number | null;
142
144
  };
143
145
  /** Context usage percent, or null when unknown (e.g. right after compaction). */
@@ -150,6 +152,11 @@ export interface SegmentContext {
150
152
  /** Blink phase for the running-speculation pulse; toggled by the component's timer. */
151
153
  speculationBlinkOn: boolean;
152
154
  subagentCount: number;
155
+ /**
156
+ * Spend of every subagent under the main session (descendants included),
157
+ * from the Agent Hub projection; 0 while a subagent is focused or unknown.
158
+ */
159
+ subagentTreeCost?: number;
153
160
  /**
154
161
  * Active processing time accumulated this session, in ms — the union of
155
162
  * every `agent_start`→`agent_end` window plus the currently-streaming
@@ -224,7 +231,11 @@ export interface SegmentView {
224
231
  * and branch, and the composer the rest.
225
232
  */
226
233
  export interface ComposerFacts {
227
- /** `meter` (role `omp.composer.context`): context usage along the composer's top edge. */
234
+ /**
235
+ * `meter` (role `omp.composer.context`): context usage along the composer's top edge,
236
+ * the whole window wide, with the speculation and compaction points as icon marks,
237
+ * the share as `label` and the window as `total`.
238
+ */
228
239
  readonly context: NativeNode;
229
240
  /** The model chip's label: name plus the advisor, fast-mode and slow-mode marks. */
230
241
  readonly model: SegmentView;
@@ -233,7 +244,7 @@ export interface ComposerFacts {
233
244
  * segments as `seg`s; the bar's flexible space, so present even when empty.
234
245
  */
235
246
  readonly extras: NativeNode;
236
- /** `text` (role `omp.composer.usage`): the context share of the window and the session cost. */
247
+ /** `text` (role `omp.composer.usage`): the session cost (empty when there is none). */
237
248
  readonly usage: NativeNode;
238
249
  }
239
250
 
package/src/terminal.ts CHANGED
@@ -33,6 +33,26 @@ const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000;
33
33
  const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07";
34
34
  const TERMINAL_PROGRESS_CLEAR_SEQUENCE = "\x1b]9;4;0;\x07";
35
35
  const WINDOWS_TERMINAL_OSC11_POLL_MS = 30_000;
36
+
37
+ /**
38
+ * Terminal → program TSP messages through a Windows ConPTY: its input parser
39
+ * discards APC strings but passes OSC through, so Tern writes
40
+ * `ESC ] 877 ; tsp;… ESC \` there instead of `ESC _ tsp;… ESC \`.
41
+ */
42
+ const TSP_OSC_PREFIX = `\x1b]877;${TSP_PREFIX.slice(2)}`;
43
+
44
+ /**
45
+ * The TSP message a reassembled input string holds, in APC form, or
46
+ * `undefined` while its terminator has not arrived. An OSC 877 message may end
47
+ * with ST or BEL (a JSON body never contains BEL).
48
+ */
49
+ function completeTspInput(buffered: string): string | undefined {
50
+ if (!buffered.startsWith(TSP_OSC_PREFIX)) return buffered.endsWith("\x1b\\") ? buffered : undefined;
51
+ const terminator = buffered.endsWith("\x1b\\") ? 2 : buffered.endsWith("\x07") ? 1 : 0;
52
+ if (terminator === 0) return undefined;
53
+ return `${TSP_PREFIX}${buffered.slice(TSP_OSC_PREFIX.length, -terminator)}\x1b\\`;
54
+ }
55
+
36
56
  function shouldEnableModifyOtherKeysFallback(env: NodeJS.ProcessEnv = Bun.env): boolean {
37
57
  if (!env.SSH_CONNECTION && !env.SSH_TTY && !env.SSH_CLIENT) return true;
38
58
  return TERMINAL.id !== "base" && TERMINAL.id !== "trueColor";
@@ -1520,16 +1540,17 @@ export class ProcessTerminal implements Terminal {
1520
1540
  }
1521
1541
  }
1522
1542
 
1523
- // Tern Surface Protocol APC (`ESC _ tsp ; … ESC \`): the hello reply
1524
- // resolves the probe; events go to the input handler whole, where the
1525
- // TUI routes them to the native backend before key matching.
1526
- if (this.#tspReplyBuffer || sequence.startsWith(TSP_PREFIX)) {
1543
+ // Tern Surface Protocol APC (`ESC _ tsp ; … ESC \`), or its OSC 877
1544
+ // framing through a ConPTY: the hello reply resolves the probe; events
1545
+ // go to the input handler whole, in APC form, where the TUI routes them
1546
+ // to the native backend before key matching.
1547
+ if (this.#tspReplyBuffer || sequence.startsWith(TSP_PREFIX) || sequence.startsWith(TSP_OSC_PREFIX)) {
1527
1548
  if (this.#tspReplyBuffer && sequence.startsWith("\x1b") && sequence !== "\x1b\\") {
1528
1549
  this.#tspReplyBuffer = "";
1529
1550
  } else {
1530
1551
  this.#tspReplyBuffer += sequence;
1531
- if (!this.#tspReplyBuffer.endsWith("\x1b\\")) return;
1532
- const message = this.#tspReplyBuffer;
1552
+ const message = completeTspInput(this.#tspReplyBuffer);
1553
+ if (message === undefined) return;
1533
1554
  this.#tspReplyBuffer = "";
1534
1555
  this.#handleTspMessage(message);
1535
1556
  return;
package/src/tools/edit.ts CHANGED
@@ -693,6 +693,29 @@ function getHashlineInputSections(input: string): HashlineInputEntry[] {
693
693
  return entries;
694
694
  }
695
695
 
696
+ /** Extract display targets using the existing parsers for supported freeform edit modes. */
697
+ export function getEditInputPaths(input: string, resolvedMode?: EditMode): readonly string[] {
698
+ const mode =
699
+ resolvedMode ??
700
+ (/^\*\*\* (?:Add|Update|Delete) File:/m.test(input)
701
+ ? "apply_patch"
702
+ : /^[ \t]*\*{3}[ \t]+Edit[ \t]+File:/im.test(input)
703
+ ? "sloppy"
704
+ : undefined);
705
+ try {
706
+ const entries =
707
+ mode && mode !== "hashline" ? inspectInputEntries({}, mode, input) : getHashlineInputSections(input);
708
+ const paths: string[] = [];
709
+ for (const entry of entries) {
710
+ if (entry.path) paths.push(entry.path);
711
+ if (entry.rename && entry.rename !== entry.path) paths.push(entry.rename);
712
+ }
713
+ return paths;
714
+ } catch {
715
+ return [];
716
+ }
717
+ }
718
+
696
719
  function getHashlineInputRenderSummary(
697
720
  args: EditRenderArgs,
698
721
  editMode: EditMode | undefined,
package/src/tools/eval.ts CHANGED
@@ -689,9 +689,8 @@ interface EvalCellSection {
689
689
  }
690
690
 
691
691
  /**
692
- * One cell as a borderless section: its highlighted code (no line numbers),
693
- * then its output. Only multi-cell calls caption the section (`2/3 title ·
694
- * 1.2s`); a single cell's title is the tool head, never repeated.
692
+ * A notebook cell with separate input and output gutters. Only multi-cell
693
+ * calls repeat titles; prompts show execution state, never invented counts.
695
694
  */
696
695
  function evalCellSection(cell: EvalCellSection, index: number, total: number): NativeNode {
697
696
  let head: TspSpan[] | undefined;
@@ -702,13 +701,36 @@ function evalCellSection(cell: EvalCellSection, index: number, total: number): N
702
701
  if (cell.status === "error") head.push(span(" · failed", "error"));
703
702
  if (cell.durationMs !== undefined) head.push(span(` · ${(cell.durationMs / 1000).toFixed(2)}s`, "muted"));
704
703
  }
704
+ const prompt = cell.status === "running" ? "In [*]:" : !cell.status || cell.status === "pending" ? "In [ ]:" : "In:";
705
705
  return node(
706
- "section",
707
- { head, role: "omp.tool.eval.cell", tone: cell.status === "error" ? "error" : undefined },
708
- [
709
- keyed(codeNode(cell.code, { lang: languageForHighlighter(cell.language), numbers: false }), "code"),
710
- ...(cell.output ?? []),
711
- ],
706
+ "col",
707
+ {
708
+ role: "omp.tool.eval.cell",
709
+ tone: cell.status === "error" ? "error" : cell.status === "running" ? "pending" : undefined,
710
+ },
711
+ compact<NativeChild>([
712
+ head ? text(head, { role: "omp.tool.eval.caption" }) : undefined,
713
+ node(
714
+ "row",
715
+ { role: "omp.tool.eval.input", align: "start" },
716
+ [
717
+ text(prompt, { role: "omp.tool.eval.prompt", wrap: "none" }),
718
+ keyed(codeNode(cell.code, { lang: languageForHighlighter(cell.language), numbers: false }), "code"),
719
+ ],
720
+ "input",
721
+ ),
722
+ cell.output?.length
723
+ ? node(
724
+ "row",
725
+ { role: "omp.tool.eval.result", align: "start" },
726
+ [
727
+ text("Out:", { role: "omp.tool.eval.prompt", wrap: "none" }),
728
+ node("col", { role: "omp.tool.eval.outputs" }, cell.output),
729
+ ],
730
+ "output",
731
+ )
732
+ : undefined,
733
+ ]),
712
734
  `cell-${index}`,
713
735
  );
714
736
  }
@@ -1061,6 +1083,7 @@ export const evalToolRenderer = {
1061
1083
  cell.status === "error",
1062
1084
  ),
1063
1085
  ...(cell.statusEvents ?? []).map(describeStatusEvent),
1086
+ ...(i === cellResults.length - 1 ? jsonNodes : []),
1064
1087
  ],
1065
1088
  },
1066
1089
  i,
@@ -1086,17 +1109,26 @@ export const evalToolRenderer = {
1086
1109
  result.isError === true,
1087
1110
  ),
1088
1111
  ...(details?.statusEvents ?? []).map(describeStatusEvent),
1112
+ ...jsonNodes,
1089
1113
  ];
1090
1114
  cells =
1091
1115
  argCells.length > 0
1092
1116
  ? argCells.map((cell, i) =>
1093
- evalCellSection(i === argCells.length - 1 ? { ...cell, output } : cell, i, argCells.length),
1117
+ evalCellSection(
1118
+ {
1119
+ ...cell,
1120
+ status: isPartial ? "running" : result.isError ? "error" : "complete",
1121
+ output: i === argCells.length - 1 ? output : undefined,
1122
+ },
1123
+ i,
1124
+ argCells.length,
1125
+ ),
1094
1126
  )
1095
1127
  : output;
1096
1128
  }
1097
1129
  return {
1098
1130
  tool: head,
1099
- body: compact<NativeChild>([...cells, ...jsonNodes, footer]),
1131
+ body: compact<NativeChild>([...cells, footer]),
1100
1132
  preview: { lines: previewLines },
1101
1133
  };
1102
1134
  },
@@ -300,6 +300,8 @@ export function* walkGroupedOutput(lines: readonly string[]): Generator<GroupedO
300
300
 
301
301
  /** `*12│text`, ` 12|text`, `*12:text`: optional match marker, line number, gutter, content. */
302
302
  const GROUPED_NUMBERED_LINE_RE = /^\s*(\*?)(\d+)(?:│|[:|])(.*)$/;
303
+ /** ` │...`: grep's gap between non-adjacent context runs (drawn as {@link numberedCode}'s `…` row). */
304
+ const GROUPED_GAP_LINE_RE = /^\s*│\.\.\.$/;
303
305
 
304
306
  /** One source line for {@link numberedCode}: its number (`null` for an elision marker), text and match mark. */
305
307
  export interface NumberedLine {
@@ -400,6 +402,10 @@ export function describeGroupedOutput(
400
402
  if (event.kind === "file") current = { path: event.path, suffix: event.suffix, children: [], marks: 0 };
401
403
  continue;
402
404
  }
405
+ if (GROUPED_GAP_LINE_RE.test(event.text)) {
406
+ numbered.push({ n: null, text: "" });
407
+ continue;
408
+ }
403
409
  const match = GROUPED_NUMBERED_LINE_RE.exec(event.text);
404
410
  if (!match) {
405
411
  if (numbered.length > 0) flushLines();
package/src/tools/task.ts CHANGED
@@ -33,7 +33,9 @@ import {
33
33
  previewLine,
34
34
  previewWindowRows,
35
35
  replaceTabs,
36
+ shortenEmbeddedPaths,
36
37
  shortenPath,
38
+ shortenToolArgumentPaths,
37
39
  type ToolUIStatus,
38
40
  TRUNCATE_LENGTHS,
39
41
  truncateToWidth,
@@ -663,6 +665,11 @@ function renderRouteLine(route: string | undefined, continuePrefix: string, maxW
663
665
  ];
664
666
  }
665
667
 
668
+ /** A tool call's own intent, else its argument, with home paths shortened as the subagent HUD does. */
669
+ function toolCallDetail(intent: string | undefined, args: string | undefined, argsKey: string | undefined): string {
670
+ return intent ? shortenEmbeddedPaths(intent) : shortenToolArgumentPaths(args ?? "", argsKey);
671
+ }
672
+
666
673
  /**
667
674
  * Render streaming progress for a single agent.
668
675
  */
@@ -723,7 +730,11 @@ function renderAgentProgress(
723
730
  if (progress.status === "running") {
724
731
  if (progress.currentTool) {
725
732
  let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", sanitizeText(progress.currentTool))}`;
726
- const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
733
+ const toolDetail = toolCallDetail(
734
+ progress.currentToolIntent,
735
+ progress.currentToolArgs,
736
+ progress.currentToolArgsKey,
737
+ );
727
738
  if (toolDetail) {
728
739
  toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
729
740
  }
@@ -738,7 +749,7 @@ function renderAgentProgress(
738
749
  // Show most recent completed tool when idle between tools
739
750
  const recent = progress.recentTools[0];
740
751
  let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", sanitizeText(recent.tool))}`;
741
- const toolDetail = progress.lastIntent ?? recent.args;
752
+ const toolDetail = toolCallDetail(recent.intent, recent.args, recent.argsKey);
742
753
  if (toolDetail) {
743
754
  toolLine += `: ${theme.fg("dim", previewLine(sanitizeText(toolDetail), 40))}`;
744
755
  }
@@ -1795,7 +1806,10 @@ function describeProgressAgent(progress: AgentProgress, state: AgentDescribeStat
1795
1806
  running && progress.currentTool
1796
1807
  ? {
1797
1808
  name: plainText(progress.currentTool),
1798
- intent: plainText(progress.lastIntent ?? progress.currentToolArgs ?? "") || undefined,
1809
+ intent:
1810
+ plainText(
1811
+ toolCallDetail(progress.currentToolIntent, progress.currentToolArgs, progress.currentToolArgsKey),
1812
+ ) || undefined,
1799
1813
  age: progress.currentToolStartMs ? Math.max(0, nowMs - progress.currentToolStartMs) : undefined,
1800
1814
  }
1801
1815
  : null;
@@ -2252,8 +2266,19 @@ export interface AgentProgress {
2252
2266
  lastIntent?: string;
2253
2267
  currentTool?: string;
2254
2268
  currentToolArgs?: string;
2269
+ /** Argument key selected for the display preview, when known. */
2270
+ currentToolArgsKey?: string;
2271
+ /** Intent the model attached to the current call; undefined when that call carried none. */
2272
+ currentToolIntent?: string;
2255
2273
  currentToolStartMs?: number;
2256
- recentTools: Array<{ tool: string; args: string; endMs: number }>;
2274
+ recentTools: Array<{
2275
+ tool: string;
2276
+ args: string;
2277
+ argsKey?: string;
2278
+ intent?: string;
2279
+ isError?: boolean;
2280
+ endMs: number;
2281
+ }>;
2257
2282
  recentOutput: string[];
2258
2283
  toolCount: number;
2259
2284
  /** Count of assistant requests (assistant message_end events) across the run. Drives the soft request budget guard. */
package/src/tui.ts CHANGED
@@ -27,7 +27,7 @@ import { LoopWatchdog } from "./loop-watchdog";
27
27
  import { assumedTspHello, NativeBackend, type NativeHost } from "./native/backend";
28
28
  import { col } from "./native/describe";
29
29
  import { TSP_PREFIX, type TspHello } from "./native/encode";
30
- import type { DescribeContext, NativeNode, NativeSurfaceProvider, NativeUiEvent } from "./native/node";
30
+ import type { DescribeContext, NativeNode, NativeScreen, NativeSurfaceProvider, NativeUiEvent } from "./native/node";
31
31
  import { STDOUT_BACKLOG_CLEAR_BYTES, setAltScreenActive, type Terminal } from "./terminal";
32
32
  import {
33
33
  encodeKittyDeleteAllImages,
@@ -274,6 +274,14 @@ export interface Component {
274
274
  */
275
275
  nativeSheet?(cx: DescribeContext): boolean;
276
276
 
277
+ /**
278
+ * A fullscreen overlay that is a page of its own rather than one block:
279
+ * the regions (and role) of the screen surface it fills, instead of the
280
+ * default lone `main` child. Put the component itself in `dock` to keep
281
+ * its focus target and events.
282
+ */
283
+ describeScreen?(cx: DescribeContext): NativeScreen;
284
+
277
285
  /** User actions on nodes this component described (toggle, select, activate, custom). */
278
286
  handleNativeEvent?(event: NativeUiEvent): void;
279
287