fix(tui): dedupe FileWritesPanel rows — one row per path with edit count#676
Merged
Conversation
The store appends one entry per inner-agent emission, so when the agent edits the same file multiple times the panel rendered duplicate rows (user report: "oh god look how bad this is" — 14 rows for ~6 distinct files with several paths repeated 2–4×). Fix at the render layer: group by path in `dedupeByPath`, keep the latest emission's status / bytes / duration, count repeats, and annotate the row with `× N` when N > 1. Header counters now reflect deduped totals too. No store changes — the underlying ledger and other consumers (rollback, agent UI) keep their append-only shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 9, 2026
Merged
kelsonpw
added a commit
that referenced
this pull request
May 9, 2026
… — no more 2-line wraps (#684) * fix(tui): file-writes rows pin keyword + metadata, head-truncate path — no more 2-line wraps Long paths (e.g. `src/app/(category-sidebar)/products/[category]/[subcategory]/[product]/page.tsx`) made every row in the FileWritesPanel reflow onto two visual lines, and Yoga stole columns from neighboring cells in the process — the keyword printed as `CREAT` (missing trailing E) and the gap between keyword and path collapsed (`MODIFYsrc/app/...`). Three coupled bugs in one fix: A. Path wraps to 2 rows when it overflows the row width. B. `CREATE` truncated to `CREAT` on wrapped rows. C. Space between keyword and path lost on wrapped rows. Root cause: the row was a flat sequence of `<Text>` nodes with implicit flex behavior. When the path overflowed, Yoga shrank every cell to make room — including the keyword, the icon, and the leading space. Fix: every cell becomes an explicit `<Box>` with width pinned via `flexShrink={0}`. The keyword cell is `width=7` so `MODIFY` + 1 trailing space always fits. The trailing detail cell is also `flexShrink={0}` so it stays glued to the row. The path is the only `flexShrink={1}` cell and absorbs overflow via head-truncation (`…/[product]/page.tsx`), implemented via `truncatePathHead()` against a width budget computed from the visible terminal columns. `wrap="truncate-end"` on the inner path Text is a defense-in-depth safety net for resize lag. Coordinates with #676 (dedupe `× N` annotation): the merge auto-resolved cleanly, both behaviors are preserved by the test suite (21/21 pass). Test plan: - 5 widths (25 / 60 / 80 / 120 / 200 cols) × long path → 1 row each - assert `CREATE\s` matches and `CREATE[^\s]` doesn't (bug B/C) - assert head-truncated form contains `…` and the basename survives - 3637 / 3637 vitest pass; tsc clean; pnpm lint clean (only pre-existing unrelated warning in EventPlanFullScreen.test.tsx) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: correct off-by-one errors in truncatePathHead and path budget calculation - Fix loop guard from +1 to +2 to account for the 2-char '…/' prefix, preventing returned strings from exceeding maxWidth by one. - Change middle-truncation guard from 'basename.length + 1 >= maxWidth' to 'basename.length > maxWidth' so basenames that fit within budget are returned as-is instead of being unnecessarily mangled. - Return basename directly when no parent segments fit, avoiding the '…/' prefix that would overflow. - Set KEYWORD_PATH_GAP to 0 since the keyword box width (7) already includes the trailing space — the phantom gap made pathBudget 1 too small, which previously masked the truncatePathHead overflow. Applied via @cursor push command * fix: correct off-by-one in middle-truncation reserving space for both ellipsis characters Applied via @cursor push command * fix: subtract paddingX from width budget passed to FileWritesPanel Applied via @cursor push command --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User report
The Files panel shows duplicate rows whenever the inner agent edits the same file more than once during a run:
WizardStore.recordFileChangePlanned/recordFileChangeAppliedappend oneFileWriteEntryper emission — they only collapse a back-to-back planned duplicate. As soon as the first edit completes (applied), every subsequent edit of the same file produces another row.After
One row per file. The latest emission's
editTime/byteswin. Repeats are annotated with× N.Implementation
Render-time dedupe in
FileWritesPanel(smaller blast radius — store, agent UI, file-change-ledger, and rollback paths all keep their append-only shape).dedupeByPath(entries)helper builds aMap<path, { entry: latest, editCount }>, then sorts by lateststartedAtso "most recent activity at the bottom" still holds.maxVisibleslice — otherwise we'd drop distinct files to make room for duplicates of the same one.X/Y written,N written) read off the deduped view too — the original "14 written" was misleading.× Nannotation appears in every status branch (applied,failed,planned) wheneditCount > 1. Single-edit rows render unchanged so we don't add noise to the common case.keyswitches from${startedAt}-${path}topath— paths are unique within the deduped list and stable across rerenders.Dedupe key
Plain string equality on
entry.path. The inner agent normalizes to absolute paths before emitting (per the comment onFileWriteEntry), so two emissions for the same file always carry the same string. No additional path normalization needed at the render layer.Tests
Added 4 cases in
FileWritesPanel.test.tsx:1 written2×bytes/completedAtwin on the collapsed row1×annotationThe 10 pre-existing snapshot/contract tests still pass — no frame regressions.
Verification
node_modules/.bin/tsc -p tsconfig.json— cleannode_modules/.bin/vitest run --pool=forks --maxWorkers=1 src/ui/tui/components/__tests__/FileWritesPanel.test.tsx— 14/14 passnode_modules/.bin/vitest run --pool=forks --maxWorkers=1— 3575/3575 pass (243 files)pnpm lint— only pre-existing unrelated warning inEventPlanFullScreen.test.tsxCoordination
FileWritesPanel.tsx+ its test file.🤖 Generated with Claude Code
Note
Low Risk
Low risk: render-only behavior change in the TUI that collapses duplicate rows; main risk is subtle ordering/count display regressions, covered by new tests.
Overview
Fixes the TUI
FileWritesPanelto collapse multiple write emissions for the same file path into a single row, showing the latest status/bytes/duration and annotating repeats with× N.Dedupe is applied before
maxVisibleslicing, header counters now reflect the deduped view, and row keys switch topathfor stability. Adds targeted tests to lock down dedupe behavior, latest-entry selection, and× Nrendering.Reviewed by Cursor Bugbot for commit c4cf07f. Bugbot is set up for automated code reviews on this repo. Configure here.