New Threads & Where Work Happens v2
Every new thread makes it obvious where work happens and what it starts from — isolated by default wherever there's no local checkout in front of the user, visible and explicit where there is.
What changed from v1, and why
V1's thesis was "safe by default, explicit by exception," implemented as: every ordinary new thread gets a fresh worktree from a just-fetched origin/main, everywhere, with a blocking fetch at send time and a hard-fail dialog when the remote is unreachable. Four things move in this revision:
- The default is derived from the surface, not universal. Safety was never about worktrees per se — it was about the user never being surprised. A hidden worktree is itself a surprise when the user's editor is open on the checkout.
- Freshness is a window, not a blocking fetch. "Latest" means fetched within the last 30 seconds, made nearly-always true by prefetching when the composer opens. When we do have to wait, a first-class loading state carries it.
- Failure splits into fail-visible and fail-closed. A slightly stale base proceeds with provenance shown. Only genuinely stale or absent data — and any fallback to the current checkout — stops the world.
- Worktree ↔ thread association is a first-class record we own. Deterministic naming, tracked server-side, so lifecycle/GC (deferred) is attributable by construction. The four-scope settings architecture also decouples into its own phase so the behavior ships on today's settings.
Defaults by surface
One rule generates every default — no per-surface policy table to memorize:
The attention test. Is the user's attention attached to a local checkout they can see? If yes, work there — visibly. If no, isolate in a fresh worktree pinned to the latest remote default branch.
| Surface | Default | Rationale |
|---|---|---|
| Desktop app, opened on a local project | Current checkout | The user's editor is open on this folder. Changes landing in a hidden worktree read as "nothing happened" — more confusing than non-isolation is dangerous. |
| CLI, launched inside a repo | Current checkout | Same posture: the user is standing in the checkout. Matches every other CLI tool's mental model. |
| CLI, outside a repo / driving remote threads | Fresh worktree | No checkout in view; the CLI is acting as a launcher. |
| Web (app.t3.codes) | Fresh worktree | There is no local checkout in front of the user. Isolation is the only coherent option. |
| Mobile | Fresh worktree | Same as web. |
| Remote / cloud environments | Fresh worktree | Same as web; the environment owns the workspace entirely. |
No onboarding question. Asking "worktrees or current directory?" during onboarding collects a coin flip from exactly the people it's meant to help — they haven't formed the concept yet. Instead, the composer's intent label is the teaching moment: on a user's first-ever thread per surface, the label gets slightly more visual weight so the choice is legible right when it's meaningful, and flippable in one click.
V1's invariant "New thread means the same thing across site, app, and CLI" is retired. Uniformity was standing in for predictability; the intent label delivers predictability directly. The replacement invariant: the composer always shows where work happens and what it starts from, on every surface.
The ordinary flow
- Composer opens. No prompts. Intent label shows the surface default. In worktree mode, a background
git fetchfires immediately (deduped per remote — concurrent drafts share one in-flight fetch). - User types. By the time they hit send, the prefetch has almost always landed.
- Send. Freshness check: if the last successful fetch is within the freshness window (default 30s), pin the SHA immediately — zero added latency. If stale, fetch now behind a visible loading state.
- Workspace materializes. Branch + worktree created from the pinned commit, under deterministic thread-derived names. Setup stages render inline in the thread: fetching → pinning → creating worktree → running setup commands. The wait is accounted for, never hidden.
- Agent starts only after setup completes. The intent label resolves from promise to fact: "Worktree · pinned at
a1b2c3f(origin/main, 12s ago)".
Latency is acceptable here because the loading state is first-class. The freshness window and prefetch exist to make the common case instant, not to hide the work when it does happen.
Send-time flow for worktree-mode threads. The green path is the overwhelmingly common case thanks to the prefetch.
Composer UX
The intent label sits beside the send button — descriptive, not a stateful toggle. It reads differently per surface default:
- "Change…" opens the exception picker with the two non-default paths, each marked this draft only: fresh worktree ⇄ current checkout, and worktree from another ref (branch, tag, or commit — pinned from that ref).
- The escape hatch lives in the picker, not the toggle. Beneath the draft-only choice: a quiet "Always for this project" link that writes the default to settings, explicitly. The invariant becomes: the toggle never silently retrains the default; settings do, visibly. This is what keeps non-sticky exceptions from punishing people whose legitimate workflow is the non-default mode.
- Dirty-state disclosure, both directions. CWD-mode composer on a dirty checkout says so inline (agent edits will mix with uncommitted work). Worktree-mode composer on an environment whose checkout is dirty gets a small hint that those uncommitted changes won't be included — with a one-click switch. This covers the "fix the thing I'm looking at right now" intent that a fresh-from-origin default would otherwise silently miss.
- First-send emphasis. On a user's first thread per surface, the intent label is visually promoted (not a modal, not a tour) so the workspace concept is taught at the exact moment it matters.
- Promise → fact. Before send the label states intent ("latest origin/main"); after setup it states the outcome ("pinned at
a1b2c3f, origin/main as of 12s ago"). This same surface carries the provenance banner in the fail-visible case ("as of 14 min ago — remote unreachable").
Failure behavior: fail-visible, then fail-closed
V1 had one absolute: no silent fallback, enforced by a modal whenever the remote was unreachable. That conflates two very different situations. The invariant splits:
Absolute, forever: never silently fall back to the current checkout. Crossing the isolation boundary always requires explicit confirmation.
Graduated: staleness of the base is a disclosure problem, not a stop-the-world problem — until it isn't.
- Fail-visible (fetch fails, last good fetch within the usable horizon, default ~1h): proceed on the last-known remote commit with a provenance banner on the thread. No dialog. Flaky wifi does not turn every new thread into an error screen.
- Fail-closed (no usable data: nothing fetched within the horizon, or no remote at all): dialog states that nothing started and the checkout was not modified, and offers — retry fetch · inspect connection · use last-known commit (confirm) · use current checkout (confirm). Unchanged from v1.
Worktree ↔ thread association
We track this ourselves, as a first-class server-side record — not inferred from git config or reconstructed by scanning paths.
- Registry record per worktree: thread id, worktree path, branch name, pinned base SHA, base ref, created-at, status (active / thread-closed / orphaned). The connected environment remains the single authority that creates the workspace and reports the actual path/ref/SHA; the registry is where that report lands.
- Deterministic naming, derived from the thread id: branch
t3/<thread-id-short>, path<worktreesRoot>/<repo>/<thread-id-short>. Every worktree is attributable to a thread by construction; nothing about attribution depends on a future migration. - Lifecycle/GC policy is explicitly deferred — but the registry and naming are what make it cheap later. Open questions parked for that pass: auto-remove clean worktrees when a thread ends; prompt-or-GC for merged and abandoned threads; a disk ceiling on the worktrees root; surfacing per-worktree disk usage in the existing orphan-tracking UI.
Phasing
Phase 1 — this pass ships the default experience
- Surface-derived defaults via the attention test; retire sticky per-draft mode inheritance.
- Freshness window (30s) + prefetch-on-composer-open + per-remote fetch dedup; inline setup-stage loading state.
- Composer intent label (promise → fact), exception picker with draft-only paths + "Always for this project" escape hatch, dirty-state disclosure, first-send emphasis.
- Fail-visible / fail-closed ladder.
- Worktree registry + deterministic thread-derived naming.
- Built on today's settings — extend
defaultThreadEnvModeto per-surface resolution and foldnewWorktreesStartFromOrigininto the freshness model. No new settings architecture required.
Phase 2 — settings scopes decoupled
- V1's four-scope architecture (Account / Device / Environment / Project) with resolution + provenance stands as designed — it's a project of its own and the new-thread behavior must not wait on it.
- Scope guidance carries over: secrets stay on the environment; policy syncs on the account; "Always for this project" writes from phase 1 slot cleanly into project scope when it lands.
- Keep provenance UI proportionate: one resolved answer, scope shown on hover/inspection — resist per-control provenance chrome everywhere.
Back of mind — worktree lifecycle deferred, enabled now
- GC policy, disk ceilings, merged-thread cleanup. Not this pass; the registry and deterministic naming from phase 1 are the down payment.
Invariants
- The composer always shows where work happens and what it starts from — on every surface. ("New thread means the same thing across site, app, and CLI" → legibility replaces uniformity)
- No silent fallback to the current checkout, ever. Crossing the isolation boundary requires explicit confirmation.
- Worktree bases are pinned SHAs with visible provenance — including how fresh the base was and whether the fetch succeeded.
- Exceptions are draft-only. Defaults change only through settings, and only explicitly — the composer offers that path but never takes it for you.
- Every worktree is attributable to a thread by construction, via the registry and thread-derived naming.
- The agent starts only after the workspace is ready; the wait is shown as setup stages, never hidden; success is never claimed early.
- The connected environment remains the single server-side authority for validating, fetching, pinning, and creating workspaces.
Definition of done
- On web, mobile, and remote surfaces, an ordinary new thread lands on an isolated worktree pinned to a ≤30s-fresh
origin/mainwith no manual pull and, in the common case, no perceptible setup delay. - In the desktop app and CLI-in-repo, an ordinary new thread works in the current checkout with that fact — and any dirty state — stated in the composer.
- Switching modes is one click, draft-only by default, with an explicit settings path for people whose workflow is the non-default.
- A failed fetch with a recent base produces a provenance banner, not a dialog; a failed fetch with no usable base produces the fail-closed dialog; the current checkout is never touched without confirmation.
- Every worktree on disk maps to a thread in the registry; branch and path names are derivable from the thread id alone.
- None of the above waits on the settings-scope architecture.