@oh-my-pi/pi-tui 18.14.2 → 18.15.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 (37) hide show
  1. package/CHANGELOG.md +29 -33
  2. package/README.md +1 -1
  3. package/dist/types/chat/transcript-render-helpers.d.ts +8 -0
  4. package/dist/types/chrome/transcript-status.d.ts +2 -2
  5. package/dist/types/overlays/hook-editor.d.ts +2 -0
  6. package/dist/types/overlays/jobs-panel.d.ts +4 -0
  7. package/dist/types/render/render-utils.d.ts +17 -0
  8. package/dist/types/terminal-capabilities.d.ts +1 -1
  9. package/dist/types/tools/grouped-file-output.d.ts +76 -8
  10. package/dist/types/tools/hashline-format.d.ts +5 -0
  11. package/dist/types/tools/native-view.d.ts +2 -0
  12. package/dist/types/tools/read.d.ts +10 -0
  13. package/dist/types/tools/wait.d.ts +2 -0
  14. package/package.json +9 -9
  15. package/src/chat/chat-transcript-builder.ts +4 -2
  16. package/src/chat/read-tool-group.ts +48 -21
  17. package/src/chat/transcript-render-helpers.ts +36 -2
  18. package/src/chrome/transcript-status.ts +5 -3
  19. package/src/overlays/hook-editor.ts +6 -0
  20. package/src/overlays/jobs-panel.ts +45 -19
  21. package/src/render/file-list.ts +4 -7
  22. package/src/render/render-utils.ts +31 -3
  23. package/src/terminal-capabilities.ts +5 -0
  24. package/src/tmux.ts +1 -0
  25. package/src/tools/ast-edit.ts +48 -93
  26. package/src/tools/ast-grep.ts +22 -66
  27. package/src/tools/edit.ts +11 -2
  28. package/src/tools/find.ts +6 -3
  29. package/src/tools/glob.ts +2 -1
  30. package/src/tools/grep.ts +43 -287
  31. package/src/tools/grouped-file-output.ts +250 -64
  32. package/src/tools/hashline-format.ts +5 -0
  33. package/src/tools/lsp.ts +15 -15
  34. package/src/tools/native-view.ts +11 -5
  35. package/src/tools/proc-render.ts +2 -2
  36. package/src/tools/read.ts +14 -0
  37. package/src/tools/wait.ts +6 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,34 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.15.0] - 2026-11-01
6
+
7
+ ### Breaking Changes
8
+
9
+ - `GroupedLineContext.isUrl` is replaced by `url`, the internal URL a header names or a body row belongs to ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
10
+
11
+ ### Added
12
+
13
+ - Added `FILE_NAME_COLOR`, `formatStyledPath` and `pathNameStart` to `render/render-utils` for paths in tool bodies and Tern file rows; `createGroupedRowStyler`, `groupedLinkTargets`, `fitGroupsToBudget`, `groupsByFile` and `isMatchRow` to `tools/grouped-file-output`; `splitReadQuery` to `tools/read`; and `HASHLINE_TAG_SUFFIX_RE` to `tools/hashline-format`. `classifyGroupedLines` reports each file header's full `displayPath` and `suffix`, and a `TranscriptStatusPart` without `color` keeps its already-styled text ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
14
+ - `JobsPanelJob.caption`: the jobs sheet's detail pane shows it, muted, between the job's purpose and its command (`omp.jobs.caption`) ([#17649](https://github.com/can1357/oh-my-pi/pull/17649) by [@wolfiesch](https://github.com/wolfiesch))
15
+
16
+ ### Changed
17
+
18
+ - The Background jobs sheet names each job by what it is for when the model said so, shows that above the full command in the detail pane, and no longer repeats the command in an empty widget box for jobs that print nothing ([#17649](https://github.com/can1357/oh-my-pi/pull/17649) by [@wolfiesch](https://github.com/wolfiesch))
19
+ - File paths in tool output share one style: a dim directory and the file name in the custom-message label color (bronze in titanium), with line locations and selectors (`:1-175`, `:42:14`, `?q=`) muted. This covers Grep, AST Grep, AST Edit, Glob, Find, LSP, diagnostics, grouped Reads and file mentions; Glob's folder icon is muted like the file-type icons, and paths in tool headers keep their color ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
20
+ - Grep, AST Grep and AST Edit results label each file with its full path instead of the model-facing `# dir/` and `## file#tag` headers, and each file gets its own tree branch. Their collapsed previews fit the same row budget the same way, cutting the last file short rather than overflowing, and end with `… N more matches` (`… N more files` for AST Edit) ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
21
+ - AST Grep match rows link to their line, as Grep's do ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
22
+ - LSP diagnostics carry their severity color on the `[error]`/`[warning]` tag instead of the location, and collapsed diagnostic rows now show that tag ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
23
+
24
+ ### Fixed
25
+
26
+ - Rex sessions now render Kitty graphics without forcing the protocol, including when only `TERM=xterm-rex` identifies the client ([#17651](https://github.com/can1357/oh-my-pi/issues/17651)).
27
+ - Fixed collapsed AST Grep showing no matches when its first file or folder was taller than the preview, and its `… N more` row counting hidden folders instead of hidden matches ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
28
+ - Fixed collapsed AST Edit overflowing its preview when the first file's diff was long ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
29
+ - Fixed search and diagnostic file paths in Tern dropping the leading `/` of absolute paths and a folder's own `(…)` ending, and AST Edit diffs in Tern losing highlighting to the file's `#tag`; a file name ending in lowercase or other-length `#…` text keeps it ([#17608](https://github.com/can1357/oh-my-pi/pull/17608) by [@JustasMasiulis](https://github.com/JustasMasiulis))
30
+ - Fixed a read of a URL drawing as dead text in the folded read row: its target is not a file path, so no link was built for it, and clicking the URL did nothing. The row (and each row of a grouped read) now links to the URL itself, gaining an `https://` scheme for a bare `www.` host ([#17598](https://github.com/can1357/oh-my-pi/pull/17598) by [@iacore](https://github.com/iacore))
31
+ - Fixed replayed transcripts ignoring a registered `async-result` message renderer, so a replayed conversation rendered differently from the live one ([#17372](https://github.com/can1357/oh-my-pi/pull/17372) by [@AlexBrd999](https://github.com/AlexBrd999), from [#14484](https://github.com/can1357/oh-my-pi/pull/14484) by [@F0Rextasy](https://github.com/F0Rextasy); [#14131](https://github.com/can1357/oh-my-pi/issues/14131))
32
+
5
33
  ## [18.14.2] - 2026-10-31
6
34
 
7
35
  ### Added
@@ -2478,36 +2506,4 @@
2478
2506
 
2479
2507
  - Fixed the OSC 11 appearance poll re-querying every 2s forever on terminals that support Mode 2031 but never change theme, whose repeated OSC 11/DA1 writes cleared the user's active text selection (breaking copy every 2 seconds). The poll now stops as soon as DECRQM confirms Mode 2031 support, since push notifications make polling redundant.
2480
2508
 
2481
- ## [15.9.0] - 2026-06-04
2482
-
2483
- ### Added
2484
-
2485
- - Added Kitty `CSI 22 J` screen-to-scrollback clears for non-destructive full paints, while keeping ED3 for destructive history/session rebuilds.
2486
- - Added Kitty OSC 99 rich notification formatting and startup capability probing.
2487
- - Added Kitty OSC 66 text-sized Markdown H1 headings (2x scale) plus native text-width support for OSC 66 spans. Off by default and gated to Kitty (the only terminal implementing OSC 66) via the `TERMINAL.textSizing` capability; hosts enable it through `setTextSizing`.
2488
- - Added Kitty Unicode placeholder image rendering (`U=1` + U+10EEEE with explicit row/column diacritics): inline images are drawn as real text cells that carry the image id in their foreground color, so they survive horizontal slicing, reflow, and overlapping draws instead of relying on cursor-positioned `a=p` placements. Enabled by default on Kitty-family terminals; opt out with `PI_NO_KITTY_PLACEHOLDERS=1`, and falls back to direct placement when a grid exceeds the diacritic table's addressable range.
2489
- - Added Kitty temp-file image transmission (`t=t`): on local sessions, decoded PNG bytes are written to a `tty-graphics-protocol` temp file and the path is sent instead of in-band base64, gated behind a startup `a=q,t=t` support probe. Controlled by `PI_KITTY_IMAGE_TRANSMISSION=direct|temp-file|auto`; disabled over SSH unless explicitly forced.
2490
- - Added DECRQM capability detection for DEC private modes 2026 (synchronized output) and 2048 (in-band resize). Synchronized-output paint wrappers are dropped when the terminal reports 2026 unsupported (preserving the `PI_NO_SYNC_OUTPUT` override), and DEC 2048 in-band resize is enabled when supported — reported geometry and cell pixel size are updated from `CSI 48 ; rows ; cols ; yPx ; xPx t` reports, with SIGWINCH and `CSI 16 t` kept as fallbacks.
2491
- - Added an injectable render scheduler for TUI tests, allowing deterministic render drains without patching global clocks or event-loop timing.
2492
- - Added `ImageBudget`, an inline-image cap that keeps only the most recent N images as live terminal graphics and demotes older ones to their text fallback. Once a new image pushes the count past the cap, the renderer hides the oldest via a full redraw plus an explicit Kitty graphics purge (`a=d,d=I`) — text-clear escapes (`CSI 2 J`/`CSI 3 J`) do not remove Kitty images. Configure the cap via `TUI#setMaxInlineImages` (`0` disables it).
2493
- - Changed Kitty inline images to a transmit-once + placement scheme: the base64 data is sent a single time (`a=t`) keyed by a stable image id, then every repaint emits only the tiny placement (`a=p,i=…,p=…`). Repaints — including full redraws — no longer re-send image data or stack duplicate placements, and the diff/line buffers and render caches hold short placement strings instead of multi-KB base64. The `ImageBudget` doubles as the transmit store (it tracks which ids are loaded and re-transmits after a purge frees the data). iTerm2/Sixel, which have no addressable image store, keep sending inline data as before.
2494
- - Added a renderer-level DECCARA rectangular-SGR optimizer that paints solid background panels/rows (Box/Text/Markdown fills, status bars, any full-width `theme.bg` row) as a single coalesced rectangle escape (`CSI 2*x` / `CSI Pt;Pl;Pb;Pr;<sgr>$r` / `CSI *x`) instead of emitting a full-width run of background-styled spaces on every visible row. It operates at emit time on the final ANSI strings — components are unchanged — and strips only trailing padding it can prove sits under a single non-default background span, coalescing vertically adjacent identical fills into one rectangle and falling back to the original bytes whenever the rectangle would not save bytes. Enabled only on Kitty, which implements the SGR-background extension (`docs/deccara.rst`); **Ghostty is intentionally excluded** because its `CSI $r` is unimplemented (ghostty-org/ghostty#632) and would drop the background entirely. Scrollback-bound rows and the append/scroll paths always keep the padded representation so native history preserves colored cells, and the `PI_NO_DECCARA` kill switch (plus tmux/screen/zellij detection) forces the fallback.
2495
- - Added `CMUX_SURFACE_ID` environment variable support to `getTerminalId()`, so cmux terminal surfaces get a stable identifier alongside kitty, tmux, macOS Terminal.app, and Windows Terminal — enabling per-surface session breadcrumbs for `omp -c` in cmux.
2496
-
2497
- ### Changed
2498
-
2499
- - Changed TUI tests to use Ghostty's VT engine (`ghostty-web`) instead of `@xterm/headless`.
2500
- - Changed the default inline-image live graphics budget from 3 to 8 images.
2501
-
2502
- ### Fixed
2503
-
2504
- - Fixed the DECCARA background-fill optimizer rejecting or repainting the wrong cells when a trailing fill crossed from default-background spaces into colored spaces.
2505
- - Fixed DEC private-mode reports with DECRPM status 3/4 being treated as unsupported, so permanent 2026/2048 reports stay recognized.
2506
- - Fixed OSC 66 text-sizing width and slicing edge cases, including ZWJ emoji payloads and partial slices through scaled spans.
2507
- - Fixed focused `Input` components following `TUI#setShowHardwareCursor`, so single-line prompts render either the terminal cursor or software cursor consistently with the editor.
2508
- - Fixed the DECCARA background-fill optimizer painting fills on the wrong rows ("split into unaligned halves") in the differential repaint path. When a diff grew the transcript past the viewport, writing the rewritten rows scrolled the terminal, but the absolute DECCARA rectangle coordinates were derived from the pre-scroll viewport top, so every fill landed `scrollAmount` rows too low while the relatively-positioned text settled correctly; rows scrolled into history were also shortened, dropping their background padding from native scrollback. Rectangles now target the post-scroll rows and only rows remaining in the final viewport are optimized.
2509
- - Fixed native scrollback desynchronization after terminal width or height changes reflowed overflowing content while the viewport was not at the bottom
2510
- - Fixed a notification chip (or any injected block) rendering on top of an actively streaming tool render on ED3-risk terminals (Ghostty/kitty/Alacritty/iTerm2). While a foreground tool streams, its header's elapsed-time counter ticks every frame; once output scrolls the header above the viewport top, each tick is an offscreen edit that — because the eager scrollback-rebuild opt-in is gated off on these terminals — repaints the viewport in place and advances the rendered line count without committing the new overflow to native history. `#scrollbackHighWater` then lagged the logical viewport top, so a later content shrink whose changes landed in the visible region slipped past the shrink-across-boundary guard and reached the differential emitter, which is anchored to `#maxLinesRendered - height`: it rewrote only the suffix, dropped the newly exposed top row, and left a blank at the bottom, drifting every row below the edit one line up so it painted over the rows above. Such shrinks now re-anchor the bottom of the viewport with a non-destructive repaint, and the foreground-streaming shrink-across-boundary case repaints the live tail instead of padding and pinning the pre-shrink viewport.
2511
- - Fixed a terminal resize during foreground-tool streaming on an unknown-viewport / ED3-risk host (Ghostty/kitty/Alacritty/iTerm2/WSL) leaving native scrollback permanently out of sync, so scrolling back after the turn showed missing rows. A pure geometry resize (no content change) takes the in-place viewport-repaint path, which — unlike a content-bearing resize that rebuilds via the geometry branch — never flagged native history. Because the prompt-submit checkpoint (`refreshNativeScrollbackIfDirty`) only rebuilds when scrollback is marked dirty on these hosts, the discrepancy was never reconciled. Overflowing geometry repaints whose viewport is not known to be at the bottom now mark scrollback dirty so the next checkpoint rebuilds an exact copy of the transcript.
2512
-
2513
- Older entries are archived in [packages/tui/CHANGELOG.md@6693ddbfd58c](https://github.com/can1357/oh-my-pi/blob/6693ddbfd58ce9edcdc0dc3fe98a8882c9bae834/packages/tui/CHANGELOG.md).
2509
+ Older entries are archived in [packages/tui/CHANGELOG.md@6b354a56a890](https://github.com/can1357/oh-my-pi/blob/6b354a56a890b69b6bc9503c16c510be33675b5a/packages/tui/CHANGELOG.md).
package/README.md CHANGED
@@ -493,7 +493,7 @@ const spacer = new Spacer(2); // 2 empty lines (default: 1)
493
493
 
494
494
  ### Image
495
495
 
496
- Renders images inline for terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm, and Warp on macOS/Linux), iTerm2 inline images or SIXEL. Falls back to a text placeholder on unsupported terminals.
496
+ Renders images inline for terminals that support the Kitty graphics protocol (including Kitty, Ghostty, WezTerm, Rex, and Warp on macOS/Linux), iTerm2 inline images or SIXEL. Falls back to a text placeholder on unsupported terminals.
497
497
 
498
498
  ```typescript
499
499
  interface ImageTheme {
@@ -8,6 +8,7 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
8
  import { type Component } from "../tui.js";
9
9
  import { type FileMentionMessage } from "./messages.js";
10
10
  import { ToolActivityContainer } from "../chrome/tool-activity.js";
11
+ import type { MessageRenderer } from "./extension-types.js";
11
12
  import { type TranscriptBlock } from "../chrome/transcript-container.js";
12
13
  type CustomOrHookMessage = Extract<AgentMessage, {
13
14
  role: "custom" | "hookMessage";
@@ -21,6 +22,13 @@ type AssistantAgentMessage = Extract<AgentMessage, {
21
22
  * row per job.
22
23
  */
23
24
  export declare function buildAsyncResultBlock(message: CustomOrHookMessage): ToolActivityContainer;
25
+ /**
26
+ * The `async-result` card for both transcript surfaces: a registered renderer owns it (#14131);
27
+ * the built-in block covers no renderer, a declining renderer, and a throwing one.
28
+ */
29
+ export declare function buildAsyncResultComponent(message: CustomOrHookMessage, renderer: MessageRenderer | undefined): Component & {
30
+ setExpanded(expanded: boolean): void;
31
+ };
24
32
  /**
25
33
  * Render a `launch-completion` custom message (terminal supervised-process
26
34
  * exits from the launch broker) as a transcript block of one compact
@@ -3,10 +3,10 @@ import type { NativeNode } from "../native/node.js";
3
3
  import { type ThemeColor } from "../theme/index.js";
4
4
  import { TranscriptBlock } from "./transcript-container.js";
5
5
  import { type NoticeTone } from "./transcript-notice.js";
6
- /** One themed run of a status row's terminal line. */
6
+ /** One run of a status row's terminal line, in `color` unless the text is already styled. */
7
7
  export interface TranscriptStatusPart {
8
8
  readonly text: string;
9
- readonly color: ThemeColor;
9
+ readonly color?: ThemeColor;
10
10
  }
11
11
  /** A status row in a native terminal: its {@link noticeRow}'s tone, icon and message. */
12
12
  export interface TranscriptStatusNative {
@@ -85,4 +85,6 @@ export declare class HookEditorComponent extends OverlayPanel implements Focusab
85
85
  * enhanced-paste routing falls back to the main prompt editor hidden behind the
86
86
  * dialog (#2127 routing contract). */
87
87
  pasteText(text: string): void;
88
+ /** Replace the draft, as Ctrl+R history search does for the main composer. */
89
+ setText(text: string): void;
88
90
  }
@@ -8,6 +8,10 @@ export interface JobsPanelJob {
8
8
  readonly type: string;
9
9
  readonly status: "running" | "completed" | "failed" | "cancelled";
10
10
  readonly label: string;
11
+ /** What the job is for, in the model's words; rows and the detail lead with it over a command `label`. */
12
+ readonly intent?: string;
13
+ /** One line on what a command does and when it ends, from the cheap model; the detail shows it under the intent. */
14
+ readonly caption?: string;
11
15
  readonly startTime: number;
12
16
  readonly endTime?: number;
13
17
  /** Registry id of the agent that owns the job. */
@@ -190,6 +190,23 @@ export declare function formatErrorMessage(message: string | undefined, theme: T
190
190
  export declare function formatErrorDetail(message: string | undefined, theme: Theme): string;
191
191
  /** Render a muted empty-state message with a warning icon. */
192
192
  export declare function formatEmptyMessage(message: string, theme: Theme): string;
193
+ /**
194
+ * Color of a file's name, or a whole URL, in tool bodies: it keeps paths apart
195
+ * from the tool titles above them (bronze in titanium).
196
+ */
197
+ export declare const FILE_NAME_COLOR = "customMessageLabel";
198
+ /**
199
+ * Where a display path's final segment (file or folder name) starts, past its
200
+ * last `/` or `\` separator; a trailing-slash directory keeps its slash on the
201
+ * name. Shared by the terminal ({@link formatStyledPath}) and Tern file rows.
202
+ */
203
+ export declare function pathNameStart(filePath: string): number;
204
+ /**
205
+ * A file path in a tool body: the directory (with its trailing separator) dim,
206
+ * the final segment in {@link FILE_NAME_COLOR}. Tool headers keep paths in
207
+ * their plain subject color instead.
208
+ */
209
+ export declare function formatStyledPath(filePath: string, theme: Theme): string;
193
210
  /** Marker displayed in the gutter of a code frame. */
194
211
  export type CodeFrameMarker = "" | " " | "*" | "+" | "-" | ">";
195
212
  /** Format a code-frame row with a padded line-number gutter. */
@@ -18,7 +18,7 @@ export declare enum NotifyProtocol {
18
18
  Osc99 = "\u001B]99;;",
19
19
  Osc9 = "\u001B]9;"
20
20
  }
21
- export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "otty" | "rio" | "tern" | "monstar" | "base" | "trueColor";
21
+ export type TerminalId = "kitty" | "ghostty" | "wezterm" | "iterm2" | "vscode" | "alacritty" | "warp" | "orca" | "otty" | "rio" | "tern" | "monstar" | "rex" | "base" | "trueColor";
22
22
  /** Terminal capability details used for rendering and protocol selection. */
23
23
  export declare class TerminalInfo {
24
24
  readonly id: TerminalId;
@@ -52,22 +52,35 @@ export interface FileMatchSection {
52
52
  * Files with no model lines are omitted.
53
53
  */
54
54
  export declare function formatFileMatches(files: string[], grouped: boolean, renderFile: (filePath: string) => FileMatchSection): GroupedFilesOutput;
55
- /** Per-line classification of grouped output, used by renderers for hyperlinks. */
55
+ /** Whether a row of grouped output is a code-frame row the search matched (`*12│…`). */
56
+ export declare function isMatchRow(line: string): boolean;
57
+ /** Per-line classification of grouped output, used by renderers for file rows and hyperlinks. */
56
58
  export interface GroupedLineContext {
57
59
  /** Directory header, file header, or any non-header body/content line. */
58
60
  kind: "dir" | "file" | "content";
59
61
  /** Number of leading `#` for headers; 0 for content lines. */
60
62
  depth: number;
61
- /** Resolved absolute path of the dir/file a header points at (when resolvable). */
63
+ /** Resolved absolute path of the file a header points at (when resolvable). */
62
64
  headerPath?: string;
65
+ /**
66
+ * For file headers, the full display path: the folded directory chain joined
67
+ * with the name, without `#` markers or the hashline tag.
68
+ */
69
+ displayPath?: string;
70
+ /** For file headers, a trailing parenthesized note such as `(2 replacements)`. */
71
+ suffix?: string;
63
72
  /** For content lines, the absolute path of the owning file (line hyperlinks). */
64
73
  filePath?: string;
65
- /** Header is an internal/url-like target the caller resolves itself. */
66
- isUrl?: boolean;
74
+ /**
75
+ * The internal or url-like target, as written, that a header names or a
76
+ * content line belongs to; the caller resolves it (no filesystem path).
77
+ */
78
+ url?: string;
67
79
  }
68
80
  /**
69
81
  * Walk grouped output lines, tracking a directory stack keyed by header depth, so
70
- * each header and body line can be linked back to its absolute filesystem path.
82
+ * each header and body line can be linked back to its absolute filesystem path
83
+ * and each file header to its full display path.
71
84
  * Reconstruction is stack-based (not per-blank-group) so nested directory headers
72
85
  * resolve correctly across the whole output.
73
86
  *
@@ -76,9 +89,64 @@ export interface GroupedLineContext {
76
89
  * formatted relative to cwd regardless of the (sub)directory the search was
77
90
  * scoped to. `fileScope` is the initial owning file for body lines that appear
78
91
  * before any header (single-file scopes have no `#` headers); it defaults to
79
- * `headerBase` and should be passed the scoped file's absolute path.
92
+ * `headerBase` and should be passed the scoped file's absolute path. A URL base
93
+ * or scope (`local://…`) is no filesystem path: headerless lines belong to the
94
+ * URL, and headers under a URL base get no path.
80
95
  */
81
96
  export declare function classifyGroupedLines(lines: readonly string[], headerBase: string | undefined, fileScope?: string | undefined): GroupedLineContext[];
97
+ /**
98
+ * Line groups with one file per group, for terminal tree lists: `# dir/`
99
+ * header rows are dropped (each file row carries its full path), and every
100
+ * file row starts a group of its own, so each file gets its own branch instead
101
+ * of hanging under the previous file of its directory. Rows before a group's
102
+ * first file row (notes, a single-file scope's matches) keep their group.
103
+ */
104
+ export declare function groupsByFile(groups: readonly (readonly number[])[], contexts: readonly GroupedLineContext[]): number[][];
105
+ /** Details fields a grouped-output tool records for links: the display base paths and resolved URL targets. */
106
+ export interface GroupedLinkDetails {
107
+ cwd?: string;
108
+ searchPath?: string;
109
+ displayTargets?: Record<string, string>;
110
+ }
111
+ /**
112
+ * Link targets for the rows of sanitized grouped output: a file row's file, a
113
+ * body row's owning file, and for an internal URL the file `displayTargets`
114
+ * resolves it to. Destinations keep the original path bytes: while the raw
115
+ * `displayContent` still aligns line-for-line with the sanitized `displayText`,
116
+ * a row takes its raw classification's target. Raw text never links a row the
117
+ * sanitized output leaves unlinked.
118
+ */
119
+ export declare function groupedLinkTargets(contexts: readonly GroupedLineContext[], displayText: string, details: GroupedLinkDetails | undefined, rawDetails: unknown): (index: number) => string | undefined;
120
+ /**
121
+ * A memoized styler for the rows of grouped output, so a collapsed preview
122
+ * styles only the rows it shows. A file row is its full path (dim directory,
123
+ * name in {@link FILE_NAME_COLOR}; a URL whole in that color) and any dim
124
+ * suffix, linked to `linkTarget`'s file or, for an unresolved URL, the URL.
125
+ * Body rows come from `styleBody`, tabs expanded; with `linkLines` a numbered
126
+ * code-frame row links to its line in the owning file.
127
+ */
128
+ export declare function createGroupedRowStyler(lines: readonly string[], contexts: readonly GroupedLineContext[], theme: Theme, options: {
129
+ linkTarget: (index: number) => string | undefined;
130
+ styleBody: (index: number) => string;
131
+ linkLines?: boolean;
132
+ }): (index: number) => string;
133
+ /**
134
+ * Fit line groups (one file each) into `budget` rows, the `… N more` summary row
135
+ * included: whole groups first, then the next group cut short when it can still
136
+ * show a row under its file row (the first group always shows its head). The
137
+ * summary counts the `unit`s left out, the rows `isUnit` accepts, out of `total`
138
+ * when the caller knows it; with none left out it counts hidden lines. `gap`
139
+ * rows separate shown groups (framed lists put a blank line between files).
140
+ */
141
+ export declare function fitGroupsToBudget(groups: readonly (readonly number[])[], budget: number, options: {
142
+ isUnit: (index: number) => boolean;
143
+ unit: string;
144
+ total?: number;
145
+ gap?: number;
146
+ }): {
147
+ shown: (readonly number[])[];
148
+ summary: string;
149
+ };
82
150
  /**
83
151
  * Split line indices into blank-line-separated groups, mirroring
84
152
  * `splitGroupsByBlankLine`: when any blank line is present, break on runs of
@@ -108,8 +176,8 @@ export type GroupedOutputEvent = {
108
176
  };
109
177
  /**
110
178
  * Walk grouped output (`# dir/`, `## file.ts (suffix)`, body lines), resolving
111
- * each file header to its full display path (the folded directory chain
112
- * joined with the name) and dropping blank separator lines.
179
+ * each file header to its full display path (see {@link classifyGroupedLines})
180
+ * and dropping blank separator lines.
113
181
  */
114
182
  export declare function walkGroupedOutput(lines: readonly string[]): Generator<GroupedOutputEvent>;
115
183
  /** One source line for {@link numberedCode}: its number (`null` for an elision marker, which adds no line), text and match mark. */
@@ -6,6 +6,11 @@ export declare const HL_FILE_SUFFIX = "]";
6
6
  export declare const HL_FILE_HASH_SEP = "#";
7
7
  /** Number of characters in a displayed file hash. */
8
8
  export declare const HL_FILE_HASH_LENGTH = 4;
9
+ /**
10
+ * A trailing `#TAG` file hash exactly as the hashline store writes it (uppercase
11
+ * hex), so a name's own `#dead` or `#v2` ending survives stripping.
12
+ */
13
+ export declare const HASHLINE_TAG_SUFFIX_RE: RegExp;
9
14
  /** Keyword identifying a file move operation. */
10
15
  export declare const HL_MOVE_KEYWORD = "MV";
11
16
  /** Keyword identifying a file removal operation. */
@@ -8,6 +8,8 @@ import type { NativeToolHead, NativeToolView, ToolRenderResult } from "./rendere
8
8
  export declare function displayPath(filePath: string): string;
9
9
  /** Configured file link for a head's path target, when the path is absolute. */
10
10
  export declare function fileHref(filePath: string | undefined): string | undefined;
11
+ /** Link for a head target that is a web URL (`www.` gains `https://`), else undefined. */
12
+ export declare function urlHref(target: string | undefined): string | undefined;
11
13
  /** Diff stats as one head `meta` string, `+8 −1` (U+2212 minus), or undefined when nothing changed. */
12
14
  export declare function diffStatsMeta(added: number, removed: number): string | undefined;
13
15
  /**
@@ -111,6 +111,16 @@ export declare function splitInternalUrlSel(rawPath: string, specOf?: (scheme: s
111
111
  path: string;
112
112
  sel?: string;
113
113
  };
114
+ /**
115
+ * Split a filesystem read target from its query (`data.json?q=.items` →
116
+ * `data.json`, `?q=.items`), the display twin of the read tool's JSON, SQLite
117
+ * and image-question parsers. URLs own their query string and stay whole, as
118
+ * does a `?` inside a file name.
119
+ */
120
+ export declare function splitReadQuery(readPath: string): {
121
+ path: string;
122
+ query?: string;
123
+ };
114
124
  /** Recognize HTTP URLs and www-prefixed external read targets. */
115
125
  export declare function isReadableUrlPath(value: string): boolean;
116
126
  /** Return the first selected line when every range has valid one-based bounds. */
@@ -17,6 +17,8 @@ export interface JobSnapshot {
17
17
  type: "bash" | "task" | "eval";
18
18
  status: "running" | "completed" | "failed" | "cancelled";
19
19
  label: string;
20
+ /** What the job is for, in the model's words (a process job's `label` is its command line). */
21
+ intent?: string;
20
22
  durationMs: number;
21
23
  /** Process exit status when the job reports one. */
22
24
  exitCode?: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "18.14.2",
4
+ "version": "18.15.0",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": {
@@ -41,14 +41,14 @@
41
41
  "gen:glyphs": "bun scripts/gen-glyphs.ts"
42
42
  },
43
43
  "dependencies": {
44
- "@oh-my-pi/omptype": "18.14.2",
45
- "@oh-my-pi/pi-agent-core": "18.14.2",
46
- "@oh-my-pi/pi-ai": "18.14.2",
47
- "@oh-my-pi/pi-catalog": "18.14.2",
48
- "@oh-my-pi/pi-natives": "18.14.2",
49
- "@oh-my-pi/pi-utils": "18.14.2",
50
- "@oh-my-pi/pi-wire": "18.14.2",
51
- "@oh-my-pi/snapcompact": "18.14.2"
44
+ "@oh-my-pi/omptype": "18.15.0",
45
+ "@oh-my-pi/pi-agent-core": "18.15.0",
46
+ "@oh-my-pi/pi-ai": "18.15.0",
47
+ "@oh-my-pi/pi-catalog": "18.15.0",
48
+ "@oh-my-pi/pi-natives": "18.15.0",
49
+ "@oh-my-pi/pi-utils": "18.15.0",
50
+ "@oh-my-pi/pi-wire": "18.15.0",
51
+ "@oh-my-pi/snapcompact": "18.15.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -37,7 +37,7 @@ import { theme } from "../theme";
37
37
  import {
38
38
  assistantHasVisibleContent,
39
39
  assistantUsageIsBilled,
40
- buildAsyncResultBlock,
40
+ buildAsyncResultComponent,
41
41
  buildFileMentionBlock,
42
42
  buildIrcMessageCard,
43
43
  buildLaunchCompletionBlock,
@@ -599,7 +599,9 @@ export class ChatTranscriptBuilder {
599
599
  #appendCustomMessage(message: Extract<AgentMessage, { role: "custom" | "hookMessage" }>): void {
600
600
  if (!message.display) return;
601
601
  if (message.customType === "async-result") {
602
- const component = buildAsyncResultBlock(message);
602
+ const renderer = this.#deps.getMessageRenderer?.(message.customType);
603
+ const component = buildAsyncResultComponent(message, renderer);
604
+ this.#trackExpandable(component);
603
605
  this.container.addChild(component);
604
606
  return;
605
607
  }
@@ -3,7 +3,7 @@ import type { AssistantMessage, Usage } from "@oh-my-pi/pi-ai";
3
3
  import { type Component } from "../tui";
4
4
  import { Container } from "../tui";
5
5
  import { Text } from "../components/text";
6
- import { getLanguageFromPath, theme } from "../theme";
6
+ import { getLanguageFromPath, theme, type ThemeColor } from "../theme";
7
7
  import { parseLineRanges, selectorLineRanges } from "../tools/line-ranges";
8
8
  import {
9
9
  type ReadRenderArgs,
@@ -12,8 +12,9 @@ import {
12
12
  readResultIsMarkdown,
13
13
  readSourceFsPath,
14
14
  splitPathAndSel,
15
+ splitReadQuery,
15
16
  } from "../tools/read";
16
- import { PREVIEW_LIMITS, shortenPath } from "../render/render-utils";
17
+ import { FILE_NAME_COLOR, formatStyledPath, PREVIEW_LIMITS, shortenPath } from "../render/render-utils";
17
18
  import { fileHyperlink, renderCodeCell, renderMarkdownCell } from "../render";
18
19
  import { wrapTreeLine } from "../render/hanging-wrap";
19
20
  import { wrapTextWithAnsi } from "../utils";
@@ -21,7 +22,7 @@ import { canonicalizeMessage } from "./thinking-display";
21
22
  import { internalUrlSchemeSpec, splitUrlScheme } from "../tools/url-scheme-host";
22
23
  import type { ToolExecutionHandle } from "./tool-execution";
23
24
  import { formatUsageRow } from "../overlays/usage-row";
24
- import { formatCount, sanitizeText, sanitizeTextDeep } from "@oh-my-pi/pi-utils";
25
+ import { formatCount, isUrlLikePath, sanitizeText, sanitizeTextDeep } from "@oh-my-pi/pi-utils";
25
26
  import type { TspCardStatus, TspSpan, TspText } from "@oh-my-pi/pi-wire";
26
27
  import type { NativeToolHead } from "../tools/renderer";
27
28
  import { card, code, keyed, md, node, span, text, withHidden } from "../native/describe";
@@ -34,7 +35,7 @@ import {
34
35
  } from "../native/node";
35
36
  import { plainText } from "../native/spans";
36
37
  import { Memo } from "../native/memo";
37
- import { fileHref, fileRow, inlineErrorView } from "../tools/native-view";
38
+ import { fileHref, fileRow, inlineErrorView, urlHref } from "../tools/native-view";
38
39
 
39
40
  /**
40
41
  * Extract the read call's target path. `path` is the canonical arg; `file_path`
@@ -158,6 +159,21 @@ const COLLAPSED_PREVIEW_LINES = PREVIEW_LIMITS.OUTPUT_COLLAPSED;
158
159
  */
159
160
  const TRIMMED_BODY_PREVIEW = { lines: 1000 } as const;
160
161
 
162
+ /** How a read path is painted: its path (`isUrl` for a whole URL) and its selectors' color. */
163
+ interface PathPaint {
164
+ path(filePath: string, isUrl: boolean): string;
165
+ selectors: ThemeColor;
166
+ }
167
+
168
+ /** A `Read <path>` header line keeps the accent subject color. */
169
+ const HEADER_PATH_PAINT: PathPaint = { path: filePath => theme.fg("accent", filePath), selectors: "accent" };
170
+
171
+ /** Group rows share the tool-body path style with muted selectors. */
172
+ const ROW_PATH_PAINT: PathPaint = {
173
+ path: (filePath, isUrl) => (isUrl ? theme.fg(FILE_NAME_COLOR, filePath) : formatStyledPath(filePath, theme)),
174
+ selectors: "muted",
175
+ };
176
+
161
177
  /** Native item tone per read status; successful reads stay neutral. */
162
178
  const READ_STATUS_TONE: Record<ReadEntry["status"], "pending" | "warning" | "error" | undefined> = {
163
179
  success: undefined,
@@ -695,7 +711,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
695
711
  return {
696
712
  target: filePath ? `${filePath}${split.sel ? `:${split.sel}` : ""}` : "…",
697
713
  targetKind: "path",
698
- href: fileHref(linkPathForTargets(row.targets)),
714
+ href: fileHref(linkPathForTargets(row.targets)) ?? urlHref(split.path),
699
715
  meta: correctedFrom ? [`corrected from ${shortenPath(correctedFrom)}`] : undefined,
700
716
  badges: conflicts
701
717
  ? [{ text: `${conflicts} conflict${conflicts === 1 ? "" : "s"}`, tone: "warning" }]
@@ -718,7 +734,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
718
734
  : undefined;
719
735
  return fileRow(shortenPath(split.sel ? split.path : row.targetPath), {
720
736
  range: split.sel ? `:${split.sel}` : undefined,
721
- href: fileHref(linkPathForTargets(row.targets)),
737
+ href: fileHref(linkPathForTargets(row.targets)) ?? urlHref(split.path),
722
738
  icon: status === "error" ? "x-circle" : undefined,
723
739
  tone: READ_STATUS_TONE[status],
724
740
  chip: conflicts ? { text: `${conflicts} conflict${conflicts === 1 ? "" : "s"}` } : undefined,
@@ -915,7 +931,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
915
931
  const row = displayRows[0]!;
916
932
  if (!this.#shouldRenderPreviewRow(row)) {
917
933
  const statusSymbol = this.#formatStatus(this.#statusForTargets(row.targets));
918
- const pathDisplay = this.#formatRowPath(row);
934
+ const pathDisplay = this.#formatRowPath(row, HEADER_PATH_PAINT);
919
935
  const lines = [` ${statusSymbol} ${theme.fg("toolTitle", theme.bold("Read"))} ${pathDisplay}`.trimEnd()];
920
936
  const usageRows = this.#usageRowsBySummaryRow(displayRows).get(0) ?? [];
921
937
  this.#appendUsageRows(lines, usageRows, " ");
@@ -1084,12 +1100,13 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
1084
1100
  return `${statusPrefix}${this.#formatRowPath(row)}`;
1085
1101
  }
1086
1102
 
1087
- #formatRowPath(row: ReadSummaryRow): string {
1088
- return this.#formatPathValue(row.targetPath, {
1103
+ #formatRowPath(row: ReadSummaryRow, paint = ROW_PATH_PAINT): string {
1104
+ return this.#formatPathValue(row.targetPath, paint, {
1089
1105
  correctedFrom: this.#correctedFromForTargets(row.targets),
1090
1106
  conflictCount: this.#conflictCountForTargets(row.targets),
1091
1107
  line: firstSelectorLineForTargets(row.targets),
1092
1108
  linkPath: linkPathForTargets(row.targets),
1109
+ basePath: row.basePath,
1093
1110
  });
1094
1111
  }
1095
1112
 
@@ -1137,25 +1154,35 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
1137
1154
 
1138
1155
  #formatPathValue(
1139
1156
  value: string,
1140
- options: { correctedFrom?: string; conflictCount?: number; line?: number; linkPath?: string } = {},
1157
+ paint: PathPaint,
1158
+ options: {
1159
+ correctedFrom?: string;
1160
+ conflictCount?: number;
1161
+ line?: number;
1162
+ linkPath?: string;
1163
+ /** The path `value` selects from; merged selectors (`1-5,…,9`) do not parse back out of it. */
1164
+ basePath?: string;
1165
+ } = {},
1141
1166
  ): string {
1142
- const split = splitPathAndSel(value);
1143
- const selectorSuffix = split.sel ? `:${split.sel}` : "";
1144
- const baseValue = split.sel ? split.path : value;
1145
- const filePath = shortenPath(baseValue);
1146
- let pathDisplay = filePath ? theme.fg("accent", filePath) : theme.fg("toolOutput", "…");
1167
+ const { basePath } = options;
1168
+ const split =
1169
+ basePath !== undefined && value.startsWith(`${basePath}:`)
1170
+ ? { path: basePath, sel: value.slice(basePath.length + 1) }
1171
+ : splitPathAndSel(value);
1172
+ // `?q=…` queries read like selectors: they follow the path, outside its link.
1173
+ const target = splitReadQuery(split.sel ? split.path : value);
1174
+ const filePath = shortenPath(target.path);
1175
+ const selectors = split.sel ? `${target.query ?? ""}:${split.sel}` : (target.query ?? "");
1176
+ let pathDisplay = filePath ? paint.path(filePath, isUrlLikePath(target.path)) : theme.fg("toolOutput", "…");
1147
1177
  if (filePath && options.linkPath) {
1148
1178
  const linkOptions = options.line !== undefined ? { line: options.line } : undefined;
1149
1179
  pathDisplay = fileHyperlink(options.linkPath, pathDisplay, linkOptions);
1150
1180
  }
1151
- if (selectorSuffix) {
1152
- pathDisplay += theme.fg("accent", selectorSuffix);
1153
- }
1181
+ if (selectors) pathDisplay += theme.fg(paint.selectors, selectors);
1154
1182
  if (options.correctedFrom) {
1155
1183
  pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(options.correctedFrom)})`);
1156
1184
  }
1157
- pathDisplay += this.#formatConflictBadge(options.conflictCount);
1158
- return pathDisplay;
1185
+ return pathDisplay + this.#formatConflictBadge(options.conflictCount);
1159
1186
  }
1160
1187
 
1161
1188
  #formatConflictBadge(conflictCount: number | undefined): string {
@@ -1174,7 +1201,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
1174
1201
  const lang = getLanguageFromPath(split.path);
1175
1202
  const pathValue = shortenPath(entry.path);
1176
1203
  const pathDisplay = pathValue
1177
- ? this.#formatPathValue(entry.path, {
1204
+ ? this.#formatPathValue(entry.path, HEADER_PATH_PAINT, {
1178
1205
  correctedFrom: entry.correctedFrom,
1179
1206
  conflictCount: entry.conflictCount,
1180
1207
  line: firstSelectorLine(split.sel),
@@ -15,9 +15,11 @@ import { type CustomMessage, type FileMentionMessage, resolveAbortLabel, shouldR
15
15
  import { createIrcMessageCard } from "../tools/wait";
16
16
  import { formatArtifactErrorNotice, type OutputMeta } from "../tools/output-meta";
17
17
  import type { OutputArtifactError } from "../tools/streaming-output";
18
- import { replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../render/render-utils";
18
+ import { formatStyledPath, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../render/render-utils";
19
19
  import { canonicalizeMessage } from "./thinking-display";
20
20
  import { ToolActivityContainer } from "../chrome/tool-activity";
21
+ import { CustomMessageComponent } from "./custom-message";
22
+ import type { MessageRenderer } from "./extension-types";
21
23
  import { type TranscriptBlock } from "../chrome/transcript-container";
22
24
  import { TranscriptStatusBlock, type TranscriptStatusRow } from "../chrome/transcript-status";
23
25
  import { theme } from "../theme";
@@ -81,6 +83,38 @@ export function buildAsyncResultBlock(message: CustomOrHookMessage): ToolActivit
81
83
  return new ToolActivityContainer(new TranscriptStatusBlock(rows));
82
84
  }
83
85
 
86
+ /** Hosts an extension renderer; relays `display.hideToolActivity` to the nested built-in fallback block. */
87
+ class AsyncResultCard extends CustomMessageComponent {
88
+ readonly #block: ToolActivityContainer;
89
+
90
+ constructor(message: CustomOrHookMessage, renderer: MessageRenderer, block: ToolActivityContainer) {
91
+ super(message as CustomMessage<unknown>, (delegate, options, rendererTheme) => {
92
+ try {
93
+ return renderer(delegate, options, rendererTheme) ?? block;
94
+ } catch {
95
+ return block;
96
+ }
97
+ });
98
+ this.#block = block;
99
+ }
100
+
101
+ setToolActivityVisible(visible: boolean): void {
102
+ this.#block.setToolActivityVisible(visible);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * The `async-result` card for both transcript surfaces: a registered renderer owns it (#14131);
108
+ * the built-in block covers no renderer, a declining renderer, and a throwing one.
109
+ */
110
+ export function buildAsyncResultComponent(
111
+ message: CustomOrHookMessage,
112
+ renderer: MessageRenderer | undefined,
113
+ ): Component & { setExpanded(expanded: boolean): void } {
114
+ const block = buildAsyncResultBlock(message);
115
+ return renderer ? new AsyncResultCard(message, renderer, block) : block;
116
+ }
117
+
84
118
  /** The warning row under a background job whose output artifact could not be saved. */
85
119
  function artifactErrorRow(error: OutputArtifactError): TranscriptStatusRow {
86
120
  const notice = formatArtifactErrorNotice(error);
@@ -203,7 +237,7 @@ export function buildFileMentionBlock(files: FileMentionMessage["files"], indent
203
237
  parts: [
204
238
  { text: theme.tree.last, color: "dim" },
205
239
  { text: "Read", color: "muted" },
206
- { text: file.path, color: "accent" },
240
+ { text: formatStyledPath(file.path, theme) },
207
241
  { text: `(${detail})`, color: "dim" },
208
242
  ],
209
243
  indent,