Skip to content

Render changed files as expandable tree with aggregated diff stats - #172

Merged
juliusmarminge merged 3 commits into
mainfrom
t3code/changed-files-tree-view
Mar 5, 2026
Merged

juliusmarminge merged 3 commits into
mainfrom
t3code/changed-files-tree-view

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Mar 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Replace flat changed-file chips in ChatView with an expandable directory/file tree for each turn.
  • Add shared diff-stat helpers (summarizeTurnDiffStats, DiffStatLabel) so totals and per-node stats render consistently.
  • Introduce buildTurnDiffTree to normalize paths, build nested directory nodes, and aggregate additions/deletions up the tree.
  • Support file-level diff navigation from tree leaves while keeping directory expand/collapse state in the UI.
  • Add unit coverage for diff stat summarization, nested tree construction, missing stat handling, and Windows path normalization.

Testing

  • apps/web/src/lib/turnDiffTree.test.ts validates aggregated stats and tree structure for nested paths.
  • apps/web/src/lib/turnDiffTree.test.ts verifies files without numeric stats are preserved and excluded from totals.
  • apps/web/src/lib/turnDiffTree.test.ts verifies \\ path normalization into POSIX-style segments.
  • bun lint — Not run
  • bun typecheck — Not run

Note

Medium Risk
Moderate UI/UX change in ChatView that 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 ChatView is 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.ts to normalize paths (including Windows separators), build/sort/compact directory nodes, and aggregate additions/deletions (with summarizeTurnDiffStats), plus unit tests covering stats summarization, nesting/aggregation, missing stats handling, path normalization, and compaction behavior. Also updates VscodeEntryIcon to accept an optional className for 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 ChangedFilesTree with per-directory aggregation and expand/collapse controls, pass resolvedTheme for icon rendering, and provide summarizeTurnDiffStats and buildTurnDiffTree utilities in turnDiffTree.ts, with tests in turnDiffTree.test.ts.

📍Where to Start

Start with buildTurnDiffTree and summarizeTurnDiffStats in turnDiffTree.ts, then review ChangedFilesTree integration in ChatView.tsx.

Macroscope summarized c45ea6d.

- 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
@coderabbitai

coderabbitai Bot commented Mar 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3b27b19c-6d02-4bf6-972b-6b999b491f03

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch t3code/changed-files-tree-view

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/web/src/components/ChatView.tsx Outdated
Comment thread apps/web/src/lib/turnDiffTree.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Create PR

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">

Comment thread apps/web/src/components/ChatView.tsx
Comment thread apps/web/src/components/ChatView.tsx Outdated
- 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
Comment thread apps/server/src/main.ts
Comment thread apps/server/src/attachmentPaths.ts Outdated
Comment thread apps/server/src/imageMime.ts Outdated
- 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>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 showParentheses prop is dead code
    • Removed the unused showParentheses prop and its associated rendering logic from DiffStatLabel since no caller ever passes it.

Create PR

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)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

additions: number;
deletions: number;
showParentheses?: boolean;
}) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

@juliusmarminge
juliusmarminge merged commit 6841fa1 into main Mar 5, 2026
5 checks passed
@juliusmarminge
juliusmarminge deleted the t3code/changed-files-tree-view branch March 5, 2026 23:56
ikeboy003 pushed a commit to YCWagerWise/t3code-new that referenced this pull request Aug 26, 2026
…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.
aorwall added a commit to aorwall/t3code that referenced this pull request Sep 16, 2026
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
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