Render changed files as expandable tree with aggregated diff stats - #172
Conversation
- Replace flat changed-files chips in ChatView with a nested, expandable tree - Extract diff tree/stat logic into turnDiffTree utilities - Add unit tests for stat aggregation, nesting, and Windows path normalization
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: First toggle click fails for unregistered top-level directories
- Changed toggleDirectory to accept the effective expanded state (which accounts for the fallback default) instead of toggling the raw undefined state value.
- ✅ Fixed: Directory always shows +0/-0 when children lack stats
- Wrapped the directory DiffStatLabel render with a hasNonZeroStat guard, consistent with file nodes and the summary header.
Or push these changes by commenting:
@cursor push 7346e62eab
Preview (7346e62eab)
diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx
--- a/apps/web/src/components/ChatView.tsx
+++ b/apps/web/src/components/ChatView.tsx
@@ -3176,10 +3176,10 @@
() => buildInitiallyExpandedDirectoryState(treeNodes),
);
- const toggleDirectory = useCallback((pathValue: string) => {
+ const toggleDirectory = useCallback((pathValue: string, currentlyExpanded: boolean) => {
setExpandedDirectories((current) => ({
...current,
- [pathValue]: !current[pathValue],
+ [pathValue]: !currentlyExpanded,
}));
}, []);
@@ -3193,7 +3193,7 @@
type="button"
className="group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-background/80"
style={{ paddingLeft: `${leftPadding}px` }}
- onClick={() => toggleDirectory(node.path)}
+ onClick={() => toggleDirectory(node.path, isExpanded)}
>
<ChevronRightIcon
aria-hidden="true"
@@ -3210,9 +3210,11 @@
<span className="truncate font-mono text-[11px] text-muted-foreground/90 group-hover:text-foreground/90">
{node.name}
</span>
- <span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
- <DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
- </span>
+ {hasNonZeroStat(node.stat) && (
+ <span className="ml-auto shrink-0 font-mono text-[10px] tabular-nums">
+ <DiffStatLabel additions={node.stat.additions} deletions={node.stat.deletions} />
+ </span>
+ )}
</button>
{isExpanded && (
<div className="space-y-0.5">- Compact single-directory chains in turn diff trees and preserve branch points - Add per-turn expand/collapse-all controls and themed VS Code file icons in ChatView - Centralize attachment route prefix stripping and tighten image/extension normalization helpers
- revert unrelated server-side attachment/image changes from this branch\n- keep changed-files tree UX fixes (icons, directory behavior, whitespace-safe paths, stat visibility)\n\nCo-authored-by: codex <codex@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Inconsistent path format passed to diff handler
- Added normalizeFilePath() export to turnDiffTree.ts and applied it to the 'View diff' button's checkpointFiles[0]?.path so it matches the normalized paths used by tree node click handlers.
- ✅ Fixed: Unused
showParenthesesprop is dead code- Removed the unused showParentheses prop and its associated rendering logic from DiffStatLabel since no caller ever passes it.
Or push these changes by commenting:
@cursor push 74d445af4d
| type="button" | ||
| className="group flex w-full items-center gap-1.5 rounded-md py-1 pr-2 text-left hover:bg-background/80" | ||
| style={{ paddingLeft: `${leftPadding}px` }} | ||
| onClick={() => onOpenTurnDiff(turnId, node.path)} |
There was a problem hiding this comment.
Inconsistent path format passed to diff handler
Medium Severity
When a tree file node is clicked, onOpenTurnDiff receives the normalized node.path (backslashes converted to forward slashes, consecutive separators collapsed via normalizePathSegments). However, the "View diff" button still passes the original unnormalized checkpointFiles[0]?.path directly from server data. If paths contain Windows-style backslash separators, the two call sites pass different string representations of the same file to the same handler, which could cause a mismatch in the downstream diff viewer.
Additional Locations (1)
| additions: number; | ||
| deletions: number; | ||
| showParentheses?: boolean; | ||
| }) { |
There was a problem hiding this comment.
Unused showParentheses prop is dead code
Low Severity
The showParentheses prop on DiffStatLabel is defined with a default of false, but no caller ever passes true. This appears to be leftover scaffolding from the old inline-chip layout that rendered parentheses around per-file stats — a format that no longer exists after this refactor.
…xes (pingdotgg#172 pingdotgg#189 pingdotgg#225 pingdotgg#204) NOT MY WORK. These four files were sitting uncommitted in the shared checkout — 228 insertions, 50 deletions, no merge in progress, no conflict markers — and they are the real fixes for four open findings. At committed HEAD the validate_* helpers existed as DEAD CODE, never called, so the findings were live no matter what the working tree said. That is pingdotgg#393 verbatim: a fix that is one `git commit` from existing. What it contains: pingdotgg#189 sourcecontrol.rs — validate_repository_name() called before `gh repo create`, so a flag-shaped repository name cannot reach argv pingdotgg#172 sourcecontrol.rs — validate_visibility(), a closed public|private|internal enum instead of `unwrap_or("private")` interpolated into --{visibility} pingdotgg#225 sourcecontrol.rs — validate_remote_name() on the same path pingdotgg#204 keybindings.rs — Rule::from_wire per-rule failure now names the index and refuses the load instead of filter_map silently dropping the bad rule plus the settings.rs / server_main.rs edits from the same body of work. Verified before committing, on woodbine, against COMMITTED sibling HEADs (agent-sdk-rs ac2765f, cairn f7cdfd3, hearth 170a72b) rather than their dirty working trees: cargo test --manifest-path backend/Cargo.toml --release -p t3code-agent --lib 119 passed / 0 failed / 0 ignored (114 at HEAD + the 5 new negative tests) All five named tests pass: flag_shaped_repository_names_are_refused only_the_closed_visibility_enum_is_admitted flag_shaped_remote_names_are_refused an_unparseable_stored_blob_refuses_the_load_rather_than_defaulting a_single_malformed_rule_refuses_the_whole_load I am committing, not judging. Whoever wrote this should still review it.
Merges `pingdotgg/t3code` at `0bf2d6b01` into the fork, from base `5623089ae` — 45 upstream commits. `255` files landed against `251` changed in the upstream range; the gap of 4 reconciles exactly (five landed-not-in-range — the three fork docs and the two fork-only files the typecheck fix touched — against one in-range-not-landed, `SidebarChrome.tsx`, whose resolution is byte-identical to `HEAD^1` because the fork's wordmark decision stands). Fork delta against upstream is now 776 files. Four conflicts, each resolved with the verdict `preflight.mjs` printed: | Path | Verdict | Resolution | | --- | --- | --- | | `AGENTS.md` | `decide` (`agent-instructions`) | fork's rewrite kept; upstream's new sentence folded into the existing bullet | | `apps/web/src/state/threads.ts` | unlisted → `decide, then add an entry` | the fork's `adoptedEnvironmentSnapshotAtom` graft moved up to upstream's new snapshot argument (pingdotgg#8309) | | `ProjectSettingsPanel.tsx` | unlisted → `decide, then add an entry` | took upstream's `monogram` arm and its required `projectName`; kept the fork's flag read and Workspace sections | | `SidebarChrome.tsx` | `decide` (`sidebar-brand`) | upstream reintroduced `T3Wordmark`; the fork's single `APP_BASE_NAME` span stands | `pnpm-lock.yaml` did not conflict this time. Owned-concern sweep: 2 of 27 upstream additions hit the pattern (`client-runtime/src/connection/compatibility.ts` and its test — upstream's own protocol check extracted whole by pingdotgg#11990, accepted unmodified), plus the `@clerk/expo` patch rename at R100 with identical content. Unsupported methods: ADD 0, DROP 0; 99 of 157 methods declare, KEEP 2, five known exceptions unchanged. ## Usable as-is - **Queue-or-steer follow-ups** (pingdotgg#11964, pingdotgg#11673). `followUpBehavior` lands in `ClientSettingsSchema`, not `ServerSettings` — the queue is client-side and a steer is an ordinary send, so this needs nothing from the backend. - **Monogram project icons** on the project page (pingdotgg#11845, pingdotgg#11993, pingdotgg#11984), which ride `project.meta.update`. Note this is the *project* surface only; see the Workspace caveat below. - `a5da32750` cached turns and older-page loading (pingdotgg#8309); `3efdcc529` diff tree order and collapsed folders; `9ea892e3b` thread state before remote replies. - Desktop fixes: `96bddf812` paste-as-text, `b20d29dc4` double startup, `c1b221041` sidebar alignment. - Web polish: `f0a0ead94`, `9a6b57be2`, `bf3be75c4`, `3c4c9a125`. - `37a8ab2b2` Hermes API ban lint rule; `87a12b53f` usage-limit refresh. - Dependency bumps: `844203d4f` Clerk, `b18a560bb` Reanimated/Worklets. Mobile fixes land inert. ## Unsupported in Moatless / needs implementation - **Monograms on Workspace icons.** This is the one upstream change that broke something. `ProjectIconPickerDialog` is upstream's, the fork's Workspace settings page borrows it, and upstream gave monograms their own `ProjectIconOverride` arm — but the Workspace API's `WorkspaceIcon` has only `lucide` and `emoji`, so there is no field for the letters. `workspaceIconFromOverride` now returns `null` for a monogram, which saves as no icon: the same automatic glyph the project drew before the pick. Hiding the mode instead would mean threading a prop into an upstream component, which the Stable Fork Rules exist to avoid. Recorded in `docs/fork/gaps.md`, *Workspace icons cannot hold a monogram*; it closes when the Workspace API's icon schema grows a monogram arm and `packages/moatless-api/src/generated/model/workspaceIcon.ts`, regenerated, carries it. - Everything behind `FEATURES.connections: false` — pingdotgg#11990 discovery compatibility, pingdotgg#11974 and pingdotgg#11862 mobile connection gating — lands inert. - `b84f63bb1` legacy-launcher update blocking and `e6ae764f4` mobile v2 store builds are outside what this fork ships. ## Backend behavior to consider reproducing in Moatless Eight upstream server fixes, all added to `docs/fork/gaps.md` under *Runtime fixes upstream made to its own server*: - pingdotgg#11954 — rewind against history whose length changed. - pingdotgg#10792 — checkpoint capture reuses index metadata. - pingdotgg#11633 — fetch/checkout correctness. - pingdotgg#11405 — git processes capped at 8 by a semaphore, **with long operations exempt**. The exemption is the easy half to miss; capping without it stalls clones behind short status calls. - pingdotgg#11381 — preview host released after an unanswered request. - pingdotgg#11345 — a missing provider executable names the setting that points at it. - pingdotgg#12008 — health checks clean up `_MEI` folders. - pingdotgg#11888 — GitHub GraphQL budget, rate-limit gate, and read cache. Also worth noting: with `followUpBehavior: "steer"` a message is dispatched mid-turn, which touches the existing gap *A message sent during context compaction should be queued, not dropped*. ## Verification `verify.mjs` — tripwires, resolution-check, unsupported-methods, fmt:check, lint and typecheck all green; full test pass run sequentially by package. Two caveats, both pre-existing and neither from this merge: 1. **`@t3tools/desktop` fails `scripts/browser-secret-native.test.mjs`** — it shells out to `pkg-config` for `libsecret-1`, which the sandbox does not have. The file is not in the merge diff and 106 of its 108 suites pass (1365 tests, 12 skipped). Standing entry in `gaps.md`. 2. **`duplicate-adds.mjs` exits 1 on `packages/contracts/src/orchestration.test.ts`** — a false positive. The fork's script-port test (line 644) and upstream's new monogram test (line 1538) share `const command = yield* decodeOrchestrationCommand({` and `assert.strictEqual(command.type, "project.meta.update");` at different indentation, and the script trims whitespace before comparing. Both tests are wanted; no edit is correct, and typecheck and lint both pass over the file. The next merge's base moves past it. ## Inventory: a hole that this merge closed `resolution-check.mjs` listed seven paths both sides changed with no `pathPolicy` entry. Every one of them carries a real fork delta, which means next merge's `theirs` fallback would have dropped it silently. All seven are now covered — four entries extended and four added (`branch-toolbar-gates`, `thread-adoption-graft`, `project-settings-panel`, `git-vcs-driver-core-test`). The last of those is the one worth reading: `apps/server/src/vcs/GitVcsDriverCore.test.ts` is **the fork's only delta in `apps/server` outside `auth.ts` and `rpc.ts`** — an SSH-wrapper test rewritten to intercept `ChildProcessSpawner` because the sandbox has neither a reliable `ssh` nor an executable temp dir — and it was recorded nowhere. `resolution-check` now reports 25 paths checked and each still differs from upstream, 19 `theirs-verbatim` paths byte-identical to upstream, and no unlisted paths both sides changed. `tripwires.mjs` reports `ok 3 active workflow(s), all allowed` — the previous merge's off-repo action has been done, and **no off-repository action is outstanding for this merge**. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- Moatless task: https://moatless.soaplabstest.com/tasks/e037ca6d-4fc5-4a9e-9341-2a2ba75ada8b



Summary
ChatViewwith an expandable directory/file tree for each turn.summarizeTurnDiffStats,DiffStatLabel) so totals and per-node stats render consistently.buildTurnDiffTreeto normalize paths, build nested directory nodes, and aggregate additions/deletions up the tree.Testing
apps/web/src/lib/turnDiffTree.test.tsvalidates aggregated stats and tree structure for nested paths.apps/web/src/lib/turnDiffTree.test.tsverifies files without numeric stats are preserved and excluded from totals.apps/web/src/lib/turnDiffTree.test.tsverifies\\path normalization into POSIX-style segments.bun lint— Not runbun typecheck— Not runNote
Medium Risk
Moderate UI/UX change in
ChatViewthat introduces new tree-building/state logic and could affect diff navigation or rendering performance, but does not touch auth/security or persistence.Overview
Changed files rendering in
ChatViewis now a hierarchical tree. The per-turn “Changed files” section switches from flat file chips to an expandable directory/file tree with per-directory aggregated+/-stats, per-file stats, and a per-turn Expand all / Collapse all control.Adds
lib/turnDiffTree.tsto normalize paths (including Windows separators), build/sort/compact directory nodes, and aggregate additions/deletions (withsummarizeTurnDiffStats), plus unit tests covering stats summarization, nesting/aggregation, missing stats handling, path normalization, and compaction behavior. Also updatesVscodeEntryIconto accept an optionalclassNamefor size/styling in the new tree UI.Written by Cursor Bugbot for commit c45ea6d. This will update automatically on new commits. Configure here.
Note
Render the Assistant message Changed files section as an expandable directory/file tree with aggregated diff stats and theme-correct icons in ChatView.tsx and MessagesTimeline.tsx
Add
ChangedFilesTreewith per-directory aggregation and expand/collapse controls, passresolvedThemefor icon rendering, and providesummarizeTurnDiffStatsandbuildTurnDiffTreeutilities in turnDiffTree.ts, with tests in turnDiffTree.test.ts.📍Where to Start
Start with
buildTurnDiffTreeandsummarizeTurnDiffStatsin turnDiffTree.ts, then reviewChangedFilesTreeintegration in ChatView.tsx.Macroscope summarized c45ea6d.