jeo-code 0.8.36 → 0.8.38
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 +39 -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/tui/components/width.ts +56 -0
- package/src/util/file-attachment.ts +15 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,45 @@ 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.38] - 2026-07-21
|
|
10
|
+
_Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed._
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **`wrapTextWithAnsi` could hang the whole TUI forever at `cols<=1` with a leading wide grapheme cluster** (`src/tui/components/width.ts`) — reproduced live (a 5-second `timeout` kill was required to stop it) before fixing: when the very first cluster of the remaining text is wider than the available column budget (e.g. any CJK/emoji character at `cols=1`), `truncateToWidth` returned `""`, so `rest` never shrank and the wrap loop spun forever. This session's own floor-removal fixes (v0.8.33) made `cols=1` a real, reachable value in several call sites, directly increasing exposure to this latent defect. Fixed by forcing forward progress: when nothing fits, the one cluster that doesn't is emitted on its own (deliberately over-width) line instead of hanging — an unavoidable overflow beats a hung process.
|
|
14
|
+
- **Non-Latin combining marks (Hebrew, Arabic, Devanagari, Thai) were not zero-width**, over-counting `visibleWidth` and misaligning box borders/wraps for those scripts (`src/tui/components/width.ts`'s `charWidth`/`isAttachableCombining`) — e.g. Arabic "بَّ" (base + shadda + fatha) counted 3 instead of the 1 a terminal renders. Fixed by extending the zero-width tables with the Hebrew/Arabic/Devanagari/Thai combining-mark ranges, verified against the current `jquast/wcwidth` reference table (github.com/termux/wcwidth, Unicode-aligned as of 2022-12-16) rather than guessed from memory; the Devanagari fix was additionally cross-checked against the `unicode-width` crate's own documented example (`"क्ष".width() == 2`).
|
|
15
|
+
- **Paired regional-indicator flag emoji (e.g. 🇰🇷) were not one atomic cluster** (`nextGraphemeCluster`) — the total width was already coincidentally correct (1+1=2), but a wrap/truncate boundary could land between the two indicators, rendering a lone indicator-letter square instead of a flag. Fixed per UAX #29's RI-pairing rule: a Regional Indicator immediately followed by another Regional Indicator now absorbs into one width-2 cluster; a lone, unpaired indicator is left alone (not force-paired).
|
|
16
|
+
|
|
17
|
+
### Investigated, not changed (tech debt, not a bug)
|
|
18
|
+
- **Idle-prompt (`launch.ts`) and mid-turn (`tui/app.ts`+`tui/renderer.ts`) run two independent differential-render + resize systems.** The architect traced the actual handoff (`previewArmed` gate, `disarmPreview()`/`armPreview()`, independent `onResize` registration/teardown) and found a clean, correctly-gated transition with no shared row-accounting state to desync — confirmed live too (multiple `--tmux` resize-during-transition reproductions, including an extreme 8-column terminal, all rendered and typed correctly with no corruption). No concrete bug found; flagged only as a maintainability risk (a future renderer-level fix has to be reasoned about twice) worth a future unification, not an urgent fix.
|
|
19
|
+
|
|
20
|
+
### Verified
|
|
21
|
+
- `test/width.test.ts` — 14 pass (was 11): new tests for the combining-mark fix (Hebrew/Arabic/Devanagari/Thai, cross-checked against the unicode-width crate's own documented example), the flag-emoji atomicity fix (including the "lone indicator is not force-paired" case), and the hang fix (reproducing the exact previously-hanging input, plus a mixed narrow/wide case and a lone emoji case).
|
|
22
|
+
- Live (unmocked) verification beyond the subagent's read-only review: real `jeo --tmux` sessions resized exactly at the idle↔mid-turn transition boundary (including across a real turn error and a real successful reply) showed clean transitions with no corruption; a real 8-column-wide terminal degraded gracefully (no crash, no hang) end to end.
|
|
23
|
+
- Full `bun test` — 3123 pass / 0 fail across 307 files.
|
|
24
|
+
- `bun run typecheck` — no errors.
|
|
25
|
+
|
|
26
|
+
## [0.8.37] - 2026-07-21
|
|
27
|
+
_Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full)._
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **A dropped/pasted image path with no size limit could read an arbitrarily large file entirely into memory before the format check ever ran** (`src/util/file-attachment.ts`'s `attachImagePaths`) — gjc parity in spirit (#2658's "source size" bound, scoped down for jeo's much simpler single-read attach path — jeo has none of gjc's symlink/hardlink/TOCTOU/remote-`file://`/consent hardening, and doesn't need most of it: unlike gjc's arbitrary bracketed-paste-from-a-possibly-untrusted-clipboard threat model, this reads a path the user directly typed/dropped into their own composer, and the existing magic-byte check already blocks the worst case of exfiltrating an arbitrary non-image file's contents via a misleadingly-named path). The default file reader now `stat()`s the path first and rejects non-files or anything over 25 MiB before ever calling `readFile`, instead of unconditionally reading the whole file into memory first.
|
|
31
|
+
|
|
32
|
+
### Investigated, confirmed N/A (with direct evidence, not guesses)
|
|
33
|
+
- **#2622** "fail closed on tab worker startup" — gjc's browser tool spawns a separate native-addon-resolving tab-WORKER PROCESS; jeo's `browser-tool.ts` runs Playwright in-process, with no analogous worker-process bootstrap to fail-close on.
|
|
34
|
+
- **#2664** "isolate keybinding configuration arrays" — jeo has no user-configurable keybinding-array system to alias in the first place (confirmed via search: only a static, non-mutable key-hint display in `hints.ts`).
|
|
35
|
+
- **#2665** "share restricted state argv classification" — this hardens gjc's own restricted-role-agent access to a `state` TOOL; jeo's `state.ts` is a human-invoked CLI command only, never a callable agent tool (`DEFAULT_TOOLS` has no `state` entry), so the vulnerability class (a subagent bypassing an argv-based restriction) cannot occur.
|
|
36
|
+
- **#2670** "resource-gc: advance earlier sweep deadlines" — gjc tracks MULTIPLE independently-deadlined cleanup policies behind one shared re-armable timer (the bug: a newly-registered EARLIER deadline didn't preempt it); jeo's `process-reaper.ts` is a single global periodic sweep with no per-policy deadline tracking, so this specific race cannot occur.
|
|
37
|
+
- **#2669** "bound malformed compatible responses" (web-search) — verified live (not just read) that jeo's own `parseOpenAISearchResponse`/`parseGeminiSearchResponse` (added in v0.8.31) already degrade gracefully on a wrong-shaped-but-valid-JSON response body (e.g. `output` being a string instead of an array) via existing `?? []`/`Array.isArray()` guards — fed exactly that shape through both parsers directly; neither threw, both returned an empty-but-valid result. Already correct; no change needed.
|
|
38
|
+
- **#2652** "recover malformed Round 0 intent asks" — specific to gjc's own multi-turn "deep-interview Round 0" AskTool wire-schema/coordinator state machine; jeo's `deep-interview.ts` has no equivalent wire-schema-to-runtime-validation mismatch surface of this shape.
|
|
39
|
+
|
|
40
|
+
### Verified
|
|
41
|
+
- `test/file-attachment.test.ts` — 18 pass (was 17): new test writes a REAL oversized file (25 MiB + 1 KiB) and a real small PNG to a temp directory and drives them through the REAL (non-injected) filesystem reader — confirms the oversized one is rejected without ever being attached, and the small one still attaches normally through the same code path.
|
|
42
|
+
- Full `bun test` — 3120 pass / 0 fail across 307 files.
|
|
43
|
+
- `bun run typecheck` — no errors.
|
|
44
|
+
|
|
45
|
+
### Audit status
|
|
46
|
+
- 11 of gjc v0.11.5's ~65 PRs now triaged across this and the prior entry (2 fixed — #2591, #2658's size-cap; 9 confirmed N/A with evidence — #2667, #2749, #2721, #2622, #2664, #2665, #2670, #2669/already-correct, #2652). Tracked as an ongoing goal; remaining PRs continue in subsequent passes.
|
|
47
|
+
|
|
9
48
|
## [0.8.36] - 2026-07-21
|
|
10
49
|
_First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase)._
|
|
11
50
|
|
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.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
287
|
+
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
286
288
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
287
289
|
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
288
290
|
- **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
|
|
289
|
-
- **[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.
|
|
290
|
-
- **[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.
|
|
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.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
288
|
+
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
287
289
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
288
290
|
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
289
291
|
- **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
|
|
290
|
-
- **[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.
|
|
291
|
-
- **[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.
|
|
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.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
295
|
+
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
294
296
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
295
297
|
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
296
298
|
- **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
|
|
297
|
-
- **[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.
|
|
298
|
-
- **[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.
|
|
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.38]** (2026-07-21) — Independent architect-subagent review of this whole session's TUI/workflow changes (v0.8.29-v0.8.37), requested explicitly to verify stability before calling the work done. Verdict: WATCH/COMMENT, no P0/P1 — the resize fix, the idle↔mid-turn transition, the new `calc` tool wiring, and the web-search credential gating all held up under independent scrutiny. Found and fixed 3 real, previously-unknown bugs in `width.ts`; 1 architectural observation recorded as tech debt, not fixed.
|
|
287
|
+
- **[0.8.37]** (2026-07-21) — Long-term goal opened to triage every remaining PR in gjc v0.11.5's ~65-entry list, one batch at a time. This entry covers the first batch (10 PRs read in full).
|
|
286
288
|
- **[0.8.36]** (2026-07-21) — First full audit of gjc v0.11.5's actual ~65-PR "What's Changed" list (prior passes had used release-note prose or direct-source structural diffs — this is the first time every individual PR was read and triaged against jeo's own codebase).
|
|
287
289
|
- **[0.8.35]** (2026-07-21) — Deeper gjc-parity pass: re-cloned the real, public `Yeachan-Heo/gajae-code` source and, this time, structurally compared its actual tool implementations (`packages/coding-agent/src/tools/`) against jeo's own tool surface, not just the provider catalog.
|
|
288
290
|
- **[0.8.34]** (2026-07-21) — Provider/model-catalog sync against gjc's actual current source (a shallow clone of the real, public `Yeachan-Heo/gajae-code` repo at v0.11.5 — direct `packages/ai/src/models.json` comparison, not inference from release notes). The system prompt's philosophy, the `/route` command, and the `tencent` provider were explicitly out of scope and are unchanged.
|
|
289
|
-
- **[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.
|
|
290
|
-
- **[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.
|
|
291
291
|
|
|
292
292
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
293
293
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -26,6 +26,11 @@ export function charWidth(cp: number): number {
|
|
|
26
26
|
// C0/C1 control characters have no width here (callers strip/By handle them).
|
|
27
27
|
if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
28
28
|
// Zero-width: combining marks, ZWSP/ZWNJ/ZWJ, variation selectors, BOM.
|
|
29
|
+
// Non-Latin combining-mark ranges (Hebrew points/cantillation, Arabic vowel signs,
|
|
30
|
+
// Devanagari matras/virama, Thai vowels/tone marks) verified against the current
|
|
31
|
+
// jquast/wcwidth zero-width table (github.com/termux/wcwidth, table_zero.py,
|
|
32
|
+
// Unicode-aligned as of 2022-12-16) — a real, reproduced gap: e.g. Arabic "بّّ"َ
|
|
33
|
+
// (base + shadda + fatha) previously counted visibleWidth 3 instead of 1.
|
|
29
34
|
if (
|
|
30
35
|
(cp >= 0x0300 && cp <= 0x036f) || // combining diacritical marks
|
|
31
36
|
(cp >= 0x200b && cp <= 0x200f) || // zero-width space..RLM
|
|
@@ -33,6 +38,14 @@ export function charWidth(cp: number): number {
|
|
|
33
38
|
(cp >= 0x1ab0 && cp <= 0x1aff) || // combining diacritical marks extended
|
|
34
39
|
(cp >= 0x1dc0 && cp <= 0x1dff) || // combining diacritical marks supplement
|
|
35
40
|
(cp >= 0x20d0 && cp <= 0x20ff) || // combining marks for symbols
|
|
41
|
+
(cp >= 0x0591 && cp <= 0x05bd) || // Hebrew accents/points (Etnahta..Meteg)
|
|
42
|
+
cp === 0x05bf || (cp >= 0x05c1 && cp <= 0x05c2) || (cp >= 0x05c4 && cp <= 0x05c5) || cp === 0x05c7 || // Hebrew points (Rafe, Shin/Sin Dot, upper/lower dot, Qamats Qatan)
|
|
43
|
+
(cp >= 0x0610 && cp <= 0x061a) || // Arabic honorific/small signs
|
|
44
|
+
(cp >= 0x064b && cp <= 0x065f) || cp === 0x0670 || // Arabic vowel signs/tanwin, superscript alef
|
|
45
|
+
(cp >= 0x06d6 && cp <= 0x06dc) || (cp >= 0x06df && cp <= 0x06e4) || (cp >= 0x06e7 && cp <= 0x06e8) || (cp >= 0x06ea && cp <= 0x06ed) || // Arabic Quranic annotation signs
|
|
46
|
+
(cp >= 0x0900 && cp <= 0x0902) || cp === 0x093a || cp === 0x093c || // Devanagari candrabindu/anusvara/vowel-sign-oe/nukta
|
|
47
|
+
(cp >= 0x0941 && cp <= 0x0948) || cp === 0x094d || (cp >= 0x0951 && cp <= 0x0957) || (cp >= 0x0962 && cp <= 0x0963) || // Devanagari vowel signs/virama/stress/vocalic
|
|
48
|
+
cp === 0x0e31 || (cp >= 0x0e34 && cp <= 0x0e3a) || (cp >= 0x0e47 && cp <= 0x0e4e) || // Thai vowels/tone marks
|
|
36
49
|
cp === 0xfeff
|
|
37
50
|
) {
|
|
38
51
|
return 0;
|
|
@@ -64,6 +77,12 @@ const VS16 = 0xfe0f; // variation selector: force EMOJI (wide) presentation
|
|
|
64
77
|
const FITZPATRICK_MIN = 0x1f3fb; // Emoji Modifier Fitzpatrick Type-1-2 .. Type-6
|
|
65
78
|
const FITZPATRICK_MAX = 0x1f3ff;
|
|
66
79
|
const KEYCAP_COMBINING = 0x20e3; // combining enclosing keycap (1️⃣, #️⃣, *️⃣)
|
|
80
|
+
const RI_MIN = 0x1f1e6; // Regional Indicator Symbol Letter A
|
|
81
|
+
const RI_MAX = 0x1f1ff; // Regional Indicator Symbol Letter Z
|
|
82
|
+
|
|
83
|
+
function isRegionalIndicator(cp: number): boolean {
|
|
84
|
+
return cp >= RI_MIN && cp <= RI_MAX;
|
|
85
|
+
}
|
|
67
86
|
|
|
68
87
|
function isFitzpatrick(cp: number): boolean {
|
|
69
88
|
return cp >= FITZPATRICK_MIN && cp <= FITZPATRICK_MAX;
|
|
@@ -78,6 +97,10 @@ function isAttachableCombining(cp: number): boolean {
|
|
|
78
97
|
|| (cp >= 0x1ab0 && cp <= 0x1aff)
|
|
79
98
|
|| (cp >= 0x1dc0 && cp <= 0x1dff)
|
|
80
99
|
|| (cp >= 0x20d0 && cp <= 0x20ff)
|
|
100
|
+
|| (cp >= 0x0591 && cp <= 0x05bd) || cp === 0x05bf || (cp >= 0x05c1 && cp <= 0x05c2) || (cp >= 0x05c4 && cp <= 0x05c5) || cp === 0x05c7 // Hebrew
|
|
101
|
+
|| (cp >= 0x0610 && cp <= 0x061a) || (cp >= 0x064b && cp <= 0x065f) || cp === 0x0670 || (cp >= 0x06d6 && cp <= 0x06dc) || (cp >= 0x06df && cp <= 0x06e4) || (cp >= 0x06e7 && cp <= 0x06e8) || (cp >= 0x06ea && cp <= 0x06ed) // Arabic
|
|
102
|
+
|| (cp >= 0x0900 && cp <= 0x0902) || cp === 0x093a || cp === 0x093c || (cp >= 0x0941 && cp <= 0x0948) || cp === 0x094d || (cp >= 0x0951 && cp <= 0x0957) || (cp >= 0x0962 && cp <= 0x0963) // Devanagari
|
|
103
|
+
|| cp === 0x0e31 || (cp >= 0x0e34 && cp <= 0x0e3a) || (cp >= 0x0e47 && cp <= 0x0e4e) // Thai
|
|
81
104
|
|| cp === 0xfeff;
|
|
82
105
|
}
|
|
83
106
|
|
|
@@ -110,6 +133,17 @@ export function nextGraphemeCluster(s: string, i: number): { length: number; wid
|
|
|
110
133
|
let len = base.length;
|
|
111
134
|
let j = i + base.length;
|
|
112
135
|
const peek = (): number => s.codePointAt(j) ?? -1;
|
|
136
|
+
// A pair of adjacent Regional Indicators forms ONE flag-emoji cluster (UAX #29's
|
|
137
|
+
// RI-pairing rule) — e.g. 🇰🇷 = REGIONAL INDICATOR K + REGIONAL INDICATOR R. Each
|
|
138
|
+
// RI alone is already width 1 (no dedicated charWidth range; falls through to the
|
|
139
|
+
// default), so an unpaired scan already SUMS to the right total (2) even without
|
|
140
|
+
// this — but without pairing, a wrap/truncate boundary can land BETWEEN the two,
|
|
141
|
+
// rendering a lone indicator-letter square instead of a flag. No further
|
|
142
|
+
// modifiers (VS16/ZWJ/Fitzpatrick) are defined for a flag pair, so return early.
|
|
143
|
+
if (isRegionalIndicator(base.cp) && isRegionalIndicator(peek())) {
|
|
144
|
+
const second = codePointAndLength(s, j);
|
|
145
|
+
return { length: len + second.length, width: 2 };
|
|
146
|
+
}
|
|
113
147
|
|
|
114
148
|
if (peek() === VS15) { width = 1; len += 1; j += 1; }
|
|
115
149
|
else if (peek() === VS16) { width = 2; len += 1; j += 1; }
|
|
@@ -322,6 +356,28 @@ export function wrapTextWithAnsi(text: string, cols: number): string[] {
|
|
|
322
356
|
: head.endsWith(RESET) && rest.startsWith(head.slice(0, -RESET.length))
|
|
323
357
|
? head.slice(0, -RESET.length)
|
|
324
358
|
: head;
|
|
359
|
+
if (consumed.length === 0) {
|
|
360
|
+
// `truncateToWidth` couldn't fit even ONE whole cluster in `width` — a wide
|
|
361
|
+
// grapheme cluster (CJK/emoji/ZWJ sequence) wider than the available column
|
|
362
|
+
// budget, e.g. a 2-wide cluster at cols=1. Left alone, `rest` never shrinks
|
|
363
|
+
// and this loop spins forever — a REAL, reproduced infinite loop (hangs the
|
|
364
|
+
// whole TUI; confirmed live via a 5s timeout kill before this fix). Force
|
|
365
|
+
// forward progress: emit that one cluster on its own, deliberately
|
|
366
|
+
// over-width, line — an unavoidable overflow beats a hung process. `rest[0]`
|
|
367
|
+
// is guaranteed to be real content here, never an escape sequence (those are
|
|
368
|
+
// always consumed into `head` regardless of the width budget, so an empty
|
|
369
|
+
// `head` can only mean the first REAL cluster itself didn't fit).
|
|
370
|
+
const forced = nextGraphemeCluster(rest, 0);
|
|
371
|
+
const forcedText = rest.slice(0, forced.length);
|
|
372
|
+
let forcedLine = active + forcedText;
|
|
373
|
+
const forcedActive = sgrStateAfter(active, forcedText);
|
|
374
|
+
if (forcedActive && !forcedLine.endsWith(RESET)) forcedLine += RESET;
|
|
375
|
+
out.push(forcedLine);
|
|
376
|
+
active = forcedActive;
|
|
377
|
+
rest = rest.slice(forced.length);
|
|
378
|
+
if (rest.length === 0) break;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
325
381
|
if (consumed.length === rest.length) {
|
|
326
382
|
let line = active + rest;
|
|
327
383
|
if (active && !line.endsWith(RESET)) line += RESET;
|
|
@@ -13,11 +13,23 @@
|
|
|
13
13
|
* Only paths with a known image extension are considered, so ordinary prose is
|
|
14
14
|
* never mistaken for a file. Non-image / unreadable paths are left untouched.
|
|
15
15
|
*/
|
|
16
|
-
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { readFile, stat } from "node:fs/promises";
|
|
17
17
|
import type { ImageAttachment } from "../ai/types";
|
|
18
18
|
|
|
19
19
|
const IMAGE_EXT_RE = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
20
20
|
|
|
21
|
+
/** Reject a dropped/pasted image path bigger than this before reading it fully into
|
|
22
|
+
* memory (gjc parity, #2658's "source size" bound in spirit — a scoped-down version
|
|
23
|
+
* for jeo's simpler single-read attach path, not the full symlink/TOCTOU/consent
|
|
24
|
+
* hardening that PR added for a materially different threat model: gjc validates
|
|
25
|
+
* arbitrary bracketed-paste text that may originate from an untrusted clipboard
|
|
26
|
+
* source, while this reads a path the user directly typed/dropped into their own
|
|
27
|
+
* composer). Without a cap, a stray path to a multi-GB file — a typo, or a dropped
|
|
28
|
+
* huge video misidentified by extension — would be read ENTIRELY into memory before
|
|
29
|
+
* the magic-byte check below ever gets to reject it. 25 MiB comfortably covers any
|
|
30
|
+
* real photo/screenshot while bounding the worst case. */
|
|
31
|
+
export const MAX_ATTACH_IMAGE_BYTES = 25 * 1024 * 1024;
|
|
32
|
+
|
|
21
33
|
|
|
22
34
|
/**
|
|
23
35
|
* Normalize the whitespace around every `[image #N]` tag to a single space:
|
|
@@ -150,6 +162,8 @@ export type FileReader = (path: string) => Promise<Uint8Array | null>;
|
|
|
150
162
|
|
|
151
163
|
const defaultReader: FileReader = async (p) => {
|
|
152
164
|
try {
|
|
165
|
+
const st = await stat(p);
|
|
166
|
+
if (!st.isFile() || st.size > MAX_ATTACH_IMAGE_BYTES) return null;
|
|
153
167
|
return new Uint8Array(await readFile(p));
|
|
154
168
|
} catch {
|
|
155
169
|
return null;
|