Skip to content

Opening an archived thread by its link sends you back to the home screen #16443

Open

Description

@yaacovcorcos

Observed

The command palette lists a pull request's archived threads as "Archived thread", and the thread relationships control lists archived related threads. Both navigate to /$environmentId/$threadId. For an archived thread, that route never shows the conversation: it redirects to / (or shows an empty pane when the environment has no other threads). Pasting an archived thread's URL behaves the same way.

Reproduction

  1. Create a thread, send a message, then archive it.
  2. Open it from the command palette's linked-thread list for its pull request, or from a related thread's relationships control, or paste its /$environmentId/$threadId URL.
  3. The app returns to the home screen instead of showing the thread.

Cause

  • The shell snapshot the web client keeps holds active threads only; archived ones live in a separate archive snapshot. So useThreadShell returns null for an archived thread.
  • ThreadRouteView treats a missing shell after bootstrap as missing (resolveThreadRouteRenderState), does not mount ChatView, and its cleanup effect navigates to /.
  • Even if ChatView were mounted, it uses that same shell as its active thread and renders the empty state without it.

The server can serve the thread: /api/orchestration/threads/:threadId (getThreadSnapshot) and the thread subscription do not filter archived threads, and a truly absent id comes back as not found, which the client turns into status deleted.

Expected

An archived thread opened by its link shows its conversation. A thread that does not exist or was deleted still leaves the route as today.

Environment

macOS, T3 Code main at 9bd1d80. Found from code reading and a unit-level reproduction (shell atoms with an archived thread, then the route's render state) that resolves missing.

Proposed direction

While the active shell lacks the target, keep the route loading and mount the chat so the thread detail loads; treat the route as missing only once the detail reports deleted. The chat then needs a shell for the archived thread, either from the archive shell snapshot (orchestrationEnvironment.archivedShellSnapshot) or by deriving it from the loaded detail (the server's threadShellFromProjection would have to move to a shared package). Which source to use is a maintainer choice.

I haven't written a fix, because the shell source is a design choice.

Found while porting fixes from a downstream fork (Scient). Investigated with Claude Opus 5.5 in Claude Code.

Activity

  1. juliusmarminge commented on Oct 6, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Triage

    Thanks @yaacovcorcos for the thorough cause analysis, it lined up closely with what I found in the code.

    What I found

    • This reproduces from the code on current main. A pull request's linked-thread list includes archived threads (listLinkedPullRequestThreads doesn't filter archived_at); the command palette labels them "Archived thread" and navigates to /$environmentId/$threadId (buildLinkedThreadActionItems). The lineage panel merges the archive snapshot into the relationship graph and opens the same route, and pasting the URL hits the same view.
    • The live shell only keeps active threads: environmentThreadsAtom reads snapshot.threads, and archiving publishes thread.removed on the active shell stream, so useThreadShell is null. After bootstrap, resolveThreadRouteRenderState turns that into missing, ThreadRouteView never mounts ChatView, and the URL is replaced with / (or the inset stays empty if there's no other thread).
    • The server can still load it: getThreadSnapshot looks the row up by id without filtering archivedAt, and a real miss returns 404, which the client stores as deleted.
    • Mounting ChatView alone wouldn't be enough, since its active thread is the same shell and !activeThread renders NoActiveThreadState.
    • Mobile already handles this case: when the active shell is missing, useResolvedThreadSelection loads the thread detail and builds a local shell with threadDetailToShell, and ThreadRouteScreen stays loading until that shell exists.

    Likely fix area

    Some options for the shell source you flagged:

    • Mirror mobile: keep the route loading until the detail resolves, build a local shell from the loaded projection, mount the chat, and leave the route only when the detail is deleted. Keeping that shell local (rather than inserting it into the live shell list) would avoid the archived thread showing up in the sidebar and palette.
    • Use the archive shell snapshot (orchestrationEnvironment.archivedShellSnapshot). Note it's a separate list and can lag the live shell.

    Either way, message.dispatch already rejects archived threads as not active, so the composer would need to stay disabled (or offer unarchive) when archivedAt is set. Desktop is affected too, since it wraps the web client.

    A maintainer will decide on the fix direction.

  2. added
    bugSomething is broken or behaving incorrectly.
    via-triageFiled through npx t3 triage
    on Oct 6, 2026
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