Skip to content

fix: resolve high-severity terminal correctness bugs - #61

Merged
simota merged 9 commits into
mainfrom
fix/high-severity-terminal-bugs
Sep 4, 2026
Merged

fix: resolve high-severity terminal correctness bugs#61
simota merged 9 commits into
mainfrom
fix/high-severity-terminal-bugs

Conversation

@simota

@simota simota commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

  • VT: raw C1 bytes in ground state decode as invalid UTF-8 instead of opening a control string; colon-form SGR colour groups stop at the first semicolon and never borrow the following attribute.
  • Grid: DECOM origin-relative addressing is applied (CUP/CHA/VPA, DECSTBM/DECSLRM homing, CPR, saved-cursor and seed replay); IRM insert mode shifts existing cells; ICH/DCH/IL/DL/SU/SD and TAB/CBT respect DECSLRM margins; shrinking rows no longer discards populated rows below the cursor.
  • IPC: noa.getText responses are trimmed to fit tungstenite's write buffer so a large read cannot disconnect the control client.

Why

Each of these could lose visible content, corrupt display state, or drop a control connection under ordinary program output (full-screen apps using scroll regions and margins, editors relying on IRM, a stray byte in damaged UTF-8, a large getText on a screen full of escapable characters).

Review notes

  • Seven commits, one behaviour each; each builds and passes on its own. Read crates/noa-grid/src/screen/edit.rs first for the DECOM and DECSLRM changes.
  • tests/parity/fixtures/scroll_region_origin.txt is re-blessed: its old expectation recorded the DECOM gap as known-divergent.
  • The lone-truecolor SGR fast path now covers only the semicolon spelling; colon spellings go through the general decoder.
  • One #TODO(agent): marker is added in insert_blank_chars (DEFERRED): IL/DL outside the horizontal margins and ICH/DCH left of the left margin are unreachable while cursor addressing clamps to DECSLRM; the guard is needed if that clamping is ever relaxed.
  • No migration, flag, or ordering requirement. Reverting any single commit restores the previous behaviour for that feature only.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --workspace
  • cargo test --workspace (run per commit, checked out individually)
  • git diff --check

Ground-state input is UTF-8, where 0x80..0x9f only occur as continuation
bytes. Honouring a lone 0x9b/0x9d/0x90/0x9f there as an 8-bit CSI/OSC/DCS/
APC introducer let one stray byte from a damaged multi-byte sequence open
a control string and swallow the visible text that followed until an ST
happened to arrive. Such bytes now fall through to the UTF-8 decoder and
print U+FFFD; C1 controls are still recognised once a sequence is already
in progress (ST terminating a string, for example).
…ibutes

`38:2:r:g:b;1` is one colon-delimited group followed by a separate `;1`.
The extended-colour decoder read its slots by position, so an incomplete
colon group (`38:2:255;1`, `38:5;1`) consumed the next attribute as a
colour component and dropped it, and the colorspace-less colon truecolor
spelling swallowed the bold that followed it. The group now ends at the
first semicolon; a group too short for its colour kind yields no colour
rather than a garbage one.

The lone-truecolor fast path is limited to the semicolon spelling because
it cannot see where a colon group ends.
DECOM (?6) was tracked but never applied: CUP/CHA/VPA, the homing done by
DECSTBM/DECSLRM/DECOM itself, and the CPR report all used absolute
coordinates, so full-screen programs that address a scroll region in
origin mode painted outside it.

Origin mode is saved and restored with the cursor (DECSC/DECRC, ?1048,
?1049) as in xterm, so a restore reinstates the addressing mode the
position was saved under. The synthetic seed replays that state so a
restored terminal homes and reports identically.

The scroll_region_origin parity fixture is re-blessed; its previous
expectation recorded this gap as known-divergent.
IRM (CSI 4 h) was accepted as a mode but printing always overwrote, so
programs that rely on insert mode to open space in a line rather than
repaint it lost the characters under the cursor.

The ASCII batch fast paths write cells in place, so they are bypassed
while the mode is on; REP takes the same per-cell path. The mode is also
answered by DECRQM and replayed by the synthetic seed.
With left/right margins set, insert/delete character shifted the row all
the way to its last column, line insert/delete and scroll moved whole
rows, and TAB/CBT ran past the margins, so a program using DECSLRM for a
side pane saw the other pane's content pushed around.

Row moves inside narrowed margins copy the margin columns cell by cell
instead of rotating rows, leaving the columns outside untouched. Cursor
addressing still clamps to the margins, so the cursor cannot currently sit
left of the left margin or on a row outside the region; a marker in
insert_blank_chars notes the guard needed if that changes.
Shrinking the row count removed rows from the bottom whenever they were
below the cursor, regardless of content, so text under the cursor was
discarded instead of the top rows moving into scrollback. Only blank,
unwrapped rows not covered by a Kitty image placement are disposable now;
the rest of the shrink pushes from the top.
getText may return MAX_TEXT_MAX_BYTES of text, and JSON escaping can
inflate that (a NUL becomes six bytes) past tungstenite's
max_write_buffer_size, which the server sets to the attach output
capacity. Exceeding it fails the write and disconnects the control
client, so one large read tore down the session.

The response text is trimmed until the serialised message fits, and
flagged truncated. The payload budget is derived from the same constant
as the buffer size so the two cannot drift apart.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T14:01:40.097940Z bb116c6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bb116c6374

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/noa-grid/src/screen/reflow.rs Outdated
let y = usize::from(old_rows - 1 - offset);
let row = &self.grid[y];
let abs_row = live_top + y;
row.occupied() == 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat erased bottom rows as disposable

When a bottom row previously held text and is then cleared with default-background EL/ED, Row::occupied() can remain nonzero because it is deliberately only an upper bound and those erase paths do not lower it. This predicate therefore treats a visually empty row as populated; a height-only shrink then drains rows from the top instead of discarding the empty rows below the cursor, unnecessarily moving visible content into scrollback. Check the cells for actual defaultness or reset occupancy when a row is fully cleared.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8bdd245: the shrink now checks the cells within the occupancy watermark (Row::is_blank), so rows cleared by EL/ED count as disposable. Regression: resize_shrink_rows_treats_erased_rows_below_cursor_as_disposable.

Comment thread crates/noa-grid/src/screen/edit.rs Outdated
}
if self.has_narrow_horizontal_margins() {
self.scroll_rectangle_up(top, bottom, n);
self.remove_placements_intersecting_grid_rows(top, bottom);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve images outside a horizontal scroll rectangle

When DECSLRM defines a narrow horizontal region, a Kitty placement wholly to the left or right of that region should remain unchanged during a scroll. This row-only removal call deletes every placement overlapping the vertical band regardless of its anchor_col/cols, so an unrelated image can disappear after LF/IND or SU; the corresponding down/IL/DL branches have the same issue. Limit removal to placements that also intersect the active horizontal margins.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a34a566: rectangle scrolls (LF/IND, SU/SD, IL/DL under narrow DECSLRM) now remove only placements that also overlap the horizontal margins. Regression: kitty_rectangle_scroll_keeps_placements_outside_the_margins.

Row::occupied() is a monotonic upper bound that EL/ED never lower, so a
row that once held text and was then cleared still read as populated.
Shrinking the row count then drained rows from the top into scrollback
instead of dropping the visually blank rows below the cursor. The shrink
now inspects the cells within the watermark, so erased rows count as
blank.
Scrolling inside narrowed DECSLRM margins removed every Kitty placement
overlapping the scrolled rows, ignoring columns, so an image in a side
pane vanished whenever the other pane scrolled (LF/IND, SU/SD, IL/DL).
Rectangle scrolls now remove only placements that also overlap the
horizontal margins; full-width scrolls are unchanged.
@simota
simota merged commit 93326ea into main Sep 4, 2026
1 check passed
@simota
simota deleted the fix/high-severity-terminal-bugs branch September 4, 2026 14:12
@simota simota mentioned this pull request Sep 4, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant