Skip to content

fix(mobile): new threads no longer get stuck on Starting - #15870

Open
nekohasekai wants to merge 1 commit into
pingdotgg:mainfrom
nekohasekai:agent/fix-15605-mobile-image-thread-starting
Open

nekohasekai wants to merge 1 commit into
pingdotgg:mainfrom
nekohasekai:agent/fix-15605-mobile-image-thread-starting

Conversation

@nekohasekai

@nekohasekai nekohasekai commented Oct 5, 2026 •

Copy link
Copy Markdown

Problem

When you start a new task from the mobile app with a photo attached, the thread stays on "Starting…" even though the agent runs normally on the server. If you go back and reopen the thread, it shows "Thread unavailable" until the app restarts.

The new-task flow opens the thread screen before the outbox has sent thread.create. use-thread-selection.ts already keeps selectedThreadDetailRef null until the creation is delivered. Four readers on that screen use the route's thread id instead:

  • the composer's queue workflow
  • the thread screen's reported model
  • the thread screen's queued count
  • the thread screen's turn subagents

The first of them to load asks the server for the thread snapshot. The server answers 404 because the thread does not exist yet. client-runtime treats a 404 as a definitive delete, so it marks the thread deleted and stops subscribing. A photo makes this likely because the outbox checks the uploaded attachment with one more request before it sends thread.create.

Fixes #15605.

Change

  • The four readers now read through selectedThreadDetailRef, so the shared thread state starts loading only after the server has the thread. The three hooks accept a null target and return their empty value, like the existing useSelectedThread* hooks.
  • ThreadRouteScreen passes the ref to ThreadDetailScreen as a prop, next to selectedThread. The prop's doc comment says to read thread details through it.
  • The comment in use-thread-selection.ts now says what an early read does: the 404 is kept as deleted, not retried.

The gate waits for delivery, not for a timeout, so it holds on any connection speed, with or without attachments.

Scope and approval

Fixes the triaged bug #15605. The triage confirmed the cause on main. Its first fix option is this change: gate the composer queue workflow and the ThreadDetailScreen subscriptions on the condition selectedThreadDetailRef already uses. Another report reproduced it on a real iPhone over Tailscale with no added delay. There the 404 came 54–79 ms before thread.create.

This PR changes mobile only. Web's chat view already waits for the server shell before it reads a draft thread's details (resolveThreadDetailRef with waitForShell). The client-runtime rule that a 404 is a definitive delete stays as it is. The triage's second option, changing that rule for pending creations, would change behavior that web shares, and this fix does not need it.

I checked the other mobile readers of thread details. Each one falls into one of these groups:

  • it already uses the gated ref;
  • it renders only for items that came from the server, such as handoff rows, subagent groups, and the workspace-setup retry button;
  • it lives on a different route.

The Supacode fork merged an equivalent change as supabitapp/supacode-next#37. That version moves the three reads into one useSelected… hook instead of passing the ref down.

Verification

I tested the native app on an iPhone 17 Pro Simulator, iOS 26.5. It ran against an isolated dev server with a synthetic demo-app project and Codex (GPT-6-Astra). Each run started a new task with one photo attached.

  • Before (upstream 4ee6bfd50e): the snapshot request returned 404 1.66 s before thread.create. The screen stayed on "Starting…" while the server finished the turn and renamed the thread. Reopening the thread showed "Thread unavailable".
  • After (this fix): thread.create ran first. The first snapshot request came 0.58 s later and returned 200, and the reply appeared. Reopening the thread showed the same reply.
  • Added delay: both recordings used a temporary 2-second delay on the dev server's assets.createUrl for attachments. It stands in for the extra round trip on a remote connection, because over loopback the race reproduces only sometimes. The delay is not part of this PR.
  • Recording base: both runs were recorded on 4ee6bfd50e. The PR commit is the same change rebased onto a1d9d72aef. Of the commits in between, only fix(mobile): a message that fails to send now says why in the thread #15807 touches the changed files: it adds a send-failure notice to ThreadDetailScreen and use-thread-composer-state.ts. It adds no thread-detail reads, and its only conflict with this PR was an adjacent line. No commit in between touches packages/client-runtime/src/state/threads.ts.
  • Checks: tsc --noEmit passed in apps/mobile. Lint on the 7 changed files reported 0 errors and the same 54 existing warnings as main. Formatting passed.

I did not add an automated test. The gate already exists in useThreadSelection, and this change only picks which ref the four readers use. A test would have to render the whole thread screen.

Not exercised: Android (same code), a physical device, and a remote connection without the added delay.

Before: stuck on Starting… Before: reopened After: reply After: reopened
Before: the thread stays on Starting while the server has already renamed it Before: reopening shows Thread unavailable After: the photo and the agent's reply After: reopening shows the same reply

Side-by-side video (53 s) · Before video (52 s) · After video (47 s)

The videos are native simulator captures. Before sending, the setup plays at 2x, and the waits for the photo picker and the upload are cut. Everything from sending the task onward is unedited at 1x. Labels were added afterward. The evidence is uploaded as GitHub release attachments on the contributor fork, not committed to the repository.

Model: Claude Opus 5.5. Harness: Claude Code in T3 Code.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:M 30-99 changed lines (additions + deletions). labels Oct 5, 2026
@macroscopeapp

macroscopeapp Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 9d8aabc

Macroscope's review found this PR approvable — This is a contained mobile bug fix that prevents premature subscriptions to a thread before its queued creation reaches the server, avoiding the 404/deleted state that caused new threads to remain on “Starting.” Existing server-backed thread behavior and product defaults remain unchanged.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: pingdotgg/t3code/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7d00c6c2-257a-4a7c-96e5-5910a1561381
📥 Commits

Reviewing files that changed from the base of the PR and between 9d8aabc and 06243d7.

📒 Files selected for processing (2)
  • apps/mobile/src/features/threads/ThreadDetailScreen.tsx
  • apps/mobile/src/state/use-thread-composer-state.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Mobile thread detail and composer readers now use the nullable selected thread detail reference. If the reference is null, detail readers use fallback atoms. Otherwise, they read state through the scoped reference.

Changes

Mobile thread reference handling

Layer / File(s) Summary
Null-safe thread state readers
apps/mobile/src/state/entities.ts, apps/mobile/src/features/threads/ThreadQueueControl.tsx, apps/mobile/src/features/threads/ThreadAgentsSheet.tsx
Reported-model, queued-count, and turn-subagent readers accept nullable references or targets. Each uses a fallback atom when its input is null.
Thread detail scoped-reference wiring
apps/mobile/src/features/threads/ThreadRouteScreen.tsx, apps/mobile/src/features/threads/ThreadDetailScreen.tsx, apps/mobile/src/state/use-thread-selection.ts
ThreadRouteContent passes selectedThreadDetailRef to ThreadDetailScreen. The detail readers receive that reference. The selection comment directs detail readers to use it while creation is pending.
Composer queue workflow selection
apps/mobile/src/state/use-thread-composer-state.ts
Queue workflow selection uses selectedThreadDetailRef. A null reference selects the empty workflow atom; otherwise, the scoped reference selects the workflow.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: juliusmarminge

Merge Risk: ⚪ Minimal · up to 06243

Pending mobile thread creation avoids premature detail requests, and the reviewed change appears ready to merge.

Security Architecture Review

Security architecture risk: 🔵 Low · up to 06243

The change delays thread-detail loading until the existing creation gate permits it and preserves environment/thread identity. No introduced security concern was established, but creation recovery and authorization coverage is incomplete.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The demonstrated effect is confined to when existing mobile readers access the selected environment/thread state. The internal prop does not establish a newly attacker-accessible service boundary or additional authority.

Trust Boundaries and Controls

  • observed — The nullable gate controls readiness, not authorization: null consumers select empty atoms, while non-null consumers use the existing scoped atom families. No new authentication or permission decision appears in these changed reader paths.

Resilience and Maintainability Implications

  • observed — Existing recovery state rejects a previous creation whose environment/thread key differs from the current selection. Failed creation state remains available for draft recovery; terminal failed, cancelled, stopped or interrupted run states clear the preparing presentation. These mechanisms predate the reader rewiring.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The incremental changes add a separate composer-error feature that is not needed to prevent the premature detail request in [#15605]. thread-composer-error.ts stores and clears send-error state, `Co… Remove the unrelated composer-error notice, state management, cleanup, and associated tests. Retain the changes that gate thread-detail readers on selectedThreadDetailRef.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed [#15605] The change gates the composer queue workflow and the reported-model, queued-count, and turn-subagent readers on selectedThreadDetailRef. The nullable paths avoid detail loading until `threa…
Title check ✅ Passed The title clearly summarizes the mobile fix for new threads stuck on “Starting…”.
Description check ✅ Passed The description covers the problem, change, scope and issue approval, verification results, limitations, and UI evidence required by the template.
Full details: Out of Scope Changes check

Explanation

The incremental changes add a separate composer-error feature that is not needed to prevent the premature detail request in [#15605]. thread-composer-error.ts stores and clears send-error state, ComposerErrorNotice.tsx displays and dismisses errors, and ThreadDetailScreen.tsx renders the notice. The changes also add environment cleanup and tests for this feature. The linked issue concerns thread-detail loading during pending creation, not composer error presentation or recovery.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@mwolson

mwolson commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Thanks for this fix. On current main it no longer passes the mobile typecheck: the branch is based on a1d9d72aef, from before #16138 moved Effect's effect/unstable/reactivity module to effect/reactivity, and the two imports this PR adds in ThreadAgentsSheet.tsx and ThreadQueueControl.tsx still use the old path. It merges into main without conflicts, so rebasing onto current main and switching those two imports to effect/reactivity should be all it needs. With that change, the mobile typecheck and the mobile state tests pass for me.


Sent by Mike's agent (Claude Opus 5.5)

This branch has not been deployed

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

Labels

size:M 30-99 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Mobile image thread creation stays on Starting and becomes unavailable until app restart

2 participants