Skip to content

[Bug]: CodeView crashes with duplicate id when a regular file becomes a symlink #11074

Description

@branislavbudzak

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. Create a Git repository where AGENTS.md is a regular tracked file and CLAUDE.md is another regular tracked file:

    git init t3-codeview-type-change
    cd t3-codeview-type-change
    git config user.name "T3 repro"
    git config user.email "t3-repro@example.invalid"
    printf 'canonical instructions\n' > CLAUDE.md
    printf 'duplicated instructions\n' > AGENTS.md
    git add AGENTS.md CLAUDE.md
    git commit -m baseline
  2. Open the repository in T3 Code and start a thread.

  3. During a turn, replace the regular file with a relative symlink and commit it:

    rm AGENTS.md
    ln -s CLAUDE.md AGENTS.md
    git add AGENTS.md
    git commit -m 'use shared instructions'
  4. After the turn checkpoint is created, open the turn diff or Changes view.

The checkpoint transition is a valid Git type change:

mode change 100644 => 120000 AGENTS.md

With git diff --no-renames, Git represents it as two file blocks with the same path:

diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
--- a/AGENTS.md
+++ /dev/null

diff --git a/AGENTS.md b/AGENTS.md
new file mode 120000
--- /dev/null
+++ b/AGENTS.md

This was also reproduced outside the original project with a minimal temporary repository.

Expected behavior

CodeView should render the regular-file-to-symlink transition as one type-change item, or safely two patch blocks with distinct internal IDs. A valid Git diff should not crash the thread view.

Actual behavior

The CodeView renderer throws a duplicate-ID error and T3 Code displays a full-page "Something went wrong" state:

Error: CodeView.addItem: duplicate id "AGENTS.mdAGENTS.md"
    at appendItemsInternal (t3code://app/assets/diffFileContents-*.js)
    at setItems (t3code://app/assets/diffFileContents-*.js)

The repository and symlink remain valid. In a second occurrence on 2026-09-10, Try again, Reload app, and fully restarting the application did not recover the affected thread: its persisted open diff panel was restored and triggered the crash again. The original conversation was recovered by closing only its persisted right panel, as described below.

The path-only identity collision has since been confirmed by a maintainer in this comment. The actual key contains a NUL separator: AGENTS.md\0AGENTS.md, which is invisible in the displayed error.

Related but distinct:

Impact

Major degradation or frequent failure

The affected turn cannot be reviewed in CodeView. If its diff panel was open, the entire existing thread can remain inaccessible across application restarts. Creating a new thread is not necessary to recover the original conversation.

Version or commit

T3 Code desktop 0.0.40

Environment

  • macOS 26.6.1, Apple Silicon arm64
  • T3 Code desktop 0.0.40
  • Codex CLI 0.154.0
  • Node.js 26.8.1

Logs or stack traces

T3 Code (Alpha) 0.0.40
Error: CodeView.addItem: duplicate id "AGENTS.mdAGENTS.md"
    at appendItemsInternal (t3code://app/assets/diffFileContents-*.js)
    at setItems (t3code://app/assets/diffFileContents-*.js)

Verified recovery of the original thread (desktop 0.0.40)

A second occurrence happened after a turn completed using Claude Opus 5, so the observed failure is not limited to the Codex provider. This is an additional observation, not proof of provider causality.

Inspection of the installed 0.0.40 source maps and the live application showed:

  • rightPanelStore.ts persists panel state in localStorage under t3code:right-panel-state:v2 (persisted schema version 11).
  • The affected entry in state.byThreadKey had isOpen: true, surfaces: [{ id: "diff", kind: "diff" }], and activeSurfaceId: "diff".
  • Changing only that entry's isOpen to false, then reloading, restored the same thread. The conversation, latest completed response, and message composer were visibly accessible again.
  • No conversation database, checkpoint, repository file, or symlink was modified for this recovery.

For another user stuck on the error page in 0.0.40, open View > Toggle Developer Tools > Console. This version-specific workaround identifies the current thread from its URL, saves a sessionStorage backup, closes only that thread's panel, and reloads:

(() => {
  const storageKey = "t3code:right-panel-state:v2";
  const raw = localStorage.getItem(storageKey);
  const data = JSON.parse(raw ?? "null");
  const threadId = location.hash.split("?")[0].split("/").filter(Boolean).at(-1);
  const panels = data?.state?.byThreadKey;
  const matches = Object.keys(panels ?? {}).filter(
    key => key.endsWith(":" + threadId)
  );
  if (!threadId || matches.length !== 1) {
    throw new Error("Expected exactly one current-thread panel; nothing changed.");
  }
  sessionStorage.setItem("t3-recovery-right-panel-backup", raw);
  panels[matches[0]].isOpen = false;
  localStorage.setItem(storageKey, JSON.stringify(data));
  location.reload();
})();

The recovery operation was verified on the affected thread using its explicit scoped key. The snippet above generalizes key selection to the current route. Avoid reopening the affected diff until the renderer fix is available. Clearing all localStorage or deleting the thread is unnecessary.

Additional fix and regression suggestions

The maintainer's proposed unique identity fix addresses the collision. These additional cases would cover the persistent lockout and prevent partial fixes:

  1. Preserve both patch halves. Do not deduplicate by path and silently discard the deletion or symlink addition. If rendering two items, assign deterministic distinct IDs and use them consistently for collapse, selection/reveal, and annotation behavior. If merging, retain the old/new modes and contents.
  2. Contain rendering failures within the diff panel. Keep the conversation and composer mounted, with a local error fallback offering to close the panel. The close action should persist isOpen: false, so restarting cannot restore the same failing panel indefinitely.
  3. Regression coverage: regular file to symlink and the reverse; both patch halves and contents preserved; unique stable item IDs; collapse/reveal behavior after rerender; and reopening a thread with a persisted open diff panel. A deliberately failing diff renderer should leave the conversation usable and allow persistent panel closure.

The persisted-panel recovery is directly verified. The implementation and regression items above are suggestions, not a tested code patch.

Activity

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

    acceptedfeature request acceptedbugSomething 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