@oh-my-pi/pi-tui 18.10.4 → 18.11.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 +22 -100
  2. package/dist/types/chat/figure-markdown.d.ts +7 -2
  3. package/dist/types/components/markdown.d.ts +8 -0
  4. package/dist/types/components/text.d.ts +2 -0
  5. package/dist/types/keybinding-matchers.d.ts +2 -0
  6. package/dist/types/multiplexers/tmux.d.ts +1 -0
  7. package/dist/types/multiplexers/types.d.ts +6 -0
  8. package/dist/types/overlays/approval-dialog.d.ts +24 -6
  9. package/dist/types/prompt/custom-editor.d.ts +18 -10
  10. package/dist/types/render/hanging-wrap.d.ts +5 -0
  11. package/dist/types/terminal-capabilities.d.ts +8 -0
  12. package/dist/types/terminal-multiplexer.d.ts +1 -0
  13. package/dist/types/tmux.d.ts +13 -4
  14. package/package.json +9 -9
  15. package/src/chat/assistant-message.ts +19 -4
  16. package/src/chat/figure-markdown.ts +10 -3
  17. package/src/chat/read-tool-group.ts +5 -1
  18. package/src/chat/svg-figure.ts +3 -1
  19. package/src/chat/svg-source.ts +6 -3
  20. package/src/components/markdown.ts +12 -0
  21. package/src/components/text.ts +11 -1
  22. package/src/keybinding-matchers.ts +5 -0
  23. package/src/multiplexers/tmux.ts +3 -0
  24. package/src/multiplexers/types.ts +6 -0
  25. package/src/overlays/approval-dialog.ts +115 -24
  26. package/src/prompt/custom-editor.ts +149 -64
  27. package/src/render/code-cell.ts +10 -4
  28. package/src/render/hanging-wrap.ts +31 -0
  29. package/src/terminal-capabilities.ts +25 -7
  30. package/src/terminal.ts +14 -3
  31. package/src/tmux.ts +42 -16
  32. package/src/tools/ask.ts +20 -9
  33. package/src/tools/edit.ts +4 -1
  34. package/src/tools/lsp.ts +9 -2
  35. package/src/tools/todo.ts +3 -2
  36. package/src/tools/write.ts +14 -4
  37. package/src/tui.ts +11 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,27 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.11.0] - 2026-10-14
6
+
7
+ ### Added
8
+
9
+ - Added native composer microphone controls with visible starting, recording and transcription states; a start still loading its model can be cancelled, and Enter and Send wait while dictation is recording or transcribing into the draft ([#15759](https://github.com/can1357/oh-my-pi/pull/15759) by [@daaximus](https://github.com/daaximus)).
10
+ - `ApprovalDialogComponent` takes a `draft` option that routes typing to the composer beneath it, so a message can be written while a tool call waits ([#15642](https://github.com/can1357/oh-my-pi/pull/15642) by [@H4vC](https://github.com/H4vC))
11
+ - `ApprovalDialogComponent` takes a `fullPrompt` option that the expand key (`app.tools.expand`) or a Details button swaps in for the summarized prompt; it replaces `wrapDetails` ([#15642](https://github.com/can1357/oh-my-pi/pull/15642) by [@H4vC](https://github.com/H4vC))
12
+
13
+ ### Fixed
14
+
15
+ - SVG fences in CRLF or CR-only assistant Markdown now render as figures instead of source code ([#15663](https://github.com/can1357/oh-my-pi/issues/15663)).
16
+ - Fixed clipboard-image and image-path pastes being dropped while the composer draft is edited beneath an ask or approval dialog ([#15642](https://github.com/can1357/oh-my-pi/pull/15642) by [@H4vC](https://github.com/H4vC))
17
+ - Fixed inline images inside tmux assuming Kitty graphics from the pane's `TERM`; the attached client is identified from tmux's terminal-type reply or its `TERM` (e.g. `xterm-kitty`), and unrecognized clients go to the SIXEL probe ([#15524](https://github.com/can1357/oh-my-pi/issues/15524)).
18
+ - Fixed ```svg fences rendering as their source code instead of a drawing when no theme was installed yet, e.g. in an extension rendering a message that opens with the fence ([#15660](https://github.com/can1357/oh-my-pi/issues/15660))
19
+ - Fixed pastes inside tmux 3.5a and older submitting line by line after another program turned bracketed paste off on the pane; omp now re-enables it on the next render or keypress ([#15596](https://github.com/can1357/oh-my-pi/issues/15596))
20
+ - Fixed assistant replies containing a table (on image-capable terminals with table charts on) publishing still-streaming rows to native scrollback, which froze the block's append-only retirement and could leave overwritten reply rows in scrollback ([#15693](https://github.com/can1357/oh-my-pi/issues/15693))
21
+ - Fixed streamed thinking publishing a line to native scrollback just before a code fence or `<!-- -->` sentinel split across deltas rewrote it with the prose-fold ellipsis ([#15693](https://github.com/can1357/oh-my-pi/issues/15693))
22
+ - Fixed streamed assistant text or thinking containing a tab stopping all later rows of the reply from retiring into native scrollback until the turn ended ([#15693](https://github.com/can1357/oh-my-pi/issues/15693))
23
+ - Fixed wrapped lines in Write, Read, Edit, Todo, Ask and LSP tool cards losing their alignment under line numbers, tree connectors, and option or code rails ([#15677](https://github.com/can1357/oh-my-pi/issues/15677))
24
+ - Fixed SIXEL images leaving their previous raster on screen, drawn again over itself, when the transcript moves them within the viewport ([#15657](https://github.com/can1357/oh-my-pi/issues/15657))
25
+
5
26
  ## [18.10.4] - 2026-10-13
6
27
 
7
28
  ### Added
@@ -2658,103 +2679,4 @@
2658
2679
 
2659
2680
  - Fixed terminal scrolling when displaying overlays after rendering large content, preventing hundreds of blank lines from being output
2660
2681
 
2661
- ## [11.3.0] - 2026-02-06
2662
-
2663
- ### Breaking Changes
2664
-
2665
- - Removed `getCursorPosition()` method from Component interface and implementations, eliminating hardware cursor positioning support
2666
-
2667
- ### Added
2668
-
2669
- - Added sticky column behavior for vertical cursor movement, preserving target column when navigating through lines of varying lengths
2670
- - Added `drainInput()` method to Terminal interface to prevent Kitty key release events from leaking to parent shell over slow SSH connections
2671
- - Added `setClearOnShrink()` method to control whether full re-render occurs when content shrinks below working area
2672
- - Added support for hidden paths (e.g., `.pi`, `.github`) in autocomplete while excluding `.git` directories
2673
-
2674
- ### Changed
2675
-
2676
- - Changed default value of `PI_HARDWARE_CURSOR` environment variable from implicit true to explicit `"1"` for clarity
2677
- - Changed default value of `PI_CLEAR_ON_SHRINK` environment variable from implicit false to explicit `"0"` for clarity
2678
- - Changed TUI to clear screen on startup to prevent shell prompts and status messages from bleeding into the first rendered frame
2679
- - Refactored full-render logic into reusable helper function to reduce code duplication across multiple render paths
2680
- - Changed autocomplete to include hidden paths but filter out `.git` and its contents
2681
- - Changed Input component to properly handle surrogate pairs in Unicode text, preventing cursor display corruption with emoji and multi-byte characters
2682
- - Changed Editor to use `setCursorCol()` for all cursor column updates, enabling sticky column tracking
2683
- - Changed Editor's vertical navigation to implement sticky column logic via `moveToVisualLine()` and `computeVerticalMoveColumn()`
2684
- - Changed Editor's Enter key handling to extract submit logic into `submitValue()` method for better code organization
2685
- - Changed SettingsList to truncate long lines to viewport width, preventing text overflow
2686
- - Changed Terminal's `stop()` method to drain stdin before restoring raw mode, fixing race condition where Ctrl+D could close parent shell over SSH
2687
- - Changed TUI rendering to add `clearOnShrink` option (controlled by `PI_CLEAR_ON_SHRINK` env var) for reducing redraws on slower terminals
2688
- - Changed TUI rendering to detect when extra lines exceed viewport height and trigger full re-render instead of incremental updates
2689
-
2690
- ### Fixed
2691
-
2692
- - Fixed rendering of extra blank lines when content shrinks by improving cursor positioning logic during line deletion
2693
- - Fixed cursor display position in Input component when scrolling horizontally through long text
2694
- - Fixed Kitty keyboard protocol disable sequence to use safe write method, preventing potential output buffering issues
2695
- - Fixed unnecessary full-screen redraws when changes occur in out-of-view components (e.g., spinners), reducing terminal scroll events and improving performance on slower connections
2696
- - Fixed scrollback clearing behavior to only clear screen instead of scrollback when resizing or shrinking content, preventing loss of terminal history
2697
- - Fixed `.git` directory appearing in autocomplete suggestions when filtering by prefix
2698
- - Fixed cursor position corruption in Input component when displaying text with emoji and combining characters
2699
- - Fixed `.git` directory appearing in autocomplete suggestions
2700
- - Fixed race condition where Kitty key release events could leak to parent shell after TUI exit over slow SSH connections
2701
- - Fixed Editor's word movement (Ctrl+Left/Right) to properly reset sticky column for subsequent vertical navigation
2702
- - Fixed Editor's undo operation to reset sticky column state when restoring cursor position
2703
- - Fixed Editor's right arrow key at end of last line to set sticky column for subsequent up/down navigation
2704
- - Fixed TUI rendering to correctly detect viewport changes and avoid false full-redraws after content shrinks
2705
- - Fixed Kitty protocol key parsing to prefer codepoint over base layout for Latin letters and symbols, fixing keyboard layout issues (e.g., Dvorak)
2706
-
2707
- ## [11.0.0] - 2026-02-05
2708
-
2709
- ### Added
2710
-
2711
- - Introduced `terminal-capabilities.ts` module consolidating terminal detection and image protocol support
2712
- - Added `TerminalInfo` class with methods for detecting image lines and formatting notifications
2713
- - Added `NotifyProtocol` enum supporting Bell, OSC 99, and OSC 9 notification protocols
2714
- - Added `isNotificationSuppressed()` function to check `OMP_NOTIFICATIONS` environment variable
2715
- - Added `TERMINAL` constant providing detected terminal capabilities at runtime
2716
-
2717
- ### Changed
2718
-
2719
- - Changed notification suppression environment variable from `OMP_NOTIFICATIONS` to `PI_NOTIFICATIONS`
2720
- - Changed TUI write log environment variable from `OMP_TUI_WRITE_LOG` to `PI_TUI_WRITE_LOG`
2721
- - Changed hardware cursor environment variable from `OMP_HARDWARE_CURSOR` to `PI_HARDWARE_CURSOR`
2722
- - Updated environment variable access to use `getEnv()` utility function from `@oh-my-pi/pi-utils` for consistent handling
2723
- - Renamed `TERMINAL_INFO` export to `TERMINAL` for clearer API semantics
2724
- - Reorganized terminal image exports from `terminal-image` to `terminal-capabilities` module
2725
- - Updated all internal references to use `TERMINAL` instead of `TERMINAL_INFO`
2726
-
2727
- ### Removed
2728
-
2729
- - Removed `terminal-image` module exports from public API (functionality migrated to `terminal-capabilities`)
2730
-
2731
- ## [10.5.0] - 2026-02-04
2732
-
2733
- ### Fixed
2734
-
2735
- - Treated inline image lines with cursor-move prefixes as image sequences to prevent width overflow crashes
2736
-
2737
- ## [9.8.0] - 2026-02-01
2738
-
2739
- ### Changed
2740
-
2741
- - Moved `wrapTextWithAnsi` export to `@oh-my-pi/pi-natives` package
2742
-
2743
- ### Fixed
2744
-
2745
- - Improved Kitty terminal key sequence parsing to correctly handle text field codepoints in CSI-u sequences
2746
- - Fixed handling of private use Unicode codepoints (U+E000 to U+F8FF) in Kitty key decoding to prevent invalid character interpretation
2747
-
2748
- ## [9.7.0] - 2026-02-01
2749
-
2750
- ### Breaking Changes
2751
-
2752
- - Removed `Key` helper object from public API; use string literals like `"ctrl+c"` instead of `Key.ctrl("c")`
2753
- - Removed `KeyEventType` export from public API
2754
-
2755
- ### Changed
2756
-
2757
- - Migrated key parsing and matching logic to native implementation for improved performance
2758
- - Simplified `isKeyRelease()` and `isKeyRepeat()` to use regex pattern matching instead of string inclusion checks
2759
-
2760
- Older entries are archived in [packages/tui/CHANGELOG.md@e6b8074bbe4b](https://github.com/can1357/oh-my-pi/blob/e6b8074bbe4b0d459aa812948822c2a634c8e62d/packages/tui/CHANGELOG.md).
2682
+ Older entries are archived in [packages/tui/CHANGELOG.md@a66558815743](https://github.com/can1357/oh-my-pi/blob/a66558815743a61164ceebceb2bb528989fb09b3/packages/tui/CHANGELOG.md).
@@ -1,5 +1,5 @@
1
1
  import type { ImageBudget } from "../components/image.js";
2
- import type { Markdown } from "../components/markdown.js";
2
+ import { type Markdown } from "../components/markdown.js";
3
3
  import { Container } from "../tui.js";
4
4
  export interface FigureMarkdownOptions {
5
5
  /** Markdown for one prose run (and a failed figure's code fallback); FigureMarkdown drives its transience. */
@@ -32,7 +32,12 @@ export declare class FigureMarkdown extends Container {
32
32
  /**
33
33
  * The trimmed prose ahead of the first figure, or "" when the block opens
34
34
  * with one. These are the only rows plain Markdown reproduces byte for
35
- * byte, so they are all a transcript may publish ahead of the figure.
35
+ * byte, so they are all a transcript may publish ahead of the figure. While
36
+ * that prose is still the streaming tail (no figure follows it yet), only
37
+ * its settled prefix ({@link Markdown.getLastRenderStableText}) qualifies:
38
+ * the rest is still being written. Both forms are in
39
+ * {@link streamingMarkdownSource} form, so the settled prefix stays a byte
40
+ * prefix of the whole run once a figure splits it off.
36
41
  */
37
42
  get leadingProse(): string;
38
43
  /** Whether a figure's final source still waits for its raster, or a chart for its pick or raster. */
@@ -2,6 +2,14 @@ import { type TokensList } from "@oh-my-pi/pi-utils/marked";
2
2
  import type { DescribeContext, NativeNode } from "../native/node.js";
3
3
  import type { SymbolTheme } from "../symbols.js";
4
4
  import type { Component } from "../tui.js";
5
+ /**
6
+ * `text` as the streaming renderer lexes it: OSC 8 terminators unified and
7
+ * tabs expanded. {@link Markdown.getLastRenderStableText} is a prefix of this
8
+ * form, not of the raw source, so append-only publishers that mix settled
9
+ * prefixes with whole blocks must compare both in this form. Rendering it is
10
+ * byte-identical to rendering `text`.
11
+ */
12
+ export declare function streamingMarkdownSource(text: string): string;
5
13
  /** @internal exported for tests — the grown-line block-kind gate. */
6
14
  export declare function fastLineStartHazard(grownLine: string): boolean;
7
15
  /**
@@ -12,6 +12,8 @@ import type { Component } from "../tui.js";
12
12
  export declare class Text implements Component {
13
13
  #private;
14
14
  setIgnoreTight(ignore: boolean): this;
15
+ /** Override row wrapping when the source carries structural prefixes. */
16
+ setWrapLine(wrapLine: (line: string, width: number) => string[]): this;
15
17
  constructor(text?: string, paddingX?: number, paddingY?: number, customBgFn?: (text: string) => string);
16
18
  /** Return bounded text and layout state for debug inspection. */
17
19
  debugState(): Record<string, unknown>;
@@ -9,6 +9,8 @@ import type { KeyId } from "./app-keybindings.js";
9
9
  export declare function matchesAppInterrupt(data: string): boolean;
10
10
  /** Match `app.tools.expand`, falling back to Ctrl+O when the action is unbound. */
11
11
  export declare function matchesAppToolsExpand(data: string): boolean;
12
+ /** Effective tools-expand key: the first bound key, or Ctrl+O when unbound. */
13
+ export declare function appToolsExpandKey(): KeyId;
12
14
  /** Match the generic selector cancel keybinding. */
13
15
  export declare function matchesSelectCancel(data: string): boolean;
14
16
  /** Match the generic selector up-navigation keybinding. */
@@ -22,6 +22,7 @@ export declare const tmuxMultiplexer: {
22
22
  readonly passthrough: typeof wrapTmuxPassthrough;
23
23
  readonly settleMs: 100;
24
24
  };
25
+ readonly gatesBracketedPaste: true;
25
26
  readonly expandsRepPadding: true;
26
27
  readonly expiresSynchronizedOutput: true;
27
28
  readonly altRestoreEndsSynchronizedOutput: true;
@@ -73,6 +73,12 @@ export interface TerminalMultiplexerModule<Id extends string = string> {
73
73
  * session, and an unrecognized report does not turn it off.
74
74
  */
75
75
  readonly honorsSynchronizedOutput?: boolean;
76
+ /**
77
+ * Tracks DEC 2004 per pane and forwards paste markers only while it is set,
78
+ * yet may leave DECRQM 2004 unanswered, so bracketed paste is reasserted
79
+ * without waiting for a DECRPM confirmation.
80
+ */
81
+ readonly gatesBracketedPaste?: boolean;
76
82
  /**
77
83
  * The session does not reveal whether its client renders graphics while
78
84
  * outer-terminal identity variables leak in, so images and Kitty
@@ -5,7 +5,11 @@
5
5
  * so an Enter typed for the composer as the prompt appears never approves a
6
6
  * call, and the chord itself only arms {@link APPROVE_ARM_MS} after mount: it
7
7
  * is also the follow-up key, pressed mid-run exactly when prompts appear.
8
- * Esc denies.
8
+ * Esc denies. With an {@link ApprovalDialogDraft}, every other key edits the
9
+ * composer mounted beneath the dialog, so a message can steer the turn while
10
+ * the call waits; an empty composer ignores Enter and arrows scroll the details.
11
+ * The prompt stays a few rows so the composer keeps its room; with a full
12
+ * prompt, the expand key (`app.tools.expand`) swaps it in, wrapped.
9
13
  */
10
14
  import { type Component, type TUI } from "../index.js";
11
15
  import type { NativeNode, NativeUiEvent } from "../native/node.js";
@@ -15,10 +19,16 @@ interface ApprovalDialogCallbacks {
15
19
  onApprove(): void;
16
20
  onDeny(): void;
17
21
  }
22
+ /** The composer mounted beneath the dialog: it receives every key the dialog does not own. */
23
+ export interface ApprovalDialogDraft {
24
+ /** True while the composer holds a draft — text, attachments, or a paste still landing: keys then edit and send it. */
25
+ hasDraft(): boolean;
26
+ handleInput(keyData: string): void;
27
+ }
18
28
  interface ApprovalDialogOptions {
19
29
  tui?: TUI;
20
- /** Wrap long details instead of clipping them to a row: set when the dialog is the only place they show. */
21
- wrapDetails?: boolean;
30
+ /** The prompt with every detail in full, which the expand key swaps in (wrapped) for the summarized one. */
31
+ fullPrompt?: string;
22
32
  /** Deny once this many ms pass without input (any key restarts the countdown). */
23
33
  timeout?: number;
24
34
  /** Called when {@link timeout} expires, before the dialog denies. */
@@ -27,6 +37,8 @@ interface ApprovalDialogOptions {
27
37
  onTimeoutStart?: () => void;
28
38
  /** Called each time a key restarts the countdown. */
29
39
  onTimeoutReset?: () => void;
40
+ /** Route typing to the composer instead of dropping it. */
41
+ draft?: ApprovalDialogDraft;
30
42
  }
31
43
  export declare class ApprovalDialogComponent implements Component {
32
44
  #private;
@@ -37,15 +49,21 @@ export declare class ApprovalDialogComponent implements Component {
37
49
  invalidate(): void;
38
50
  dispose(): void;
39
51
  handleInput(keyData: string): void;
52
+ /**
53
+ * Swap the summarized prompt for every detail, wrapped, or back, as the expand key does here;
54
+ * for hosts that intercept that key first. Restarts the countdown like any key. False (nothing
55
+ * changes) without a full prompt, so the host's own expand handling (tool previews) can run.
56
+ */
57
+ toggleDetails(): boolean;
40
58
  render(width: number): readonly string[];
41
59
  /**
42
60
  * In the dock, in the composer's place and framed as the composer is (an
43
61
  * `omp.editor.*` root role), like the ask dialog, rather than a card over
44
- * the transcript: the title, the details, then Deny and the primary
45
- * Approve, whose keys show in their tooltips.
62
+ * the transcript: the title, the details, then Details (with a full prompt),
63
+ * Deny, and the primary Approve, whose keys show in their tooltips.
46
64
  */
47
65
  describe(): NativeNode;
48
- /** Deny and Approve run exactly what Esc and the approve key run. */
66
+ /** Deny, Approve, and Details run exactly what Esc, the approve key, and the expand key run. */
49
67
  handleNativeEvent(event: NativeUiEvent): void;
50
68
  }
51
69
  export {};
@@ -24,6 +24,8 @@ export interface ComposerNativeState {
24
24
  readonly rate?: number;
25
25
  /** A turn is running: the send keycap becomes a Stop button. */
26
26
  readonly running: boolean;
27
+ /** Native dictation control; undefined hides it for hosts without speech-to-text. */
28
+ readonly dictation?: "idle" | "starting" | "recording" | "transcribing";
27
29
  /** The viewed subagent's lineage, outermost first and the viewed agent last; undefined on the main session. */
28
30
  readonly viewing?: readonly string[];
29
31
  /** The session's title, quoted in italics as the TSP placeholder; undefined until it has one. */
@@ -220,7 +222,8 @@ export declare class CustomEditor extends Editor {
220
222
  * once after construction (and again after `setEditorComponent` swaps the
221
223
  * editor). Passing `undefined` clears any pending frame. */
222
224
  setShimmerRepaintHandler(handler: (() => void) | undefined): void;
223
- /** Editing is available during bootstrap; atomic sends wait until submission is wired and enabled. */
225
+ /** Editing is available during bootstrap; atomic sends wait until submission is wired and enabled
226
+ * and no dictation is still writing into the draft. */
224
227
  protected get nativeSendable(): boolean;
225
228
  /** Viewing a subagent, the draft goes to it: the placeholder names it; else the session's title, quoted in italics. */
226
229
  describePlaceholder: () => TspText;
@@ -238,17 +241,17 @@ export declare class CustomEditor extends Editor {
238
241
  * subagent is focused, the attachment chips, a `line` row of the
239
242
  * shell-mode chip and the input, and the `bar`: model chip, effort chip
240
243
  * (or the effort glyph as the model chip's icon), the generation rate,
241
- * the other status facts, usage, then send (Stop while a turn runs).
242
- * Clicks come back as `status.model`, `thinking.cycle`, `submit`,
243
- * `interrupt` and `focus:<id>` actions.
244
+ * the other status facts, usage, dictation, then send (Stop while a turn runs).
245
+ * Clicks come back as `status.model`, `thinking.cycle`, `dictation.toggle`,
246
+ * `submit`, `interrupt` and `focus:<id>` actions.
244
247
  */
245
248
  describeLayout: (input: NativeNode, cx: DescribeContext) => NativeEditorLayout;
246
249
  /**
247
- * Clicks on the composer's controls take the same paths as their keys: ⇧⇥,
248
- * ⏎ and Esc; the viewing header's links (`focus:<id>`) go to the host,
249
- * the status facts' clicks (`status.*`) to their source. Selection edits
250
- * go to the buffer; `send` submits its own prompt after saving the old draft
251
- * for recall and waiting for in-flight clipboard work.
250
+ * Dictation clicks go directly to the host, independent of keybindings.
251
+ * Other controls take the same paths as their keys; the viewing header's
252
+ * links (`focus:<id>`) go to the host, the status facts' clicks to their source.
253
+ * Selection edits go to the buffer; `send` submits its own prompt after
254
+ * saving the old draft for recall and waiting for in-flight clipboard work.
252
255
  */
253
256
  handleNativeEvent(event: NativeUiEvent): void;
254
257
  onEscape?: () => void;
@@ -275,6 +278,8 @@ export declare class CustomEditor extends Editor {
275
278
  onRetry?: () => void;
276
279
  /** Called when Caps Lock is pressed. */
277
280
  onCapsLock?: () => void;
281
+ /** Called directly by the native microphone/stop control, without synthesizing a keypress. */
282
+ onToggleDictation?: () => void;
278
283
  /** Called when left-arrow is pressed while the editor is empty (cursor necessarily at start). */
279
284
  onLeftAtStart?: () => void;
280
285
  /** Called when the viewing header asks to view agent `id` ({@link MAIN_AGENT_ID}: the main session). */
@@ -296,6 +301,8 @@ export declare class CustomEditor extends Editor {
296
301
  * Clear all custom key handlers.
297
302
  */
298
303
  clearCustomKeyHandlers(): void;
304
+ /** True while a clipboard-image or image-path paste is still landing; later keys queue behind it. */
305
+ get pasteInFlight(): boolean;
299
306
  capturesInput(data: string): boolean;
300
307
  handleInput(data: string): void;
301
308
  /**
@@ -305,7 +312,8 @@ export declare class CustomEditor extends Editor {
305
312
  * an Ask dialog opened over a non-empty prompt — so finishing or submitting
306
313
  * the draft cannot fire an editor-slot shortcut that clears
307
314
  * `editorContainer` and orphans the overlay. Only text editing, cursor
308
- * movement, submission, and the clear action reach the buffer.
315
+ * movement, submission, the clear action, and pastes (clipboard images and
316
+ * image paths attach as on the composer) reach the buffer.
309
317
  */
310
318
  handleDraftEdit(data: string): void;
311
319
  }
@@ -0,0 +1,5 @@
1
+ import type { Theme } from "../theme/theme.js";
2
+ /** Wrap content under a distinct first-row prefix, preserving its column on continuation rows. */
3
+ export declare function wrapPrefixedLine(prefix: string, continuationPrefix: string, content: string, width: number): string[];
4
+ /** Preserve a tree connector's ancestor guides while hanging a long node beneath its label. */
5
+ export declare function wrapTreeLine(line: string, width: number, theme: Theme): string[] | undefined;
@@ -327,6 +327,14 @@ export interface ParsedKittyPlacementLine {
327
327
  * callers fall back to writing the line verbatim.
328
328
  */
329
329
  export declare function parseKittyDirectPlacementLine(line: string): ParsedKittyPlacementLine | null;
330
+ /**
331
+ * Rows above its own line that an {@link Image} direct-placement line paints
332
+ * over: the `CUU(rows-1)` of its lead-in, or 0 for any other line. The renderer
333
+ * erases those rows before a SIXEL/iTerm2 block is written again, because
334
+ * those protocols have no delete command and the block's reserved rows are
335
+ * byte-identical between frames, so the diff never rewrites them.
336
+ */
337
+ export declare function directPlacementLift(line: string): number;
330
338
  /**
331
339
  * Rebuild an {@link Image} direct-placement line for the viewport row it is
332
340
  * written at. The component-rendered line encodes `CUU(rows-1)`, which clamps
@@ -38,6 +38,7 @@ declare const registry: readonly [{
38
38
  readonly passthrough: typeof import("./tmux.js").wrapTmuxPassthrough;
39
39
  readonly settleMs: 100;
40
40
  };
41
+ readonly gatesBracketedPaste: true;
41
42
  readonly expandsRepPadding: true;
42
43
  readonly expiresSynchronizedOutput: true;
43
44
  readonly altRestoreEndsSynchronizedOutput: true;
@@ -1,6 +1,14 @@
1
1
  export { wrapTmuxPassthrough } from "./multiplexers/tmux.js";
2
2
  /** Pass a control sequence through tmux, leaving direct-terminal output unchanged. */
3
3
  export declare function wrapTmuxPassthroughIfNeeded(payload: string, env?: NodeJS.ProcessEnv): string;
4
+ /**
5
+ * Emulator name candidates from a `#{client_termtype}<TAB>#{client_termname}`
6
+ * line, best first: the terminal-type reply (tmux 3.4+, XTVERSION), then the
7
+ * emulator the client's `TERM` names when it ships its own terminfo entry. Both
8
+ * are kept because the reply can name an unrecognized program (a nested tmux
9
+ * answers `tmux 3.4`) while `TERM` still identifies the emulator.
10
+ */
11
+ export declare function tmuxClientProgramsFromReport(report: string): string[];
4
12
  /**
5
13
  * Whether the terminal hosting this tmux session draws TSP programs natively:
6
14
  * it sets the session user option `@tern-tsp` while it watches the session.
@@ -12,13 +20,14 @@ export declare function tmuxSessionSpeaksTsp(env?: NodeJS.ProcessEnv): boolean;
12
20
  */
13
21
  export declare function setTmuxPaneApcTitles(on: boolean, env?: NodeJS.ProcessEnv): void;
14
22
  /**
15
- * Resolve the terminal emulator name recorded for this tmux client.
23
+ * Resolve the terminal emulator names recorded for this tmux client, best first.
16
24
  *
17
25
  * tmux overwrites pane identity variables with its own values, but retains the
18
- * attached client's terminal-type reply in `#{client_termtype}`. The local IPC
19
- * query runs once per process and degrades to `null` when unavailable.
26
+ * attached client's terminal-type reply in `#{client_termtype}` and its `TERM`
27
+ * in `#{client_termname}` (see {@link tmuxClientProgramsFromReport}). The local
28
+ * IPC query runs once per process and degrades to `[]` when unavailable.
20
29
  */
21
- export declare function resolveTmuxClientTerminalName(env?: NodeJS.ProcessEnv): string | null;
30
+ export declare function resolveTmuxClientTerminalNames(env?: NodeJS.ProcessEnv): readonly string[];
22
31
  /**
23
32
  * Whether tmux draws SIXEL to the client attached to this session: its
24
33
  * effective terminal description for that client carries `Sxl`.
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.10.4",
4
+ "version": "18.11.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.10.4",
45
- "@oh-my-pi/pi-agent-core": "18.10.4",
46
- "@oh-my-pi/pi-ai": "18.10.4",
47
- "@oh-my-pi/pi-catalog": "18.10.4",
48
- "@oh-my-pi/pi-natives": "18.10.4",
49
- "@oh-my-pi/pi-utils": "18.10.4",
50
- "@oh-my-pi/pi-wire": "18.10.4",
51
- "@oh-my-pi/snapcompact": "18.10.4"
44
+ "@oh-my-pi/omptype": "18.11.0",
45
+ "@oh-my-pi/pi-agent-core": "18.11.0",
46
+ "@oh-my-pi/pi-ai": "18.11.0",
47
+ "@oh-my-pi/pi-catalog": "18.11.0",
48
+ "@oh-my-pi/pi-natives": "18.11.0",
49
+ "@oh-my-pi/pi-utils": "18.11.0",
50
+ "@oh-my-pi/pi-wire": "18.11.0",
51
+ "@oh-my-pi/snapcompact": "18.11.0"
52
52
  },
53
53
  "devDependencies": {
54
54
  "kitty-vt-wasm": "^0.2.0"
@@ -7,6 +7,7 @@ import {
7
7
  Markdown,
8
8
  type MarkdownTheme,
9
9
  rewriteMarkdownLinkDestinations,
10
+ streamingMarkdownSource,
10
11
  } from "../components/markdown";
11
12
  import { Spacer } from "../components/spacer";
12
13
  import { Text } from "../components/text";
@@ -111,6 +112,14 @@ interface StableRenderInputs {
111
112
  readonly color: ((text: string) => string) | undefined;
112
113
  }
113
114
 
115
+ /**
116
+ * A partial last thinking line the prose fold may still drop: the start of a
117
+ * fence marker (`` ` ``, `~`) or of an empty `<!-- -->` sentinel. Once it turns
118
+ * into either, the fold's ellipsis lands back on the line above it
119
+ * (`formatThinkingForDisplay`), so that line is not yet settled.
120
+ */
121
+ const UNSETTLED_THINKING_TAIL = /(?:^|\n)[^\S\n]*(?:`{1,2}|~{1,2}|<(?:!(?:-(?:-[^\S\n]*-{0,2})?)?)?)[^\S\n]*$/;
122
+
114
123
  function isSnapshotExtension(previous: readonly StablePart[], current: readonly StablePart[]): boolean {
115
124
  if (previous.length > current.length) return false;
116
125
  for (let index = 0; index < previous.length; index++) {
@@ -1240,17 +1249,23 @@ export class AssistantMessageComponent extends Container {
1240
1249
  // Streaming child: publish Markdown's frozen prefix, and only
1241
1250
  // once non-blank content exists past it — the block's last
1242
1251
  // non-blank line is still being written (thinking's prose fold
1243
- // may rewrite it) and must stay out of published bytes.
1252
+ // may rewrite it) and must stay out of published bytes. A
1253
+ // partial thinking line the fold may still drop does not count.
1254
+ // The frozen prefix is in streamingMarkdownSource form, so
1255
+ // offsets into the block use that form too.
1244
1256
  const raw = md.getLastRenderStableText();
1245
1257
  const frozen = raw.trim();
1246
- if (frozen.length > 0 && /\S/.test(item.lastText.slice(raw.length))) {
1258
+ let past = streamingMarkdownSource(item.lastText).slice(raw.length);
1259
+ if (item.blockType === "thinking") past = past.replace(UNSETTLED_THINKING_TAIL, "");
1260
+ if (frozen.length > 0 && /\S/.test(past)) {
1247
1261
  parts.push({ kind: item.blockType, text: frozen });
1248
1262
  }
1249
1263
  break;
1250
1264
  }
1251
1265
  // Closed child: only the streaming tail may mutate in place, so
1252
- // everything before it is final.
1253
- parts.push({ kind: item.blockType, text: item.lastText });
1266
+ // everything before it is final. Same form as the frozen prefix it
1267
+ // published while streaming, so the snapshot still extends.
1268
+ parts.push({ kind: item.blockType, text: streamingMarkdownSource(item.lastText) });
1254
1269
  itemIndex++;
1255
1270
  continue;
1256
1271
  }
@@ -1,6 +1,6 @@
1
1
  import type { Tokens } from "@oh-my-pi/pi-utils/marked";
2
2
  import type { ImageBudget } from "../components/image";
3
- import type { Markdown } from "../components/markdown";
3
+ import { type Markdown, streamingMarkdownSource } from "../components/markdown";
4
4
  import { Spacer } from "../components/spacer";
5
5
  import { Container } from "../tui";
6
6
  import { SvgFigure, svgFigureRendering } from "./svg-figure";
@@ -68,11 +68,18 @@ export class FigureMarkdown extends Container {
68
68
  /**
69
69
  * The trimmed prose ahead of the first figure, or "" when the block opens
70
70
  * with one. These are the only rows plain Markdown reproduces byte for
71
- * byte, so they are all a transcript may publish ahead of the figure.
71
+ * byte, so they are all a transcript may publish ahead of the figure. While
72
+ * that prose is still the streaming tail (no figure follows it yet), only
73
+ * its settled prefix ({@link Markdown.getLastRenderStableText}) qualifies:
74
+ * the rest is still being written. Both forms are in
75
+ * {@link streamingMarkdownSource} form, so the settled prefix stays a byte
76
+ * prefix of the whole run once a figure splits it off.
72
77
  */
73
78
  get leadingProse(): string {
74
79
  const first = this.#parts[0];
75
- return first?.kind === "markdown" ? first.text : "";
80
+ if (first?.kind !== "markdown") return "";
81
+ if (this.#transient && this.#parts.length === 1) return first.md.getLastRenderStableText().trim();
82
+ return streamingMarkdownSource(first.text);
76
83
  }
77
84
 
78
85
  /** Whether a figure's final source still waits for its raster, or a chart for its pick or raster. */
@@ -16,6 +16,8 @@ import {
16
16
  } from "../tools/read";
17
17
  import { PREVIEW_LIMITS, shortenPath } from "../render/render-utils";
18
18
  import { fileHyperlink, renderCodeCell, renderMarkdownCell } from "../render";
19
+ import { wrapTreeLine } from "../render/hanging-wrap";
20
+ import { wrapTextWithAnsi } from "../utils";
19
21
  import { canonicalizeMessage } from "./thinking-display";
20
22
  import { internalUrlSchemeSpec, splitUrlScheme } from "../tools/url-scheme-host";
21
23
  import type { ToolExecutionHandle } from "./tool-execution";
@@ -891,7 +893,9 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
891
893
 
892
894
  // Clear previous children and rebuild the summary and preview blocks.
893
895
  this.clear();
894
- this.#text = new Text("", 0, 0);
896
+ this.#text = new Text("", 0, 0).setWrapLine(
897
+ (line, width) => wrapTreeLine(line, width, theme) ?? wrapTextWithAnsi(line, width),
898
+ );
895
899
 
896
900
  if (displayRows.length === 0) {
897
901
  this.#text.setText(` ${theme.format.bullet} ${theme.fg("toolTitle", theme.bold("Read"))}`);
@@ -17,7 +17,7 @@ import { hexToOklch, hueDistance, logger, oklchCusp, oklchToHex } from "@oh-my-p
17
17
  import { Image, type ImageBudget, imageMaxColumns } from "../components/image";
18
18
  import { fencedCode, type Markdown } from "../components/markdown";
19
19
  import { type CellDimensions, getCellDimensions, getImageDimensions } from "../terminal-capabilities";
20
- import { getThemeEpoch, type ThemeColor, theme } from "../theme";
20
+ import { ensureThemeSync, getThemeEpoch, type ThemeColor, theme } from "../theme";
21
21
  import type { Component } from "../tui";
22
22
  import { closePartialSvg, prepareSvg } from "./svg-source";
23
23
 
@@ -59,6 +59,7 @@ const SERIES_CHROMA = 0.19;
59
59
 
60
60
  /** The active theme's color per figure token, for resolving a figure with `prepareSvg`. */
61
61
  export function svgFigurePalette(): Record<string, string> {
62
+ ensureThemeSync();
62
63
  const palette: Record<string, string> = { surface: theme.getBgHex("selectedBg") };
63
64
  for (const token in PALETTE) palette[token] = theme.getColorHex(PALETTE[token]!);
64
65
  seriesColors(palette.accent!, theme.isLight).forEach((color, at) => {
@@ -181,6 +182,7 @@ export class SvgFigure implements Component {
181
182
  #placeholder: readonly string[] | undefined;
182
183
 
183
184
  constructor(options: SvgFigureOptions) {
185
+ ensureThemeSync();
184
186
  this.#options = options;
185
187
  }
186
188
 
@@ -35,9 +35,12 @@ export function splitSvgFences(markdown: string): FigureSegment[] {
35
35
  let proseStart = 0;
36
36
  let lineStart = 0;
37
37
  while (lineStart <= markdown.length) {
38
- const newline = markdown.indexOf("\n", lineStart);
39
- const lineEnd = newline < 0 ? markdown.length : newline;
40
- const next = newline < 0 ? markdown.length + 1 : newline + 1;
38
+ let lineEnd = lineStart;
39
+ while (lineEnd < markdown.length && markdown[lineEnd] !== "\n" && markdown[lineEnd] !== "\r") lineEnd++;
40
+ const next =
41
+ lineEnd === markdown.length
42
+ ? markdown.length + 1
43
+ : lineEnd + (markdown[lineEnd] === "\r" && markdown[lineEnd + 1] === "\n" ? 2 : 1);
41
44
  const line = markdown.slice(lineStart, lineEnd);
42
45
  if (fence) {
43
46
  if (closesFence(line, fence.marker)) {
@@ -52,6 +52,18 @@ function normalizeOsc8Terminators(text: string): string {
52
52
  return text.replace(OSC8_ST_PREFIX_REGEX, "$1\x07");
53
53
  }
54
54
 
55
+ /**
56
+ * `text` as the streaming renderer lexes it: OSC 8 terminators unified and
57
+ * tabs expanded. {@link Markdown.getLastRenderStableText} is a prefix of this
58
+ * form, not of the raw source, so append-only publishers that mix settled
59
+ * prefixes with whole blocks must compare both in this form. Rendering it is
60
+ * byte-identical to rendering `text`.
61
+ */
62
+ export function streamingMarkdownSource(text: string): string {
63
+ if (!text.includes("\t") && !text.includes("\x1b")) return text;
64
+ return replaceTabs(normalizeOsc8Terminators(text));
65
+ }
66
+
55
67
  /** The longest suffix of `text` a future append could still complete into a
56
68
  * full `\x1b]8;[^\x07\x1b]*\x1b\\` match: the last `\x1b]8;` plus clean
57
69
  * body (or that plus the pending ST-ESC `\x1b`), or a strict prefix of the