@oh-my-pi/pi-tui 18.8.6 → 18.8.7

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 (69) hide show
  1. package/CHANGELOG.md +18 -16
  2. package/README.md +7 -5
  3. package/dist/types/chat/assistant-message.d.ts +1 -0
  4. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  5. package/dist/types/chat/user-message.d.ts +2 -0
  6. package/dist/types/components/box.d.ts +1 -0
  7. package/dist/types/components/disclosure.d.ts +1 -0
  8. package/dist/types/components/image.d.ts +1 -0
  9. package/dist/types/components/key-value-list.d.ts +1 -0
  10. package/dist/types/components/layout/row.d.ts +1 -0
  11. package/dist/types/components/layout/stack.d.ts +1 -0
  12. package/dist/types/components/markdown.d.ts +1 -0
  13. package/dist/types/components/scroll-view.d.ts +1 -0
  14. package/dist/types/components/section.d.ts +1 -0
  15. package/dist/types/components/table.d.ts +1 -0
  16. package/dist/types/components/text.d.ts +1 -0
  17. package/dist/types/components/truncated-text.d.ts +1 -0
  18. package/dist/types/native/blobs.d.ts +3 -1
  19. package/dist/types/overlays/session-selector.d.ts +1 -1
  20. package/dist/types/render/output-pane.d.ts +1 -0
  21. package/dist/types/render/render-utils.d.ts +1 -0
  22. package/dist/types/render/tool-card.d.ts +6 -0
  23. package/dist/types/render/width-aware-text.d.ts +2 -0
  24. package/dist/types/terminal.d.ts +10 -2
  25. package/dist/types/tools/write.d.ts +1 -1
  26. package/dist/types/tools/yield.d.ts +22 -0
  27. package/dist/types/tui.d.ts +11 -0
  28. package/package.json +9 -9
  29. package/src/chat/advisor-message.ts +11 -0
  30. package/src/chat/assistant-message.ts +6 -0
  31. package/src/chat/compaction-summary-message.ts +8 -0
  32. package/src/chat/read-tool-group.ts +6 -4
  33. package/src/chat/skill-message.ts +6 -0
  34. package/src/chat/table-chart.ts +17 -20
  35. package/src/chat/tool-execution.ts +18 -0
  36. package/src/chat/user-message.ts +14 -0
  37. package/src/chrome/transcript-container.ts +32 -1
  38. package/src/components/box.ts +5 -0
  39. package/src/components/disclosure.ts +8 -0
  40. package/src/components/image.ts +7 -5
  41. package/src/components/key-value-list.ts +4 -0
  42. package/src/components/layout/row.ts +7 -0
  43. package/src/components/layout/stack.ts +7 -0
  44. package/src/components/markdown.ts +8 -0
  45. package/src/components/scroll-view.ts +5 -0
  46. package/src/components/section.ts +5 -0
  47. package/src/components/table.ts +4 -0
  48. package/src/components/text.ts +7 -0
  49. package/src/components/truncated-text.ts +5 -0
  50. package/src/native/backend.ts +16 -15
  51. package/src/native/blobs.ts +43 -10
  52. package/src/native/reconcile.ts +4 -0
  53. package/src/overlays/session-selector.ts +29 -19
  54. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  55. package/src/prompt/attachment-chips.ts +13 -10
  56. package/src/render/output-pane.ts +6 -0
  57. package/src/render/render-utils.ts +11 -0
  58. package/src/render/tool-card.ts +15 -0
  59. package/src/render/width-aware-text.ts +5 -0
  60. package/src/terminal.ts +24 -3
  61. package/src/tools/bash.ts +10 -11
  62. package/src/tools/edit.ts +142 -120
  63. package/src/tools/fetch.ts +5 -6
  64. package/src/tools/index.ts +2 -0
  65. package/src/tools/mcp.ts +15 -2
  66. package/src/tools/read.ts +7 -5
  67. package/src/tools/write.ts +70 -56
  68. package/src/tools/yield.ts +225 -0
  69. package/src/tui.ts +25 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.8.7] - 2026-10-09
6
+
7
+ ### Added
8
+
9
+ - Added cache-release hooks for TUI components and tool cards, allowing extensions to discard derived render data without rebuilding content ([#13632](https://github.com/can1357/oh-my-pi/pull/13632) by [@iliaal](https://github.com/iliaal)).
10
+
11
+ ### Changed
12
+
13
+ - Subagent "Submit Result" cards now show the submitted result (its fields as a tree, or the report text), the section it fills, and why a submission was rejected, instead of only "Result submitted.".
14
+ - Reduced memory held by retired transcript history in long sessions ([#13632](https://github.com/can1357/oh-my-pi/pull/13632) by [@iliaal](https://github.com/iliaal)).
15
+
16
+ ### Fixed
17
+
18
+ - Reduced memory retained by discarded TSP images and previews ([#14336](https://github.com/can1357/oh-my-pi/pull/14336) by [@iliaal](https://github.com/iliaal)).
19
+ - Fixed startup capability probes printing as text in the prompt (e.g. `25a1;stsp;q;{…}pppppp`) on terminals that cannot parse them, such as macOS Terminal.app.
20
+ - Fixed the `/resume` picker flashing while a search runs over a large session history: background fuzzy matches now land in one update instead of reordering the list dozens of times per keystroke.
21
+
5
22
  ## [18.8.6] - 2026-10-08
6
23
 
7
24
  ### Changed
@@ -3023,19 +3040,4 @@ Initial release under @oh-my-pi scope. See previous releases at [badlogic/pi-mon
3023
3040
  - Cursor now moves to end of content on exit, preventing status line from being overwritten ([#629](https://github.com/badlogic/pi-mono/pull/629) by [@tallshort](https://github.com/tallshort))
3024
3041
  - Reset ANSI styles after each rendered line to prevent style leakage
3025
3042
 
3026
- ## [0.42.5] - 2026-01-11
3027
-
3028
- ### Fixed
3029
-
3030
- - Reduced flicker by only re-rendering changed lines ([#617](https://github.com/badlogic/pi-mono/pull/617) by [@ogulcancelik](https://github.com/ogulcancelik))
3031
- - Cursor position tracking when content shrinks with unchanged remaining lines
3032
- - TUI renders with wrong dimensions after suspend/resume if terminal was resized while suspended ([#599](https://github.com/badlogic/pi-mono/issues/599))
3033
- - Pasted content containing Kitty key release patterns (e.g., `:3F` in MAC addresses) was incorrectly filtered out ([#623](https://github.com/badlogic/pi-mono/pull/623) by [@ogulcancelik](https://github.com/ogulcancelik))
3034
-
3035
- ## [0.39.0] - 2026-01-08
3036
-
3037
- ### Added
3038
-
3039
- - **Experimental:** Overlay compositing for `ctx.ui.custom()` with `{ overlay: true }` option ([#558](https://github.com/badlogic/pi-mono/pull/558) by [@nicobailon](https://github.com/nicobailon))
3040
-
3041
- Older entries are archived in [packages/tui/CHANGELOG.md@fa14205f838f](https://github.com/can1357/oh-my-pi/blob/fa14205f838f282fcea048c64fca74026789a492/packages/tui/CHANGELOG.md).
3043
+ Older entries are archived in [packages/tui/CHANGELOG.md@4787659281ba](https://github.com/can1357/oh-my-pi/blob/4787659281ba47f5a3707bbf5ee61742e9373c42/packages/tui/CHANGELOG.md).
package/README.md CHANGED
@@ -66,14 +66,16 @@ interface Component {
66
66
  render(width: number): readonly string[];
67
67
  handleInput?(data: string): void;
68
68
  invalidate?(): void;
69
+ releaseRenderCaches?(): void;
69
70
  }
70
71
  ```
71
72
 
72
- | Method | Description |
73
- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
74
- | `render(width)` | Returns an array of strings, one per line. Each line **must not exceed `width`** or the TUI will error. Use `truncateToWidth()` or manual wrapping to ensure this. The result is component-owned and immutable to callers; return the same array reference when unchanged (enables renderer memoization) and a new array when content changed. |
75
- | `handleInput?(data)` | Called when the component has focus and receives keyboard input. The `data` string contains raw terminal input (may include ANSI escape sequences). |
76
- | `invalidate?()` | Called to clear any cached render state. Components should re-render from scratch on the next `render()` call. |
73
+ | Method | Description |
74
+ | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
75
+ | `render(width)` | Returns an array of strings, one per line. Each line **must not exceed `width`** or the TUI will error. Use `truncateToWidth()` or manual wrapping to ensure this. The result is component-owned and immutable to callers; return the same array reference when unchanged (enables renderer memoization) and a new array when content changed. |
76
+ | `handleInput?(data)` | Called when the component has focus and receives keyboard input. The `data` string contains raw terminal input (may include ANSI escape sequences). |
77
+ | `invalidate?()` | Called to clear any cached render state. Components should re-render from scratch on the next `render()` call. |
78
+ | `releaseRenderCaches?()` | Drops only derived render caches. The next `render()` must return the same rows without eager rebuilds, renderer callbacks, image conversions, or child replacement. |
77
79
 
78
80
  ## Built-in Components
79
81
 
@@ -54,6 +54,7 @@ export declare class AssistantMessageComponent extends Container {
54
54
  */
55
55
  setServedModelMismatch(info: ServedModelMismatch | undefined): void;
56
56
  invalidate(): void;
57
+ releaseRenderCaches(): void;
57
58
  setHideThinkingBlock(hide: boolean): void;
58
59
  /**
59
60
  * Allow or withhold retiring finished lines into native scrollback while the
@@ -31,6 +31,7 @@ declare class SummaryMessageComponent implements Component {
31
31
  handleNativeEvent(event: NativeUiEvent): void;
32
32
  setIgnoreTight(ignore: boolean): this;
33
33
  invalidate(): void;
34
+ releaseRenderCaches(): void;
34
35
  dispose(): void;
35
36
  render(width: number): readonly string[];
36
37
  }
@@ -33,6 +33,7 @@ export declare function userBubbleColor(options?: UserBubbleOptions, tokenRegex?
33
33
  export declare class UserMessageComponent extends Container implements ReactionTarget {
34
34
  #private;
35
35
  constructor(text: string, options?: UserBubbleOptions);
36
+ releaseRenderCaches(): void;
36
37
  setReaction(emoji: string): void;
37
38
  /**
38
39
  * A head-less user-toned frame with the prompt as markdown under its
@@ -75,6 +76,7 @@ export declare class CollapsedSyntheticMessageComponent implements Component {
75
76
  handleNativeEvent(event: NativeUiEvent): void;
76
77
  setIgnoreTight(ignore: boolean): this;
77
78
  invalidate(): void;
79
+ releaseRenderCaches(): void;
78
80
  dispose(): void;
79
81
  render(width: number): readonly string[];
80
82
  }
@@ -30,6 +30,7 @@ export declare class Box implements Component {
30
30
  setBgFn(bgFn?: (text: string) => string): void;
31
31
  setBorder(border?: BoxBorder): void;
32
32
  invalidate(): void;
33
+ releaseRenderCaches(): void;
33
34
  /**
34
35
  * A `card` when the box has a border or background: the background token
35
36
  * picks tone/role (user message, tool state, selection), a border colour
@@ -36,6 +36,7 @@ export declare class Disclosure implements Component {
36
36
  setExpanded(expanded: boolean): void;
37
37
  setIgnoreTight(ignore: boolean): this;
38
38
  invalidate(): void;
39
+ releaseRenderCaches(): void;
39
40
  dispose(): void;
40
41
  debugState(): Record<string, unknown>;
41
42
  get debugChildren(): readonly Component[];
@@ -227,6 +227,7 @@ export declare class Image implements Component {
227
227
  /** Return source metadata without exposing the encoded image buffer. */
228
228
  debugState(): Record<string, unknown>;
229
229
  invalidate(): void;
230
+ releaseRenderCaches(): void;
230
231
  /**
231
232
  * A native `image` backed by a content-addressed blob; the terminal fits
232
233
  * it. Cell caps become `ch`/`lines` bounds. The inline-image budget and
@@ -21,6 +21,7 @@ export declare class KeyValueList implements Component {
21
21
  constructor(rows: readonly KeyValueRow[], options: KeyValueListOptions);
22
22
  setRows(rows: readonly KeyValueRow[]): boolean;
23
23
  invalidate(): void;
24
+ releaseRenderCaches(): void;
24
25
  /** An aligned `kv` grid; label width and value wrapping are the terminal's. */
25
26
  describe(_cx: DescribeContext): NativeNode;
26
27
  render(width: number): readonly string[];
@@ -48,6 +48,7 @@ export declare class Row implements Component, MouseRoutable {
48
48
  setDecorations(prefix: LayoutDecoration | undefined, gap: LayoutDecoration | undefined, suffix: LayoutDecoration | undefined): void;
49
49
  setIgnoreTight(ignore: boolean): this;
50
50
  invalidate(): void;
51
+ releaseRenderCaches(): void;
51
52
  dispose(): void;
52
53
  /**
53
54
  * A flex `row`: each child sits in a slot carrying its width bounds
@@ -43,6 +43,7 @@ export declare class Stack implements Component, MouseRoutable {
43
43
  setHeight(height: number | undefined): void;
44
44
  setIgnoreTight(ignore: boolean): this;
45
45
  invalidate(): void;
46
+ releaseRenderCaches(): void;
46
47
  dispose(): void;
47
48
  /**
48
49
  * A `col` of slots carrying each child's height bounds (fixed → min = max
@@ -124,6 +124,7 @@ export declare class Markdown implements Component {
124
124
  debugState(): Record<string, unknown>;
125
125
  setText(text: string): boolean;
126
126
  invalidate(): void;
127
+ releaseRenderCaches(): void;
127
128
  /**
128
129
  * The unrendered source as an `md` node, `stream` while the streaming
129
130
  * (transient) cache is on. Append-only growth yields a fresh node of the
@@ -92,6 +92,7 @@ export declare class ScrollView implements Component {
92
92
  /** Apply Arrow, Shift+Arrow, Page, Home, and End navigation keys. */
93
93
  handleScrollKey(data: string): boolean;
94
94
  invalidate(): void;
95
+ releaseRenderCaches(): void;
95
96
  /** Permanently release a wrapped child's timers, subscriptions, and other resources. */
96
97
  dispose(): void;
97
98
  /**
@@ -15,6 +15,7 @@ export declare class Section implements Component {
15
15
  #private;
16
16
  constructor(options: SectionOptions);
17
17
  invalidate(): void;
18
+ releaseRenderCaches(): void;
18
19
  dispose(): void;
19
20
  /** A `section` headed by the styled title; the rule is the terminal's. */
20
21
  describe(_cx: DescribeContext): NativeNode;
@@ -29,6 +29,7 @@ export declare class Table implements Component {
29
29
  constructor(rows: readonly (readonly TableCell[])[], columns: readonly TableColumn[], options?: TableOptions);
30
30
  setRows(rows: readonly (readonly TableCell[])[]): boolean;
31
31
  invalidate(): void;
32
+ releaseRenderCaches(): void;
32
33
  /**
33
34
  * A native `table`. Column priorities carry over unchanged (lower gives
34
35
  * way first); truncating columns truncate at the end, the rest keep
@@ -26,6 +26,7 @@ export declare class Text implements Component {
26
26
  */
27
27
  setStyleFn(styleFn?: (text: string) => string): this;
28
28
  invalidate(): void;
29
+ releaseRenderCaches(): void;
29
30
  /**
30
31
  * Wrapped text; styling re-derived from the styled source so a theme swap
31
32
  * re-resolves tokens. A background fill becomes the node's tone.
@@ -9,6 +9,7 @@ export declare class TruncatedText implements Component {
9
9
  /** Return bounded source text and the last-known truncation state. */
10
10
  debugState(): Record<string, unknown>;
11
11
  invalidate(): void;
12
+ releaseRenderCaches(): void;
12
13
  /** First line only, clamped to one visual line with an end ellipsis. */
13
14
  describe(_cx: DescribeContext): NativeNode;
14
15
  render(width: number): readonly string[];
@@ -14,10 +14,12 @@ export interface NativeBlob {
14
14
  readonly mime: string;
15
15
  readonly bytes: Uint8Array;
16
16
  }
17
- /** Register `bytes` and return their content address (sha256 hex) for `image.p.blob`. */
17
+ /** Register bytes for process-lifetime, id-only use. Prefer {@link nativeImageNode} for owned images. */
18
18
  export declare function registerNativeBlob(bytes: Uint8Array, mime: string): string;
19
19
  /** A registered blob by id. */
20
20
  export declare function getNativeBlob(id: string): NativeBlob | undefined;
21
+ /** An image whose props own its bytes, including while hidden, unmounted, or awaiting upload. */
22
+ export declare function nativeImageNode(bytes: Uint8Array, mime: string, p?: Omit<TspProps<"image">, "blob" | "builtin">, key?: string): NativeNode;
21
23
  /**
22
24
  * An `image` node for a base64 payload: the bytes are registered as a blob and
23
25
  * the pixel size probed from the header. Decoding and hashing cost real time,
@@ -71,7 +71,7 @@ declare class SessionList<T extends SessionSelectorEntry> implements Component {
71
71
  onExit: () => void;
72
72
  onToggleScope?: () => void;
73
73
  onDeleteRequest?: (session: T) => void;
74
- /** Re-render hook for async list updates (fuzzy scan chunks, history merge). */
74
+ /** Re-render hook for async list updates (fuzzy scan completion, history merge). */
75
75
  onRequestRender?: () => void;
76
76
  constructor(sessions: T[], showCwd?: boolean, historyMatcher?: SessionHistoryMatcher, getTerminalRows?: () => number, pinnedIds?: ReadonlySet<string>, currentSessionPath?: string | (() => string | undefined));
77
77
  /** Replace the visible dataset, e.g. when toggling folder/all-projects scope. */
@@ -92,6 +92,7 @@ export declare class OutputPane implements Component {
92
92
  describe(_cx: DescribeContext): NativeNode;
93
93
  render(width: number): readonly string[];
94
94
  invalidate(): void;
95
+ releaseRenderCaches(): void;
95
96
  }
96
97
  /** Default styling for plain tool output while preserving existing ANSI. */
97
98
  export declare function styleToolOutputLine(line: string, theme: Theme): string;
@@ -298,6 +298,7 @@ export interface RenderedStringCache {
298
298
  export declare function createRenderedStringCache(): RenderedStringCache;
299
299
  /** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
300
300
  export declare function invalidateRenderedStringCache(cache: RenderedStringCache): void;
301
+ export declare function releaseRenderedStringCache(cache: RenderedStringCache): void;
301
302
  /** Reuse a rendered string while its theme, expansion, content, and salt match. */
302
303
  export declare function cachedRenderedString(cache: RenderedStringCache | undefined, theme: Theme, expanded: boolean, salt: string, content: string, render: () => string): string;
303
304
  /**
@@ -46,6 +46,11 @@ export interface ToolCardOptions {
46
46
  paddingY?: number;
47
47
  ignoreTight?: boolean;
48
48
  onInvalidate?: () => void;
49
+ /**
50
+ * Drop builder-owned memos the next build recomputes. Called by
51
+ * {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
52
+ */
53
+ onReleaseRenderCaches?: () => void;
49
54
  onDispose?: () => void;
50
55
  }
51
56
  /** Map transcript lifecycle terminology onto the output frame's visual states. */
@@ -71,6 +76,7 @@ export declare class ToolCard implements Component {
71
76
  handleInput(data: string): void;
72
77
  setIgnoreTight(ignore: boolean): void;
73
78
  invalidate(): void;
79
+ releaseRenderCaches(): void;
74
80
  dispose(): void;
75
81
  }
76
82
  /** Construct a lazy framed tool card. */
@@ -20,6 +20,8 @@ export declare class WidthAwareText implements Component {
20
20
  setCustomBgFn(customBgFn?: (text: string) => string): void;
21
21
  setIgnoreTight(ignore: boolean): this;
22
22
  invalidate(): void;
23
+ /** The formatted text stays: the inner `Text` renders from it, so dropping it would free nothing. */
24
+ releaseRenderCaches(): void;
23
25
  /**
24
26
  * Re-run the formatter on the next render because its inputs changed.
25
27
  * Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
@@ -128,13 +128,21 @@ export type TspHelloHandler = (hello: TspHello | null) => void;
128
128
  export type CursorShape = "default" | "block" | "underline" | "bar";
129
129
  export declare const CURSOR_SHAPE_CODES: Record<CursorShape, number>;
130
130
  export interface Terminal {
131
+ /**
132
+ * Start the terminal with input, resize, and host-disconnect handlers.
133
+ * Unless input is deferred, this sends capability probes; a terminal that
134
+ * cannot parse one may leave concealed bytes on the cursor row, so the
135
+ * caller repaints after starting.
136
+ */
131
137
  start(onInput: (data: string) => void, onResize: () => void, onDisconnect?: () => void, options?: TerminalStartOptions): void;
132
138
  /**
133
139
  * Take ownership of stdin after a `deferInput` start: enable raw mode,
134
140
  * attach input handlers, and run the capability probes start() skipped.
135
141
  * Bytes the user typed in cooked mode meanwhile are replayed through
136
- * `onInput`. No-op when input was never deferred. Optional so custom
137
- * Terminals built against older pi-tui versions keep working.
142
+ * `onInput`. No-op when input was never deferred. Like {@link start}, the
143
+ * probes may leave concealed bytes on the cursor row, so the caller
144
+ * repaints afterwards. Optional so custom Terminals built against older
145
+ * pi-tui versions keep working.
138
146
  */
139
147
  enableInput?(): void;
140
148
  stop(): void;
@@ -81,7 +81,7 @@ export declare const writeToolRenderer: {
81
81
  renderCall(args: WriteRenderArgs, options: RenderResultOptions & WriteStreamingPreviewStateCarrier & {
82
82
  renderContext?: WriteRenderContext;
83
83
  }, uiTheme: Theme): Component | undefined;
84
- renderResult(result: WriteResult, options: RenderResultOptions & {
84
+ renderResult(result: WriteResult, options: RenderResultOptions & WriteStreamingPreviewStateCarrier & {
85
85
  renderContext?: WriteRenderContext;
86
86
  }, uiTheme: Theme, args?: WriteRenderArgs): Component;
87
87
  describeCall(args: WriteRenderArgs, options: RenderResultOptions & {
@@ -0,0 +1,22 @@
1
+ import type { Theme } from "../theme/theme.js";
2
+ import type { Component } from "../tui.js";
3
+ import type { NativeToolView, RenderResultContextOptions, RenderResultOptions, ToolRenderResult } from "./renderer.js";
4
+ import type { YieldItem } from "./task.js";
5
+ /** `yield` arguments as sent; strict-mode providers send omitted optionals as `null`. */
6
+ interface YieldRenderArgs {
7
+ type?: string | string[] | null;
8
+ data?: unknown;
9
+ error?: string | null;
10
+ /** 1-based workpool item number. */
11
+ key?: number;
12
+ }
13
+ /** Shows a `yield` call's submitted data instead of its model-facing acknowledgement. */
14
+ export declare const yieldToolRenderer: {
15
+ renderCall(args: YieldRenderArgs, options: RenderResultOptions, theme: Theme): Component;
16
+ renderResult(result: ToolRenderResult<YieldItem>, options: RenderResultContextOptions, theme: Theme, args?: YieldRenderArgs): Component;
17
+ describeCall(args: YieldRenderArgs, options: RenderResultOptions): NativeToolView;
18
+ describeResult(result: ToolRenderResult<YieldItem>, _options: RenderResultContextOptions, args?: YieldRenderArgs): NativeToolView;
19
+ mergeCallAndResult: true;
20
+ animatedPendingPreview: true;
21
+ };
22
+ export {};
@@ -169,6 +169,16 @@ export interface Component {
169
169
  * Called when theme changes or when component needs to re-render from scratch.
170
170
  */
171
171
  invalidate?(): void;
172
+ /**
173
+ * Optional hook to drop memoized render output (rows, parse and wrap state)
174
+ * that the next `render()` can rebuild from state the component keeps.
175
+ * Unlike {@link invalidate}, it must not rebuild anything eagerly: no
176
+ * renderer or extension callbacks, no image conversions, and no child
177
+ * replacement or disposal. The next render must return the same rows it
178
+ * would have returned without the release. Components without it are
179
+ * simply not released.
180
+ */
181
+ releaseRenderCaches?(): void;
172
182
  /**
173
183
  * Optional hook to set whether this component ignores tight layout mode.
174
184
  */
@@ -308,6 +318,7 @@ export declare class Container implements Component {
308
318
  /** Dispose every child, then detach it from this container. */
309
319
  disposeChildren(): void;
310
320
  invalidate(): void;
321
+ releaseRenderCaches(): void;
311
322
  /**
312
323
  * Propagate teardown to children. Call when the container's children are
313
324
  * being permanently discarded (not when they are detached for reuse — use
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.8.6",
4
+ "version": "18.8.7",
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.8.6",
45
- "@oh-my-pi/pi-agent-core": "18.8.6",
46
- "@oh-my-pi/pi-ai": "18.8.6",
47
- "@oh-my-pi/pi-catalog": "18.8.6",
48
- "@oh-my-pi/pi-natives": "18.8.6",
49
- "@oh-my-pi/pi-utils": "18.8.6",
50
- "@oh-my-pi/pi-wire": "18.8.6",
51
- "@oh-my-pi/snapcompact": "18.8.6"
44
+ "@oh-my-pi/omptype": "18.8.7",
45
+ "@oh-my-pi/pi-agent-core": "18.8.7",
46
+ "@oh-my-pi/pi-ai": "18.8.7",
47
+ "@oh-my-pi/pi-catalog": "18.8.7",
48
+ "@oh-my-pi/pi-natives": "18.8.7",
49
+ "@oh-my-pi/pi-utils": "18.8.7",
50
+ "@oh-my-pi/pi-wire": "18.8.7",
51
+ "@oh-my-pi/snapcompact": "18.8.7"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -52,6 +52,10 @@ class AdvisorHeader implements Component {
52
52
  this.#uiTheme = uiTheme;
53
53
  }
54
54
 
55
+ releaseRenderCaches(): void {
56
+ this.#cache = undefined;
57
+ }
58
+
55
59
  invalidate(): void {
56
60
  this.#cache = undefined;
57
61
  }
@@ -82,6 +86,10 @@ class AdvisorNotes implements Component {
82
86
  this.#uiTheme = uiTheme;
83
87
  }
84
88
 
89
+ releaseRenderCaches(): void {
90
+ this.#cache = undefined;
91
+ }
92
+
85
93
  invalidate(): void {
86
94
  this.#cache = undefined;
87
95
  }
@@ -236,6 +244,9 @@ export function createAdvisorMessageCard(
236
244
  invalidate(): void {
237
245
  disclosure.invalidate();
238
246
  },
247
+ releaseRenderCaches(): void {
248
+ disclosure.releaseRenderCaches();
249
+ },
239
250
  dispose(): void {
240
251
  disclosure.dispose();
241
252
  },
@@ -575,6 +575,12 @@ export class AssistantMessageComponent extends Container {
575
575
  }
576
576
  }
577
577
 
578
+ override releaseRenderCaches(): void {
579
+ super.releaseRenderCaches();
580
+ this.#dropStableRenders();
581
+ this.#emergencyText?.releaseRenderCaches();
582
+ }
583
+
578
584
  setHideThinkingBlock(hide: boolean): void {
579
585
  this.#hideThinkingBlock = hide;
580
586
  this.#thinkingCollapsed.clear();
@@ -53,6 +53,10 @@ class DividerSummary implements Component {
53
53
  this.#cache = undefined;
54
54
  }
55
55
 
56
+ releaseRenderCaches(): void {
57
+ this.#cache = undefined;
58
+ }
59
+
56
60
  render(width: number): readonly string[] {
57
61
  width = Math.max(1, width);
58
62
  if (this.#cache?.width === width) return this.#cache.lines;
@@ -150,6 +154,10 @@ class SummaryMessageComponent implements Component {
150
154
  if (this.#ignoreTight !== undefined) this.#disclosure.setIgnoreTight(this.#ignoreTight);
151
155
  }
152
156
 
157
+ releaseRenderCaches(): void {
158
+ this.#disclosure.releaseRenderCaches();
159
+ }
160
+
153
161
  dispose(): void {
154
162
  if (this.#disposed) return;
155
163
  this.#disposed = true;
@@ -1143,6 +1143,10 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
1143
1143
  const title = pathDisplay ? `Read ${pathDisplay}` : "Read";
1144
1144
  let cachedWidth: number | undefined;
1145
1145
  let cachedLines: string[] | undefined;
1146
+ const dropCache = () => {
1147
+ cachedWidth = undefined;
1148
+ cachedLines = undefined;
1149
+ };
1146
1150
  const expanded = this.#expanded;
1147
1151
  const component: Component = {
1148
1152
  render: (width: number) => {
@@ -1164,10 +1168,8 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
1164
1168
  cachedWidth = width;
1165
1169
  return cachedLines;
1166
1170
  },
1167
- invalidate: () => {
1168
- cachedWidth = undefined;
1169
- cachedLines = undefined;
1170
- },
1171
+ invalidate: dropCache,
1172
+ releaseRenderCaches: dropCache,
1171
1173
  };
1172
1174
  this.addChild(component);
1173
1175
  }
@@ -271,6 +271,12 @@ class SkillCallout implements Component {
271
271
  this.#lines = undefined;
272
272
  }
273
273
 
274
+ releaseRenderCaches(): void {
275
+ this.#source = undefined;
276
+ this.#lines = undefined;
277
+ this.#box.releaseRenderCaches();
278
+ }
279
+
274
280
  render(width: number): readonly string[] {
275
281
  const inner = this.#box.render(Math.max(1, width - 1));
276
282
  if (this.#source === inner && this.#lines !== undefined) return this.#lines;
@@ -16,8 +16,8 @@ import { chartAlt, renderChartSvg } from "../charts/chart-svg";
16
16
  import { analyzeTable, type TableAnalysis } from "../charts/table-data";
17
17
  import type { ImageBudget } from "../components/image";
18
18
  import { lexDocument } from "../components/markdown";
19
- import { node } from "../native/describe";
20
- import { registerNativeBlob } from "../native/blobs";
19
+ import { keyed } from "../native/describe";
20
+ import { nativeImageNode } from "../native/blobs";
21
21
  import type { NativeNode } from "../native/node";
22
22
  import { getThemeEpoch } from "../theme";
23
23
  import type { Component } from "../tui";
@@ -85,11 +85,11 @@ interface ChartEntry {
85
85
  let chartMode: TableChartMode = "off";
86
86
  let chartPlanner: TableChartPlanner | undefined;
87
87
  const charts = new Map<string, ChartEntry>();
88
- /** The theme-resolved native blob last registered for a chart. */
89
- const kNativeBlob = Symbol("tableChart.nativeBlob");
88
+ /** The theme-resolved image node owning a chart's native bytes. */
89
+ const kNativeImage = Symbol("tableChart.nativeImage");
90
90
 
91
91
  interface NativeTagged extends TableChart {
92
- [kNativeBlob]?: { epoch: number; blob: string };
92
+ [kNativeImage]?: { epoch: number; image: NativeNode };
93
93
  }
94
94
 
95
95
  /** Set how tables become charts (the host's `tui.autoGraph`) and its `smart` planner; the host rebuilds the transcript after. */
@@ -205,24 +205,21 @@ function drawChart(analysis: TableAnalysis, plan: ChartPlan, guessed = false): T
205
205
  export function describeTableChart(chart: TableChart, key: string): NativeNode {
206
206
  const epoch = getThemeEpoch();
207
207
  const tagged: NativeTagged = chart;
208
- let entry = tagged[kNativeBlob];
208
+ let entry = tagged[kNativeImage];
209
209
  if (entry?.epoch !== epoch) {
210
210
  const svg = prepareSvg(chart.svg, svgFigurePalette());
211
- entry = { epoch, blob: registerNativeBlob(new TextEncoder().encode(svg), "image/svg+xml") };
212
- tagged[kNativeBlob] = entry;
211
+ entry = {
212
+ epoch,
213
+ image: nativeImageNode(new TextEncoder().encode(svg), "image/svg+xml", {
214
+ alt: chart.alt,
215
+ w: chart.width,
216
+ h: chart.height,
217
+ max: { w: `${Math.round(chart.width / UNITS_PER_COLUMN)}ch` },
218
+ }),
219
+ };
220
+ tagged[kNativeImage] = entry;
213
221
  }
214
- return node(
215
- "image",
216
- {
217
- blob: entry.blob,
218
- alt: chart.alt,
219
- w: chart.width,
220
- h: chart.height,
221
- max: { w: `${Math.round(chart.width / UNITS_PER_COLUMN)}ch` },
222
- },
223
- undefined,
224
- key,
225
- );
222
+ return keyed(entry.image, key);
226
223
  }
227
224
 
228
225
  export interface TableChartFigureOptions {
@@ -170,6 +170,24 @@ class SafeToolRendererComponent implements Component {
170
170
  invalidate.call(this.#component);
171
171
  }
172
172
 
173
+ releaseRenderCaches(): void {
174
+ const release = this.#component.releaseRenderCaches;
175
+ if (release === undefined) return;
176
+ try {
177
+ release.call(this.#component);
178
+ } catch (err) {
179
+ if (!this.#warned) {
180
+ this.#warned = true;
181
+ logger.warn("Tool renderer failed", {
182
+ tool: this.#toolName,
183
+ stage: this.#stage,
184
+ phase: "releaseRenderCaches",
185
+ error: String(err),
186
+ });
187
+ }
188
+ }
189
+ }
190
+
173
191
  setIgnoreTight(ignore: boolean): void {
174
192
  const setIgnoreTight = this.#component.setIgnoreTight;
175
193
  if (setIgnoreTight === undefined) return;
@@ -177,6 +177,12 @@ export class UserMessageComponent extends Container implements ReactionTarget {
177
177
  this.addChild(markdown);
178
178
  }
179
179
 
180
+ override releaseRenderCaches(): void {
181
+ this.#zoneSource = undefined;
182
+ this.#zoneLines = undefined;
183
+ super.releaseRenderCaches();
184
+ }
185
+
180
186
  setReaction(emoji: string): void {
181
187
  if (this.#reaction === emoji) return;
182
188
  this.#reaction = emoji;
@@ -337,6 +343,10 @@ class SyntheticSummary implements Component {
337
343
  this.#summary = summary;
338
344
  }
339
345
 
346
+ releaseRenderCaches(): void {
347
+ this.#cache = undefined;
348
+ }
349
+
340
350
  invalidate(): void {
341
351
  this.#cache = undefined;
342
352
  }
@@ -425,6 +435,10 @@ export class CollapsedSyntheticMessageComponent implements Component {
425
435
  this.#disclosure.invalidate();
426
436
  }
427
437
 
438
+ releaseRenderCaches(): void {
439
+ this.#disclosure.releaseRenderCaches();
440
+ }
441
+
428
442
  dispose(): void {
429
443
  this.#disclosure.dispose();
430
444
  }