jeo-code 0.8.31 → 0.8.33
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 +29 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/commands/launch.ts +8 -8
- package/src/tui/components/input-box.ts +7 -1
- package/src/tui/components/status.ts +9 -2
- package/src/tui/components/welcome.ts +11 -1
- package/src/tui/components/width.ts +85 -6
- package/src/tui/renderer.ts +23 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
|
|
8
8
|
|
|
9
|
+
## [0.8.33] - 2026-07-20
|
|
10
|
+
_Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Narrow-terminal overflow across three width-floor call sites, the actual root cause of the live-reproduced corruption** — `src/tui/components/status.ts`'s `renderStatusBar` and its internal `leftBudget`, and `src/tui/components/input-box.ts`'s `renderInputFrame`, each clamped their `cols` UP to a fixed floor (24, 24, 8) regardless of the REAL terminal width, so a genuinely narrow terminal (e.g. 20 columns) still received a 24-column-wide status bar / input box. A real terminal hard-wraps that overflow, which desynced the idle-prompt footer's own row bookkeeping on every subsequent repaint — reproduced live via tmux (`resize-window` down + repeated keystrokes) as a stack of duplicate `⬢ model … ▦ ctx%` lines that grew by one on every keystroke, and confirmed via a byte-for-byte replay of the exact captured ANSI sequences through `cat` into a fresh tmux pane with jeo entirely absent — proving the bug lived in the emitted bytes themselves, not timing or readline interaction. All three floors now clamp to a minimum of 1, degrading gracefully (a small, correctly-sized box/bar) instead of overflowing.
|
|
14
|
+
- **`src/tui/components/welcome.ts`'s narrow (`cols < 30`) one-line fallback no longer overflows** — an unbounded `jeo v{version} · {model}` line previously overflowed a real narrow terminal by up to 47 columns with a realistic provider-qualified model id (e.g. `antigravity/claude-sonnet-4-6 (antigravity)`). Now truncated to `cols - 1` — the SAME "leave the last column free" margin the boxed banner already uses elsewhere in this file, deliberately NOT `cols` exactly: a line filled to the exact terminal width followed by its own trailing newline is ambiguous to real terminals (pending-autowrap vs. explicit-LF), which independently reproduced the same duplicate-line corruption even after the overflow itself was fixed.
|
|
15
|
+
- **`src/tui/renderer.ts`'s reserve-mode resize path no longer scrolls the terminal on a shrink** — `Renderer.render()`'s resize branch called `clear()` unconditionally, which zeroes `coverRows`/`prev` as an independent write; the SAME call's own reserve-block then read that now-wrong occupancy (0) and spuriously inserted real `"\n"` characters even when the post-resize frame was SHORTER than the pre-resize one. Literal `"\n"` genuinely scrolls a real terminal, corrupting whatever sits above the live frame and permanently desyncing the renderer's row math. Reserve mode now uses the same `reset()`-style bookkeeping the periodic self-heal resync already relies on (remembers the occupied rows via `coverRows`, no output) instead of an immediate `clear()`; non-reserve (alt-screen/pipe) mode is unaffected and keeps its defensive full clear. Reproduced deterministically with a standalone `Renderer` unit harness (no tmux/timing involved) before and after the fix.
|
|
16
|
+
|
|
17
|
+
### Verified
|
|
18
|
+
- `test/status-bar.test.ts`, `test/input-box.test.ts` — new regression tests: neither ever emits a line wider than a realistically narrow `cols` (5 through 23), closing the exact floor-overflow class found live.
|
|
19
|
+
- `test/tui-welcome.test.ts` — updated + new tests: the narrow fallback is now `cols - 1`-capped (never overflows, even with a long provider-qualified model id).
|
|
20
|
+
- `test/tui-renderer.test.ts` — new regression test: a reserve-mode resize down to a shorter frame never emits `"\n"`, and a second render right after stays clean (no lingering desync).
|
|
21
|
+
- Full live tmux reproduction cycle for BOTH the idle-prompt path and the mid-turn `LaunchTui` path: resized a real `jeo --tmux` session down (e.g. 90×26 → 55×18, 80×24 → 20×10) and typed multiple keystrokes after each — before this pass, the idle-prompt footer visibly duplicated once per keystroke; after, it stays clean across repeated keystrokes at both a moderate (55×18) and an extreme (20×10) narrow width, confirmed via direct pane capture, not just unit assertions.
|
|
22
|
+
- Full `bun test` — 3097 pass / 0 fail across 304 files.
|
|
23
|
+
- `bun run typecheck` — no errors.
|
|
24
|
+
|
|
25
|
+
## [0.8.32] - 2026-07-20
|
|
26
|
+
_gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected._
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Display-width no longer over/under-counts emoji sequences, so box borders and hard-wraps never misalign when one appears in a message** (`src/tui/components/width.ts`) — gjc parity (`#1996` "preserve … Unicode grapheme semantics for … VS16 emoji presentation, ZWJ, keycaps, and emoji modifiers"). The old `visibleWidth`/`truncateToWidth` summed `charWidth()` per CODE POINT, which is wrong for any sequence a terminal renders as ONE glyph: `❤️` (heart + VS16) summed to 1 column where terminals render 2; `👍🏽` (thumbs-up + Fitzpatrick skin-tone modifier) summed to 4 where terminals render 2; a 4-person ZWJ family emoji (`👨👩👧👦`) summed to 8 where terminals render 2 — quantified live against the actual old per-code-point path before fixing (not asserted from inference). New `nextGraphemeCluster()` absorbs VS15/VS16, Fitzpatrick modifiers (`U+1F3FB`-`U+1F3FF`), keycap combiners (`U+20E3`), combining marks, and ZWJ-joined chains into one atomic width-2 (or width-1 for VS15 text-presentation) unit; `visibleWidth`/`truncateToWidth` (and therefore `wrapTextWithAnsi`, built on both) now consume it instead of raw per-code-point `charWidth()`, and `truncateToWidth` can no longer split one of these sequences in half at a wrap/truncation boundary (a second corruption vector the old code point-by-point cut was exposed to). Korean/CJK/Hangul-only text (no emoji sequences) was already correct and is byte-identical before/after.
|
|
30
|
+
|
|
31
|
+
### Verified
|
|
32
|
+
- `test/width.test.ts` — 11 pass (was 9): new `nextGraphemeCluster` unit coverage for the VS15/VS16/Fitzpatrick/keycap/ZWJ cases above, plus a `truncateToWidth` atomicity test proving a 2-wide cluster is dropped whole (never half) at a 1-column budget.
|
|
33
|
+
- Live (unmocked) reproduction: quantified the OLD per-code-point sum against the NEW `visibleWidth` for all 4 sequence classes — heart 1→2, thumbs-up+modifier 4→2, ZWJ family 8→2, keycap 1→2 — confirming this was a real, sometimes-severe (family emoji off by 6 columns) miscount, not a hypothetical.
|
|
34
|
+
- Live (unmocked) box-render smoke test: wrapped a real mixed Korean/emoji message (family emoji, VS16 heart, skin-tone thumbs-up) into a 20-column box via `wrapTextWithAnsi` — every wrapped line's `visibleWidth` fit the declared budget with correct padding, confirming no overflow/misalignment end-to-end (not just at the unit level).
|
|
35
|
+
- Full `bun test` — 3092 pass / 0 fail across 304 files (2 transient parallel-load flakes on the first run — unrelated `/model` pin routing + a Node `--inspect-brk` debug-session test, neither touching `width.ts` — both isolated-pass and a full clean re-run confirmed no regression).
|
|
36
|
+
- `bun run typecheck` — no errors.
|
|
37
|
+
|
|
9
38
|
## [0.8.31] - 2026-07-20
|
|
10
39
|
_`web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else._
|
|
11
40
|
|
package/README.ja.md
CHANGED
|
@@ -283,11 +283,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
283
283
|
## 変更履歴 (Changelog)
|
|
284
284
|
|
|
285
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
|
|
287
|
+
- **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
|
|
286
288
|
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
287
289
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
288
290
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
289
|
-
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
290
|
-
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
291
291
|
|
|
292
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
293
293
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -284,11 +284,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
284
284
|
## 변경 이력 (Changelog)
|
|
285
285
|
|
|
286
286
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
287
|
+
- **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
|
|
288
|
+
- **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
|
|
287
289
|
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
288
290
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
289
291
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
290
|
-
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
291
|
-
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
292
292
|
|
|
293
293
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
294
294
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -291,11 +291,11 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
291
291
|
## Changelog
|
|
292
292
|
|
|
293
293
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
294
|
+
- **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
|
|
295
|
+
- **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
|
|
294
296
|
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
295
297
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
296
298
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
297
|
-
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
298
|
-
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
299
299
|
|
|
300
300
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
301
301
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -283,11 +283,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
283
283
|
## 更新日志 (Changelog)
|
|
284
284
|
|
|
285
285
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
286
|
+
- **[0.8.33]** (2026-07-20) — Real terminal-resize testing (repeated tmux resize-window + keystroke reproductions, byte-for-byte ANSI replay outside jeo/tmux to isolate root causes deterministically) uncovered and closed a progressive screen-corruption bug: resizing down to a narrow terminal (e.g. a ~20-column tmux pane) produced a growing stack of duplicate status-bar lines on every subsequent keystroke, and a separate, independently-reproducible scroll bug in the mid-turn live renderer.
|
|
287
|
+
- **[0.8.32]** (2026-07-20) — gjc parity: TUI display-width/wrapping is now grapheme-cluster-aware for emoji sequences (VS16 presentation, skin-tone modifiers, keycaps, ZWJ-joined emoji like family/profession glyphs), fixing box-border and wrap misalignment ("깨짐") that the old per-code-point width summation produced whenever such a sequence appeared in a message — Korean/CJK-only text was already correctly handled and is unaffected.
|
|
286
288
|
- **[0.8.31]** (2026-07-20) — `web_search` no longer silently degrades every non-Anthropic model to keyless DuckDuckGo scraping — OpenAI and Gemini sessions now get their own native, hosted search tool (matching the same active-model-gated, credential-required design Anthropic already had), with DuckDuckGo remaining the always-on terminal fallback for everyone else.
|
|
287
289
|
- **[0.8.30]** (2026-07-20) — `bun run build` (the host-only dev binary build) has been silently broken for every user who ran it — a drift between the plain `package.json` `build` script and the actual working release-binary build command (`scripts/ci-release-build-binaries.ts`), which had already documented and fixed the exact same crash.
|
|
288
290
|
- **[0.8.29]** (2026-07-20) — gajae-code (gjc) v0.10.2→v0.11.4 gap analysis: most of that range is gjc-specific architecture jeo intentionally does not replicate (SDK broker/session-index recovery, Gajae Pet, coordinator-mcp, Telegram/Discord rich-delivery internals, browser tab workers, worktree-subcommand removal, RPC durable model selection) — one genuinely applicable TUI bug was found and closed; the CLI empty-non-TTY-stdin fix (gjc #2586) was investigated and found already-correct by construction (jeo's one-shot gate forces true whenever stdin isn't a TTY, regardless of args, so the hang gjc fixed cannot occur here).
|
|
289
|
-
- **[0.8.28]** (2026-07-16) — Improved the welcome UI's right-side table to display dynamic sections (What's New, Flow keys, Project pulse, and Session trail) matching gajae-code (gjc) >= 0.8.0 features.
|
|
290
|
-
- **[0.8.27]** (2026-07-16) — Repeat-read recovery now survives both context-overflow and refusal-result elision without reopening mutating-call loops.
|
|
291
291
|
|
|
292
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
293
293
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
package/src/commands/launch.ts
CHANGED
|
@@ -764,7 +764,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
764
764
|
* mid-turn TUI binding): full last reply + full last tool output. */
|
|
765
765
|
const composeDetailLines = (): string[] => {
|
|
766
766
|
if (!lastReply && !lastToolDetail) return [];
|
|
767
|
-
const sep = "─".repeat(Math.min(48, Math.max(
|
|
767
|
+
const sep = "─".repeat(Math.min(48, Math.max(1, (process.stdout.columns ?? 80) - 1)));
|
|
768
768
|
const toolDetail = lastToolDetail
|
|
769
769
|
? [sep, `detail · full last tool output (${lastToolDetail.tool})`, sep, ...lastToolDetail.output.split("\n").slice(0, 2000).map(sanitizeForTerminal)]
|
|
770
770
|
: [];
|
|
@@ -816,7 +816,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
816
816
|
// exact prior conversation instead of a bare message count.
|
|
817
817
|
if (messages.length > 0) {
|
|
818
818
|
resumedWithHistory = true;
|
|
819
|
-
const sep = "─".repeat(Math.min(48, Math.max(
|
|
819
|
+
const sep = "─".repeat(Math.min(48, Math.max(1, (process.stdout.columns ?? 80) - 1)));
|
|
820
820
|
console.log([sep, ...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }), sep].join("\n"));
|
|
821
821
|
}
|
|
822
822
|
|
|
@@ -880,7 +880,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
880
880
|
// screen instead of just a bare message count.
|
|
881
881
|
if (messages.length > 0) {
|
|
882
882
|
resumedWithHistory = true;
|
|
883
|
-
const sep = "─".repeat(Math.min(48, Math.max(
|
|
883
|
+
const sep = "─".repeat(Math.min(48, Math.max(1, (process.stdout.columns ?? 80) - 1)));
|
|
884
884
|
console.log([sep, ...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }), sep].join("\n"));
|
|
885
885
|
}
|
|
886
886
|
|
|
@@ -2209,7 +2209,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2209
2209
|
// so it never repeats. Screen-safe: prints BEFORE the prompt is armed.
|
|
2210
2210
|
try {
|
|
2211
2211
|
const whatsNew = await consumeLaunchWhatsNew({
|
|
2212
|
-
cols: Math.min(100, Math.max(
|
|
2212
|
+
cols: Math.min(100, Math.max(1, (process.stdout.columns ?? 80) - 2)),
|
|
2213
2213
|
unicode: supportsUnicode(),
|
|
2214
2214
|
color: welcomeTheme.color,
|
|
2215
2215
|
});
|
|
@@ -2224,7 +2224,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2224
2224
|
{
|
|
2225
2225
|
const card = formatForgeBox(
|
|
2226
2226
|
{ title: "[skill]", lines: skillInvocationCard(skill, intent) },
|
|
2227
|
-
{ width: scaleForgeWidth(Math.min(100, Math.max(
|
|
2227
|
+
{ width: scaleForgeWidth(Math.min(100, Math.max(1, (process.stdout.columns ?? 80) - 2))), unicode: supportsUnicode(), paint: accentPaint(uiTheme), paintShadow: accentShadowPaint(uiTheme), color: uiTheme.color },
|
|
2228
2228
|
);
|
|
2229
2229
|
logLines(card);
|
|
2230
2230
|
}
|
|
@@ -2729,7 +2729,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2729
2729
|
let lastFooterKey = "";
|
|
2730
2730
|
const logLines = (lines: string | string[]) => {
|
|
2731
2731
|
const arr = Array.isArray(lines) ? lines : [lines];
|
|
2732
|
-
const cols = Math.max(
|
|
2732
|
+
const cols = Math.max(1, (process.stdout.columns ?? 80) - 1);
|
|
2733
2733
|
for (const line of arr) {
|
|
2734
2734
|
console.log(truncateAnsi(line, cols));
|
|
2735
2735
|
}
|
|
@@ -2934,7 +2934,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2934
2934
|
});
|
|
2935
2935
|
};
|
|
2936
2936
|
const previewLines = (line: string, selected = -1): string[] => {
|
|
2937
|
-
const cols = Math.max(
|
|
2937
|
+
const cols = Math.max(1, (process.stdout.columns ?? 80) - 1);
|
|
2938
2938
|
// Caret offset comes from readline's live cursor when it matches the rendered
|
|
2939
2939
|
// line (arrow keys/Home/End move it); otherwise (history nav mismatch) caret
|
|
2940
2940
|
// sits at the end of the text.
|
|
@@ -2983,7 +2983,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2983
2983
|
// with ↑/↓ counters. Recomputes the scroll bound/page size each paint so scroll
|
|
2984
2984
|
// keys can clamp correctly. Mirrors the mid-turn LaunchTui panel.
|
|
2985
2985
|
const historyPreviewLines = (detail: string[]): string[] => {
|
|
2986
|
-
const cols = Math.max(
|
|
2986
|
+
const cols = Math.max(1, (process.stdout.columns ?? 80) - 1);
|
|
2987
2987
|
const physical = detail.flatMap(line => line.split("\n")).map(line => truncateAnsi(line, cols));
|
|
2988
2988
|
const bodyLimit = Math.max(1, footerRows - 2); // status bar + title rows
|
|
2989
2989
|
const scrollable = physical.length > bodyLimit;
|
|
@@ -107,7 +107,13 @@ function normalizeHighlights(
|
|
|
107
107
|
* can park the REAL terminal cursor right after `>` — moving with the arrow keys.
|
|
108
108
|
*/
|
|
109
109
|
export function renderInputFrame(line: string, opts: InputBoxOptions = {}): InputFrame {
|
|
110
|
-
|
|
110
|
+
// Never clamp UP past what the caller reports as the real terminal width — a
|
|
111
|
+
// fixed floor here (previously 24) silently overflowed any narrower terminal
|
|
112
|
+
// (e.g. a resize down to 20 cols still drew a 24-col box), which real terminals
|
|
113
|
+
// then hard-wrap/split at the boundary (the exact box-border corruption a real
|
|
114
|
+
// resize-down reproduces live). Degrading to a very small, ugly-but-correctly-
|
|
115
|
+
// sized box is strictly better than one that overflows and gets torn.
|
|
116
|
+
const cols = Math.max(1, Math.trunc(opts.cols ?? 80));
|
|
111
117
|
const useColor = opts.color !== false;
|
|
112
118
|
const placeholder = opts.placeholder ?? "Type your message...";
|
|
113
119
|
const bodyWidth = Math.max(1, cols - 4);
|
|
@@ -66,7 +66,14 @@ function shortenCwd(cwd: string, maxLen: number, unicode: boolean): string {
|
|
|
66
66
|
export function renderStatusBar(d: StatusBarData): string {
|
|
67
67
|
const unicode = d.unicode !== false;
|
|
68
68
|
const useColor = d.color !== false;
|
|
69
|
-
|
|
69
|
+
// Never clamp UP past the caller's real terminal width — a fixed floor here
|
|
70
|
+
// (previously 24) silently overflowed any narrower terminal (e.g. a resize down
|
|
71
|
+
// to 20 cols still emitted a 24-col status bar), which a real terminal then
|
|
72
|
+
// hard-wraps at the boundary and desyncs the idle-footer's own row bookkeeping
|
|
73
|
+
// on every subsequent repaint (reproduced live: a growing stack of duplicate
|
|
74
|
+
// status-bar lines on each keystroke after a resize-down, root-caused to this
|
|
75
|
+
// exact overflow via a byte-for-byte ANSI replay outside jeo/tmux entirely).
|
|
76
|
+
const cols = Math.max(1, Math.trunc(d.cols));
|
|
70
77
|
const sep = " / ";
|
|
71
78
|
|
|
72
79
|
// Right side first — it has priority and fixed width.
|
|
@@ -97,7 +104,7 @@ export function renderStatusBar(d: StatusBarData): string {
|
|
|
97
104
|
bits.push(`${unicode ? "⑂" : "y"} ${d.branch}${dirty}`);
|
|
98
105
|
}
|
|
99
106
|
// Budget the cwd into whatever width remains (right stats + 2-col gap reserved).
|
|
100
|
-
const leftBudget = Math.max(
|
|
107
|
+
const leftBudget = Math.max(1, cols - rightWidth - (rightWidth > 0 ? 2 : 0));
|
|
101
108
|
if (d.cwd) {
|
|
102
109
|
const used = visibleWidth(` ${bits.join(sep)}${sep}${unicode ? "▸" : ">"} `);
|
|
103
110
|
const room = leftBudget - used - 1;
|
|
@@ -185,7 +185,17 @@ export function renderWelcome(d: WelcomeData): string[] {
|
|
|
185
185
|
const useColor = d.color !== false;
|
|
186
186
|
|
|
187
187
|
if (cols < 30) {
|
|
188
|
-
|
|
188
|
+
// Narrow-terminal one-line fallback — MUST itself be width-capped: an unbounded
|
|
189
|
+
// `jeo v{version} · {model}` overflows badly with a realistic provider-qualified
|
|
190
|
+
// model id (e.g. "antigravity/claude-sonnet-4-6 (antigravity)" overflows a 10-col
|
|
191
|
+
// terminal by 47 columns), which is exactly the box-border/wrap misalignment a
|
|
192
|
+
// real terminal-resize-down reproduces live. `cols - 1` (not `cols`) — same
|
|
193
|
+
// "leave the last column free" convention as the boxed banner below: a line
|
|
194
|
+
// truncated to EXACTLY `cols` is ambiguous to real terminals (a full-width row
|
|
195
|
+
// followed by this line's own trailing "\n" can double-advance a row via the
|
|
196
|
+
// pending-autowrap/explicit-LF ambiguity), which is a second, independent
|
|
197
|
+
// corruption vector reproduced live — not just a cosmetic nicety.
|
|
198
|
+
return [truncate(`jeo v${d.version} · ${d.model}`, Math.max(0, cols - 1))];
|
|
189
199
|
}
|
|
190
200
|
|
|
191
201
|
// The banner fills the full terminal width (gjc forge: flush with the input box and
|
|
@@ -58,6 +58,85 @@ export function charWidth(cp: number): number {
|
|
|
58
58
|
}
|
|
59
59
|
return 1;
|
|
60
60
|
}
|
|
61
|
+
const ZWJ = 0x200d;
|
|
62
|
+
const VS15 = 0xfe0e; // variation selector: force TEXT (narrow) presentation
|
|
63
|
+
const VS16 = 0xfe0f; // variation selector: force EMOJI (wide) presentation
|
|
64
|
+
const FITZPATRICK_MIN = 0x1f3fb; // Emoji Modifier Fitzpatrick Type-1-2 .. Type-6
|
|
65
|
+
const FITZPATRICK_MAX = 0x1f3ff;
|
|
66
|
+
const KEYCAP_COMBINING = 0x20e3; // combining enclosing keycap (1️⃣, #️⃣, *️⃣)
|
|
67
|
+
|
|
68
|
+
function isFitzpatrick(cp: number): boolean {
|
|
69
|
+
return cp >= FITZPATRICK_MIN && cp <= FITZPATRICK_MAX;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** True for combining/zero-width marks that attach to a preceding base rather than
|
|
73
|
+
* starting a new cluster on their own (diacritics etc.) — the zero-width branch of
|
|
74
|
+
* `charWidth` MINUS variation selectors (VS15/VS16 are handled explicitly above,
|
|
75
|
+
* since they change the CLUSTER's width rather than merely contributing 0 to it). */
|
|
76
|
+
function isAttachableCombining(cp: number): boolean {
|
|
77
|
+
return (cp >= 0x0300 && cp <= 0x036f)
|
|
78
|
+
|| (cp >= 0x1ab0 && cp <= 0x1aff)
|
|
79
|
+
|| (cp >= 0x1dc0 && cp <= 0x1dff)
|
|
80
|
+
|| (cp >= 0x20d0 && cp <= 0x20ff)
|
|
81
|
+
|| cp === 0xfeff;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function codePointAndLength(s: string, i: number): { cp: number; length: number } {
|
|
85
|
+
const cp = s.codePointAt(i)!;
|
|
86
|
+
return { cp, length: cp > 0xffff ? 2 : 1 };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the visual CLUSTER starting at `s[i]` (not an SGR escape or tab — callers
|
|
91
|
+
* handle those separately) into its total consumed UTF-16 length and DISPLAY width,
|
|
92
|
+
* absorbing variation selectors, skin-tone modifiers, keycap combiners, combining
|
|
93
|
+
* marks, and ZWJ-joined sequences into ONE atomic unit (gjc parity: "preserve …
|
|
94
|
+
* Unicode grapheme semantics for … Korean tone marks, VS16 emoji presentation, ZWJ,
|
|
95
|
+
* keycaps, and emoji modifiers").
|
|
96
|
+
*
|
|
97
|
+
* Per-code-point summing (the old behavior) over- or under-counts every one of these:
|
|
98
|
+
* 👍🏽 (thumbs-up + Fitzpatrick modifier) summed to 4 columns where a terminal renders
|
|
99
|
+
* ONE 2-wide glyph; ❤️ (heart + VS16) summed to 1 where a terminal renders 2; a 4-person
|
|
100
|
+
* ZWJ family emoji summed to 8 where a terminal renders 2. Every one of those is a real,
|
|
101
|
+
* visible box-border/wrap misalignment ("깨짐") the instant such a sequence appears in a
|
|
102
|
+
* message — this collapses each sequence to the width a terminal ACTUALLY renders it at,
|
|
103
|
+
* atomically, so `truncateToWidth`/`wrapTextWithAnsi` also never split one in half at a
|
|
104
|
+
* boundary (this is a pragmatic subset of UAX #29 grapheme-cluster breaking scoped to
|
|
105
|
+
* the sequences a chat/coding TUI actually sees — not a general script-shaping engine).
|
|
106
|
+
*/
|
|
107
|
+
export function nextGraphemeCluster(s: string, i: number): { length: number; width: number } {
|
|
108
|
+
const base = codePointAndLength(s, i);
|
|
109
|
+
let width = charWidth(base.cp);
|
|
110
|
+
let len = base.length;
|
|
111
|
+
let j = i + base.length;
|
|
112
|
+
const peek = (): number => s.codePointAt(j) ?? -1;
|
|
113
|
+
|
|
114
|
+
if (peek() === VS15) { width = 1; len += 1; j += 1; }
|
|
115
|
+
else if (peek() === VS16) { width = 2; len += 1; j += 1; }
|
|
116
|
+
|
|
117
|
+
while (isFitzpatrick(peek())) {
|
|
118
|
+
const mod = codePointAndLength(s, j);
|
|
119
|
+
len += mod.length;
|
|
120
|
+
j += mod.length;
|
|
121
|
+
width = 2;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (peek() === KEYCAP_COMBINING) { len += 1; j += 1; width = 2; }
|
|
125
|
+
|
|
126
|
+
while (isAttachableCombining(peek())) {
|
|
127
|
+
const mark = codePointAndLength(s, j);
|
|
128
|
+
len += mark.length;
|
|
129
|
+
j += mark.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (peek() === ZWJ && j + 1 < s.length) {
|
|
133
|
+
const joined = nextGraphemeCluster(s, j + 1);
|
|
134
|
+
len += 1 + joined.length;
|
|
135
|
+
width = 2;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { length: len, width };
|
|
139
|
+
}
|
|
61
140
|
|
|
62
141
|
/**
|
|
63
142
|
* Visible display width of a string: SGR escapes count 0, tabs advance to the
|
|
@@ -81,9 +160,9 @@ export function visibleWidth(s: string): number {
|
|
|
81
160
|
i += 1;
|
|
82
161
|
continue;
|
|
83
162
|
}
|
|
84
|
-
const
|
|
85
|
-
w +=
|
|
86
|
-
i +=
|
|
163
|
+
const cluster = nextGraphemeCluster(s, i);
|
|
164
|
+
w += cluster.width;
|
|
165
|
+
i += cluster.length;
|
|
87
166
|
}
|
|
88
167
|
return w;
|
|
89
168
|
}
|
|
@@ -130,9 +209,9 @@ export function truncateToWidth(s: string, cols: number): string {
|
|
|
130
209
|
cw = TAB_STOP - (w % TAB_STOP);
|
|
131
210
|
chunk = "\t";
|
|
132
211
|
} else {
|
|
133
|
-
const
|
|
134
|
-
cw =
|
|
135
|
-
chunk =
|
|
212
|
+
const cluster = nextGraphemeCluster(s, i);
|
|
213
|
+
cw = cluster.width;
|
|
214
|
+
chunk = s.slice(i, i + cluster.length);
|
|
136
215
|
}
|
|
137
216
|
if (w + cw > limit) break;
|
|
138
217
|
out += chunk;
|
package/src/tui/renderer.ts
CHANGED
|
@@ -49,7 +49,29 @@ export class Renderer {
|
|
|
49
49
|
const currentRows = size().rows;
|
|
50
50
|
if ((this.prevCols !== undefined && this.prevCols !== currentCols) ||
|
|
51
51
|
(this.prevRows !== undefined && this.prevRows !== currentRows)) {
|
|
52
|
-
this.
|
|
52
|
+
if (this.reserve) {
|
|
53
|
+
// Inline/reserve mode: a resize needs a full repaint, but NOT an immediate
|
|
54
|
+
// clear() — clear() zeroes coverRows/prev to 0 as an independent write, which
|
|
55
|
+
// then fools the reserve block below (right after this) into believing NOTHING
|
|
56
|
+
// currently occupies the screen (occupied=0) — even though the old frame's rows
|
|
57
|
+
// are still physically there. With occupied wrongly 0, `next.length > occupied`
|
|
58
|
+
// spuriously trips even when the new (post-resize) frame is SHORTER than the
|
|
59
|
+
// old one, inserting real "\n" characters that ACTUALLY SCROLL the terminal —
|
|
60
|
+
// corrupting whatever sits above the live frame and permanently desyncing this
|
|
61
|
+
// renderer's row bookkeeping from reality (every later diff then paints at the
|
|
62
|
+
// wrong absolute row, producing progressively worse duplicate/torn content on
|
|
63
|
+
// each subsequent tick — reproduced deterministically with a standalone
|
|
64
|
+
// Renderer harness before this fix landed). reset() is the same self-heal path
|
|
65
|
+
// the periodic full-resync already uses: it remembers the old occupied rows via
|
|
66
|
+
// coverRows (no output), so the diff loop below EL-clears exactly the excess
|
|
67
|
+
// rows in place — no scroll, no desync.
|
|
68
|
+
this.reset();
|
|
69
|
+
} else {
|
|
70
|
+
// Non-reserve (alt-screen/pipe) mode has no reserve block to fool (gated on
|
|
71
|
+
// `this.reserve`), and the underlying screen buffer's own dimensions just
|
|
72
|
+
// changed — keep the defensive full ED clear.
|
|
73
|
+
this.clear();
|
|
74
|
+
}
|
|
53
75
|
}
|
|
54
76
|
this.prevCols = currentCols;
|
|
55
77
|
this.prevRows = currentRows;
|