@oh-my-pi/pi-tui 16.5.2 → 17.0.1

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 CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.1] - 2026-07-16
6
+
7
+ ### Added
8
+
9
+ - Added native cmux notification delivery targeted to the current terminal surface.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed a tmux regression where every non-Kitty pane was forced into legacy keyboard input, collapsing Ctrl+H into Backspace and Shift+Enter into Enter even with `extended-keys on`; the xterm modifyOtherKeys fallback is requested again so tmux honors or ignores it per its own `extended-keys` setting ([#5620](https://github.com/can1357/oh-my-pi/issues/5620)).
14
+ - Fixed `@` file-reference and path completion falling through incorrectly inside slash command arguments when command-specific argument completion has no matches ([#5580](https://github.com/can1357/oh-my-pi/issues/5580)).
15
+ - Fixed streamed Markdown tables reflowing rows already written to native scrollback when later cells widen a column.
16
+ - Fixed fullscreen session-replacement overlays and resize drags exposing stale normal-buffer frames on terminals without effective DEC 2026: asynchronous replacements now keep their overlay visible until the rebuilt transcript is ready, overlay exit is fused into the destructive paint, and resize viewport frames rewrite the normal buffer without alternate-screen switches. Inconclusive DECRQM probes also no longer disable statically detected synchronized output ([#5319](https://github.com/can1357/oh-my-pi/issues/5319)).
17
+ - Fixed autocomplete popups moving Windows Terminal IME candidate windows away from the prompt by keeping the terminal cursor anchored at the text insertion point ([#4760](https://github.com/can1357/oh-my-pi/issues/4760)).
18
+
19
+ ## [17.0.0] - 2026-07-15
20
+
21
+ ### Added
22
+
23
+ - Improved LaTeX rendering for \underbrace, \overbrace, \overset, \underset, and \stackrel to use drawn horizontal braces with centered labels and stacked annotations instead of flat inline glyphs.
24
+ - Improved LaTeX rendering of multi-letter subscripts and superscripts by displaying them as raised or lowered blocks instead of ragged per-character Unicode glyphs.
25
+ - Added an opt-in Editor.setImeSafeCursorLayout() method to protect macOS IME preedit while retaining the compact bordered layout by default.
26
+
27
+ ### Fixed
28
+
29
+ - Fixed SIXEL image rendering where images with cell heights not divisible by 6 would have their bottom portion overwritten by subsequent content.
30
+ - Fixed an issue where the Kitty OSC 99 desktop-notification capability probe would leak raw text into the terminal pane when running inside a multiplexer like tmux or screen.
31
+
5
32
  ## [16.5.2] - 2026-07-14
6
33
 
7
34
  ### Fixed
@@ -92,6 +92,8 @@ export declare class Editor implements Component, Focusable {
92
92
  * Use the real terminal cursor instead of rendering a cursor glyph.
93
93
  */
94
94
  setUseTerminalCursor(useTerminalCursor: boolean): void;
95
+ /** Render a dedicated bottom border so terminal-local IME preedit cannot shift editor chrome. */
96
+ setImeSafeCursorLayout(enabled: boolean): void;
95
97
  getUseTerminalCursor(): boolean;
96
98
  setMaxHeight(maxHeight: number | undefined): void;
97
99
  setPaddingX(paddingX: number): void;
@@ -1,5 +1,5 @@
1
1
  import type { SymbolTheme } from "../symbols.js";
2
- import type { Component } from "../tui.js";
2
+ import type { Component, NativeScrollbackCommittedRows, NativeScrollbackReplay } from "../tui.js";
3
3
  /** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
4
4
  export declare function clearRenderCache(): void;
5
5
  /**
@@ -47,7 +47,7 @@ export interface MarkdownTheme {
47
47
  resolveMermaidAscii?: (source: string, maxWidth?: number) => string | null;
48
48
  symbols: SymbolTheme;
49
49
  }
50
- export declare class Markdown implements Component {
50
+ export declare class Markdown implements Component, NativeScrollbackCommittedRows, NativeScrollbackReplay {
51
51
  #private;
52
52
  setIgnoreTight(ignore: boolean): this;
53
53
  constructor(text: string, paddingX: number, paddingY: number, theme: MarkdownTheme, defaultTextStyle?: DefaultTextStyle, codeBlockIndent?: number);
@@ -65,6 +65,14 @@ export declare class Markdown implements Component {
65
65
  * lineage), and on cache-served non-streaming renders.
66
66
  */
67
67
  getLastRenderSettledRows(): number;
68
+ /**
69
+ * Freeze every table whose first physical row is already part of the native
70
+ * scrollback prefix. The recorded widths came from the exact frame that was
71
+ * just emitted, so the next streamed delta cannot retroactively widen it.
72
+ */
73
+ setNativeScrollbackCommittedRows(rows: number): void;
74
+ /** A destructive replay removes the immutable tape this layout was guarding. */
75
+ prepareNativeScrollbackReplay(): void;
68
76
  render(width: number): readonly string[];
69
77
  }
70
78
  /**
@@ -67,9 +67,11 @@ export interface Terminal {
67
67
  get appearance(): TerminalAppearance | undefined;
68
68
  /**
69
69
  * Register a callback fired once per DEC private mode when its DECRQM support
70
- * status resolves. Optional: only real terminals implement capability probing.
70
+ * status resolves. `confirmed` is false when the terminal answered the DA1
71
+ * sentinel without answering DECRQM, which proves only that querying support
72
+ * is unavailable — not that the private mode itself is unsupported.
71
73
  */
72
- onPrivateModeReport?(callback: (mode: number, supported: boolean) => void): void;
74
+ onPrivateModeReport?(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void;
73
75
  }
74
76
  /**
75
77
  * True when stdout flows through a ConPTY pseudo-console (native win32, or
@@ -91,7 +93,7 @@ export declare class ProcessTerminal implements Terminal {
91
93
  get keyboardEnhancementExitSequence(): string | null;
92
94
  get appearance(): TerminalAppearance | undefined;
93
95
  onAppearanceChange(callback: (appearance: TerminalAppearance) => void): void;
94
- onPrivateModeReport(callback: (mode: number, supported: boolean) => void): void;
96
+ onPrivateModeReport(callback: (mode: number, supported: boolean, confirmed?: boolean) => void): void;
95
97
  start(onInput: (data: string) => void, onResize: () => void): void;
96
98
  drainInput(maxMs?: number, idleMs?: number): Promise<void>;
97
99
  stop(): void;
@@ -251,7 +251,7 @@ export interface OverlayHandle {
251
251
  /**
252
252
  * Container - a component that contains other components
253
253
  */
254
- export declare class Container implements Component {
254
+ export declare class Container implements Component, NativeScrollbackCommittedRows, NativeScrollbackReplay {
255
255
  #private;
256
256
  children: Component[];
257
257
  setIgnoreTight(ignore: boolean): this;
@@ -267,6 +267,16 @@ export declare class Container implements Component {
267
267
  * {@link clear} for that). Idempotent per child via each child's own dispose.
268
268
  */
269
269
  dispose(): void;
270
+ /**
271
+ * Split the committed prefix from the container's most recently rendered
272
+ * rows across its children. The memoized child arrays are the exact geometry
273
+ * that produced that frame; when the child list was invalidated or rebuilt,
274
+ * there is no safe old-to-new coordinate mapping, so propagation waits for
275
+ * the next render/post-emit publication.
276
+ */
277
+ setNativeScrollbackCommittedRows(rows: number): void;
278
+ /** Recursively discard layout locks that are meaningful only to the old tape. */
279
+ prepareNativeScrollbackReplay(): void;
270
280
  render(width: number): readonly string[];
271
281
  }
272
282
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "16.5.2",
4
+ "version": "17.0.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "16.5.2",
41
- "@oh-my-pi/pi-utils": "16.5.2",
40
+ "@oh-my-pi/pi-natives": "17.0.1",
41
+ "@oh-my-pi/pi-utils": "17.0.1",
42
42
  "lru-cache": "11.5.2",
43
43
  "marked": "^18.0.6"
44
44
  },
@@ -464,10 +464,9 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
464
464
  // path (`/tmp/fo` at prompt start, `see /tmp` mid-prompt); fall
465
465
  // through to file-path completion.
466
466
  } else if (!isMidPromptSkillLookup) {
467
- // Submitted slash commands own their argument text only when the
468
- // matched command accepts args. No-arg slash-looking prompts such
469
- // as `/settings @file` still fall through to prompt-composer
470
- // completions because submit treats them as normal prompt text.
467
+ // Give matched commands first chance to complete arguments, then
468
+ // fall through to prompt-composer file completion when they have
469
+ // no argument provider or it has no matches.
471
470
  const commandName = commandText.slice(1, spaceIndex); // Command without "/"
472
471
  const argumentText = commandText.slice(spaceIndex + 1); // Text after space
473
472
 
@@ -475,20 +474,19 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
475
474
  if (command && "allowArgs" in command && command.allowArgs === false && !/\S/.test(argumentText)) {
476
475
  return null;
477
476
  }
478
- if (command && (!("allowArgs" in command) || command.allowArgs !== false)) {
479
- if (!("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
480
- return null; // No argument completion for this command
481
- }
482
-
477
+ if (
478
+ command &&
479
+ (!("allowArgs" in command) || command.allowArgs !== false) &&
480
+ "getArgumentCompletions" in command &&
481
+ command.getArgumentCompletions
482
+ ) {
483
483
  const argumentSuggestions = await command.getArgumentCompletions(argumentText);
484
- if (!Array.isArray(argumentSuggestions) || argumentSuggestions.length === 0) {
485
- return null;
484
+ if (Array.isArray(argumentSuggestions) && argumentSuggestions.length > 0) {
485
+ return {
486
+ items: argumentSuggestions,
487
+ prefix: argumentText,
488
+ };
486
489
  }
487
-
488
- return {
489
- items: argumentSuggestions,
490
- prefix: argumentText,
491
- };
492
490
  }
493
491
  }
494
492
  }
@@ -382,6 +382,7 @@ export class Editor implements Component, Focusable {
382
382
 
383
383
  #theme: EditorTheme;
384
384
  #useTerminalCursor = false;
385
+ #imeSafeCursorLayout = false;
385
386
 
386
387
  /** When set, replaces the normal cursor glyph at end-of-text with this ANSI-styled string. */
387
388
  cursorOverride: string | undefined;
@@ -539,6 +540,11 @@ export class Editor implements Component, Focusable {
539
540
  this.#useTerminalCursor = useTerminalCursor;
540
541
  }
541
542
 
543
+ /** Render a dedicated bottom border so terminal-local IME preedit cannot shift editor chrome. */
544
+ setImeSafeCursorLayout(enabled: boolean): void {
545
+ this.#imeSafeCursorLayout = enabled;
546
+ }
547
+
542
548
  getUseTerminalCursor(): boolean {
543
549
  return this.#useTerminalCursor;
544
550
  }
@@ -852,8 +858,9 @@ export class Editor implements Component, Focusable {
852
858
  }
853
859
 
854
860
  // Render each layout line
855
- // Emit hardware cursor marker only when focused and not showing autocomplete
856
- const emitCursorMarker = this.focused && !this.#autocompleteState;
861
+ // Keep the hardware cursor at the text insertion point while autocomplete
862
+ // rows render below it; terminals use that position to anchor IME candidates.
863
+ const emitCursorMarker = this.focused;
857
864
  const lineContentWidth = contentAreaWidth;
858
865
 
859
866
  // Compute inline hint text (dim ghost text after cursor)
@@ -866,6 +873,7 @@ export class Editor implements Component, Focusable {
866
873
  let displayWidth = visibleWidth(layoutLine.text);
867
874
  let cursorPaddingOverflow = 0;
868
875
  let decorated = false;
876
+ let imeSafeCursorTail = false;
869
877
  const showPromptGutter = promptGutter !== undefined && visibleIndex === 0;
870
878
  const gutterText =
871
879
  promptGutter === undefined ? "" : showPromptGutter ? promptGutter.firstLine : promptGutter.continuation;
@@ -922,7 +930,13 @@ export class Editor implements Component, Focusable {
922
930
  if (marker) {
923
931
  const before = displayText.slice(0, layoutLine.cursorPos);
924
932
  const after = displayText.slice(layoutLine.cursorPos);
925
- if (after.length === 0 && inlineHint) {
933
+ if (this.#imeSafeCursorLayout && after.length === 0 && borderVisible) {
934
+ // Terminal frontends render IME marked text locally before committed bytes
935
+ // reach the application. Keep the end-of-input cursor row empty to its
936
+ // right so that insertion cannot shift box chrome onto the next row.
937
+ displayText = before + marker;
938
+ imeSafeCursorTail = true;
939
+ } else if (after.length === 0 && inlineHint) {
926
940
  const availWidth = Math.max(0, lineContentWidth - displayWidth);
927
941
  const hintText = hintStyle(truncateToWidth(inlineHint, availWidth));
928
942
  displayText = before + marker + hintText;
@@ -1022,6 +1036,15 @@ export class Editor implements Component, Focusable {
1022
1036
  // trailing `─`, but never the corner/vertical bar itself.
1023
1037
  const isLastLine = visibleIndex === visibleLayoutLines.length - 1;
1024
1038
  const rightChromeCells = Math.max(1, paddingX + 1 - cursorPaddingOverflow);
1039
+ if (isLastLine && imeSafeCursorTail) {
1040
+ const leftBorder = this.borderColor(`${box.vertical}${padding(paddingX)}`);
1041
+ const bottomBorder = this.borderColor(
1042
+ `${box.bottomLeft}${box.horizontal.repeat(Math.max(0, width - 2))}${box.bottomRight}`,
1043
+ );
1044
+ result.push(leftBorder + displayText);
1045
+ result.push(bottomBorder);
1046
+ continue;
1047
+ }
1025
1048
  if (isLastLine) {
1026
1049
  const rightPad = Math.max(0, rightChromeCells - 2);
1027
1050
  const includeHorizontal = rightChromeCells >= 2;