Skip to content

feat(chat): restore the run-location picker and isolate repository sessions by default - #166

Merged
devswha merged 1 commit into
mainfrom
fix/restore-session-worktree-picker
Sep 16, 2026
Merged

devswha merged 1 commit into
mainfrom
fix/restore-session-worktree-picker

Conversation

@devswha

@devswha devswha commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Refs #156.

What was broken

565c741 shipped managed session worktrees. All of it is still here and still green:

server/modules/providers/provider.routes.ts:159   POST /api/providers/worktree-sessions
server/modules/providers/services/session-worktrees.service.ts   createWorktreeSession()
server/services/session-worktree-runtime.ts
auth.db                                           session_worktrees

$ node --test server/services/session-worktree-runtime.test.ts \
               server/modules/providers/tests/session-project-binding.test.ts
# tests 24   # pass 24   # fail 0

Then 32da9da"fix(chat): finalize composer controls and compact model picker" — deleted SessionWorktreePicker.tsx (41 lines) and the sessionWorktree wording 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:

$ grep -rn "worktree-sessions" src/
src/components/chat/view/SessionWorktreePicker.dom.bun.test.tsx:   ... is never called ...

The only remaining reference was a test asserting the route is not used. session_worktrees has 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:

02:04  HEAD = fix/promotion-gates                        working tree dirty (7 files)
02:12  HEAD = f14c006 [fix/git-checkout-filter-isolation]   clean

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 through sessionLocationControl, 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.bgIsolation defaults 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

worktreeChoice is boolean | null. null means 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

createWorktreeSession validates 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.tsx survived the deletion with its name and lost its subject. It tested useChatComposerState and asserted:

assert.equal(requests.some(({ url }) => url.includes('/worktree-sessions')), false);

A file named after the deleted control, pinning the shared-checkout behaviour in place. It now covers:

  • the control offers both locations and reports the choice
  • an existing session reports its location instead of offering a choice (prepared and not-yet-prepared)
  • a project-bound session renders nothing
  • choosing the worktree location allocates through /worktree-sessions — same payload, same identity shape, only the route moves
  • an untouched picker follows the default; an explicit choice outranks it
  • a choice made for one project is not carried into the next

The two original false assertions stay, now as what they actually mean: the project location was selected, so the worktree route must stay unused.

Verification

npm test                                 1471/540/407 tests   0 fail
bun test SessionWorktreePicker.dom…      12 pass  0 fail   (6 existing + 6 new)
bun test ChatComposer.dom…                8 pass  0 fail
bun test ChatInterface.dom…               3 pass  0 fail
node --test session-worktree-runtime + session-project-binding   24 pass  0 fail
node --test check-locale-parity           2 pass  0 fail
npm run lint                             clean
npm run check:identity                   passed
npx tsc -p tsconfig.json --noEmit        clean

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/Write in 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.ts already overwrites the client's cwd with the stored project path, so the boundary is already one place — but that is a separate change. #156 stays open for it.

…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
devswha merged commit 14ff20d into main Sep 16, 2026
6 checks passed
@devswha
devswha deleted the fix/restore-session-worktree-picker branch September 16, 2026 19:02
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant