@oh-my-pi/pi-tui 15.2.4 → 15.3.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 +13 -0
- package/package.json +3 -3
- package/src/components/editor.ts +8 -2
- package/src/components/input.ts +15 -2
- package/src/utils.ts +17 -1
package/CHANGELOG.md
CHANGED
|
@@ -33,6 +33,19 @@
|
|
|
33
33
|
### Breaking Changes
|
|
34
34
|
|
|
35
35
|
- Increased the minimum required Bun version for the TUI package from >=1.3.7 to >=1.3.14
|
|
36
|
+
- Fixed `TerminalInfo.sendNotification` not delivering desktop notifications on macOS. macOS requires per-app notification permission, which terminal emulators (kitty, ghostty, alacritty, …) almost never have, so OSC 9/99 sequences were silently dropped at the OS layer. `sendNotification` now shells out to `alerter` or `terminal-notifier` when either is on `$PATH` (both register their own LSApplication and ship a "Terminal" / `>_` icon). When neither is installed the dispatch is a deliberate no-op + a single `logger.warn` line on the first miss (subsequent dispatches stay silent) so the user can spot the missing binary in `~/.omp/logs/omp.YYYY-MM-DD.log` and `brew install alerter`. Linux/Windows still go through the OSC/Bell path.
|
|
37
|
+
- Fixed `TerminalInfo.formatNotification` losing OSC 9/99 desktop notifications when running inside tmux. The OSC sequence is now wrapped in tmux's DCS passthrough envelope (`\ePtmux;…\e\\` with embedded ESC bytes doubled) when `TMUX` is set, so notifications reach the parent terminal. `set -g allow-passthrough on` is still required on the tmux side for the wrapped sequence to be forwarded. Bell-only terminals are unchanged.
|
|
38
|
+
- Fixed alerter desktop notifications staying on screen indefinitely. `scripts/mac-alerter.sh` previously passed `--timeout 30` (which makes alerter call `removeDeliveredNotification` after 30 s, also purging the Notification Center entry) and forced Alert-style via `--actions "Open"` (persistent until user click). It now ships Banner-style argv (no `--actions`, no `--timeout`): macOS auto-dismisses the toast after ~10 s and archives the entry to Notification Center for later review. Click-to-focus is preserved through `@CONTENTCLICKED` body clicks. NC archival also requires "Show in Notification Center" enabled for Terminal under macOS System Settings → Notifications.
|
|
39
|
+
- Fixed `composeNotificationSubtitle` showing a stale tmux `pane_title` (typically `π: kitty & tmux` or the cwd prefix written before auto-naming runs) instead of the live OMP session name. The OMP-supplied `fallback` is now consulted first for the pane component; the cached tmux pane title is only used when no session name is available. Window name handling is unchanged.
|
|
40
|
+
- Fixed `sendDesktopNotification` always routing through `alerter` / `terminal-notifier` on darwin, even for terminals (ghostty / iTerm2 / wezterm) that surface OSC 9 / OSC 99 as native notifications through their own bundle. The dispatch now prefers the OSC path on darwin when the terminal advertises native macOS notification capability; the fallback only kicks in for kitty / alacritty / vscode / unknown shells whose host app isn't a notification-capable bundle. This unblocks the user-controlled per-app notification settings flow for ghostty / iTerm2 / wezterm — toast style, NC archival, and click-to-focus all attach to the terminal app's own System Settings entry rather than to `com.apple.Terminal` (which `alerter` would post under).
|
|
41
|
+
- Fixed Korean IME composition leaving a growing horizontal gap between typed jamo and the cursor inside the OMP prompt under tmux + ghostty (and other macOS terminals). `Bun.stringWidth` and the underlying UAX#11 East Asian Width tables classify Hangul Compatibility Jamo (U+3131..U+318E — ㄱ ㄴ ㄷ ㄹ ㅁ ㅂ ㅅ ㅇ ㅈ ㅊ ㅋ ㅌ ㅍ ㅎ + filler) as Wide (2 cells), but every macOS terminal we ship to (Ghostty / Terminal.app / iTerm2) actually renders them as a single cell in monospace fonts. `#extractCursorPosition` was computing `col = visibleWidth(beforeMarker)` and feeding the doubled value to `\x1b[(col+1)G`, placing the hardware cursor (and therefore the IME candidate window) `N_jamo` cells past the visible glyph — exactly the gap the user saw growing as they typed. `visibleWidthRaw` now subtracts 1 cell for each Compatibility Jamo character, returning the column count macOS terminals actually use. Hangul Syllables (U+AC00..U+D7A3, e.g. `안`) stay at 2 cells in both Bun and the terminal — unaffected. Other CJK widths (Chinese / Japanese / Halfwidth Hangul) are unchanged. NOTE: the Rust `pi-natives` width tables (used by `sliceWithWidth` / `truncateToWidth` / `wrapTextWithAnsi`) also count Compatibility Jamo as 2 cells; truncation and word-wrap on jamo-heavy lines will still be slightly aggressive. The defect is invisible in normal use because the AI composes Korean as syllables, not jamo, and users type syllables once IME composition completes. A follow-up will reconcile the Rust side.
|
|
42
|
+
- Fixed a brief black-flash flicker in the TUI when streaming long markdown responses inside tmux (especially noticeable in ghostty with multiple panes open). Root cause: when a markdown fence line above the viewport changed between two streaming tokens (e.g. `` ``` `` → `` ```python ``), `#doRender()` would take the `firstChanged < prevViewportTop` branch and emit `\x1b[2J\x1b[H` (full screen clear + cursor home) wrapped in BSU. The BSU envelope can split across PTY reads, leaving tmux briefly displaying a blank pane before the rest of the buffer arrives — multiplied across panes during repaint. The viewport-above branch now calls a new `viewportRefresh()` helper that does cursor-home + per-line `\x1b[2K` + line content (no `\x1b[2J`), so the visible viewport content is repainted without ever clearing the screen. Scrollback above the viewport may briefly show stale rendering, but only of the SAME lines that just changed — invisible during streaming when the user isn't scrolled up. Other full-redraw paths (resize, first render, etc.) keep the hard `fullRender(true)` behavior unchanged.
|
|
43
|
+
|
|
44
|
+
### Tests
|
|
45
|
+
|
|
46
|
+
- Added `test/no-2k-anywhere.test.ts` — lint guard that scans `packages/tui/src/` for `\x1b[2K` string literals outside comments. The earlier streaming-flicker fix re-introduced the BSU-split flash bug by moving `\x1b[2K`-before-content from `fullRender` to `viewportRefresh` (same anti-pattern in a new location). This test catches that class of regression at CI time so future changes can't silently revive it.
|
|
47
|
+
- Added `test/render-emit-snapshot.test.ts` — four scenario-based byte-snapshot guards (single-line mutation, streaming append, above-viewport mutation triggering `viewportRefresh`, trailing-line clear on shrink). Asserts structural invariants on the EMITTED BYTES from `terminal.write(…)`: no `\x1b[2K`, no `\x1b[2J`, the new content appears, the BSU close `\x1b[?2026l` is present. Catches render-path changes that achieve the right final viewport state via a transient blank frame (which is exactly how the typing-flicker bug slipped past `render-regressions.test.ts`).
|
|
48
|
+
- Added `test/ime-jamo-cursor.test.ts` — six cases asserting the Input component's hardware cursor marker column does not grow at 2× per typed Korean compatibility jamo. Before commit `79e3170c6` typing 14 jamo produced a 14-cell gap between the visible text and the IME candidate window; the test caps the cursor column at `PROMPT_WIDTH + N_jamo` and asserts the per-keystroke delta is at most 1. NOTE: the Rust `pi-natives` `sliceWithWidth` still treats jamo as 2 cells (binary package, follow-up); the test guard accepts a small residual offset but flags the doubling regression.
|
|
36
49
|
|
|
37
50
|
## [14.9.8] - 2026-05-12
|
|
38
51
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.3.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": "15.
|
|
41
|
-
"@oh-my-pi/pi-utils": "15.
|
|
40
|
+
"@oh-my-pi/pi-natives": "15.3.1",
|
|
41
|
+
"@oh-my-pi/pi-utils": "15.3.1",
|
|
42
42
|
"lru-cache": "11.3.6",
|
|
43
43
|
"marked": "^18.0.3"
|
|
44
44
|
},
|
package/src/components/editor.ts
CHANGED
|
@@ -1582,8 +1582,14 @@ export class Editor implements Component, Focusable {
|
|
|
1582
1582
|
return match;
|
|
1583
1583
|
});
|
|
1584
1584
|
|
|
1585
|
-
// Clean the pasted text
|
|
1586
|
-
|
|
1585
|
+
// Clean the pasted text. NFC-normalize so macOS Finder drag-drops of
|
|
1586
|
+
// Korean filenames (which arrive as NFD: e.g. `ᄒ`+`ᅪ` instead of `화`)
|
|
1587
|
+
// land in the buffer as the same precomposed syllables a terminal
|
|
1588
|
+
// renders — without this, cursor column accounting drifts by
|
|
1589
|
+
// `(NFD cells − NFC cells)` and the visible glyph desyncs from the
|
|
1590
|
+
// hardware cursor. Matches the `Input` component's prior fix; this
|
|
1591
|
+
// is the same fix on the real OMP prompt component (`Editor`).
|
|
1592
|
+
const cleanText = decodedText.replace(/\r\n?/g, "\n").normalize("NFC");
|
|
1587
1593
|
|
|
1588
1594
|
// Convert tabs to spaces (4 spaces per tab)
|
|
1589
1595
|
const tabExpandedText = cleanText.replace(/\t/g, " ");
|
package/src/components/input.ts
CHANGED
|
@@ -357,8 +357,21 @@ export class Input implements Component, Focusable {
|
|
|
357
357
|
this.#lastAction = null;
|
|
358
358
|
this.#pushUndo();
|
|
359
359
|
|
|
360
|
-
// Clean the pasted text
|
|
361
|
-
|
|
360
|
+
// Clean the pasted text — remove newlines and carriage returns, normalize
|
|
361
|
+
// tabs, AND normalize Unicode to NFC.
|
|
362
|
+
//
|
|
363
|
+
// NFC normalization rationale: macOS Finder drag-drops file paths in NFD
|
|
364
|
+
// (Conjoining Jamo, U+1100..U+11FF). `Bun.stringWidth` counts each
|
|
365
|
+
// conjoining jamo as a separate cell — a Korean syllable like `화` is
|
|
366
|
+
// 1 char and 2 cells in NFC, but 2 chars and 3 cells in NFD (ᄒ=2 cells
|
|
367
|
+
// + ᅪ=1 cell). The terminal renders the NFD sequence as a single
|
|
368
|
+
// combined syllable (2 cells visible), so the width mismatch shows up
|
|
369
|
+
// as cursor drift past the visible filename — N×~1.5 cells for a path
|
|
370
|
+
// with N Korean syllables. NFC normalization at paste time stores the
|
|
371
|
+
// value in the same form everything else in the codebase assumes.
|
|
372
|
+
const cleanText = replaceTabs(pastedText.replace(/\r\n/g, "").replace(/\r/g, "").replace(/\n/g, "")).normalize(
|
|
373
|
+
"NFC",
|
|
374
|
+
);
|
|
362
375
|
|
|
363
376
|
// Insert at cursor position
|
|
364
377
|
this.#value = this.#value.slice(0, this.#cursor) + cleanText + this.#value.slice(this.#cursor);
|
package/src/utils.ts
CHANGED
|
@@ -100,18 +100,34 @@ export function visibleWidthRaw(str: string): number {
|
|
|
100
100
|
let tabLength = 0;
|
|
101
101
|
const tabWidth = getDefaultTabWidth();
|
|
102
102
|
let isPureAscii = true;
|
|
103
|
+
let jamoOvercount = 0;
|
|
103
104
|
for (let i = 0; i < str.length; i++) {
|
|
104
105
|
const code = str.charCodeAt(i);
|
|
105
106
|
if (code === 9) {
|
|
106
107
|
tabLength += tabWidth;
|
|
107
108
|
} else if (code < 0x20 || code > 0x7e) {
|
|
108
109
|
isPureAscii = false;
|
|
110
|
+
// Hangul Compatibility Jamo (U+3131..U+318E) is EAW=W per UAX#11,
|
|
111
|
+
// so `Bun.stringWidth` returns 2 for each — but every macOS
|
|
112
|
+
// terminal we ship to (Ghostty, Terminal.app, iTerm2) renders
|
|
113
|
+
// them as a single cell in monospace fonts. Without this
|
|
114
|
+
// correction every jamo a Korean IME emits during composition
|
|
115
|
+
// adds 1 cell of drift to `#extractCursorPosition`, displacing
|
|
116
|
+
// the hardware cursor (and therefore the IME candidate window)
|
|
117
|
+
// `N_jamo` cells past the visible glyph. Hangul Syllables
|
|
118
|
+
// (U+AC00..U+D7A3, e.g. `안`) are correctly 2 cells in both Bun
|
|
119
|
+
// and the terminal — leave those alone. The Halfwidth Hangul
|
|
120
|
+
// block (U+FFA0..U+FFDC) is already Narrow in Bun, so no
|
|
121
|
+
// correction needed there.
|
|
122
|
+
if (code >= 0x3131 && code <= 0x318e) {
|
|
123
|
+
jamoOvercount++;
|
|
124
|
+
}
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
127
|
if (isPureAscii) {
|
|
112
128
|
return str.length + tabLength;
|
|
113
129
|
}
|
|
114
|
-
return Bun.stringWidth(str) + tabLength;
|
|
130
|
+
return Bun.stringWidth(str) - jamoOvercount + tabLength;
|
|
115
131
|
}
|
|
116
132
|
|
|
117
133
|
/**
|