Remove per-thread scope pickers from workspace-bound chat panes - #211
Remove per-thread scope pickers from workspace-bound chat panes#211Zeus-Deus wants to merge 2 commits into
Conversation
A workspace owns exactly one project root and one checkout (cwd / worktree_path), shared by every tab, surface and pane inside it. But AgentChatPane's empty-thread ThreadScopeRow offered both a "New worktree" checkout control and a project location picker, on EVERY additional chat tab — including tabs of a workspace that was already worktree-backed, i.e. offering a choice that workspace could not honour. Neither control rescoped the thread. "New worktree" created a SECOND workspace off the parent repo mid-send and relocated the user into it, orphaning the tab (and its already-started session) behind an app-wide workspace flip. The location picker activated another project's first workspace, abandoning whatever had been typed in the pane. Both were app-wide relocations wearing per-thread clothing. ThreadScopeRow is now DraftChatSurface-only — the one surface where "what should the first send create?" has an answer, because nothing exists yet — and the pane renders the read-only Context Row from its first render, so the strip never changes shape across the first send. Creating a worktree belongs to the draft surface and the sidebar worktree flow; switching project or workspace belongs to the sidebar. With one consumer left, the two-surface machinery comes out: the ThreadScopeLocation / ThreadScope discriminated props go back to flat props, and the trailing slot plus LocationControl's workspace-mode branches are deleted along with handleDeferredWorktreeSubmit, prestart-worktree-session.ts, and the pane's scope state and callbacks. Backing this up in the backend: create_worktree_workspace_impl gains an adopt-existing guard. git_create_worktree can return an ALREADY-EXISTING checkout without creating anything (its path-reuse short-circuit, when the conventional path is on disk and registered to the same branch), and the impl called create_workspace_with_layout regardless — minting two workspaces over one on-disk worktree, i.e. two agents editing the same files. It now looks for a live local workspace already claiming that path (worktree_path, falling back to cwd; canonicalized comparison; remote/attach-only rows excluded) and adopts + focuses it instead, skipping PTY spawn, setup scripts, .mcp.json rewrite and preset launch. initial_prompt / agent_preset_id are deliberately dropped rather than injected into a session someone else is already using, and archived workspaces neither block creation nor get silently un-archived. Verified with npm run check, npm run test (3026), cargo check, cargo test (2261), and a dev-mock browser drive of both surfaces.
Review — Opus 5 (high effort)The diagnosis is right and the fix is the correct shape — a workspace owns exactly one project root and one checkout, so a per-thread control offering to change either was always an app-wide relocation in disguise. Deleting the two-surface machinery rather than gating it is the right instinct. The backend adopt-guard is well built. I verified the archived-workspace reasoning independently: CI is red
That file isn't in this diff, the Linux job passes, and the other ten open PRs all pass the same job — so this is very likely a Windows-only timing flake in the branch-picker's async default resolution (deleting The adopt path loses the user's typed prompt silently// `initial_prompt` and `agent_preset_id` are DROPPED on the adopt path,
// by design.The reasoning is correct — injecting a prompt into somebody's in-flight session is unrecoverable, and dropping is the safer failure. But the user-visible result is: they type a prompt in the New Workspace dialog, type a worktree name that already exists, hit create, and the app silently focuses a different workspace with their prompt gone. No toast, no error, no trace. And this path is reachable precisely because typed names skip the deconfliction auto-names get, i.e. by the user doing something deliberate. Returning something the frontend can act on — an The
|
|
Review by GPT 5.6 Sol xhigh found one backend race in the adopt guard.
Please make lookup/adopt-or-create one atomic The frontend scope-removal behavior otherwise looks consistent, and the focused frontend suites passed 100/100 tests. The currently red Windows Coordination note: this head has pairwise textual conflicts with #205 and #212. |
- Make the lookup-adopt-or-create decision one atomic operation under the state lock (AppStateStore::adopt_or_create_worktree_workspace), which also stamps the new workspace's worktree_path claim before releasing. Two concurrent creates for the same worktree path could previously both miss the probe and both insert; git worktree add stays outside the lock. Covered by a two-thread barrier regression test asserting exactly one workspace claims the path. - Require a matching git_branch before adopting via the cwd fallback, so the guard stays no wider than git_create_worktree's same-branch path reuse; explicit worktree_path claims still adopt on path identity alone. Extended the lookup tests both ways. - Return an additive adopted flag from create_worktree_workspace (WorkspaceCreated) and the control-socket JSON. The new-workspace dialog now toasts when adoption dropped a typed prompt instead of silently focusing another workspace. - Hoist the duplicated paths_equal helper into workspace_paths and use it from scripts.rs and the adopt guard.
|
Verified: all four substantive review issues are fixed at the head — adoption is now surfaced to the user ( Two blockers before merge:
Also coordinate with #212, which touches the same component; whichever lands second inherits the harder rebase. |
The bug
Opening a second Agent Chat tab inside an existing workspace re-asked "current checkout or New worktree?" — and offered a project picker — even though the tab was already inside a workspace of a project.
That question has no valid answer there. A workspace owns exactly one project root and one checkout (
cwd/worktree_path), shared by every tab, surface and pane inside it. So neither control rescoped the thread:messages.length === 0thread, it re-asked on each additional chat tab — including tabs of a workspace that was already worktree-backed.Both were app-wide relocations wearing per-thread clothing.
The fix
ThreadScopeRowis nowDraftChatSurface-only — the one surface where "what should the first send create?" has an answer, because nothing exists yet.AgentChatPanerenders the read-only Context Row from its first render, so the strip never changes shape across the first send.Creating a worktree belongs to the surfaces honest about creating a workspace (the draft surface, the sidebar worktree flow); switching project or workspace belongs to the sidebar.
With one consumer left, the two-surface machinery comes out —
ThreadScopeLocation/ThreadScopediscriminated props back to flat props, and thetrailingslot,FixedBranchChipandLocationControl's workspace-mode branches deleted, along withhandleDeferredWorktreeSubmit,prestart-worktree-session.ts, and the pane's scope state and callbacks. Net +634 / −1179.Backend: adopt-don't-duplicate guard
Found while tracing the above.
git_create_worktreecan return an ALREADY-EXISTING checkout without creating anything (its path-reuse short-circuit, when the conventional path is on disk and registered to the same branch), butcreate_worktree_workspace_implcalledcreate_workspace_with_layoutregardless — two workspaces over one on-disk worktree, i.e. two agents editing the same files. Reachable by typing a worktree name that already exists, since typed names skip the deconfliction auto-names get.It now looks for a live local workspace already claiming that path (
worktree_path, falling back tocwd; canonicalized comparison; remote/attach-only rows excluded) and adopts + focuses it instead, skipping PTY spawn, setup scripts,.mcp.jsonrewrite and preset launch.initial_prompt/agent_preset_idare deliberately dropped rather than injected into a session someone else is already using, and archived workspaces neither block creation nor get silently un-archived.Behavior changes
project ⌄ · Current checkout ⌄ · from ⑂ branch ⌄project · ⑂ branch(read-only)Hopping to another project and "Open another project…" are now sidebar-only from within a chat.
Verification
npm run checkclean;npm run test— 208 files / 3026 testscargo checkclean;cargo test— 2261 tests, including 6 new adopt-guard cases (same-path adopt, distinct paths still create, non-canonical spelling,cwdfallback, remote rows, archived rows)Docs:
docs/features/agent-chat.md§ Thread Scope rewritten, plusdocs/INDEX.mdand adocs/core/STATUS.mdunreleased entry.Known follow-ups (not in this PR)
AgentChatPane'sfallbackCwdresolves fromactive_workspace_idrather than the pane's own workspace — much smaller exposure now that nothing flips the active workspace mid-send, but still wrong.git_create_worktreeprefersorigin/<base>when creating a branch, so a draft-surface worktree forked off a branch with unpushed local commits starts behind.