fix(ui): bound and surface large-diff highlight retries - #785
Conversation
A failed worker highlight resolved to plain rows marked retryable, which deliberately stay out of the shared cache so a recreated worker can still colorize the file. Viewport prefetch re-requests every file in its halo on each scroll, so a failure that always repeats restarted the multi-second highlight on every scroll step. Bound the retries and cache plain rows once the budget is spent. The retry also never reached the file the user was looking at: the hook commits its cache key even for a retryable result, so its layout effect will not run again for that key. Prefer a result a later prefetch has since cached instead of holding the file plain until it remounts. Charge compact results by the payload they retain rather than by their index maps. A source-backed result maps a few visible patch lines onto a whole-file payload, so the maps understated it by orders of magnitude and the cache's line budget stopped bounding memory. Dispose the worker doubles registered in the highlight tests: the client is a module-level singleton shared by every test file in a `bun test` process. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2ZhrEHs971XcBuAMP6RwR
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Greptile SummaryThe PR bounds repeated large-diff highlighting failures, allows provisional snapshots to consult later cached results, and revises compact-result cache accounting.
Confidence Score: 4/5The successful-retry refresh path should be fixed before merging because a mounted file can remain plain after highlighting has completed. A successful background prefetch only updates an unsubscribed shared cache, while the mounted hook has already committed the key and does not schedule another render to observe that result. Files Needing Attention: src/ui/diff/useHighlightedDiff.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant H as Mounted highlight hook
participant C as Shared cache
participant P as Viewport prefetch
participant W as Worker
H->>W: Initial highlight
W-->>H: Retryable failure
H->>H: Store provisional plain result
P->>W: Retry same cache key
W-->>P: Successful highlight
P->>C: Cache successful result
Note over C,H: Cache write schedules no render
H-->>H: Plain rows remain until another render
Prompt To Fix All With AI### Issue 1
src/ui/diff/useHighlightedDiff.ts:210
**Successful retry does not repaint**
When a mounted file has a provisional retryable result and a later viewport prefetch succeeds, the prefetch only updates the unsubscribed shared cache. No render is scheduled to execute this `peek`, so the file remains plain until another scroll, state update, or remount.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (1): Last reviewed commit: "fix(ui): bound and surface large-diff hi..." | Re-trigger Greptile |
| // re-run for a key it already committed, so prefer a result a later prefetch retry has since | ||
| // cached rather than holding this file plain until it remounts. | ||
| if (highlighted?.retryable) { | ||
| return SHARED_HIGHLIGHTED_DIFF_CACHE.peek(appearanceCacheKey) ?? highlighted; |
There was a problem hiding this comment.
Successful retry does not repaint
When a mounted file has a provisional retryable result and a later viewport prefetch succeeds, the prefetch only updates the unsubscribed shared cache. No render is scheduled to execute this peek, so the file remains plain until another scroll, state update, or remount.
Knowledge Base Used: Diff Rendering Pipeline
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/diff/useHighlightedDiff.ts
Line: 210
Comment:
**Successful retry does not repaint**
When a mounted file has a provisional retryable result and a later viewport prefetch succeeds, the prefetch only updates the unsubscribed shared cache. No render is scheduled to execute this `peek`, so the file remains plain until another scroll, state update, or remount.
**Knowledge Base Used:** [Diff Rendering Pipeline](https://app.greptile.com/modem/-/custom-context/knowledge-base/modem-dev/hunk/-/docs/ui-diff-rendering.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Correct, and fixed in b1538b3.
The peek only helps if something happens to re-render. Prefetch fills the shared cache from a useEffect in DiffPane and never touches this hook's state, so a file holding provisional rows had no signal at all — it stayed plain until an unrelated render or a remount.
The cache commit now notifies readers watching that key, and the hook subscribes for exactly as long as it holds a provisional result:
subscribeToHighlightedDiff(cacheKey, listener)registers interest in one key.commitHighlightResultcallsnotifyHighlightedDiffCachedon both paths that write to the cache — a successful retry, and the plain rows cached once the retry budget is spent.- The hook subscribes only while
highlighted?.retryableis set, and the repaint it performs clears that flag, which unsubscribes it. No loop.
The peek in resolveHighlightedSnapshot stays, since it still covers the window where the cache fills between render and the subscription effect.
Dispatch iterates over a copy of the listener set, because a listener repainting is what removes it mid-loop.
Covered by wakes a reader holding provisional rows when the key finally caches a result, which also asserts an unsubscribed reader stops being notified.
Generated by Claude Code
Preferring a newly cached result during render was not enough on its own. Viewport prefetch fills the shared cache without rendering anything, so a file showing the provisional plain rows of a retryable failure had no signal that a later attempt succeeded, and stayed plain until the next unrelated render or a remount. Have the cache commit notify readers watching that key, and subscribe from the hook for as long as it is holding a provisional result. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P2ZhrEHs971XcBuAMP6RwR
Follow-up to #759. Independent of #784 — different files, either can land first.
1. A repeating worker failure re-ran on every scroll
loadHighlightedDiffmarks every worker-path failureretryable, andcommitHighlightResultdeliberately keeps those out of the shared cache so a recreated worker can still colorize the file. ButDiffPanere-runsprefetchHighlightedDifffor every file in its halo wheneverhighlightPrefetchFileIdschanges — i.e. on scroll. With nothing cached and no promise retained, a failure that always repeats (unresolvable worker entry in a repackaged binary, a payload the validator always rejects) rebuilt the source plan and restarted the multi-second highlight on every scroll step. That is the exact repetition the comment above thecatchsays it prevents.Retries are now bounded per cache key. One retry covers a worker lost mid-session; past that, plain rows are cached like any other result and the work stops.
2. The retry never reached the file on screen
Even when a later attempt succeeded, the mounted file stayed plain. The hook commits
highlightedCacheKeyfor a retryable result too, so its layout effect short-circuits and never re-runs for that key — the success only surfaced after unmount/remount.resolveHighlightedSnapshotnow treats a retryable result as provisional and prefers a result a later prefetch has since cached. It usespeek, so render stays side-effect free.These two are load-bearing together: without the bound, the cost repeated forever; without the peek, the benefit never landed.
3. The cache budget stopped bounding compact entries
highlightedLineCountcharged compact results bydeletionLineMap.length— one entry per visible patch line — while the payload retains ranges for the whole source. A 4-line patch view of a 20,000-line file was charged 8 line-equivalents.Compact results are now charged via
compactHighlightedDiffByteLength, converted through the same ~1.4KB/line rate the budget was calibrated on. That keeps one budget comparable across HAST and compact shapes, and correctly keeps small compact results cheap rather than over-charging them as lines.4. Test isolation
The failing-worker doubles were registered into a module-level singleton shared by every test file in a
bun testprocess and never released. Added anafterAllteardown.Changes
src/ui/diff/useHighlightedDiff.ts— bounded retry budget; provisional retryable snapshots.src/ui/diff/highlightedDiffCache.ts— charge compact entries by retained payload.src/ui/diff/useHighlightedDiff.test.ts— budget-exhaustion coverage plus teardown.src/ui/diff/highlightedDiffCache.test.ts— source-backed compact entry is charged for its payload; a small compact entry stays cheap.Both new cache tests were verified to fail under the previous accounting, and the retry test to fail without the bound.
Validation
bun run typecheck,bun run lint,bun run formatbun test— 3030 pass, 3 fail:change-block line pairing(confirmed failing on unmodifiedmainat5ebe975), one PTY flake, andwebsite/specs missing@axe-core/playwrighthere.bun run test:integration— 114 pass, 3 fail. Unmodifiedmainfails 2 of the same 3; the third (PTY layout > dragging the sidebar divider) is flaky under load and passed 3/3 in isolation on this branch, as didtest/pty/scroll.test.ts.bun run test:tty-smoke— 0/9 in this sandbox, identical on unmodifiedmain, so unverified rather than passing.Generated by Claude Code