@oh-my-pi/pi-tui 18.14.2 → 18.16.0

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 (93) hide show
  1. package/CHANGELOG.md +64 -50
  2. package/README.md +1 -1
  3. package/dist/types/app-keybindings.d.ts +5 -0
  4. package/dist/types/apps/session-picker.d.ts +5 -3
  5. package/dist/types/chat/bash-execution.d.ts +4 -1
  6. package/dist/types/chat/eval-execution.d.ts +2 -0
  7. package/dist/types/chat/execution-shared.d.ts +14 -8
  8. package/dist/types/chat/transcript-render-helpers.d.ts +8 -0
  9. package/dist/types/chrome/dynamic-border.d.ts +6 -0
  10. package/dist/types/chrome/transcript-status.d.ts +2 -2
  11. package/dist/types/components/editor.d.ts +15 -6
  12. package/dist/types/components/loader.d.ts +2 -0
  13. package/dist/types/keybinding-matchers.d.ts +7 -0
  14. package/dist/types/overlays/btw-history-panel.d.ts +13 -0
  15. package/dist/types/overlays/btw-history.d.ts +24 -0
  16. package/dist/types/overlays/btw-inject.d.ts +22 -0
  17. package/dist/types/overlays/btw-panel.d.ts +7 -0
  18. package/dist/types/overlays/hook-editor.d.ts +2 -0
  19. package/dist/types/overlays/jobs-panel.d.ts +4 -0
  20. package/dist/types/overlays/session-selector.d.ts +24 -3
  21. package/dist/types/render/render-utils.d.ts +21 -1
  22. package/dist/types/status-line/startup.d.ts +0 -15
  23. package/dist/types/terminal-capabilities.d.ts +1 -1
  24. package/dist/types/theme/symbols.d.ts +3 -1
  25. package/dist/types/theme/theme-class.d.ts +1 -0
  26. package/dist/types/tools/grouped-file-output.d.ts +76 -8
  27. package/dist/types/tools/hashline-format.d.ts +5 -0
  28. package/dist/types/tools/native-view.d.ts +2 -0
  29. package/dist/types/tools/output-meta.d.ts +27 -0
  30. package/dist/types/tools/read.d.ts +10 -0
  31. package/dist/types/tools/wait.d.ts +2 -0
  32. package/dist/types/utils.d.ts +4 -0
  33. package/package.json +9 -9
  34. package/src/app-keybindings.ts +5 -0
  35. package/src/apps/session-picker.ts +7 -3
  36. package/src/chat/bash-execution.ts +20 -9
  37. package/src/chat/chat-transcript-builder.ts +5 -2
  38. package/src/chat/eval-execution.ts +14 -7
  39. package/src/chat/execution-shared.ts +39 -18
  40. package/src/chat/read-tool-group.ts +56 -36
  41. package/src/chat/transcript-render-helpers.ts +52 -6
  42. package/src/chrome/dynamic-border.ts +28 -2
  43. package/src/chrome/transcript-status.ts +5 -3
  44. package/src/components/editor.ts +123 -24
  45. package/src/components/key-value-list.ts +9 -9
  46. package/src/components/loader.ts +7 -0
  47. package/src/components/section.ts +1 -9
  48. package/src/components/table.ts +1 -9
  49. package/src/keybinding-matchers.ts +13 -0
  50. package/src/overlays/btw-history-panel.ts +98 -14
  51. package/src/overlays/btw-history.ts +65 -0
  52. package/src/overlays/btw-inject.ts +91 -0
  53. package/src/overlays/btw-panel.ts +53 -12
  54. package/src/overlays/hook-editor.ts +6 -0
  55. package/src/overlays/jobs-panel.ts +45 -19
  56. package/src/overlays/model-browser.ts +65 -8
  57. package/src/overlays/session-selector.ts +131 -26
  58. package/src/overlays/usage-dashboard.ts +2 -2
  59. package/src/render/code-cell.ts +1 -1
  60. package/src/render/file-list.ts +4 -7
  61. package/src/render/render-utils.ts +36 -6
  62. package/src/setup/scenes/glyph.ts +1 -1
  63. package/src/status-line/startup.ts +36 -70
  64. package/src/terminal-capabilities.ts +5 -0
  65. package/src/theme/symbols.ts +16 -9
  66. package/src/theme/theme-class.ts +3 -0
  67. package/src/tmux.ts +1 -0
  68. package/src/tools/agent-tree.ts +13 -11
  69. package/src/tools/ask.ts +13 -6
  70. package/src/tools/ast-edit.ts +48 -93
  71. package/src/tools/ast-grep.ts +22 -66
  72. package/src/tools/bash.ts +7 -0
  73. package/src/tools/default-renderer.ts +1 -1
  74. package/src/tools/edit.ts +50 -43
  75. package/src/tools/fetch.ts +2 -1
  76. package/src/tools/find.ts +6 -3
  77. package/src/tools/github.ts +14 -46
  78. package/src/tools/glob.ts +2 -1
  79. package/src/tools/grep.ts +43 -287
  80. package/src/tools/grouped-file-output.ts +250 -64
  81. package/src/tools/hashline-format.ts +5 -0
  82. package/src/tools/json-tree.ts +1 -5
  83. package/src/tools/lsp.ts +53 -25
  84. package/src/tools/native-view.ts +11 -5
  85. package/src/tools/output-meta.ts +68 -4
  86. package/src/tools/proc-render.ts +3 -3
  87. package/src/tools/read.ts +14 -0
  88. package/src/tools/task.ts +34 -77
  89. package/src/tools/vibe.ts +4 -4
  90. package/src/tools/wait.ts +8 -5
  91. package/src/tools/write.ts +17 -27
  92. package/src/tools/yield.ts +1 -1
  93. package/src/utils.ts +10 -0
@@ -1,8 +1,7 @@
1
- import { isRecord, sanitizeText } from "@oh-my-pi/pi-utils";
2
1
  import type { Component } from "../tui";
3
2
  import { Text } from "../components/text";
4
3
  import type { Theme } from "../theme/theme";
5
- import { fileHyperlink, renderStatusLine, renderTreeList } from "../render";
4
+ import { renderStatusLine, renderTreeList } from "../render";
6
5
  import {
7
6
  appendParseErrorsBulletList,
8
7
  createCachedComponent,
@@ -17,8 +16,13 @@ import {
17
16
  import {
18
17
  classifyGroupedLines,
19
18
  createCodeFrameStyler,
19
+ createGroupedRowStyler,
20
20
  describeGroupedOutput,
21
+ fitGroupsToBudget,
22
+ groupedLinkTargets,
21
23
  groupLineIndicesByBlank,
24
+ groupsByFile,
25
+ isMatchRow,
22
26
  } from "./grouped-file-output";
23
27
  import { code, compact, node, span } from "../native/describe";
24
28
  import type { NativeChild } from "../native/node";
@@ -128,64 +132,21 @@ export const astGrepToolRenderer = {
128
132
 
129
133
  const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
130
134
  const allLines = textContent.split("\n");
131
- // Resolve hyperlinks over the whole output so nested directory headers
132
- // reconstruct across the blank-line groups the tree list collapses by.
135
+ // Classify the whole output so nested directory headers resolve across blank-line groups.
133
136
  const contexts = classifyGroupedLines(allLines, details?.cwd ?? details?.searchPath, details?.searchPath);
134
137
  const styleCodeFrame = createCodeFrameStyler(allLines, contexts, "toolOutput", uiTheme, args?.lang);
135
- // The displayed lines are sanitized, but OSC 8 destinations must retain the
136
- // original path bytes. Only substitute targets for headers already linked
137
- // by the sanitized output; never let raw text create additional links.
138
- const rawDetails = options.renderContext?.rawDetails;
139
- const rawContent =
140
- isRecord(rawDetails) && typeof rawDetails.displayContent === "string" ? rawDetails.displayContent : undefined;
141
- const rawLines = rawContent?.split("\n");
142
- const rawCwd = isRecord(rawDetails) && typeof rawDetails.cwd === "string" ? rawDetails.cwd : undefined;
143
- const rawSearchPath =
144
- isRecord(rawDetails) && typeof rawDetails.searchPath === "string" ? rawDetails.searchPath : undefined;
145
- const rawContexts =
146
- rawContent !== undefined && rawLines?.length === allLines.length && sanitizeText(rawContent) === textContent
147
- ? classifyGroupedLines(rawLines, rawCwd ?? rawSearchPath, rawSearchPath)
148
- : undefined;
149
- // Style lazily: the collapsed tree list only renders the first few groups,
150
- // so a large result never styles/hyperlinks rows nobody sees.
151
- const styledLines: (string | undefined)[] = Array.from({ length: allLines.length }, () => undefined);
152
- const styleLine = (index: number): string => {
153
- const cached = styledLines[index];
154
- if (cached !== undefined) return cached;
155
- const line = allLines[index]!;
156
- const ctx = contexts[index]!;
157
- const rawContext = rawContexts?.[index];
158
- const target =
159
- ctx.headerPath && rawContext?.kind === ctx.kind && rawContext.depth === ctx.depth
160
- ? (rawContext.headerPath ?? ctx.headerPath)
161
- : ctx.headerPath;
162
- let styled: string;
163
- if (ctx.kind === "dir") {
164
- const accent = uiTheme.fg("accent", line);
165
- styled = target ? fileHyperlink(target, accent) : accent;
166
- } else if (ctx.kind === "file") {
167
- const tinted = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
168
- styled = target ? fileHyperlink(target, tinted) : tinted;
169
- } else {
170
- styled = styleCodeFrame(index) ?? uiTheme.fg(line.startsWith(" meta:") ? "dim" : "toolOutput", line);
171
- }
172
- styledLines[index] = styled;
173
- return styled;
174
- };
175
- const matchGroups = groupLineIndicesByBlank(allLines).filter(indices => {
138
+ const styleLine = createGroupedRowStyler(allLines, contexts, uiTheme, {
139
+ linkTarget: groupedLinkTargets(contexts, textContent, details, options.renderContext?.rawDetails),
140
+ styleBody: index => {
141
+ const line = allLines[index]!;
142
+ return styleCodeFrame(index) ?? uiTheme.fg(line.startsWith(" meta:") ? "dim" : "toolOutput", line);
143
+ },
144
+ linkLines: true,
145
+ });
146
+ const matchGroups = groupsByFile(groupLineIndicesByBlank(allLines), contexts).filter(indices => {
176
147
  const first = allLines[indices[0]!]!;
177
148
  return !first.startsWith("Result limit reached") && !first.startsWith("Parse issues:");
178
149
  });
179
- // Line offset at which each group starts. A collapsed tree list stops at the
180
- // first group that overflows its line budget, so groups starting at or past
181
- // the budget are never shown — hand it same-length placeholders instead of
182
- // styling them (renderTreeList only needs their line counts).
183
- const groupStarts: number[] = [];
184
- let groupStart = 0;
185
- for (const group of matchGroups) {
186
- groupStarts.push(groupStart);
187
- groupStart += group.length;
188
- }
189
150
 
190
151
  const extraLines: string[] = [];
191
152
  if (limitReached) {
@@ -200,18 +161,13 @@ export const astGrepToolRenderer = {
200
161
  return createCachedComponent(
201
162
  () => options.expanded,
202
163
  width => {
164
+ const { shown, summary } = fitGroupsToBudget(
165
+ matchGroups,
166
+ options.expanded ? Number.POSITIVE_INFINITY : COLLAPSED_MATCH_LIMIT,
167
+ { isUnit: index => isMatchRow(allLines[index]!), unit: "match", total: matchCount },
168
+ );
203
169
  const matchLines = renderTreeList(
204
- {
205
- items: matchGroups,
206
- expanded: options.expanded,
207
- maxCollapsed: matchGroups.length,
208
- maxCollapsedLines: COLLAPSED_MATCH_LIMIT,
209
- itemType: "match",
210
- renderItem: (group, context) =>
211
- !options.expanded && groupStarts[context.index]! >= COLLAPSED_MATCH_LIMIT
212
- ? Array.from({ length: group.length }, () => "")
213
- : group.map(styleLine),
214
- },
170
+ { items: shown, trailingSummary: summary, renderItem: group => group.map(styleLine) },
215
171
  uiTheme,
216
172
  );
217
173
  return fitHeaderedRows(header, [...matchLines, ...extraLines], width);
package/src/tools/bash.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  replaceTabs,
14
14
  } from "../render/render-utils";
15
15
  import {
16
+ formatMinimizedMeta,
16
17
  formatStyledTruncationWarning,
17
18
  type OutputMeta,
18
19
  stripOutputNotice,
@@ -351,6 +352,10 @@ function bashStatsParts(
351
352
  if (artifactId) {
352
353
  statsParts.push(`Artifact: ${artifactId}`);
353
354
  }
355
+ const minimized = details?.meta?.minimized;
356
+ if (minimized) {
357
+ statsParts.push(`Minimized: ${formatMinimizedMeta(minimized)}`);
358
+ }
354
359
  if (isError && typeof details?.exitCode === "number") {
355
360
  statsParts.push(`Exit: ${details.exitCode}`);
356
361
  }
@@ -400,6 +405,8 @@ function shellFootParts(details: BashToolDetails | undefined, artifactId: string
400
405
  if (service.pid !== undefined) parts.push(`PID ${service.pid}`);
401
406
  }
402
407
  if (artifactId) parts.push(`Artifact ${artifactId}`);
408
+ const minimized = details?.meta?.minimized;
409
+ if (minimized) parts.push(`Minimized ${formatMinimizedMeta(minimized)}`);
403
410
  return parts;
404
411
  }
405
412
 
@@ -51,7 +51,7 @@ function buildDefaultToolStatus(input: DefaultToolRenderInput): { status: Status
51
51
  ? "info"
52
52
  : result?.isError
53
53
  ? "error"
54
- : "done",
54
+ : "success",
55
55
  spinnerFrame: options.spinnerFrame,
56
56
  title: input.label,
57
57
  };
package/src/tools/edit.ts CHANGED
@@ -45,7 +45,13 @@ import {
45
45
  import type { ToolActivityContext, ToolActivitySummary } from "./renderer";
46
46
  import { fileHyperlink, Hasher, type RenderCache, renderStatusLine, truncateToWidth, WidthAwareText } from "../render";
47
47
  import { framedToolCard } from "../render/tool-card";
48
- import { HL_FILE_PREFIX, HL_FILE_SUFFIX, HL_MOVE_KEYWORD, HL_REM_KEYWORD } from "./hashline-format";
48
+ import {
49
+ HASHLINE_TAG_SUFFIX_RE,
50
+ HL_FILE_PREFIX,
51
+ HL_FILE_SUFFIX,
52
+ HL_MOVE_KEYWORD,
53
+ HL_REM_KEYWORD,
54
+ } from "./hashline-format";
49
55
 
50
56
  /** Edit payload syntax selected by the caller. */
51
57
  export type EditMode = "replace" | "patch" | "hashline" | "apply_patch" | "sloppy";
@@ -354,7 +360,8 @@ function renderEditHeader(
354
360
  width: number,
355
361
  uiTheme: Theme,
356
362
  options: {
357
- icon?: "pending" | "success" | "error";
363
+ icon?: "pending" | "running" | "success" | "error";
364
+ spinnerFrame?: number;
358
365
  iconOverride?: string;
359
366
  op?: Operation;
360
367
  rawPath: string;
@@ -380,6 +387,7 @@ function renderEditHeader(
380
387
  renderStatusLine(
381
388
  {
382
389
  icon: options.icon,
390
+ spinnerFrame: options.spinnerFrame,
383
391
  iconOverride: options.iconOverride,
384
392
  title,
385
393
  description,
@@ -546,8 +554,6 @@ function formatStreamingDiff(
546
554
  width: number,
547
555
  uiTheme: Theme,
548
556
  expanded: boolean,
549
- label = "streaming",
550
- spinnerFrame?: number,
551
557
  cache?: RenderedStringCache,
552
558
  ): string {
553
559
  if (!diff) return "";
@@ -564,7 +570,7 @@ function formatStreamingDiff(
564
570
  // innerWidth/budget are in the cache salt so a resize re-slices.
565
571
  const innerWidth = Math.max(1, width - 2);
566
572
  const budget = expanded ? previewWindowRows() : Math.min(PREVIEW_LIMITS.EXPANDED_LINES, previewWindowRows());
567
- let text = cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
573
+ return cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
568
574
  // "Cursor" tail window: pin the last rows to the bottom so freshly streamed
569
575
  // changes stay on screen. The whole-file diff is recomputed every chunk and
570
576
  // its Myers alignment is not monotonic in payload length, so a hunk-aware
@@ -580,18 +586,6 @@ function formatStreamingDiff(
580
586
  rendered += renderDiffColored(tail.content, { filePath: rawPath, theme: uiTheme });
581
587
  return rendered;
582
588
  });
583
- // The animated glyph rides this trailing line — inside the transcript's
584
- // volatile-tail holdback — never the block header: an animating head row
585
- // pins the native-scrollback commit boundary at the top of the block, so a
586
- // tall expanded preview could never scroll-append mid-stream.
587
- const spinner = spinnerFrame !== undefined ? `${formatStatusIcon("running", uiTheme, spinnerFrame)} ` : "";
588
- // Expanded approval previews hide the "(preview)" label (#1992) but keep
589
- // the animated glyph when one is active so the volatile tail stays live.
590
- const hideLabel = expanded && label === "preview";
591
- if (spinner || !hideLabel) {
592
- text += `\n${hideLabel ? spinner.trimEnd() : `${spinner}${uiTheme.fg("dim", `(${label})`)}`}`;
593
- }
594
- return text;
595
589
  }
596
590
 
597
591
  function formatMultiFileStreamingDiff(
@@ -599,7 +593,6 @@ function formatMultiFileStreamingDiff(
599
593
  width: number,
600
594
  uiTheme: Theme,
601
595
  expanded: boolean,
602
- spinnerFrame?: number,
603
596
  caches?: RenderedStringCache[],
604
597
  ): string {
605
598
  const parts: string[] = [];
@@ -612,19 +605,19 @@ function formatMultiFileStreamingDiff(
612
605
  continue;
613
606
  }
614
607
  if (preview.diff) {
615
- // Only the last file's preview carries the animated streaming glyph;
616
- // earlier files have settled and must stay byte-stable so their rows
617
- // can commit to native scrollback mid-stream.
618
- const isLast = index === previews.length - 1;
619
608
  const cache = previewCacheAt(caches, index);
620
- parts.push(
621
- `${header}${formatStreamingDiff(preview.diff, preview.path, width, uiTheme, expanded, "preview", isLast ? spinnerFrame : undefined, cache)}`,
622
- );
609
+ parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, width, uiTheme, expanded, cache)}`);
623
610
  }
624
611
  }
625
612
  return parts.join("");
626
613
  }
627
614
 
615
+ /** A call's streamed preview body, and the label its card shows in the bottom border. */
616
+ interface CallPreview {
617
+ text: string;
618
+ footer?: "preview" | "streaming";
619
+ }
620
+
628
621
  function getCallPreview(
629
622
  args: EditRenderArgs,
630
623
  rawPath: string,
@@ -632,41 +625,49 @@ function getCallPreview(
632
625
  uiTheme: Theme,
633
626
  renderContext: EditRenderContext | undefined,
634
627
  expanded: boolean,
635
- spinnerFrame?: number,
636
628
  caches?: RenderedStringCache[],
637
- ): string {
629
+ ): CallPreview {
638
630
  const multi = renderContext?.perFileDiffPreview;
639
631
  if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
640
- return formatMultiFileStreamingDiff(multi, width, uiTheme, expanded, spinnerFrame, caches);
632
+ return {
633
+ text: formatMultiFileStreamingDiff(multi, width, uiTheme, expanded, caches),
634
+ footer: multi.some(p => p.diff) ? "preview" : undefined,
635
+ };
641
636
  }
642
637
  const cache = previewCacheAt(caches, 0);
643
638
  if (args.previewDiff) {
644
- return formatStreamingDiff(args.previewDiff, rawPath, width, uiTheme, expanded, "preview", spinnerFrame, cache);
639
+ return {
640
+ text: formatStreamingDiff(args.previewDiff, rawPath, width, uiTheme, expanded, cache),
641
+ footer: "preview",
642
+ };
645
643
  }
646
644
  if (args.diff && args.op) {
647
- return formatStreamingDiff(args.diff, rawPath, width, uiTheme, expanded, "streaming", spinnerFrame, cache);
645
+ return { text: formatStreamingDiff(args.diff, rawPath, width, uiTheme, expanded, cache), footer: "streaming" };
648
646
  }
649
647
  if (args.diff) {
650
- return renderPlainTextPreview(args.diff, uiTheme, rawPath);
648
+ return { text: renderPlainTextPreview(args.diff, uiTheme, rawPath) };
651
649
  }
652
650
  if (args.newText || args.patch) {
653
- return renderPlainTextPreview(args.newText ?? args.patch ?? "", uiTheme, rawPath);
651
+ return { text: renderPlainTextPreview(args.newText ?? args.patch ?? "", uiTheme, rawPath) };
654
652
  }
655
653
  if (renderContext?.editStreamingFallback) {
656
- return renderContext.editStreamingFallback;
654
+ return { text: renderContext.editStreamingFallback };
657
655
  }
658
656
  const contextPreview = renderContext?.editDiffPreview;
659
657
  if (contextPreview && "error" in contextPreview && contextPreview.error) {
660
- return uiTheme.fg("error", replaceTabs(contextPreview.error));
658
+ return { text: uiTheme.fg("error", replaceTabs(contextPreview.error)) };
661
659
  }
662
- return "";
660
+ return { text: "" };
663
661
  }
664
662
 
665
663
  const MISSING_APPLY_PATCH_END_ERROR = "The last line of the patch must be '*** End Patch'";
666
664
 
665
+ /** A model-typed `[path#tag]` header may spell the tag in lowercase. */
666
+ const TYPED_TAG_SUFFIX_RE = new RegExp(HASHLINE_TAG_SUFFIX_RE.source, "i");
667
+
667
668
  function normalizeHashlineInputPreviewPath(rawPath: string): string {
668
669
  const trimmed = rawPath.trim();
669
- const hashStart = /#[0-9a-fA-F]{4}$/u.exec(trimmed)?.index;
670
+ const hashStart = TYPED_TAG_SUFFIX_RE.exec(trimmed)?.index;
670
671
  const withoutHash = hashStart === undefined ? trimmed : trimmed.slice(0, hashStart);
671
672
  if (withoutHash.length < 2) return withoutHash;
672
673
  const first = withoutHash[0];
@@ -1143,34 +1144,40 @@ export const editToolRenderer = {
1143
1144
  return framedToolCard(
1144
1145
  uiTheme,
1145
1146
  ({ width }) => {
1146
- // No status icon on the head row: it's the head of the framed block,
1147
- // and native-scrollback commits are prefix-only — an animated glyph
1148
- // would pin the commit boundary at the top, and the pending hourglass
1149
- // just adds noise. The liveness cue rides the trailing "(preview)" /
1150
- // "(streaming)" line instead.
1147
+ // Only a live spinner fills the icon slot: a pending hourglass after it
1148
+ // stops would read as a step backwards while the edit executes.
1151
1149
  const header = renderEditHeader(width, uiTheme, {
1150
+ icon: options.spinnerFrame === undefined ? undefined : "running",
1151
+ spinnerFrame: options.spinnerFrame,
1152
1152
  op,
1153
1153
  rawPath,
1154
1154
  rename,
1155
1155
  extraSuffix: fileCount > 1 ? uiTheme.fg("dim", ` (+${fileCount - 1} more)`) : undefined,
1156
1156
  });
1157
- let body = getCallPreview(
1157
+ const preview = getCallPreview(
1158
1158
  editArgs,
1159
1159
  rawPath,
1160
1160
  width,
1161
1161
  uiTheme,
1162
1162
  renderContext,
1163
1163
  options.expanded,
1164
- options?.spinnerFrame,
1165
1164
  callPreviewCaches,
1166
1165
  );
1166
+ let body = preview.text;
1167
1167
  if (applyPatchError) {
1168
1168
  body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchError), Math.max(1, width - 2)))}`;
1169
1169
  }
1170
1170
  const bodyLines = body ? body.split("\n") : [];
1171
1171
  while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
1172
+ // Expanded approval previews hide the "preview" label (#1992); "streaming" ends with the args.
1173
+ const footer =
1174
+ (preview.footer === "preview" && options.expanded) ||
1175
+ (preview.footer === "streaming" && options.argsComplete)
1176
+ ? undefined
1177
+ : preview.footer;
1172
1178
  return {
1173
1179
  header,
1180
+ footerMeta: footer ? uiTheme.fg("dim", footer) : undefined,
1174
1181
  sections:
1175
1182
  bodyLines.length > 0
1176
1183
  ? [{ content: bodyLines.flatMap(line => wrapEditRendererLine(line, Math.max(1, width - 2))) }]
@@ -14,6 +14,7 @@ import { framedToolCard } from "../render/tool-card";
14
14
  import {
15
15
  formatExpandHint,
16
16
  formatMoreItems,
17
+ formatStatusIcon,
17
18
  getDomain,
18
19
  PREVIEW_LIMITS,
19
20
  sanitizeDisplayLines,
@@ -274,7 +275,7 @@ export function renderReadUrlResult(
274
275
  metadataLines.push(`${uiTheme.fg("muted", "Lines:")} ${lineLabel}`);
275
276
  metadataLines.push(`${uiTheme.fg("muted", "Chars:")} ${charCount}`);
276
277
  if (truncated) {
277
- metadataLines.push(uiTheme.fg("warning", `${uiTheme.status.warning} Output truncated`));
278
+ metadataLines.push(`${formatStatusIcon("warning", uiTheme)} ${uiTheme.fg("warning", "Output truncated")}`);
278
279
  if (truncation?.artifactId) metadataLines.push(formatStyledArtifactReference(truncation.artifactId, uiTheme));
279
280
  }
280
281
  if (hasNotes) {
package/src/tools/find.ts CHANGED
@@ -10,10 +10,12 @@ import { Text } from "../components/text";
10
10
  import { fileHyperlink, getTreeBranch, renderStatusLine, renderTreeList, uriHyperlink } from "../render";
11
11
  import {
12
12
  createCachedComponent,
13
+ FILE_NAME_COLOR,
13
14
  fitHeaderedRows,
14
15
  formatCount,
15
16
  formatEmptyMessage,
16
17
  formatErrorMessage,
18
+ formatStyledPath,
17
19
  PREVIEW_LIMITS,
18
20
  replaceTabs,
19
21
  } from "../render/render-utils";
@@ -168,9 +170,10 @@ function renderHit(hit: FindHit, rangeLimit: number, cwd: string | undefined, ra
168
170
  return absPath === undefined ? text : fileHyperlink(absPath, text, { line });
169
171
  };
170
172
  const coverage = hit.truncated ? `${hit.linesSeen} lines judged, partial` : `${hit.linesSeen} lines judged`;
171
- const lines = [
172
- `${gauge(hit.contentScore, theme)} ${theme.fg(scoreColor(hit.contentScore), hit.contentScore.toFixed(2))} ${link(theme.fg("accent", hit.rel))} ${theme.fg("dim", coverage)}`,
173
- ];
173
+ // URL hits show whole in the file-name color; file hits get the dim directory.
174
+ const label = isUrlHit ? theme.fg(FILE_NAME_COLOR, hit.rel) : formatStyledPath(hit.rel, theme);
175
+ const score = theme.fg(scoreColor(hit.contentScore), hit.contentScore.toFixed(2));
176
+ const lines = [`${gauge(hit.contentScore, theme)} ${score} ${link(label)} ${theme.fg("dim", coverage)}`];
174
177
  const ranges = [...hit.ranges].sort((a, b) => b.p - a.p || a.start - b.start).slice(0, rangeLimit);
175
178
  const lang = getLanguageFromPath(hit.rel);
176
179
  ranges.forEach((range, index) => {
@@ -18,7 +18,6 @@ import {
18
18
  formatStatusIcon,
19
19
  PREVIEW_LIMITS,
20
20
  replaceTabs,
21
- type ToolUIColor,
22
21
  type ToolUIStatus,
23
22
  TRUNCATE_LENGTHS,
24
23
  sanitizeDisplayWarning,
@@ -165,52 +164,22 @@ function formatRunLine(run: GhRunWatchRunDetails, theme: Theme): string {
165
164
  return [title, ...meta].join(" ");
166
165
  }
167
166
 
168
- function getJobStateVisual(
169
- job: GhRunWatchJobDetails,
170
- theme: Theme,
171
- ): { iconRaw: string; iconColor: ToolUIColor; textColor: ThemeColor } {
172
- if (job.conclusion && SUCCESS_CONCLUSIONS.has(job.conclusion)) {
173
- return {
174
- iconRaw: theme.status.success,
175
- iconColor: "accent",
176
- textColor: "success",
177
- };
178
- }
179
-
180
- if (job.conclusion && FAILURE_CONCLUSIONS.has(job.conclusion)) {
181
- return {
182
- iconRaw: theme.status.error,
183
- iconColor: "error",
184
- textColor: "error",
185
- };
186
- }
187
-
188
- if (job.status && RUNNING_STATUSES.has(job.status)) {
189
- return {
190
- iconRaw: theme.status.enabled,
191
- iconColor: "warning",
192
- textColor: "warning",
193
- };
194
- }
195
-
196
- if (job.status && PENDING_STATUSES.has(job.status)) {
197
- return {
198
- iconRaw: theme.status.shadowed,
199
- iconColor: "muted",
200
- textColor: "muted",
201
- };
202
- }
203
-
204
- return {
205
- iconRaw: theme.status.shadowed,
206
- iconColor: "muted",
207
- textColor: "muted",
208
- };
167
+ function getJobStateVisual(job: GhRunWatchJobDetails, theme: Theme): { icon: string; textColor: ThemeColor } {
168
+ if (job.conclusion && SUCCESS_CONCLUSIONS.has(job.conclusion))
169
+ return { icon: formatStatusIcon("success", theme), textColor: "success" };
170
+ if (job.conclusion && FAILURE_CONCLUSIONS.has(job.conclusion))
171
+ return { icon: formatStatusIcon("error", theme), textColor: "error" };
172
+ if (job.status && RUNNING_STATUSES.has(job.status))
173
+ return { icon: formatStatusIcon("running", theme), textColor: "warning" };
174
+ if (job.status && PENDING_STATUSES.has(job.status))
175
+ return { icon: formatStatusIcon("pending", theme), textColor: "muted" };
176
+ // An unrecognized state (e.g. a finished job with a new conclusion) claims neither queued nor settled.
177
+ return { icon: theme.styledSymbol("status.shadowed", "muted"), textColor: "muted" };
209
178
  }
210
179
 
211
180
  function renderJobLine(job: GhRunWatchJobDetails, width: number, theme: Theme): string {
212
181
  const visual = getJobStateVisual(job, theme);
213
- const prefix = theme.fg(visual.iconColor, `${visual.iconRaw} `);
182
+ const prefix = `${visual.icon} `;
214
183
  const durationLabel = job.durationSeconds !== undefined ? `${job.durationSeconds}s` : undefined;
215
184
  const styledDuration = durationLabel ? theme.fg(visual.textColor, durationLabel) : undefined;
216
185
  const reservedWidth = visibleWidth(prefix) + (styledDuration ? 1 + visibleWidth(styledDuration) : 0);
@@ -255,9 +224,8 @@ function renderFailedLogs(
255
224
  const lines: string[] = [];
256
225
  for (const entry of failedLogs) {
257
226
  const context = entry.workflowName ? `${entry.workflowName} #${entry.runId}` : `run #${entry.runId}`;
258
- lines.push(
259
- theme.fg("error", `${theme.status.error} ${replaceTabs(entry.jobName)} ${theme.fg("muted", context)}`),
260
- );
227
+ const jobName = theme.fg("error", replaceTabs(entry.jobName));
228
+ lines.push(`${formatStatusIcon("error", theme)} ${jobName} ${theme.fg("muted", context)}`);
261
229
 
262
230
  if (!entry.available || !entry.tail) {
263
231
  lines.push(theme.fg("dim", truncateVisualWidth(` ${formatUnavailableLog(entry)}`, Math.max(8, width))));
package/src/tools/glob.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  formatCount,
23
23
  formatEmptyMessage,
24
24
  formatErrorMessage,
25
+ formatStyledPath,
25
26
  PREVIEW_LIMITS,
26
27
  } from "../render/render-utils";
27
28
  import { formatFullOutputReference } from "./output-meta";
@@ -202,7 +203,7 @@ export const globToolRenderer = {
202
203
  expanded: options.expanded,
203
204
  maxCollapsed: PREVIEW_LIMITS.COLLAPSED_ITEMS,
204
205
  itemType: "file",
205
- renderItem: line => uiTheme.fg("accent", line),
206
+ renderItem: line => formatStyledPath(line, uiTheme),
206
207
  },
207
208
  uiTheme,
208
209
  );