Skip to content

fix(tui): dedupe FileWritesPanel rows — one row per path with edit count#676

Merged
kelsonpw merged 1 commit into
mainfrom
fix/tui-file-writes-dedup
May 9, 2026
Merged

fix(tui): dedupe FileWritesPanel rows — one row per path with edit count#676
kelsonpw merged 1 commit into
mainfrom
fix/tui-file-writes-dedup

Conversation

@kelsonpw
Copy link
Copy Markdown
Member

@kelsonpw kelsonpw commented May 8, 2026

User report

"oh god look how bad this is"

The Files panel shows duplicate rows whenever the inner agent edits the same file more than once during a run:

Files · 14 written
✓ MODIFY excalidraw-app/App.tsx · edited 4ms
✓ MODIFY excalidraw-app/share/ShareDialog.tsx · edited 3ms
✓ MODIFY excalidraw-app/share/ShareDialog.tsx · edited 3ms       ← duplicate
✓ MODIFY excalidraw-app/collab/Portal.tsx · edited 10ms
✓ MODIFY excalidraw-app/collab/Portal.tsx · edited 4ms           ← duplicate
✓ MODIFY excalidraw-app/components/ExportToExcalidrawPlus.tsx · edited 6ms
✓ MODIFY excalidraw-app/components/ExportToExcalidrawPlus.tsx · edited 7ms ← duplicate
✓ MODIFY packages/excalidraw/actions/actionAddToLibrary.ts · edited 5ms

WizardStore.recordFileChangePlanned / recordFileChangeApplied append one FileWriteEntry per 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

Files · 8 written
✓ MODIFY excalidraw-app/App.tsx · edited · 4ms
✓ MODIFY excalidraw-app/share/ShareDialog.tsx · edited 2× · 3ms
✓ MODIFY excalidraw-app/collab/Portal.tsx · edited 2× · 4ms
✓ MODIFY excalidraw-app/components/ExportToExcalidrawPlus.tsx · edited 2× · 7ms
✓ MODIFY packages/excalidraw/actions/actionAddToLibrary.ts · edited · 5ms

One row per file. The latest emission's editTime / bytes win. 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).

  • New dedupeByPath(entries) helper builds a Map<path, { entry: latest, editCount }>, then sorts by latest startedAt so "most recent activity at the bottom" still holds.
  • Dedupe runs before the maxVisible slice — otherwise we'd drop distinct files to make room for duplicates of the same one.
  • Header counters (X/Y written, N written) read off the deduped view too — the original "14 written" was misleading.
  • × N annotation appears in every status branch (applied, failed, planned) when editCount > 1. Single-edit rows render unchanged so we don't add noise to the common case.
  • React key switches from ${startedAt}-${path} to path — 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 on FileWriteEntry), 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:

  • 3 emissions of the same path → 1 row, count = 3, header 1 written
  • 2 distinct paths + 1 doubled → 2 rows, only the doubled annotated
  • Latest emission's bytes / completedAt win on the collapsed row
  • Single-edit rows do not pick up a annotation

The 10 pre-existing snapshot/contract tests still pass — no frame regressions.

Verification

  • node_modules/.bin/tsc -p tsconfig.json — clean
  • node_modules/.bin/vitest run --pool=forks --maxWorkers=1 src/ui/tui/components/__tests__/FileWritesPanel.test.tsx — 14/14 pass
  • node_modules/.bin/vitest run --pool=forks --maxWorkers=1 — 3575/3575 pass (243 files)
  • pnpm lint — only pre-existing unrelated warning in EventPlanFullScreen.test.tsx

Coordination

🤖 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 FileWritesPanel to 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 maxVisible slicing, header counters now reflect the deduped view, and row keys switch to path for stability. Adds targeted tests to lock down dedupe behavior, latest-entry selection, and × N rendering.

Reviewed by Cursor Bugbot for commit c4cf07f. Bugbot is set up for automated code reviews on this repo. Configure here.

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>
@kelsonpw kelsonpw requested a review from a team as a code owner May 8, 2026 23:26
@kelsonpw kelsonpw merged commit 4010bc8 into main May 9, 2026
11 checks passed
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>
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