Skip to content

Retarget open diff panel to the visited thread#78

Merged
juliusmarminge merged 5 commits intomainfrom
codething/53bdee92
Feb 19, 2026
Merged

Retarget open diff panel to the visited thread#78
juliusmarminge merged 5 commits intomainfrom
codething/53bdee92

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 19, 2026

Summary

  • Make DiffPanel prefer the route thread when present, so diff context follows the active thread page.
  • When diff is open and the route thread changes, dispatch SET_DIFF_TARGET to retarget the panel.
  • Update reducer handling for MARK_THREAD_VISITED to retarget open diff state to the visited thread and clear stale turn/file selection.
  • Add a store reducer test covering retargeting behavior when switching visited threads with an open diff.

Testing

  • Added unit test: store reducer thread continuity -> retargets open diff state to the visited thread in apps/web/src/store.test.ts.
  • Not run: project lint scripts.
  • Not run: full test suite.

Open with Devin

Note

Medium Risk
Touches routing and navigation flow for diff viewing and removes persisted diff UI state, so regressions could affect deep-linking/back-forward behavior and diff panel visibility across thread navigation.

Overview
The diff panel is now URL-driven: open/close and turn/file targeting are read from and written to route search params (diff, diffTurnId, diffFilePath) via new helpers parseDiffRouteSearch/stripDiffSearchParams, used by both ChatView and DiffPanel.

Diff UI mounting is moved from the _chat layout to the per-thread route (_chat.$threadId), including responsive sheet/inline behavior and lazy-loading, and DiffPanel now always follows the route threadId instead of stored diff-thread state.

State management is simplified by removing diff-related fields and actions from the store/reducer (and associated tests), adding focused unit tests for diff search parsing; the diff worker-pool provider is extracted into DiffWorkerPoolProvider and applied at the chat layout level.

Written by Cursor Bugbot for commit 8a72a2a. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Diff panel is fully URL-driven (open/close and target specific turns/files) and adapts responsively (inline or sheet).
    • Background worker pool added to improve diff performance.
  • Bug Fixes

    • Diff view now follows the address bar consistently and no longer conflicts with stored UI state.
    • Closing or navigating threads cleans diff-related query parameters.
    • Removed redundant "Showing diffs for the active thread" indicator.
  • Tests

    • Added tests for parsing and normalizing diff route parameters.

- Prefer route thread id over stored diff thread when rendering diffs
- When visiting a different thread with diff open, retarget diff state and clear stale turn/file selection
- Add reducer test coverage for open-diff retargeting behavior
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 19, 2026

Drive diff panel open/close and target selection via thread route URL search in ChatView and DiffPanel to retarget the open diff panel to the visited thread

Move diff state from store to URL search params, update ChatView and DiffPanel to read/write diff, diffTurnId, and diffFilePath, add parseDiffRouteSearch/stripDiffSearchParams, mount DiffPanel within /_chat/$threadId with sheet/inline modes, and remove diff actions/state from the store. Key files: apps/web/src/components/ChatView.tsx, apps/web/src/components/DiffPanel.tsx, apps/web/src/routes/_chat.$threadId.tsx, apps/web/src/diffRouteSearch.ts, apps/web/src/components/DiffWorkerPoolProvider.tsx.

📍Where to Start

Start with parseDiffRouteSearch in apps/web/src/diffRouteSearch.ts, then review ChatThreadRouteView in apps/web/src/routes/_chat.$threadId.tsx and URL-driven logic in ChatView apps/web/src/components/ChatView.tsx and DiffPanel apps/web/src/components/DiffPanel.tsx.


Macroscope summarized 8a72a2a.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉


Walkthrough

Diff UI control moved from Redux state to URL/search parameters; diff-related store fields and actions were removed. Routes and components now parse/validate diff search params and drive diff UI via navigation. DiffWorkerPoolProvider was extracted to a separate component and wired into layout.

Changes

Cohort / File(s) Summary
Store — remove diff UI state
apps/web/src/store.ts, apps/web/src/store.test.ts
Removed diff-related AppState fields and action variants (diffOpen, diffThreadId, diffTurnId, diffFilePath, TOGGLE_DIFF, OPEN_DIFF, SET_DIFF_TARGET, CLOSE_DIFF); tests updated to remove diff fixtures/assertions.
Route search parsing & validation
apps/web/src/diffRouteSearch.ts, apps/web/src/diffRouteSearch.test.ts, apps/web/src/routes/_chat.$threadId.tsx
Added DiffRouteSearch, parseDiffRouteSearch, and stripDiffSearchParams; tests exercise parsing/normalization; thread route now uses validateSearch and derives diff state from route search.
Route layout & diff composition
apps/web/src/routes/_chat.tsx, apps/web/src/routes/_chat.$threadId.tsx
Main chat route simplified to always wrap Outlet with DiffWorkerPoolProvider; thread route conditionally lazy-loads/renders DiffPanel as sheet or inline based on parsed search and media query, and provides URL-driven close handler.
Components: router-driven diff navigation
apps/web/src/components/ChatView.tsx, apps/web/src/components/DiffPanel.tsx
Replaced dispatch-based diff control with router navigation/hooks (useNavigate, useSearch, useParams); components derive diffOpen/diffTurnId/diffFilePath via parseDiffRouteSearch and update URL search to open/close/select diffs; removed store-driven diff indicator.
Worker pool extraction
apps/web/src/components/DiffWorkerPoolProvider.tsx, apps/web/src/components/DiffPanel.tsx
Extracted DiffWorkerPoolProvider into its own file; computes pool size from navigator.hardwareConcurrency, configures WorkerPoolContextProvider, and is re-exported/used by routes/components.
Misc wiring and imports
apps/web/src/components/DiffPanel.tsx, apps/web/src/components/ChatView.tsx, apps/web/src/routes/_chat.$threadId.tsx
Added router hooks and diff-route helpers imports; selection and navigation logic now driven by route search; removed inline worker-provider implementation.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Router
  participant ChatView
  participant DiffPanel
  participant WorkerPool

  User->>Router: Update URL search (open diff or select turn/file)
  Router->>ChatView: render with rawSearch
  ChatView->>ChatView: parseDiffRouteSearch(rawSearch)
  alt diffOpen === "1"
    ChatView->>Router: (may) lazy-load DiffPanel route UI
    Router->>DiffPanel: mount with diff params
    DiffPanel->>WorkerPool: request diff processing (worker pool)
    WorkerPool-->>DiffPanel: diff results
    DiffPanel-->>User: display diffs
  else diff closed
    ChatView-->>User: render chat only
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Retarget open diff panel to the visited thread' directly reflects the PR's main objective to make the diff panel follow the active thread when navigation occurs.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/53bdee92

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 19, 2026

Greptile Summary

Makes the diff panel follow the currently routed/visited thread instead of persisting to a previously stored diff target. When the diff is open and navigation changes threads, the panel automatically retargets to show diffs for the newly visited thread.

  • Changed DiffPanel to prefer routeThreadId over state.diffThreadId when determining the active thread
  • Added useEffect in DiffPanel to dispatch SET_DIFF_TARGET when the route thread changes while diff is open
  • Updated MARK_THREAD_VISITED reducer to retarget open diff state to the visited thread and clear stale diffTurnId/diffFilePath
  • Added unit test coverage for the retargeting behavior when switching visited threads with an open diff

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Clean state management changes with proper test coverage. The logic is straightforward - it ensures diff panel follows the active thread. The priority swap in DiffPanel.tsx and the retargeting logic in the reducer are complementary and handle edge cases well. The new test validates the key behavior.
  • No files require special attention

Important Files Changed

Filename Overview
apps/web/src/components/DiffPanel.tsx Swaps priority to prefer route thread over stored diff thread, adds useEffect to retarget open diff when route changes
apps/web/src/store.ts Updates MARK_THREAD_VISITED to retarget open diff to visited thread and clear stale turn/file selection
apps/web/src/store.test.ts Adds test coverage for retargeting open diff state when switching visited threads

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User navigates to thread page] --> B[Route threadId changes]
    B --> C{Is diff panel open?}
    C -->|No| D[No action needed]
    C -->|Yes| E{Does route threadId match current diffThreadId?}
    E -->|Yes| F[No action needed]
    E -->|No| G[DiffPanel useEffect triggers]
    G --> H[Dispatch SET_DIFF_TARGET with routeThreadId]
    H --> I[Update diffThreadId to new thread]
    I --> J[Clear diffTurnId and diffFilePath]
    J --> K[Diff panel shows new thread's diffs]
    
    L[MARK_THREAD_VISITED dispatched] --> M{Is diff open?}
    M -->|No| N[Only update lastVisitedAt]
    M -->|Yes| O{Does diffThreadId match visited threadId?}
    O -->|Yes| P[Only update lastVisitedAt]
    O -->|No| Q[Retarget diff to visited thread]
    Q --> R[Update diffThreadId to visited thread]
    R --> S[Clear diffTurnId and diffFilePath]
    S --> T[Diff panel follows visited thread]
Loading

Last reviewed commit: b253d1f

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/store.test.ts (1)

836-862: Minor test setup redundancy.

The test creates a thread via makeState(makeThread(...)) but immediately overwrites the threads array. Consider simplifying the setup:

✨ Simplified test setup
   it("retargets open diff state to the visited thread", () => {
     const state: AppState = {
-      ...makeState(
-        makeThread({
-          id: "thread-local-1",
-          latestTurnCompletedAt: "2026-02-08T10:00:10.000Z",
-          lastVisitedAt: "2026-02-08T10:00:00.000Z",
-        }),
-      ),
+      ...makeState(makeThread()),
       threads: [
         makeThread({
           id: "thread-local-1",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/store.test.ts` around lines 836 - 862, The test redundantly
seeds threads by calling makeState(makeThread(...)) then immediately overriding
threads; update the setup to call makeState() without the initial makeThread
argument (or pass no args) and supply the desired threads array directly,
keeping the rest of the properties (diffOpen, diffThreadId, diffTurnId,
diffFilePath) unchanged; refer to makeState, makeThread, and the threads
property when making this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/web/src/store.test.ts`:
- Around line 836-862: The test redundantly seeds threads by calling
makeState(makeThread(...)) then immediately overriding threads; update the setup
to call makeState() without the initial makeThread argument (or pass no args)
and supply the desired threads array directly, keeping the rest of the
properties (diffOpen, diffThreadId, diffTurnId, diffFilePath) unchanged; refer
to makeState, makeThread, and the threads property when making this change.

- Parse and validate `diff`, `diffTurnId`, and `diffFilePath` from chat route search
- Switch ChatView/DiffPanel diff open/target actions to router navigation instead of store actions
- Remove diff-specific state/actions from the app store and update reducer tests
- Extract `DiffWorkerPoolProvider` into its own component and wrap `_chat` outlet with it
- Render/lazy-load `DiffPanel` from `/_chat/$threadId` so diff UI state and close behavior live with thread routing
- Keep responsive sheet vs inline diff presentation with route-level fallback loading UI
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/web/src/routes/_chat.$threadId.tsx (1)

92-101: Consider extracting loading fallback to avoid duplication.

The loading fallback has two branches with similar content. If this pattern is used elsewhere, consider a small helper component.

♻️ Optional: Extract loading fallback
const DiffLoadingFallback = ({ isInline }: { isInline: boolean }) => (
  isInline ? (
    <aside className="flex h-full w-[560px] shrink-0 items-center justify-center border-l border-border bg-card px-4 text-center text-xs text-muted-foreground/70">
      Loading diff viewer...
    </aside>
  ) : (
    <div className="flex h-full min-h-0 items-center justify-center px-4 text-center text-xs text-muted-foreground/70">
      Loading diff viewer...
    </div>
  )
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_chat`.$threadId.tsx around lines 92 - 101, Extract the
duplicated JSX into a small helper React component (e.g., DiffLoadingFallback)
and replace the inline ternary used to build diffLoadingFallback with a single
call to that component; locate the current variable diffLoadingFallback and
create DiffLoadingFallback that accepts a prop like isInline (or
shouldUseDiffSheet) to switch between the two variants and reuse the existing
classNames and "Loading diff viewer..." content so both branches are
consolidated.
apps/web/src/components/DiffPanel.tsx (2)

89-94: Type cast weakens type safety.

Casting rawSearch as Record<string, unknown> bypasses TanStack Router's typed search params. Since useSearch({ strict: false }) already returns a looser type, consider adjusting parseDiffRouteSearch to accept the actual return type or using stricter route matching.

♻️ Consider stricter typing

If parseDiffRouteSearch expects Record<string, unknown>, the function signature could be adjusted to accept the actual useSearch return type, or use route-specific search via Route.useSearch() if available in this component's context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/DiffPanel.tsx` around lines 89 - 94, The cast
"rawSearch as Record<string, unknown>" weakens type safety—update the usage so
parseDiffRouteSearch accepts the actual type returned by useSearch or obtain
typed params from the route instead of forcing a cast. Specifically, change the
parseDiffRouteSearch signature to accept the same type as useSearch()'s return
(or create an overload/utility type) and remove the cast around rawSearch in the
diffSearch memo, or switch to the route's typed Route.useSearch() if available
in this component; touch symbols: rawSearch, useSearch, parseDiffRouteSearch,
and diffSearch.

202-233: Consider extracting shared navigation logic.

Both selectTurn and selectWholeConversation share nearly identical navigation patterns, differing only in the returned search params. A helper could reduce duplication.

♻️ Optional: Extract navigation helper
+const navigateToDiff = (
+  diffParams: { diffTurnId?: string } = {}
+) => {
+  if (!activeThread) return;
+  void navigate({
+    to: "/$threadId",
+    params: { threadId: activeThread.id },
+    search: (previous) => {
+      const { diff: _d, diffTurnId: _t, diffFilePath: _f, ...rest } = previous;
+      return { ...rest, diff: "1", ...diffParams };
+    },
+  });
+};

 const selectTurn = (turnId: string) => {
-  if (!activeThread) return;
-  void navigate({
-    to: "/$threadId",
-    params: { threadId: activeThread.id },
-    search: (previous) => {
-      const { diff: _diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath, ...rest } = previous;
-      return { ...rest, diff: "1", diffTurnId: turnId };
-    },
-  });
+  navigateToDiff({ diffTurnId: turnId });
 };
 const selectWholeConversation = () => {
-  if (!activeThread) return;
-  void navigate({
-    to: "/$threadId",
-    params: { threadId: activeThread.id },
-    search: (previous) => {
-      const { diff: _diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath, ...rest } = previous;
-      return { ...rest, diff: "1" };
-    },
-  });
+  navigateToDiff();
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/DiffPanel.tsx` around lines 202 - 233, Both
selectTurn and selectWholeConversation duplicate the navigate(...) pattern;
extract a helper (e.g., navigateToThreadWithDiff or buildDiffSearch) that
accepts the activeThread (or threadId) and an optional diffTurnId and performs
the common navigate call using the same destructuring of previous ({ diff:
_diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath, ...rest }) then
returns { ...rest, diff: "1", ...(diffTurnId ? { diffTurnId } : {}) }; replace
selectTurn and selectWholeConversation to call this helper and pass the turnId
only where needed (keep references to selectTurn, selectWholeConversation, and
navigate to locate the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/routes/_chat`.$threadId.tsx:
- Line 2: The React import uses the Activity component which requires React >=
19.2.0; update the apps/web package.json peer/dependency constraint for "react"
(and "react-dom" if present) from "^19.0.0" to "^19.2.0" so the Activity symbol
imported in _chat.$threadId.tsx is guaranteed at runtime, then run npm/yarn
install and verify the app builds and that Activity is resolvable.

---

Nitpick comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 89-94: The cast "rawSearch as Record<string, unknown>" weakens
type safety—update the usage so parseDiffRouteSearch accepts the actual type
returned by useSearch or obtain typed params from the route instead of forcing a
cast. Specifically, change the parseDiffRouteSearch signature to accept the same
type as useSearch()'s return (or create an overload/utility type) and remove the
cast around rawSearch in the diffSearch memo, or switch to the route's typed
Route.useSearch() if available in this component; touch symbols: rawSearch,
useSearch, parseDiffRouteSearch, and diffSearch.
- Around line 202-233: Both selectTurn and selectWholeConversation duplicate the
navigate(...) pattern; extract a helper (e.g., navigateToThreadWithDiff or
buildDiffSearch) that accepts the activeThread (or threadId) and an optional
diffTurnId and performs the common navigate call using the same destructuring of
previous ({ diff: _diff, diffTurnId: _diffTurnId, diffFilePath: _diffFilePath,
...rest }) then returns { ...rest, diff: "1", ...(diffTurnId ? { diffTurnId } :
{}) }; replace selectTurn and selectWholeConversation to call this helper and
pass the turnId only where needed (keep references to selectTurn,
selectWholeConversation, and navigate to locate the change).

In `@apps/web/src/routes/_chat`.$threadId.tsx:
- Around line 92-101: Extract the duplicated JSX into a small helper React
component (e.g., DiffLoadingFallback) and replace the inline ternary used to
build diffLoadingFallback with a single call to that component; locate the
current variable diffLoadingFallback and create DiffLoadingFallback that accepts
a prop like isInline (or shouldUseDiffSheet) to switch between the two variants
and reuse the existing classNames and "Loading diff viewer..." content so both
branches are consolidated.

Comment thread apps/web/src/routes/_chat.$threadId.tsx
Comment thread apps/web/src/components/ChatView.tsx Outdated
@cursor

This comment has been minimized.

@juliusmarminge
Copy link
Copy Markdown
Member Author

@cursor push f87f01f

…ing logic

Consolidate the repeated pattern of destructuring and removing diff/diffTurnId/diffFilePath
search params into a single stripDiffSearchParams() function in diffRouteSearch.ts.
Replaces 5 identical destructuring blocks across 3 files with calls to the shared helper.

Applied via @cursor push command
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/routes/_chat.$threadId.tsx (1)

100-106: Consider adding an ErrorBoundary for the lazy-loaded DiffPanel.

The Suspense boundary handles loading states, but if the chunk fails to load (e.g., network error), the error will propagate up. An ErrorBoundary around or inside the DiffPanelWrapper would provide a graceful fallback UI for load failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/_chat`.$threadId.tsx around lines 100 - 106, Wrap the
lazy-loaded DiffPanel with an ErrorBoundary so chunk-load/runtime errors don’t
bubble up; either add a small class-based ErrorBoundary component (implement
componentDidCatch and render a fallback UI) or use react-error-boundary’s
ErrorBoundary and place it around the existing Suspense (or inside
DiffPanelWrapper) so failures show a graceful fallback instead of throwing;
reference DiffPanel, DiffPanelWrapper and the Suspense/diffLoadingFallback in
your change and ensure the ErrorBoundary renders a succinct error UI and an
optional retry/close action tied to closeDiff.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/routes/_chat`.$threadId.tsx:
- Line 2: The import of the Activity symbol in _chat.$threadId.tsx requires
React 19.2+ but the project is pinned to 19.0.0; either upgrade React (and
react-dom) in package.json to >=19.2.0 and reinstall/test to ensure Activity is
available, or remove/replace the Activity usage in _chat.$threadId.tsx (use a
custom placeholder component or existing loading UI) so the code no longer
imports Activity when the runtime React version is <19.2.0.

---

Nitpick comments:
In `@apps/web/src/routes/_chat`.$threadId.tsx:
- Around line 100-106: Wrap the lazy-loaded DiffPanel with an ErrorBoundary so
chunk-load/runtime errors don’t bubble up; either add a small class-based
ErrorBoundary component (implement componentDidCatch and render a fallback UI)
or use react-error-boundary’s ErrorBoundary and place it around the existing
Suspense (or inside DiffPanelWrapper) so failures show a graceful fallback
instead of throwing; reference DiffPanel, DiffPanelWrapper and the
Suspense/diffLoadingFallback in your change and ensure the ErrorBoundary renders
a succinct error UI and an optional retry/close action tied to closeDiff.

Resolve PR conflicts while preserving route-scoped diff rendering and _chat desktop menu action handling.

Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge juliusmarminge merged commit 70a13c2 into main Feb 19, 2026
4 checks passed
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

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 1 potential issue.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issue.

}

export const Route = createFileRoute("/_chat/$threadId")({
validateSearch: (search) => parseDiffRouteSearch(search),
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.

Diff panel closes when switching threads via sidebar

Medium Severity

Moving diff state from the store (state.diffOpen) to URL search params (?diff=1) causes the diff panel to close when navigating between threads. The Sidebar navigates with navigate({ to: "/$threadId", params: { threadId } }) without specifying search, so TanStack Router clears search params on navigation. The route's validateSearch lacks a retainSearchParams middleware, meaning ?diff=1 is lost on every thread switch. Previously, store-based diffOpen persisted across navigation. This contradicts the PR's stated goal of retargeting the open diff panel to the visited thread.

Fix in Cursor Fix in Web

@cursor
Copy link
Copy Markdown
Contributor

cursor bot commented Feb 19, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Diff panel closes when switching threads via sidebar
    • Added retainSearchParams(true) search middleware to the _chat.$threadId route so TanStack Router preserves diff search params (?diff=1, diffTurnId, diffFilePath) across thread navigation, preventing the diff panel from closing when switching threads via the sidebar.

Create PR

Or push these changes by commenting:

@cursor push da1cd3307d
Preview (da1cd3307d)
diff --git a/apps/web/src/routes/_chat.$threadId.tsx b/apps/web/src/routes/_chat.$threadId.tsx
--- a/apps/web/src/routes/_chat.$threadId.tsx
+++ b/apps/web/src/routes/_chat.$threadId.tsx
@@ -1,4 +1,4 @@
-import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { createFileRoute, retainSearchParams, useNavigate } from "@tanstack/react-router";
 import { Activity, Suspense, lazy, type ReactNode, useCallback, useEffect } from "react";
 
 import ChatView from "../components/ChatView";
@@ -110,5 +110,8 @@
 
 export const Route = createFileRoute("/_chat/$threadId")({
   validateSearch: (search) => parseDiffRouteSearch(search),
+  search: {
+    middlewares: [retainSearchParams(true)],
+  },
   component: ChatThreadRouteView,
 });

jjalangtry pushed a commit to jjalangtry/t3code that referenced this pull request Mar 16, 2026
Retarget open diff panel to the visited thread
smraikai pushed a commit to smraikai/t3code that referenced this pull request Apr 16, 2026
Retarget open diff panel to the visited thread
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.

2 participants