Skip to content

CPLAT-11789: fix ccx navigation freeze — preview reads block the UI loop - #161

Merged
gavin-jeong merged 1 commit into
masterfrom
CPLAT-11789-nav-freeze
Aug 30, 2026
Merged

gavin-jeong merged 1 commit into
masterfrom
CPLAT-11789-nav-freeze

Conversation

@gavin-jeong

Copy link
Copy Markdown
Collaborator

JIRA: https://sendbird.atlassian.net/browse/CPLAT-11789

Problem

Navigating the session browser freezes intermittently — most cursor moves are fine, then one lands on a large session and the UI locks up.

Three preview reads ran synchronously inside Update, blocking the Bubble Tea event loop for the entire file read:

Path Worst case (measured, warm cache)
updateSessionConvPreviewLoadMessagesSummary 465ms (180 MB transcript)
updateSessionStatsPreviewScanSessionStats 790ms
refreshSessionPreviewLive conversation re-read same, on every tick

Measured over 120 cursor moves on my real corpus with the conversation preview open:

p50 9ms   p90 28ms   p99 191ms   max 466ms
moves >100ms: 6/120

That distribution is the report — usually fine, occasionally a visible lock-up.

The existing 30ms navigation debounce does not help: it delays the stall until the cursor stops rather than removing it. LoadMessagesSummary is the worst offender per byte — it needs only the first 50 messages but keeps a ring buffer for the tail, so it scans to EOF regardless of size.

Fix

Dispatch all three as tea.Cmd, following the refs/outputs pattern already in the file: show (loading…), read off the loop, apply from a completion message.

  • Per-session in-flight latch so a mode cannot dispatch twice.
  • Stale results discarded — the cursor moves on while a read is in flight, and a late result must not overwrite the pane the user is looking at now.
  • previewDispatchesFromView is a deliberately separate predicate from previewDispatchesCmd. View() cannot dispatch, so it must not drive these modes — but project rows use previewDispatchesCmd to decide whether to fall through to the project summary, and adding conversation there would reroute them to a representative session. Merging the two would have been a silent regression.

Callers that discarded the return value had to be updated or the load would never run: handleTick (live sessions), mouse scroll, and the resize path in View — the last re-renders existing entries instead of dispatching, since View cannot dispatch and the pane would otherwise stick on (loading…).

Result

UI-loop blocking on the real corpus: 465ms → 0ms (conversation and stats both), with the preview still rendering on 120/120 moves.

Testing

  • Full suite green (go test ./...), go vet ./... clean.
  • New regression tests assert the read happens in the command, not in Update — the property that was violated. Plus: stale-result discard, double-dispatch guard, and the View-exclusion invariant including the "must stay narrow" half.
  • Mutation-tested each test by reintroducing the bug it covers (inline conversation read, inline stats scan, removed live in-flight guard) and confirming it fails.

Note

Verified by measurement, not by eye — I can't drive the TUI interactively. Worth a quick make build && ./bin/ccx scroll through the session list to confirm it feels right, particularly that the brief (loading…) on large sessions is not distracting.

Moving the cursor in the session browser froze intermittently: most
moves were fine, then one landed on a large session and the UI locked
up. Three preview reads ran synchronously inside Update, so the event
loop was blocked for the whole file read:

  updateSessionConvPreview  -> LoadMessagesSummary   465ms (180 MB)
  updateSessionStatsPreview -> ScanSessionStats      790ms
  refreshSessionPreviewLive -> LoadMessagesSummary   every tick

Measured over 120 cursor moves on a real corpus with the conversation
preview open: p50 9ms, p99 191ms, max 466ms, 6/120 moves over 100ms —
which is exactly the "sometimes" in the report.

The existing 30ms navigation debounce does not help. It delays the
stall until the cursor stops rather than removing it.

Dispatch all three as commands instead, following the refs/outputs
pattern already in the file: show "(loading…)", read off the loop, and
apply the result from a completion message. Each keeps a per-session
in-flight latch so it cannot dispatch twice, and discards results whose
session is no longer selected — the cursor moves on while a read is in
flight, and a late result must not overwrite the pane the user is
looking at now.

previewDispatchesFromView is deliberately a separate predicate from
previewDispatchesCmd rather than an extension of it. View() cannot
dispatch a command, so it must not drive these modes; but project rows
use previewDispatchesCmd to decide whether to fall through to the
project summary, and adding conversation there would reroute them to a
representative session instead.

Callers that discarded the return value had to be updated or the load
would silently never run: handleTick (live sessions), mouse scroll, and
the resize path in View — the last re-renders existing entries rather
than dispatching, since View cannot dispatch and the pane would stick
on "(loading…)".

UI-loop blocking on the real corpus is now 0ms for both conversation
and stats, with the preview still rendering on 120/120 moves.
@upwind-code-us

upwind-code-us Bot commented Aug 30, 2026

Copy link
Copy Markdown

Upwind Upwind Code Scan - ✅ Proceed with Deployment

1 newly introduced vulnerabilities · 0 resolved · 2 total in this PR vs master

🔶 1 High | 🟢 1 Low


🟢 Low · 1 finding
CVE Package Version Fix
CVE-2026-39824 golang.org/x/sys v0.42.0 0.44.0

View full analysis in Upwind Console

Scan completed in 18s

Scan history (1 scan)
Commit Scanned at New Resolved Net
71db971 < 2026-08-30 12:23 UTC +1 0 +1

Last scanned: 71db971 · 2026-08-30 12:23 UTC

@upwind-code-us

upwind-code-us Bot commented Aug 30, 2026

Copy link
Copy Markdown

Upwind Upwind IaC Scan - ✅ Proceed with Deployment

0 newly introduced misconfigurations · 0 resolved · 0 total in this PR vs main

View full analysis in Upwind Console →

Scan completed in 5s

Scan history (2 scans)
Commit Scanned at New Resolved Net
71db971 2026-08-30 12:23 UTC
71db971 < 2026-08-30 12:23 UTC 0 0 0

Last scanned: 71db971 · 2026-08-30 12:23 UTC

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

LGTM!

@jinsekim jinsekim added the auto-review/approved Auto-approved by the Slack auto-reviewer bot label Aug 30, 2026
@gavin-jeong
gavin-jeong merged commit 8cd8195 into master Aug 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review/approved Auto-approved by the Slack auto-reviewer bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants