Skip to content

[Bug]: Diff panel keeps jumping back from "Uncommitted" to "Changes" while an agent turn is running #15352

Description

@fatalist

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/web

Steps to reproduce

  1. In a thread for a git repo, send a prompt so the agent starts working.
  2. While the turn is running, open the Diff panel (it opens on "Changes").
  3. Expand a few files.
  4. Pick "Uncommitted" in the scope dropdown.
  5. Wait a few seconds without touching anything.

Expected behavior

The panel stays on "Uncommitted" until I pick another scope, and files I expanded stay expanded.

Actual behavior

A few seconds later the dropdown switches back to "Changes" on its own (about 8s in the attached recording, with no input from me). This keeps happening for as long as the turn runs, so "Uncommitted" is basically unusable mid-turn. The files I expanded also collapse again every time.

Likely cause

This looks like it came in with d1034d6 (#15005). In ChatView.tsx:

const activeThreadRef = useMemo(
  () => (activeThread ? scopeThreadRef(activeThread.environmentId, activeThread.id) : null),
  [activeThread],
);
// ...
useLayoutEffect(() => {
  const explicitThreadRef = explicitDiffOpenRef.current;
  explicitDiffOpenRef.current = null;
  // Generic openings always show Changes, including tab fallbacks and thread changes.
  if (diffOpen && activeThreadRef && explicitThreadRef !== activeThreadRef) {
    useDiffPanelStore.getState().selectGitScope(activeThreadRef, "branch");
  }
}, [activeThreadRef, diffOpen]);

activeThreadRef is memoized on the whole activeThread object, and that object gets a new identity on every thread update while a turn is streaming. So each update creates a new ref, the layout effect runs again and calls selectGitScope(..., "branch"), even though the panel was already open and the thread didn't change.

The collapse reset is probably a side effect of the same thing: in DiffPanel.tsx, collapseScopeKey / codeViewMountKey are derived from the selected scope, so getting forced back to branch swaps the collapse state and remounts the code view.

Keying the memo/effect on environmentId + threadId (or activeThreadKey) instead of the thread object should make it fire only when the panel opens or the thread actually changes.

Impact

Major degradation or frequent failure

Version or commit

T3 Code (Nightly) 0.0.46-nightly.20261003.2638 (source checked at main @ a21702d)

Environment

macOS 27.2, desktop app

Logs or stack traces

Screenshots, recordings, or supporting files

Screen.Recording.2026-10-04.at.00.42.42.mov

Workaround

Wait for the turn to finish, then switch to "Uncommitted".

Activity

  1. added
    bugSomething is broken or behaving incorrectly.
    needs-triageIssue needs maintainer review and initial categorization.
    on Oct 3, 2026
  2. juliusmarminge commented on Oct 3, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks for the clear report and the recording, @fatalist! This reproduces on current main (8d84666), your read of the cause is right, and I didn't find a duplicate.

    What happens

    In ChatView (apps/web/src/components/ChatView.tsx, around line 2169), activeThreadRef is memoized on the whole thread shell object:

    const activeThreadRef = useMemo(
      () => (activeThread ? scopeThreadRef(activeThread.environmentId, activeThread.id) : null),
      [activeThread],
    );

    A running turn keeps committing events, and each coalesced shell delta replaces the shell object, so the memo creates a new { environmentId, threadId } even though the ids haven't changed. That new ref re-runs the layout effect that forces the Changes scope (around line 2222). Its guard compares references, and explicitDiffOpenRef is only set by a timeline click, so after you pick Uncommitted the next shell update calls selectGitScope(..., "branch"). The few seconds' delay in your recording is just the wait for the next delta.

    The collapsed files come from the same reset. DiffPanel keys the code view on a collapseScopeKey that includes the scope, so switching back to Changes remounts it. The same effect also clears a selected turn (Latest turn or Turn N).

    Merged #15005 didn't add the re-run itself. It changed the forced scope from unstaged to branch, which is why Uncommitted is now the choice that gets overwritten.

    Likely fix area

    • One option is to key the activeThreadRef memo on environmentId and threadId, the way routeThreadRef and ChatHeader already do, so the effect runs only when the panel opens or the thread changes.
    • Another is to have the effect compare thread ids instead of object identity.

    A maintainer will decide on the fix direction.

  3. added
    via-triageFiled through npx t3 triage
    and removed
    needs-triageIssue needs maintainer review and initial categorization.
    on Oct 3, 2026
  4. kyrylofranko commented on Oct 6, 2026

    @kyrylofranko

    +1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions