Skip to content

fix: terminal restore on panic, per-session render caches, incremental animation frames, sanitizer gaps#12

Merged
saucam merged 7 commits into
mainfrom
fix/tui-render-perf
Jul 2, 2026
Merged

fix: terminal restore on panic, per-session render caches, incremental animation frames, sanitizer gaps#12
saucam merged 7 commits into
mainfrom
fix/tui-render-perf

Conversation

@saucam

@saucam saucam commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Fixes (from the session/rendering audit)

Panic leaves the terminal unusable (P1). No panic hook meant any panic stranded the terminal in raw mode + alternate screen + mouse capture (reset required). Now: a chained panic hook restores the terminal before the default hook prints (message + backtrace land on a usable screen), plus a TerminalGuard RAII drop for early-return paths. Restore is idempotent via an AtomicBool armed before the first mode change, so a partial setup_terminal failure also unwinds.

Session switch was a full O(N) re-parse, both directions, forever (P1). retain_only evicted every non-focused session's render cache on each rebuild, and the build slot held one session. Now: RenderCache is grouped per session (retain_session/evict_session), and assembled builds live in a 4-session MRU LRU (ScrollbackBuildCache) — Tab A→B→A is a pure cache hit both ways. Memory stays bounded: sessions falling off the LRU take their render-cache entries with them.

Whole-transcript rebuild every animation frame (P1). While any tool ran, every 100ms tick cloned all cached lines and re-ran Paragraph::line_count over the entire transcript — O(total lines) at 10Hz, degrading input latency exactly while output streams. Now: per-line wrapped-row counts are cached per message keyed by (version, width) (measurement identity with the renderer preserved — same Paragraph wrap config), the build records per-message segment ranges, and animation frames splice_message only the animating messages in place — O(animating lines)/frame. The integer-only prefix-sum rebuild runs only when a splice changes the message's wrapped shape (a spinner glyph swap doesn't). Content deltas / resize / insertion still take the full-rebuild path; a sentinel-based test proves splice-on-tick and rebuild-on-completion.

Scrollback beyond 65,535 wrapped rows was unreachable (P2). scroll_offset: u16 saturated, so g/Home on a large session landed mid-transcript with no way higher. Widened to usize end-to-end; only the intra-viewport remainder (explicitly clamped) reaches ratatui's u16 Paragraph::scroll.

DCS/SOS/PM/APC payloads flooded the transcript (P2). Both the sanitizer and the ANSI parser treated ESC P/X/^/_ as two-byte escapes, so sixel or tmux-passthrough payloads rendered as kilobytes of junk text. Both now consume to ST/BEL (mirroring the OSC arm); 8-bit C1 CSI (U+009B) parses as CSI.

Tabs silently vanished (P2). The sanitizer preserved \t on the premise that ratatui renders tab stops — false: ratatui 0.29 skips zero-width graphemes, so git status/Makefile output lost all indentation. Tabs now expand to 8-column stops at parse time, column-aware via unicode-width (CJK-correct, resets on \n/\r, persists across streamed feeds and styled spans).

Streaming message lookup was a worst-case forward scan (P2). apply_delta/upsert/status-change scanned front-to-back while streaming targets the newest message; now .rev().

Tests

45 net new (214 total, up from 169): sanitizer DCS/SOS/PM/APC/C1-CSI consumption in both parsers incl. unterminated-at-EOF; tab expansion (stops, CJK width, newline/CR reset, styled-span and streamed-feed continuity); per-session cache retention + LRU eviction/touch semantics; scroll offset past the former u16 ceiling; back-to-front lookup with duplicate ids; row-count cache invalidation (content delta = that message only, width change = all); splice correctness (shape-preserving, growing, shrinking, offset rebuild, sentinel splice-not-rebuild on animation tick).

cargo test --workspace green, cargo clippy --workspace --all-targets has zero new warnings (one fewer than main — the usize change removed a cast warning), cargo fmt clean.

Out of scope (follow-up PR material): session tab-bar scrolling past the clip width, attach/detach LRU of session subscriptions, reconnect replay storm.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved scrolling for much larger transcripts (including more reliable PageUp/PageDown movement beyond the old limit).
    • More efficient screen updates by reusing per-session cached scrollback and selectively rerendering only what changes.
    • Added session-scoped cache behavior with LRU-based scrollback build retention.
  • Bug Fixes

    • Fixed duplicate message/session updates to target the newest matching entry.
    • Improved text rendering: tab alignment via column stops, correct wide/CJK widths, and safer handling of ANSI/escape sequences.
    • Better terminal recovery with panic-safe, idempotent restoration.
  • Chores

    • Enhanced sanitization and rendering consistency with expanded test coverage.

saucam and others added 6 commits July 3, 2026 00:22
A panic anywhere in the TUI previously left the terminal stranded in
raw mode + alternate screen + mouse capture (restore only ran on the
Result path), making the shell unusable and hiding the panic message.

Install a std::panic hook chained onto the default hook that restores
the terminal (disable_raw_mode, DisableMouseCapture,
DisableBracketedPaste, LeaveAlternateScreen, cursor Show) before the
default hook prints, plus a TerminalGuard RAII drop for early-return
paths. Restore is idempotent via an AtomicBool so hook, guard, and the
normal exit path can all call it safely.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apply_delta / upsert / session status-change did front-to-back linear
scans, but streaming events target the NEWEST message — so every
streamed chunk paid a full-transcript scan on long sessions (visible as
prompt lag after many turns). Search from the back instead: the common
case becomes O(1) and duplicate-id edge cases resolve to the newest
occurrence in both paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reachable

scroll_offset was u16, silently capping the transcript scroll range at
65 535 wrapped rows — the top of big sessions was unreachable via
scroll_to_top or PgUp. Widen to usize end-to-end (scroll_up/down,
page_step, anchored-scroll maintenance in note_total_rendered, the
worker-row indicator). The final Paragraph::scroll stays u16-safe
because only the intra-viewport remainder from visible_window reaches
ratatui, and it is clamped explicitly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…NSI parser

Two transcript-corruption bugs in the shared text-scrubbing layer,
fixed in BOTH render/sanitize.rs and render/ansi.rs:

- ESC P / ESC X / ESC ^ / ESC _ (DCS/SOS/PM/APC) were treated as
  two-byte escapes, so a sixel image or tmux-passthrough payload
  flooded the transcript as text. They now consume their payload up to
  ST (ESC backslash) or BEL, mirroring the OSC arm. The 8-bit C1 CSI
  introducer (U+009B) is also parsed (previously its parameter bytes
  leaked as text; in ansi.rs it now applies SGR like ESC[).

- Tabs were kept verbatim, but ratatui 0.29 silently drops a lone
  tab as a zero-width grapheme, destroying the alignment of git
  status / Makefile output. Tabs now expand to spaces up to the next
  8-column stop, column-aware (unicode-width, CJK-correct) within the
  current line segment, including across styled spans and streamed
  feeds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Switching sessions used to evict every OTHER session's cached message
renders: the renderer called retain_only(live_ids) with only the
FOCUSED session's ids, and ScrollbackBuild held a single-session build
slot — so Tab A->B->A was a full O(N) re-parse in both directions,
forever.

- RenderCache now groups entries per session; retention
  (retain_session) prunes dead message ids within ONE session only, so
  focus switches never touch other sessions' entries.
- ScrollbackBuild becomes ScrollbackBuildCache: an LRU (4 sessions,
  most-recently-focused first) of per-session assembled builds, so
  flipping back to a recent session is a pure cache hit.
- Memory stays bounded: when a session falls off the build LRU, its
  per-message render-cache entries are evicted with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
While any tool was running, every 100 ms tick forced a full scrollback
rebuild: every cached message's Vec<Line> was re-cloned AND every line
re-measured with Paragraph::line_count — O(total lines) per frame, felt
as CPU burn and sluggishness on long transcripts.

Two-part fix:

- Wrapped-row counts are now cached alongside each message's rendered
  lines in RenderCache, keyed by (version, width): a content delta
  re-measures only that message, a resize invalidates everything. Full
  rebuilds assemble the prefix sum from cached integers instead of
  re-wrapping the transcript.
- Animation-only frames (same session epoch, same width) no longer
  rebuild at all: the build tracks per-message line segments, and
  ScrollbackBuild::splice_message re-renders JUST the animating
  messages and splices them into the cached buffer — O(animating
  lines) per frame. The prefix sum is recomputed (integer adds only)
  only when the spliced message's wrapped shape actually changed;
  a spinner glyph swap doesn't.

Correctness preserved on content deltas, resize, message insertion,
and tool-state transitions — all of those bump epoch/width and take
the full-rebuild path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6f3d115b-6fad-4cd6-8107-bc9f224e63ea

📥 Commits

Reviewing files that changed from the base of the PR and between ac67788 and 8afce81.

📒 Files selected for processing (4)
  • crates/codeoid-tui/src/state/mod.rs
  • crates/codeoid-tui/src/state/scrollback_build.rs
  • crates/codeoid-tui/src/state/sessions.rs
  • crates/codeoid-tui/src/ui/scrollback.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/codeoid-tui/src/ui/scrollback.rs
  • crates/codeoid-tui/src/state/mod.rs
  • crates/codeoid-tui/src/state/scrollback_build.rs

📝 Walkthrough

Walkthrough

This PR updates terminal restoration, duplicate-id resolution, scroll arithmetic, session-scoped render/scrollback caching, scrollback rendering, and ANSI/sanitizer escape and tab handling across codeoid-tui.

Changes

TUI reliability and rendering fixes

Layer / File(s) Summary
Terminal restore safety
crates/codeoid-tui/src/main.rs
Adds a panic hook, terminal-active flag, idempotent shared restore path, and a guard for early exits.
Newest duplicate entry wins
crates/codeoid-tui/src/app.rs, crates/codeoid-tui/src/state/messages.rs, crates/codeoid-tui/src/state/sessions.rs
Reverse scans and deduplication now target the newest duplicate session or message entry, with tests updated.
Scroll math widened to usize
crates/codeoid-tui/src/app.rs, crates/codeoid-tui/src/state/mod.rs
Scroll offset, paging, and anchored-scroll updates move from u16 to usize, including the top-of-scroll sentinel.
Session-scoped cache model
crates/codeoid-tui/src/state/render_cache.rs, crates/codeoid-tui/src/state/scrollback_build.rs, crates/codeoid-tui/src/state/mod.rs
RenderCache and ScrollbackBuild become session-scoped, store row counts and segments, and add LRU/session eviction plus splice/rebuild support.
Scrollback rendering integration
crates/codeoid-tui/src/ui/scrollback.rs
Full rebuild and animation-only paths now use cached per-session builds, row offsets, and session-local render-cache retention.
ANSI parser tab and escape handling
crates/codeoid-tui/src/render/ansi.rs
Tabs expand by visual column and escape/string sequences are consumed without leaking payload bytes.
Display sanitizer escape stripping
crates/codeoid-tui/src/render/sanitize.rs
The display sanitizer matches the column-aware tab expansion and broader escape-sequence stripping behavior.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • saucam/codeoid-ui#5: Uses the same row_offsets and viewport-slicing path in scrollback_build.rs and ui/scrollback.rs, which this PR extends with session-scoped caching and splice updates.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: terminal restore hardening, per-session caching, incremental animation rendering, and sanitizer fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tui-render-perf

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.18182% with 66 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.22%. Comparing base (18d1b22) to head (8afce81).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
crates/codeoid-tui/src/main.rs 0.00% 24 Missing ⚠️
crates/codeoid-tui/src/state/render_cache.rs 88.65% 16 Missing ⚠️
crates/codeoid-tui/src/state/scrollback_build.rs 94.35% 14 Missing ⚠️
crates/codeoid-tui/src/ui/scrollback.rs 96.15% 8 Missing ⚠️
crates/codeoid-tui/src/app.rs 0.00% 3 Missing ⚠️
crates/codeoid-tui/src/state/mod.rs 99.09% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #12      +/-   ##
==========================================
+ Coverage   51.18%   58.22%   +7.04%     
==========================================
  Files          31       31              
  Lines        5523     6344     +821     
==========================================
+ Hits         2827     3694     +867     
+ Misses       2696     2650      -46     
Flag Coverage Δ
tui 58.22% <93.18%> (+7.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
crates/codeoid-tui/src/render/ansi.rs 89.75% <100.00%> (+3.30%) ⬆️
crates/codeoid-tui/src/render/sanitize.rs 100.00% <100.00%> (+4.70%) ⬆️
crates/codeoid-tui/src/state/messages.rs 96.90% <100.00%> (+0.47%) ⬆️
crates/codeoid-tui/src/state/sessions.rs 98.84% <100.00%> (+0.15%) ⬆️
crates/codeoid-tui/src/state/mod.rs 78.37% <99.09%> (+15.98%) ⬆️
crates/codeoid-tui/src/app.rs 0.00% <0.00%> (ø)
crates/codeoid-tui/src/ui/scrollback.rs 79.78% <96.15%> (+7.45%) ⬆️
crates/codeoid-tui/src/state/scrollback_build.rs 92.92% <94.35%> (+12.92%) ⬆️
crates/codeoid-tui/src/state/render_cache.rs 89.61% <88.65%> (+0.72%) ⬆️
crates/codeoid-tui/src/main.rs 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/codeoid-tui/src/app.rs (1)

463-480: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reverse-scan needs a duplicate-id guard or test
SessionList is just a Vec, replace() can preserve duplicate ids, and upsert() updates the first match. If duplicate session ids can occur, this status change can land on the wrong entry; add a regression test or enforce uniqueness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codeoid-tui/src/app.rs` around lines 463 - 480, The reverse search in
DaemonMessage::SessionStatusChange can update the wrong session when SessionList
contains duplicate ids, since sessions.items().iter().rev().find(...) assumes
uniqueness while replace() and upsert() may preserve duplicates. Fix this by
either enforcing unique session ids in SessionList/merge_session/upsert, or by
adding a regression test that covers duplicate ids and verifies the intended
entry is updated. Keep the change localized around the SessionStatusChange
handler and the SessionList update path.
🧹 Nitpick comments (1)
crates/codeoid-tui/src/ui/scrollback.rs (1)

72-76: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Avoid the full message scan on cached idle frames.

any_animating walks the whole focused transcript before the cache-hit decision, so prompt keystrokes/idle frames are still O(message count). Consider storing animating message keys in the build during rebuild and checking that small set on hits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codeoid-tui/src/ui/scrollback.rs` around lines 72 - 76, The scrollback
render path is still scanning the full transcript via any_animating before the
cache-hit check, which makes idle frames O(message count). Update the Scrollback
build/cache flow in scrollback.rs so animating message keys are tracked during
the rebuild path and reused on cache hits, and then have the cache-hit logic
consult that small set instead of calling
messages(&session_id).iter().any(is_animating).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/codeoid-tui/src/state/mod.rs`:
- Around line 221-228: The tool-selection update logic in the focused-session
path is clearing the entire scrollback build cache instead of evicting only the
current session’s entry, which breaks the LRU/memory-bound behavior and removes
unrelated session hits. Update the code around the focused-session invalidation
block in the state module so that `scrollback_build` is evicted or invalidated
only for `self.sessions.focused_id()` (the same session used for
`render_cache.invalidate`) rather than calling `clear()`, and apply the same
targeted eviction in the corresponding other tool-selection change path.

In `@crates/codeoid-tui/src/ui/scrollback.rs`:
- Around line 101-119: The scrollback render path in
render_message_with_animation and the BuildState splice flow are keying only on
m.message_id, which breaks when a session contains duplicate ids. Update the
RenderCache lookup and BuildSegment identification to include a
per-occurrence/render key (or another disambiguator) so each duplicate message
gets its own cached render and splice target, or disable cache/splice reuse when
duplicates are detected.

---

Outside diff comments:
In `@crates/codeoid-tui/src/app.rs`:
- Around line 463-480: The reverse search in DaemonMessage::SessionStatusChange
can update the wrong session when SessionList contains duplicate ids, since
sessions.items().iter().rev().find(...) assumes uniqueness while replace() and
upsert() may preserve duplicates. Fix this by either enforcing unique session
ids in SessionList/merge_session/upsert, or by adding a regression test that
covers duplicate ids and verifies the intended entry is updated. Keep the change
localized around the SessionStatusChange handler and the SessionList update
path.

---

Nitpick comments:
In `@crates/codeoid-tui/src/ui/scrollback.rs`:
- Around line 72-76: The scrollback render path is still scanning the full
transcript via any_animating before the cache-hit check, which makes idle frames
O(message count). Update the Scrollback build/cache flow in scrollback.rs so
animating message keys are tracked during the rebuild path and reused on cache
hits, and then have the cache-hit logic consult that small set instead of
calling messages(&session_id).iter().any(is_animating).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c0f88264-020d-43f2-8d89-e970dba88b9a

📥 Commits

Reviewing files that changed from the base of the PR and between 18d1b22 and ac67788.

📒 Files selected for processing (9)
  • crates/codeoid-tui/src/app.rs
  • crates/codeoid-tui/src/main.rs
  • crates/codeoid-tui/src/render/ansi.rs
  • crates/codeoid-tui/src/render/sanitize.rs
  • crates/codeoid-tui/src/state/messages.rs
  • crates/codeoid-tui/src/state/mod.rs
  • crates/codeoid-tui/src/state/render_cache.rs
  • crates/codeoid-tui/src/state/scrollback_build.rs
  • crates/codeoid-tui/src/ui/scrollback.rs

Comment thread crates/codeoid-tui/src/state/mod.rs Outdated
Comment on lines +101 to +119
for m in messages
.messages(&session_id)
.iter()
.filter(|m| is_animating(m))
{
let per_block_expanded = expanded_ids.contains(&m.message_id);
let is_selected = selected_id.as_deref() == Some(m.message_id.as_str());
let mut new_lines = render_message(
m,
anim_tick,
verbose_tools || per_block_expanded,
is_selected,
);
if !new_lines.is_empty() {
// Trailing separator, exactly as the full rebuild
// appends per rendered message.
new_lines.push(Line::raw(""));
}
build.splice_message(&m.message_id, new_lines);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Disambiguate duplicate message ids in cache keys and build segments.

Replay can leave duplicate message_ids in one session, but this path keys RenderCache and BuildSegment only by m.message_id. A rebuild can reuse the first duplicate’s rendered lines for later duplicates, and animation splice updates the first matching segment rather than the newest/actual occurrence. Include an occurrence/render key in both the cache lookup and segment mapping, or bypass cache/splice when duplicates are present.

Also applies to: 141-188

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/codeoid-tui/src/ui/scrollback.rs` around lines 101 - 119, The
scrollback render path in render_message_with_animation and the BuildState
splice flow are keying only on m.message_id, which breaks when a session
contains duplicate ids. Update the RenderCache lookup and BuildSegment
identification to include a per-occurrence/render key (or another disambiguator)
so each duplicate message gets its own cached render and splice target, or
disable cache/splice reuse when duplicates are detected.

Tool-block selection and per-block expand are focused-session-local,
but both paths called scrollback_build.clear(), dropping every
session's assembled build from the LRU (losing A->B->A tab-flip hits)
while leaving their render-cache entries orphaned outside the
build-LRU memory bound. Add ScrollbackBuildCache::evict_session and
evict just the focused session alongside the existing per-message
invalidations.

Also harden SessionList::replace against duplicate session ids from a
buggy replay (dedupe, last occurrence wins, stable tab order) so every
by-id lookup - focus, status updates, upsert, the reverse status-change
scan - can rely on uniqueness.

Addresses CodeRabbit review on PR #12.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@saucam

saucam commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review in 8afce81:

  • Evict only the focused session's build on tool-selection changes (Major/Perf): applied. cycle_tool_block_selection and toggle_expand_selected_tool_block now call a new ScrollbackBuildCache::evict_session instead of clear() — a selection highlight only affects the focused session, and the global clear also broke the render-cache ⊆ build-LRU memory bound. The global clear() remains only on the verbose-tool-output toggle, which genuinely changes all sessions' rendering. Tests: evict_session_removes_only_that_session, tool_selection_evicts_only_the_focused_sessions_build, tool_expand_evicts_only_the_focused_sessions_build.
  • Disambiguate duplicate message ids in cache keys / build segments (Major, Heavy lift): rejected — duplicates are impossible by construction at both protocol ends. The daemon mints every messageId with randomUUID(), and duplicate replay entries are a daemon bug class already fixed upstream (codeoid#74 makes ScrollbackBuffer.push upsert by messageId as the standing regression guard). Client-side, upsert/apply_delta are newest-wins (tested), and even a double-violation degrades benignly (splice repaints on next epoch bump) rather than corrupting. Per-occurrence keys would add real invalidation machinery for a non-reachable state.
  • Reverse-scan duplicate-id guard (outside-diff): applied the cheap hardening — SessionList::replace now collapses duplicate ids (last occurrence wins, first position kept), making by-id scans safe unconditionally. Test: replace_dedupes_duplicate_ids_last_wins_first_position.
  • Cache 'any_animating' across idle frames (Trivial/Heavy lift): rejected — branch-only allocation-free pass, microseconds at 10k messages, and caching liveness in the build introduces a staleness class for negligible gain.

234 tests pass, clippy warning set identical to baseline, fmt clean.

@saucam

saucam commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Review addressed through 8afce81:

  • Per-session eviction on tool-selection changes (Major): applied — verified real; both cycle_tool_block_selection and toggle_expand_selected_tool_block were dropping every session's assembled build for a focused-session-local change. New evict_session() scopes it (tool selection can only affect the focused session's transcript; message ids are globally unique UUIDs).
  • Duplicate message-id disambiguation (Major, heavy lift): rejected with source evidence — duplicates within a session are impossible by construction at both ends: the daemon mints messageIds with randomUUID() and its ScrollbackBuffer upserts by messageId (replay is deduped at the source — see codeoid#74), and the TUI's MessageStore::apply_message upserts by id / replace_scrollback ingests the deduped list wholesale. The invariant is now documented at the cache/segment keying site. The proposed occurrence-index keys would also invalidate on any insertion.
  • Duplicate session ids vs the reverse scan (Minor, outside diff): applied the cheap hardening — SessionList::replace dedupes (last occurrence wins, first position kept), so by-id lookups rely on uniqueness unconditionally.
  • any_animating scan caching (Trivial nitpick): rejected — branch-only boolean scan over an in-memory slice, ~µs at 10k messages; caching animating keys would make the frame-path decision depend on cached rather than live tool state.

218 tests (+4), clippy = baseline exactly, fmt clean.

@saucam saucam merged commit e52bad0 into main Jul 2, 2026
4 checks passed
This was referenced Jul 2, 2026
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