feat(chat): restore the run-location picker and isolate repository sessions by default - #166
Merged
Merged
Conversation
…ssions by default `565c741` shipped managed session worktrees: a route, a `session_worktrees` binding, a runtime and 20 tests, all still passing today. `32da9da`, a composer UI pass, deleted `SessionWorktreePicker.tsx` and the ten locales' wording in the same commit as unrelated control tweaks, with a single-line message and no recorded reason. The server kept answering `POST /providers/worktree-sessions`; nothing could ask it. `session_worktrees` has been empty ever since. That is a fail-open. An unattended run commits, pushes and switches branches on whatever checkout it was given, and a second session reading the same directory watches its branch and index change underneath it. The safe path existed, was tested, and was unreachable. This restores the picker verbatim and reconnects it, and changes one thing: a project that is a git repository now defaults to a new worktree instead of the shared checkout. Opting back into the shared checkout stays one click away, and the choice is per project - `null` means "not chosen" and follows the default, so a "no" for one repository is never carried into the next, which may not be a repository at all. `useProjectGitSummary` is the only thing here that knows, and until it answers the picker stays on the project so the worktree route is never called where it would fail. The picker's test file survived the deletion with its name and lost its subject: it tested the composer instead and asserted `/worktree-sessions` is never called, pinning the shared-checkout behaviour under the deleted component's name. It now tests the control, both routes, the default, an explicit choice outranking it, and the per-project reset. Refs #156
devswha
added a commit
that referenced
this pull request
Sep 17, 2026
A session that chose the project location runs in the same working tree the user and every other session of that project are looking at. Committing, switching branches or resetting there is not a private act: it moves `HEAD` and the index underneath whoever else is reading the directory. On 2026-09-17 an unattended run committed, pushed and switched branches in a shared checkout while a second session was mid-read, and nothing asked. Every project on that machine was set to `bypass` with the warning acknowledged, so honouring the mode here would leave that case exactly as unprotected as it was. Approving a git state change in advance approves it on behalf of every other session reading that directory, which is not the granting user's to waive turn by turn - so it becomes a card even under `bypass`. The permission provider already runs inside the worker for every gated call, before anything executes, and it already sees `rawInput`. This adds one check in front of the policy lookup. A session in its own managed worktree owns its git state and is never gated, which since #166 is the normal case for a repository - so the card appears exactly when the user opted into the shared checkout, which is when it should. The run's own cwd is the isolation answer: a managed worktree is dispatched with the checkout as `cwd` while `projectPath` stays the repository root, and a project-location run gets the same path for both. A path that cannot be resolved reads as shared, so unknown asks rather than assumes. This is a prompt, not a sandbox. Command text can be written to defeat text matching, and the tests record three such shapes on purpose so the limit is documented rather than discovered. Containment belongs to the runtime. Refs #156 Co-authored-by: devswha <25837994+devswha@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #156.
What was broken
565c741shipped managed session worktrees. All of it is still here and still green:Then
32da9da— "fix(chat): finalize composer controls and compact model picker" — deletedSessionWorktreePicker.tsx(41 lines) and thesessionWorktreewording from all ten locales, inside a 20-file composer UI pass, with a single-line commit message and no recorded reason.The server kept answering. Nothing could ask it:
The only remaining reference was a test asserting the route is not used.
session_worktreeshas been empty ever since.Why it matters
An unattended run commits, pushes and switches branches on whatever checkout it was given. Observed on 2026-09-17 while a 674-turn app session ran in the same checkout a second session was reading:
The safe path existed, was tested, and was unreachable. That is a fail-open, not a missing feature.
What this does
Restores the picker verbatim from
32da9da^and reconnects it throughsessionLocationControl, exactly where it used to mount.Changes one thing: a project that is a git repository now defaults to New worktree instead of the shared checkout. Opting back into the shared checkout is one click.
This is the split Claude Code ships: interactive sessions choose, unattended ones isolate (
worktree.bgIsolationdefaults to"worktree"). This app has no separate unattended-session flag — the 674-turn run was an ordinary chat session — so the default applies per repository instead.The choice is per project
worktreeChoiceisboolean | null.nullmeans not chosen and follows the default; an explicit choice outranks it and is dropped when the project changes. A "no" for one repository says nothing about the next, which may not be a repository at all.It never calls a route that would fail
createWorktreeSessionvalidates the repository and throws otherwise.useProjectGitSummary— the same query the Environment rail already runs, so no new request — is the only thing here that knows. Until it answers, and when it answers no, the picker stays on the project and the control is not rendered at all.The test file
SessionWorktreePicker.dom.bun.test.tsxsurvived the deletion with its name and lost its subject. It testeduseChatComposerStateand asserted:A file named after the deleted control, pinning the shared-checkout behaviour in place. It now covers:
/worktree-sessions— same payload, same identity shape, only the route movesThe two original
falseassertions stay, now as what they actually mean: the project location was selected, so the worktree route must stay unused.Verification
Locale wording restored verbatim from
32da9da^for all ten locales, inserted without reformatting the surrounding JSON.Not in this PR
Enforcement. Claude Code blocks
Edit/Writein the main checkout until the session enters its worktree; this app has no such check, so an isolated session that walks out is not stopped. The app is better placed to add it than Claude Code was —chat-websocket.service.tsalready overwrites the client'scwdwith the stored project path, so the boundary is already one place — but that is a separate change. #156 stays open for it.