@oh-my-pi/pi-tui 18.12.3 → 18.12.5
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.
- package/CHANGELOG.md +27 -48
- package/dist/types/render/output-block.d.ts +2 -0
- package/dist/types/render/tool-card.d.ts +2 -0
- package/dist/types/terminal-capabilities.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +9 -8
- package/dist/types/tools/json-tree.d.ts +2 -0
- package/dist/types/tools/line-ranges.d.ts +8 -1
- package/dist/types/tools/mcp.d.ts +23 -0
- package/dist/types/tools/read.d.ts +8 -2
- package/dist/types/tools/renderer.d.ts +8 -2
- package/dist/types/tools/result-card.d.ts +1 -1
- package/package.json +9 -9
- package/src/chat/tool-execution.ts +35 -0
- package/src/chrome/transcript-container.ts +85 -12
- package/src/components/markdown.ts +11 -3
- package/src/native/backend.ts +25 -5
- package/src/overlays/jobs-panel.ts +14 -2
- package/src/render/output-block.ts +25 -12
- package/src/render/tool-card.ts +6 -0
- package/src/terminal-capabilities.ts +39 -2
- package/src/tools/bash.ts +25 -9
- package/src/tools/default-renderer.ts +54 -17
- package/src/tools/eval.ts +32 -14
- package/src/tools/find.ts +4 -5
- package/src/tools/glob.ts +4 -6
- package/src/tools/grep.ts +4 -6
- package/src/tools/json-tree.ts +8 -0
- package/src/tools/line-ranges.ts +23 -13
- package/src/tools/mcp.ts +44 -1
- package/src/tools/read.ts +64 -21
- package/src/tools/renderer.ts +8 -2
- package/src/tools/result-card.ts +1 -2
- package/src/tools/wait.ts +208 -123
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.12.4] - 2026-10-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `mergeLineRanges` and an optional `BareLineMode` argument to `parseLineRanges` (`"single"` reads a lone bare `N` as that one line) ([#16435](https://github.com/can1357/oh-my-pi/pull/16435) by [@will-bogusz](https://github.com/will-bogusz))
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `selectorLineRanges()` reads a bare `N` compounded with `raw` (`17:raw`, `raw:17`) as the single line `N`; `parseLineRanges()` takes an optional `"single"` mode for the same rule ([#16438](https://github.com/can1357/oh-my-pi/pull/16438) by [@will-bogusz](https://github.com/will-bogusz))
|
|
14
|
+
- Collapsed `wait` cards show one row per finished job (or a single "waiting on N jobs" line) instead of re-printing the whole job tree on every poll; Ctrl+O shows the full tree ([#16486](https://github.com/can1357/oh-my-pi/pull/16486) by [@H4vC](https://github.com/H4vC))
|
|
15
|
+
- Bash cards show the elapsed time while a command runs, and wall time, timeout and backgrounded job in the bottom border instead of a separate `⟨…⟩` line ([#16486](https://github.com/can1357/oh-my-pi/pull/16486) by [@H4vC](https://github.com/H4vC))
|
|
16
|
+
- The terminal `/jobs` sheet shows the selected job's full command ([#16486](https://github.com/can1357/oh-my-pi/pull/16486) by [@H4vC](https://github.com/H4vC))
|
|
17
|
+
- Grep, glob, find, IRC message, and `proc://`/`agent://`/`cfg://` result rows start at the same column as other tool rows ([#16486](https://github.com/can1357/oh-my-pi/pull/16486) by [@H4vC](https://github.com/H4vC))
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Fixed Tern sessions logging `TSP: terminal stopped acknowledging frames` every 5 seconds for as long as the terminal stops acknowledging frames; it now warns once per stall and logs the stall's length when acknowledgements resume ([#16393](https://github.com/can1357/oh-my-pi/pull/16393) by [@will-bogusz](https://github.com/will-bogusz))
|
|
22
|
+
- `splitPathAndSel` and `splitInternalUrlSel` peel comma line lists that include a tail chunk (`:1-12,-15`) or repeat the colon after a comma (`:1-30,:-30`, also beside `:raw`); `splitPathAndSel(path, { foldListColons: false })` keeps the repeated colon in the path ([#16435](https://github.com/can1357/oh-my-pi/pull/16435) by [@will-bogusz](https://github.com/will-bogusz))
|
|
23
|
+
- Generic tool cards now reveal the complete result when expanded ([#15335](https://github.com/can1357/oh-my-pi/pull/15335) by [@yuzu-octopus](https://github.com/yuzu-octopus))
|
|
24
|
+
- Added `footerMeta` to `OutputBlockOptions` and `ToolCardSnapshot` to draw short facts inside a frame's bottom border ([#16486](https://github.com/can1357/oh-my-pi/pull/16486) by [@H4vC](https://github.com/H4vC))
|
|
25
|
+
- Fixed markdown table rows shattering at narrow widths when a wide glyph sat in a one-cell column; the glyph is now clamped to its column instead of re-wrapping the row ([#16479](https://github.com/can1357/oh-my-pi/pull/16479) by [@adi-IL](https://github.com/adi-IL)).
|
|
26
|
+
- Fixed charts in answers drawing as blank bands, on screen and in scrollback, when iTerm2 drew them in a background tab; inline images now carry an explicit height in rows ([#16471](https://github.com/can1357/oh-my-pi/pull/16471) by [@galtamam-cmyk](https://github.com/galtamam-cmyk))
|
|
27
|
+
- Fixed an inline image (such as a table's chart) disappearing in iTerm2 when new output pushed it past the top of the screen; it now moves to scrollback whole ([#16467](https://github.com/can1357/oh-my-pi/pull/16467) by [@galtamam-cmyk](https://github.com/galtamam-cmyk))
|
|
28
|
+
- Fixed inline images, hyperlinks, notifications and styled underlines staying off in iTerm2 sessions reached over SSH, where the forwarded `LC_TERMINAL` marker was ignored ([#16450](https://github.com/can1357/oh-my-pi/pull/16450) by [@yuzu-octopus](https://github.com/yuzu-octopus))
|
|
29
|
+
- Added Eval `mcp_progress` status events, updated in place per call and evictable under the status-event cap, plus `formatMCPProgress()` and `MCPToolDetails.progress` for MCP progress display ([#16431](https://github.com/can1357/oh-my-pi/pull/16431) by [@alphastorm](https://github.com/alphastorm))
|
|
30
|
+
|
|
5
31
|
## [18.12.3] - 2026-10-19
|
|
6
32
|
|
|
7
33
|
### Added
|
|
@@ -2447,51 +2473,4 @@
|
|
|
2447
2473
|
- Fixed `@` file mention autocomplete stalling for seconds when the query references something outside the project root (e.g. `@../`, `@~/`, `@/abs/`). `CombinedAutocompleteProvider` now short-circuits to plain immediate-directory prefix listing in those cases instead of dispatching a recursive `fuzzyFind` walk over a sibling directory full of unrelated projects. Inside-cwd queries keep the existing fuzzy-then-prefix behavior. ([#1395](https://github.com/can1357/oh-my-pi/issues/1395))
|
|
2448
2474
|
- Gated the Hangul Compatibility Jamo width correction (U+3131..U+318E → 1 cell, originally landed in 15.0.1 for the IME / hardware-cursor displacement bug) behind `process.platform === "darwin"` in the TS path and `cfg!(target_os = "macos")` in the `pi-natives` Rust path. macOS terminals (Ghostty / Terminal.app / iTerm2) render jamo as 1 cell despite UAX#11 classifying them as Wide, but WezTerm and most Linux terminals honor UAX#11 and render them as 2 cells. The unconditional correction therefore desynced the TUI's column bookkeeping from the terminal's actual rendering off-darwin, producing corrupted layout and broken Korean input on Linux. On non-darwin the helpers now defer entirely to `Bun.stringWidth` / `UnicodeWidthStr` (also a small perf win on the multi-char-grapheme path). ([#1410](https://github.com/can1357/oh-my-pi/issues/1410))
|
|
2449
2475
|
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
### Fixed
|
|
2453
|
-
|
|
2454
|
-
- Fixed terminal scrollback gaining duplicate copies of the welcome screen (and any other header content) when the bottom tool cell mutated across the previous viewport boundary. Once a row scrolls into terminal history it cannot be retracted, so a subsequent shrink that would re-expose that row in the repainted viewport now clears stale scrollback and replays the transcript, then suppresses one immediate suffix-scroll frame so live status/editor chrome is not deposited twice. Multiplexer panes ignore `\x1b[3J`, so the recovery is gated on `!isMultiplexerSession()`.
|
|
2455
|
-
- Fixed the IME / hardware cursor sticking to the bottom of the terminal after a resize that grew the viewport taller than the rendered transcript. `#emitViewportRepaint` always writes one row per screen line (padding empty rows past the content), so the post-write hardware cursor sits at screen row `height - 1`. The bookkeeping previously clamped the tracked cursor row to `lines.length - 1`, making `#cursorControlSequence`'s relative `rowDelta` underestimate the upward move by `(height - lines.length)` rows and pinning the cursor at the viewport bottom even though the focused component's `CURSOR_MARKER` was on a content row.
|
|
2456
|
-
|
|
2457
|
-
## [15.3.2] - 2026-05-25
|
|
2458
|
-
|
|
2459
|
-
### Fixed
|
|
2460
|
-
|
|
2461
|
-
- Fixed `matchesKey(data, "ctrl+m")` (and the other named-key collisions: `ctrl+h`/`ctrl+i`/`ctrl+j`/`ctrl+[`) returning true for the bare `\r`/`\x08`/`\t`/`\n`/`\x1b` byte terminals send for Enter/Backspace/Tab/Escape in legacy mode. Binding a command to `Ctrl+M` no longer fires when the user presses Enter — the named key wins, and `ctrl+<colliding-letter>` matches only when the terminal disambiguates via the Kitty keyboard protocol or `modifyOtherKeys`. ([#1354](https://github.com/can1357/oh-my-pi/issues/1354))
|
|
2462
|
-
|
|
2463
|
-
## [15.2.3] - 2026-05-22
|
|
2464
|
-
|
|
2465
|
-
### Added
|
|
2466
|
-
|
|
2467
|
-
- Added `SettingsList#setItems` to replace the entire settings list with a new items array while automatically clamping selection to a valid index
|
|
2468
|
-
|
|
2469
|
-
### Changed
|
|
2470
|
-
|
|
2471
|
-
- Updated `Loader` to drive renders at ~60fps (16ms tick) while keeping spinner-frame advancement at 80ms so shimmer/animated message colorizers update smoothly without altering spinner cadence
|
|
2472
|
-
|
|
2473
|
-
## [15.1.9] - 2026-05-21
|
|
2474
|
-
|
|
2475
|
-
### Fixed
|
|
2476
|
-
|
|
2477
|
-
- Fixed terminal probe responses (DA1, kitty keyboard, Mode 2031) leaking into the prompt as keystrokes when the response is split across stdin reads. `ProcessTerminal` now reassembles `\x1b[?<digits>...` private CSI fragments and dispatches the complete response through the existing pattern handlers. ([#1238](https://github.com/can1357/oh-my-pi/issues/1238))
|
|
2478
|
-
|
|
2479
|
-
## [15.1.4] - 2026-05-19
|
|
2480
|
-
|
|
2481
|
-
### Fixed
|
|
2482
|
-
|
|
2483
|
-
- Fixed `renderInlineMarkdown` crashing with `TypeError: undefined is not an object (evaluating 'e.replace')` when called with a non-string value during streaming — partial JSON parsing leaves option label fields temporarily unpopulated, causing the ask tool renderer to fail. ([#1176](https://github.com/can1357/oh-my-pi/issues/1176))
|
|
2484
|
-
|
|
2485
|
-
## [15.0.2] - 2026-05-15
|
|
2486
|
-
|
|
2487
|
-
### Added
|
|
2488
|
-
|
|
2489
|
-
- Restored the `Key` runtime helper on `@oh-my-pi/pi-tui` to mirror upstream `@mariozechner/pi-tui`'s surface. `Key.enter`, `Key.escape`, `Key.tab`, … return the canonical key-name strings; modifier methods (`Key.ctrl(k)`, `Key.shift(k)`, `Key.ctrlShift(k)`, etc.) build precisely-typed `KeyId` literals like `"ctrl+c"`. Pure runtime convenience for typed key-id construction — plugins built against the upstream package surface that import `Key` (e.g. `@plannotator/pi-extension`, `@juicesharp/rpiv-ask-user-question`) load again now that the specifier shim remaps them onto this package.
|
|
2490
|
-
|
|
2491
|
-
## [15.0.1] - 2026-05-14
|
|
2492
|
-
|
|
2493
|
-
### Breaking Changes
|
|
2494
|
-
|
|
2495
|
-
- Increased the minimum required Bun version for the TUI package from >=1.3.7 to >=1.3.14
|
|
2496
|
-
|
|
2497
|
-
Older entries are archived in [packages/tui/CHANGELOG.md@04d4ab14b45f](https://github.com/can1357/oh-my-pi/blob/04d4ab14b45f8dbd4055b675519f8815a1eb1841/packages/tui/CHANGELOG.md).
|
|
2476
|
+
Older entries are archived in [packages/tui/CHANGELOG.md@ec051608a87a](https://github.com/can1357/oh-my-pi/blob/ec051608a87ad78d4930a46af7f5582192005ada/packages/tui/CHANGELOG.md).
|
|
@@ -10,6 +10,8 @@ import type { State } from "./types.js";
|
|
|
10
10
|
export interface OutputBlockOptions {
|
|
11
11
|
header?: string;
|
|
12
12
|
headerMeta?: string;
|
|
13
|
+
/** Facts drawn inside the bottom border (`╰── Wall 0.16s · Timeout 300s ──╯`). */
|
|
14
|
+
footerMeta?: string;
|
|
13
15
|
state?: State;
|
|
14
16
|
sections?: Array<{
|
|
15
17
|
label?: string;
|
|
@@ -22,6 +22,8 @@ export interface ToolCardSnapshot {
|
|
|
22
22
|
/** Structured status header, rendered by the shared status-line primitive. */
|
|
23
23
|
status?: StatusLineOptions;
|
|
24
24
|
headerMeta?: string;
|
|
25
|
+
/** Short facts drawn inside the frame's bottom border (framed); a trailing row otherwise. */
|
|
26
|
+
footerMeta?: string;
|
|
25
27
|
phase?: ToolCardPhase;
|
|
26
28
|
body?: ToolCardContent;
|
|
27
29
|
sections?: readonly ToolCardSection[];
|
|
@@ -389,6 +389,11 @@ export declare function encodeITerm2(base64Data: string, options?: {
|
|
|
389
389
|
preserveAspectRatio?: boolean;
|
|
390
390
|
inline?: boolean;
|
|
391
391
|
}): string;
|
|
392
|
+
/**
|
|
393
|
+
* The encoded image `encodeITerm2` wrote into `line`, without the size
|
|
394
|
+
* parameters a rewrap changes; undefined for a line without one.
|
|
395
|
+
*/
|
|
396
|
+
export declare function iterm2ImagePayload(line: string): string | undefined;
|
|
392
397
|
export declare function calculateImageRows(imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions?: CellDimensions): number;
|
|
393
398
|
export declare function getPngDimensions(base64Data: string): ImageDimensions | null;
|
|
394
399
|
export declare function getJpegDimensions(base64Data: string): ImageDimensions | null;
|
|
@@ -117,6 +117,7 @@ export declare function createShellRenderer<TArgs>(config: ShellRendererConfig<T
|
|
|
117
117
|
}, args?: TArgs): NativeToolView;
|
|
118
118
|
mergeCallAndResult: true;
|
|
119
119
|
inline: true;
|
|
120
|
+
tickElapsed: true;
|
|
120
121
|
};
|
|
121
122
|
/** Renders bash command previews and output. */
|
|
122
123
|
export declare const bashToolRenderer: {
|
|
@@ -146,4 +147,5 @@ export declare const bashToolRenderer: {
|
|
|
146
147
|
}, args?: BashRenderArgs | undefined): NativeToolView;
|
|
147
148
|
mergeCallAndResult: true;
|
|
148
149
|
inline: true;
|
|
150
|
+
tickElapsed: true;
|
|
149
151
|
};
|
|
@@ -23,6 +23,9 @@ export declare function formatDefaultToolExecution(input: DefaultToolRenderInput
|
|
|
23
23
|
/**
|
|
24
24
|
* TSP view of the generic fallback card: the tool label with a one-line args
|
|
25
25
|
* summary as the head target; the result as a JSON tree or raw `ansi` output.
|
|
26
|
+
* The complete output goes over the wire — the card-level disclosure clamp the
|
|
27
|
+
* terminal applies is the only bound, so an expanded card keeps every row. JSON
|
|
28
|
+
* the tree's depth, node or scalar caps would cut keeps the raw path instead.
|
|
26
29
|
*/
|
|
27
30
|
export declare function describeDefaultToolExecution(input: DefaultToolRenderInput): NativeToolView;
|
|
28
31
|
/** Render the generic fallback as the state-tinted card used by direct custom tools. */
|
|
@@ -61,7 +61,7 @@ export interface EvalCellResult {
|
|
|
61
61
|
durationMs?: number;
|
|
62
62
|
exitCode?: number;
|
|
63
63
|
statusEvents?: EvalStatusEvent[];
|
|
64
|
-
/**
|
|
64
|
+
/** Evictable events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
|
|
65
65
|
statusEventsElided?: number;
|
|
66
66
|
}
|
|
67
67
|
/** Tool result detail object surfaced to the UI/transcript. */
|
|
@@ -69,7 +69,7 @@ export interface EvalToolDetails {
|
|
|
69
69
|
cells?: EvalCellResult[];
|
|
70
70
|
images?: ImageContent[];
|
|
71
71
|
statusEvents?: EvalStatusEvent[];
|
|
72
|
-
/**
|
|
72
|
+
/** Evictable events dropped from the front of `statusEvents` (see {@link recordStatusEvent}). */
|
|
73
73
|
statusEventsElided?: number;
|
|
74
74
|
isError?: boolean;
|
|
75
75
|
meta?: OutputMeta;
|
|
@@ -104,7 +104,7 @@ interface EvalRenderContext {
|
|
|
104
104
|
timeout?: number;
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
* Coalescing key of a progress-snapshot event: `agent` and `
|
|
107
|
+
* Coalescing key of a progress-snapshot event: `agent`, `judge_batch`, and `mcp_progress`
|
|
108
108
|
* events keyed by `id`, where only the newest snapshot matters. Discrete
|
|
109
109
|
* actions (everything else) have no key. Shared by {@link upsertStatusEvent}
|
|
110
110
|
* and the executors' display collectors so both coalesce identically.
|
|
@@ -114,12 +114,12 @@ export declare function statusEventKey(event: {
|
|
|
114
114
|
[key: string]: unknown;
|
|
115
115
|
}): string | undefined;
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
117
|
+
* Evictable status events a log keeps. A cell polling a helper in a loop emits
|
|
118
118
|
* one event per call (hundreds of thousands), and every live update and the
|
|
119
119
|
* persisted tool result would otherwise carry them all.
|
|
120
120
|
*/
|
|
121
121
|
export declare const MAX_STATUS_EVENTS = 200;
|
|
122
|
-
/** A status event list plus the count of
|
|
122
|
+
/** A status event list plus the count of evictable events dropped from its front. */
|
|
123
123
|
export type StatusEventLog = Pick<EvalCellResult, "statusEvents" | "statusEventsElided">;
|
|
124
124
|
/**
|
|
125
125
|
* Append or replace a status event. Progress snapshots (see
|
|
@@ -129,9 +129,10 @@ export type StatusEventLog = Pick<EvalCellResult, "statusEvents" | "statusEvents
|
|
|
129
129
|
export declare function upsertStatusEvent(events: EvalStatusEvent[], event: EvalStatusEvent): void;
|
|
130
130
|
/**
|
|
131
131
|
* {@link upsertStatusEvent} into a bounded log. Past {@link MAX_STATUS_EVENTS},
|
|
132
|
-
* the oldest discrete event is dropped and counted.
|
|
133
|
-
* cards read them, and so does
|
|
134
|
-
*
|
|
132
|
+
* the oldest discrete event or MCP progress snapshot is dropped and counted.
|
|
133
|
+
* Agent/judgment-batch snapshots stay, since agent cards read them, and so does
|
|
134
|
+
* the newest committed `todo` result, which the session todo panel refreshes
|
|
135
|
+
* from. Renderers already show only the newest
|
|
135
136
|
* events behind an "… N earlier" row.
|
|
136
137
|
*/
|
|
137
138
|
export declare function recordStatusEvent(log: StatusEventLog, event: EvalStatusEvent): void;
|
|
@@ -70,6 +70,8 @@ export interface JsonTreeDescribeOptions {
|
|
|
70
70
|
openDepth?: number;
|
|
71
71
|
/** Root object keys omitted. Defaults to internal tool argument metadata. */
|
|
72
72
|
hiddenRootKeys?: readonly string[];
|
|
73
|
+
/** Called after the tree is built if a depth, node or scalar cap left part of the value out. */
|
|
74
|
+
onTruncated?: () => void;
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
75
77
|
* TSP `tree` of a JSON value: containers become expandable nodes labelled with
|
|
@@ -10,12 +10,19 @@ export interface LineRange {
|
|
|
10
10
|
export declare const LINE_RANGE_CHUNK_SOURCE: string;
|
|
11
11
|
/** Parse a single `N`, `N-M`, `N-`, `N+K`, or `..`-aliased (`N..M`, `N..`) chunk. Throws via {@link ToolError} on invalid bounds. */
|
|
12
12
|
export declare function parseLineRangeChunk(sel: string): LineRange | null;
|
|
13
|
+
/** What a bare `N` selects in a one-chunk list: lines `N` onward (`:50`), or the single line `N`. */
|
|
14
|
+
export type BareLineMode = "onward" | "single";
|
|
13
15
|
/**
|
|
14
16
|
* Parse a comma-separated list of line ranges (e.g. `5-16,960-973` or `19,59`). Returns
|
|
15
17
|
* the ranges in ascending order with overlapping/adjacent ranges merged so
|
|
16
18
|
* downstream consumers can stream the file in a single forward pass per range.
|
|
17
19
|
*/
|
|
18
|
-
export declare function parseLineRanges(sel: string): [LineRange, ...LineRange[]] | null;
|
|
20
|
+
export declare function parseLineRanges(sel: string, bare?: BareLineMode): [LineRange, ...LineRange[]] | null;
|
|
21
|
+
/**
|
|
22
|
+
* Sort non-empty `ranges` by start line and merge the ones that overlap or touch;
|
|
23
|
+
* an open-ended range (`endLine` undefined) reaches EOF and absorbs every later one.
|
|
24
|
+
*/
|
|
25
|
+
export declare function mergeLineRanges(ranges: readonly LineRange[]): [LineRange, ...LineRange[]];
|
|
19
26
|
/**
|
|
20
27
|
* Extract the line-range component from a read-tool selector that may also
|
|
21
28
|
* carry a verbatim/index display mode (`raw`, `conflicts`) — alone or compounded
|
|
@@ -63,6 +63,27 @@ export interface MCPResourceContent {
|
|
|
63
63
|
}
|
|
64
64
|
/** Supported MCP result content blocks retained in display metadata. */
|
|
65
65
|
export type MCPContent = MCPTextContent | MCPImageContent | MCPResourceContent;
|
|
66
|
+
/** Server-reported progress of an in-flight MCP request. */
|
|
67
|
+
export interface MCPProgress {
|
|
68
|
+
progress: number;
|
|
69
|
+
total?: number;
|
|
70
|
+
message?: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Read MCP progress fields from an untrusted record: `progress` must be a
|
|
74
|
+
* finite number; `total` is kept only when finite and `message` only when a
|
|
75
|
+
* string. Shared by the transports, the Eval bridge, and the status renderer.
|
|
76
|
+
*/
|
|
77
|
+
export declare function parseMCPProgress(value: unknown): MCPProgress | undefined;
|
|
78
|
+
/**
|
|
79
|
+
* One-line display text for MCP progress, at most `maxWidth` cells: the
|
|
80
|
+
* message, with `progress/total` when the server reports a total. The bare
|
|
81
|
+
* progress value appears only when there is no message: without a total it is
|
|
82
|
+
* an opaque counter, not a percentage. Home paths in the message show as `~`,
|
|
83
|
+
* and a long message is cut before the count, so the count stays visible.
|
|
84
|
+
* Every progress display (tool card and both Eval status renderers) uses it.
|
|
85
|
+
*/
|
|
86
|
+
export declare function formatMCPProgress(progress: MCPProgress, maxWidth?: number): string;
|
|
66
87
|
/** MCP result details shared by renderers and programmatic tool consumers. */
|
|
67
88
|
export interface MCPToolDetails {
|
|
68
89
|
/** Server name */
|
|
@@ -81,6 +102,8 @@ export interface MCPToolDetails {
|
|
|
81
102
|
provider?: string;
|
|
82
103
|
/** Provider display name (e.g., "Claude Code", "MCP Config") */
|
|
83
104
|
providerName?: string;
|
|
105
|
+
/** Latest progress of a running call; present only on partial updates. */
|
|
106
|
+
progress?: MCPProgress;
|
|
84
107
|
/** Structured output metadata (set by the spill wrapper when output is truncated to an artifact). */
|
|
85
108
|
meta?: OutputMeta;
|
|
86
109
|
}
|
|
@@ -71,8 +71,14 @@ export interface ReadToolDetails {
|
|
|
71
71
|
*/
|
|
72
72
|
displayReadTargetLinks?: Array<string | null>;
|
|
73
73
|
}
|
|
74
|
-
/**
|
|
75
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Split a filesystem path from its trailing read selector. `foldListColons: false`
|
|
76
|
+
* keeps a colon repeated inside a list as part of the path (`a:1-30,:-30` peels
|
|
77
|
+
* only `:-30`), for callers checking whether that shorter split names a real file.
|
|
78
|
+
*/
|
|
79
|
+
export declare function splitPathAndSel(rawPath: string, options?: {
|
|
80
|
+
foldListColons?: boolean;
|
|
81
|
+
}): {
|
|
76
82
|
path: string;
|
|
77
83
|
sel?: string;
|
|
78
84
|
};
|
|
@@ -27,8 +27,9 @@ export interface RenderResultOptions {
|
|
|
27
27
|
executionStarted?: boolean;
|
|
28
28
|
/**
|
|
29
29
|
* Milliseconds since this call began executing (`tool_execution_start`),
|
|
30
|
-
* or its total once settled; undefined before it starts.
|
|
31
|
-
*
|
|
30
|
+
* or its total once settled; undefined before it starts. The terminal
|
|
31
|
+
* transcript's options object computes it on read, so a render-time read
|
|
32
|
+
* is live while the block's spinner ticks.
|
|
32
33
|
*/
|
|
33
34
|
elapsedMs?: number;
|
|
34
35
|
/** The call was abandoned without a final result (turn aborted, sealed). Native describe hooks only. */
|
|
@@ -177,6 +178,11 @@ export interface ToolRenderer<TArgs = unknown, TDetails = unknown> {
|
|
|
177
178
|
* `options.spinnerFrame`.
|
|
178
179
|
*/
|
|
179
180
|
animatedPartialResult?: boolean | ((args: TArgs) => boolean);
|
|
181
|
+
/**
|
|
182
|
+
* Repaint the block once a second while its call executes, for renderers
|
|
183
|
+
* that show live `options.elapsedMs` without consuming the spinner frame.
|
|
184
|
+
*/
|
|
185
|
+
tickElapsed?: boolean;
|
|
180
186
|
/**
|
|
181
187
|
* Whether replacing a pending call render with the first result requires a
|
|
182
188
|
* full viewport repaint. Use for merged renderers whose pending rows can be
|
|
@@ -16,5 +16,5 @@ export interface CardToolResult {
|
|
|
16
16
|
export declare function firstText(result: CardToolResult): string;
|
|
17
17
|
/** Sanitize one display line: tabs become spaces, carriage returns are dropped. */
|
|
18
18
|
export declare function safe(value: string): string;
|
|
19
|
-
/**
|
|
19
|
+
/** Card whose lines are sanitized and truncated, re-rendered when the expanded state flips. */
|
|
20
20
|
export declare function card(lines: (width: number, expanded: boolean) => string[], options: RenderResultOptions): Component;
|
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.12.
|
|
4
|
+
"version": "18.12.5",
|
|
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.12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.12.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.12.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.12.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.12.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.12.
|
|
44
|
+
"@oh-my-pi/omptype": "18.12.5",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.12.5",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.12.5",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.12.5",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.12.5",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.12.5",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.12.5",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.12.5"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -438,6 +438,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
438
438
|
#spinnerActive = false;
|
|
439
439
|
// Todo write completion strikethrough reveal animation
|
|
440
440
|
#todoStrikeInterval?: NodeJS.Timeout;
|
|
441
|
+
// Once-a-second repaint for renderers that show live elapsed time (`tickElapsed`)
|
|
442
|
+
#elapsedTicker?: NodeJS.Timeout;
|
|
441
443
|
// Track if args are still being streamed (for edit/write spinner)
|
|
442
444
|
#argsComplete = false;
|
|
443
445
|
#executionStarted = false;
|
|
@@ -474,6 +476,8 @@ export class ToolExecutionComponent extends Container {
|
|
|
474
476
|
argsComplete?: boolean;
|
|
475
477
|
executionStarted?: boolean;
|
|
476
478
|
renderContext?: Record<string, unknown>;
|
|
479
|
+
/** Live: read at render time, so a ticking spinner advances it. */
|
|
480
|
+
readonly elapsedMs?: number;
|
|
477
481
|
} = {
|
|
478
482
|
expanded: false,
|
|
479
483
|
isPartial: true,
|
|
@@ -491,6 +495,10 @@ export class ToolExecutionComponent extends Container {
|
|
|
491
495
|
toolCallId?: string,
|
|
492
496
|
) {
|
|
493
497
|
super();
|
|
498
|
+
Object.defineProperty(this.#renderState, "elapsedMs", {
|
|
499
|
+
get: () => this.#elapsedMs(this.#nativeStatus()),
|
|
500
|
+
enumerable: true,
|
|
501
|
+
});
|
|
494
502
|
ensureThemeSync();
|
|
495
503
|
this.#toolCallId = toolCallId;
|
|
496
504
|
this.#toolName = toolName;
|
|
@@ -772,6 +780,32 @@ export class ToolExecutionComponent extends Container {
|
|
|
772
780
|
this.#renderState.spinnerFrame = undefined;
|
|
773
781
|
}
|
|
774
782
|
}
|
|
783
|
+
this.#updateElapsedTicker(isBackgroundAsyncRunning);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Repaint once a second while a `tickElapsed` renderer's call executes, so a
|
|
788
|
+
* quiet command's elapsed time advances without the 30fps spinner ticker.
|
|
789
|
+
*/
|
|
790
|
+
#updateElapsedTicker(isBackgroundAsyncRunning: boolean): void {
|
|
791
|
+
const needsTicker =
|
|
792
|
+
this.#renderer?.tickElapsed === true &&
|
|
793
|
+
this.#executionStarted &&
|
|
794
|
+
this.#isPartial &&
|
|
795
|
+
!this.#sealed &&
|
|
796
|
+
!isBackgroundAsyncRunning &&
|
|
797
|
+
!isNativeRendering();
|
|
798
|
+
if (needsTicker && !this.#elapsedTicker) {
|
|
799
|
+
this.#elapsedTicker = setInterval(() => this.#ui.requestComponentRender(this), 1000);
|
|
800
|
+
} else if (!needsTicker) {
|
|
801
|
+
this.#stopElapsedTicker();
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
#stopElapsedTicker(): void {
|
|
806
|
+
if (!this.#elapsedTicker) return;
|
|
807
|
+
clearInterval(this.#elapsedTicker);
|
|
808
|
+
this.#elapsedTicker = undefined;
|
|
775
809
|
}
|
|
776
810
|
|
|
777
811
|
/**
|
|
@@ -917,6 +951,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
917
951
|
this.#renderState.spinnerFrame = undefined;
|
|
918
952
|
}
|
|
919
953
|
this.#stopTodoStrikeAnimation();
|
|
954
|
+
this.#stopElapsedTicker();
|
|
920
955
|
this.#previewReady?.resolve();
|
|
921
956
|
}
|
|
922
957
|
|
|
@@ -7,6 +7,7 @@ import type { NativeNode } from "../native/node";
|
|
|
7
7
|
import { isNativeSettled, settleNative } from "../native/settle";
|
|
8
8
|
import { isUsageRowBlock } from "../overlays/usage-row";
|
|
9
9
|
import { isToolActivityComponent } from "./tool-activity";
|
|
10
|
+
import { directPlacementLift, ImageProtocol, iterm2ImagePayload, TERMINAL } from "../terminal-capabilities";
|
|
10
11
|
|
|
11
12
|
/** Shared animation time supplied by the constrained transcript root. */
|
|
12
13
|
export interface AnimationFrame {
|
|
@@ -118,6 +119,14 @@ interface RowSplit {
|
|
|
118
119
|
readonly width: number;
|
|
119
120
|
readonly hash: number | bigint;
|
|
120
121
|
readonly text: string;
|
|
122
|
+
/**
|
|
123
|
+
* Inline images in the committed rows, in order, each with its payload and
|
|
124
|
+
* the length of `text` before it. Their reserved rows carry no content, so
|
|
125
|
+
* relocating by `text` alone would stop above an image and make the
|
|
126
|
+
* archived image live again at another width; payload and offset tell the
|
|
127
|
+
* archived image apart from a replacement or one moved elsewhere.
|
|
128
|
+
*/
|
|
129
|
+
readonly images: readonly { readonly payload: string; readonly offset: number }[];
|
|
121
130
|
/** Seam of the last render it was located in; renders are memoized per frame. */
|
|
122
131
|
located?: { readonly rendered: readonly string[]; readonly count: number };
|
|
123
132
|
}
|
|
@@ -180,7 +189,33 @@ function contentOf(rows: readonly string[]): string {
|
|
|
180
189
|
*/
|
|
181
190
|
function rowSplit(scrollback: readonly string[], rows: number, width: number): RowSplit {
|
|
182
191
|
const committed = scrollback.slice(0, rows);
|
|
183
|
-
|
|
192
|
+
const images: { payload: string; offset: number }[] = [];
|
|
193
|
+
let text = "";
|
|
194
|
+
for (const row of committed) {
|
|
195
|
+
if (TERMINAL.isImageLine(row)) images.push({ payload: iterm2ImagePayload(row) ?? row, offset: text.length });
|
|
196
|
+
text += contentOf([row]);
|
|
197
|
+
}
|
|
198
|
+
return { rows, width, hash: Bun.hash(committed.join("\n")), text, images };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* `cut` moved to just below an image it would split, on terminals that cannot
|
|
203
|
+
* draw part of an image (iTerm2's inline images). There the image row draws
|
|
204
|
+
* the whole picture upward over its reserved rows; once some of those rows are
|
|
205
|
+
* in scrollback, the picture has nowhere to go and is dropped, so the image
|
|
206
|
+
* must reach scrollback whole. Kitty and SIXEL draw the visible part instead.
|
|
207
|
+
*/
|
|
208
|
+
function cutBelowWholeImage(rows: readonly string[], cut: number): number {
|
|
209
|
+
const protocol = TERMINAL.imageProtocol;
|
|
210
|
+
if (protocol === null || protocol === ImageProtocol.Kitty || protocol === ImageProtocol.Sixel) return cut;
|
|
211
|
+
// Only reserved rows sit between a cut inside an image and its image row,
|
|
212
|
+
// so the first image row at or below the cut decides.
|
|
213
|
+
for (let index = Math.max(0, cut); index < rows.length; index++) {
|
|
214
|
+
const row = rows[index]!;
|
|
215
|
+
if (!TERMINAL.isImageLine(row)) continue;
|
|
216
|
+
return index - directPlacementLift(row) < cut ? index + 1 : cut;
|
|
217
|
+
}
|
|
218
|
+
return cut;
|
|
184
219
|
}
|
|
185
220
|
|
|
186
221
|
/** Whether `prefix` matches `rows` byte-for-byte from the top. */
|
|
@@ -819,13 +854,19 @@ export class TranscriptContainer extends Container {
|
|
|
819
854
|
// screen; the rest stays live, so a later shrink of the chrome or the
|
|
820
855
|
// tail refills the screen from live rows instead of opening a band no
|
|
821
856
|
// frame can fill (rows still on screen cannot return from scrollback).
|
|
857
|
+
// One exception: on a terminal that cannot draw part of an image, a cut
|
|
858
|
+
// inside an image moves below it (cutBelowWholeImage), so up to that
|
|
859
|
+
// image's height of on-screen rows commits early. A later shrink can then
|
|
860
|
+
// open a band that tall, which beats losing the image.
|
|
822
861
|
let split: { index: number; rows: readonly string[]; split: RowSplit } | undefined;
|
|
823
862
|
if (policy === "pressure" && keep > 0 && limit === this.#frontier + keep) {
|
|
824
863
|
const boundary = live[keep - 1]!;
|
|
825
864
|
const visible = room - tailRows - (tailRows > 0 ? 1 : 0);
|
|
826
865
|
if (boundary.entry.state === "settled" && visible > 0) {
|
|
827
866
|
const offer = this.#splitOffer(boundary.entry, width, visible);
|
|
828
|
-
if (offer
|
|
867
|
+
if (offer === "whole") {
|
|
868
|
+
limit = boundary.index + 1;
|
|
869
|
+
} else if (offer !== undefined) {
|
|
829
870
|
split = { index: boundary.index, ...offer };
|
|
830
871
|
limit = boundary.index;
|
|
831
872
|
}
|
|
@@ -868,18 +909,23 @@ export class TranscriptContainer extends Container {
|
|
|
868
909
|
* The head rows of settled `entry` beyond its committed prefix that leave
|
|
869
910
|
* `visible` rows live, or undefined when nothing would remain committed
|
|
870
911
|
* past that prefix or the scrollback render would not line up row for row.
|
|
912
|
+
* `"whole"` when keeping an image whole moves the cut to the block's end:
|
|
913
|
+
* the block then retires in full, with the blank row that separates it from
|
|
914
|
+
* the next block, which a split never carries.
|
|
871
915
|
*/
|
|
872
916
|
#splitOffer(
|
|
873
917
|
entry: TranscriptEntry,
|
|
874
918
|
width: number,
|
|
875
919
|
visible: number,
|
|
876
|
-
): { rows: readonly string[]; split: RowSplit } | undefined {
|
|
920
|
+
): { rows: readonly string[]; split: RowSplit } | "whole" | undefined {
|
|
877
921
|
const measured = this.#measuredRows(entry, width);
|
|
878
922
|
const start = this.#emittedRowCount(entry, width);
|
|
879
|
-
const
|
|
880
|
-
if (
|
|
923
|
+
const cut = measured.length - visible;
|
|
924
|
+
if (cut <= start) return undefined;
|
|
881
925
|
const scrollback = renderForScrollback(() => this.#renderEntry(entry, width));
|
|
882
926
|
if (scrollback.length !== measured.length) return undefined;
|
|
927
|
+
const total = cutBelowWholeImage(scrollback, cut);
|
|
928
|
+
if (total >= scrollback.length) return "whole";
|
|
883
929
|
return { rows: scrollback.slice(start, total), split: rowSplit(scrollback, total, width) };
|
|
884
930
|
}
|
|
885
931
|
|
|
@@ -1096,13 +1142,40 @@ export class TranscriptContainer extends Container {
|
|
|
1096
1142
|
count = split.rows;
|
|
1097
1143
|
} else {
|
|
1098
1144
|
let consumed = 0;
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
if (
|
|
1104
|
-
|
|
1105
|
-
|
|
1145
|
+
let image = 0;
|
|
1146
|
+
while (count < rendered.length && (consumed < split.text.length || image < split.images.length)) {
|
|
1147
|
+
const row = rendered[count]!;
|
|
1148
|
+
const content = contentOf([row]);
|
|
1149
|
+
if (content.length > 0) {
|
|
1150
|
+
if (!split.text.startsWith(content, consumed)) break;
|
|
1151
|
+
consumed += content.length;
|
|
1152
|
+
count++;
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
// An empty-content row matches at any offset, so the walk cannot tell it
|
|
1156
|
+
// apart from a revealed row history never received; only the next
|
|
1157
|
+
// archived image carries the seam on, through its own reserved rows to
|
|
1158
|
+
// its row, whatever its height here.
|
|
1159
|
+
let index = count;
|
|
1160
|
+
while (
|
|
1161
|
+
index < rendered.length &&
|
|
1162
|
+
!TERMINAL.isImageLine(rendered[index]!) &&
|
|
1163
|
+
contentOf([rendered[index]!]).length === 0
|
|
1164
|
+
) {
|
|
1165
|
+
index++;
|
|
1166
|
+
}
|
|
1167
|
+
const imageRow = rendered[index];
|
|
1168
|
+
if (
|
|
1169
|
+
imageRow === undefined ||
|
|
1170
|
+
!TERMINAL.isImageLine(imageRow) ||
|
|
1171
|
+
(iterm2ImagePayload(imageRow) ?? imageRow) !== split.images[image]?.payload ||
|
|
1172
|
+
consumed !== split.images[image]?.offset ||
|
|
1173
|
+
index - count !== directPlacementLift(imageRow)
|
|
1174
|
+
) {
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
count = index + 1;
|
|
1178
|
+
image++;
|
|
1106
1179
|
}
|
|
1107
1180
|
}
|
|
1108
1181
|
split.located = { rendered, count };
|
|
@@ -4420,7 +4420,7 @@ export class Markdown implements Component {
|
|
|
4420
4420
|
* Delegates to wrapTextWithAnsi() so ANSI codes + long tokens are handled
|
|
4421
4421
|
* consistently with the rest of the renderer.
|
|
4422
4422
|
*/
|
|
4423
|
-
#wrapCellText(text: string, maxWidth: number): string[] {
|
|
4423
|
+
#wrapCellText(text: string, maxWidth: number, clampOversized = true): string[] {
|
|
4424
4424
|
const cellWidth = Math.max(1, maxWidth);
|
|
4425
4425
|
// Wrap the whole cell in one call so wrapTextWithAnsi() balances OSC 8
|
|
4426
4426
|
// hyperlink state across explicit newlines (e.g. `<br>` rendered as \n);
|
|
@@ -4435,7 +4435,14 @@ export class Markdown implements Component {
|
|
|
4435
4435
|
// (e.g. mdCode) would bleed into the "│" and the following cells.
|
|
4436
4436
|
// Terminate each line at default fg, clearing bold/italic but keeping
|
|
4437
4437
|
// any ambient background (message-bg rendering) intact.
|
|
4438
|
-
|
|
4438
|
+
// A wide glyph can exceed a one-cell column without wrapping (a 2-cell
|
|
4439
|
+
// emoji in a column squeezed to 1), so clamp here before the row is
|
|
4440
|
+
// spliced between borders; otherwise the overwide row is re-wrapped and
|
|
4441
|
+
// the trailing border lands on its own line.
|
|
4442
|
+
return wrapped.map(line => {
|
|
4443
|
+
const fitted = clampOversized && visibleWidth(line) > cellWidth ? truncateToWidth(line, cellWidth) : line;
|
|
4444
|
+
return `${fitted}\x1b[22m\x1b[23m\x1b[39m`;
|
|
4445
|
+
});
|
|
4439
4446
|
}
|
|
4440
4447
|
|
|
4441
4448
|
/**
|
|
@@ -4626,7 +4633,8 @@ export class Markdown implements Component {
|
|
|
4626
4633
|
if (rowCellLines[openCell].length > settledLineCount) {
|
|
4627
4634
|
const width = columnWidths[openCell];
|
|
4628
4635
|
const kept = rowCellLines[openCell].slice(0, settledLineCount);
|
|
4629
|
-
const last =
|
|
4636
|
+
const last =
|
|
4637
|
+
width > 1 ? (this.#wrapCellText(kept[settledLineCount - 1], width - 1, false)[0] ?? "") : "";
|
|
4630
4638
|
kept[settledLineCount - 1] = `${visibleWidth(last) < width ? last : ""}…`;
|
|
4631
4639
|
rowCellLines[openCell] = kept;
|
|
4632
4640
|
}
|