A folder of repositories opens as one Claude session - #91
Merged
Merged
Conversation
Pointing the IDE at a directory that holds several checkouts -- four games
under ~/src, say -- refused it as "not a git repository" and then offered to
run `git init` over it. Measured on a replica of that folder, that offer is a
trap: `git add -A` commits each checkout as a gitlink (mode 160000) with no
.gitmodules, and the parent is permanently dirty afterwards, showing
` M carcade-game-minigolf` every time an agent commits inside one. The nested
repos survive -- a nested .git stops git's upward walk -- but you are left with
a junk repository and the games still not open together.
Such a folder now opens as itself. It has no branches to cut, so it gets one
window rather than a row: one Claude session in the folder, with its todos, its
terminals and its files. The dialog says so before opening, and no longer
offers to initialise a repository where that would swallow the checkouts.
The unit is a synthetic worktree, and that is the whole of why this is small.
Everything downstream keys off a worktree id, so todos and RUN NEXT, the
top-bar pill, the amber indicator, Start Claude, sleep and wake, the layout
arithmetic and the files tree all work with no change whatever. With exactly
one unit per folder its id is `worktreeIdFor(root)` like any other -- derived,
not stored -- so "worktrees are not stored" still holds; the project is stored
and the unit falls out of it. The alternative, a reserved non-worktree unit
like the machine terminal's, was traced and rejected: every per-worktree
affordance is gated on `cell.worktree !== null`, so it would have had no panels
at all, which is most of what was wanted.
What did need doing is the places that assume git, and three of them were 500s
rather than wrong words, because git exits 128 outside a repository:
- `worktreeChanges` held this file's one unwrapped `git status`, so the Changes
route answered a git fatal in the body -- every three seconds, since it polls.
- `allFiles` (`git ls-files`) and `grepFiles` (`git grep`) are the search box,
which is where the keyboard lands when the files panel opens. Arriving in a
folder's panel put the caret in a control that 500s. Both fall back to one
bounded readdir walk skipping .git and the generated directories; that skip
list is shared with the first-commit warning rather than copied, and it is
not cosmetic -- the folder this was built for is 476MB and three of its four
checkouts carry a node_modules, so a naive walk reads a third of a gigabyte
per keystroke. Measured through the real route: searching README returns the
three checkouts' and nothing under node_modules.
`openProject` skips both `isGitRepo` and `repoRoot` on the folder path, and
both matter: `isGitRepo` walks up, so a folder sitting inside a checkout
answers true and `repoRoot` then normalises to that ancestor -- asking for
`<repo>/games` would quietly register `<repo>`. `describeProjects` suppresses
`defaultBase`, since `resolveDefaultBase` is the one git helper that does not
throw outside a repository and falls through to HEAD, naming a base for
branches that cannot be cut.
In the row, the project pane drops the new-worktree form -- and has to take the
keyboard itself where it used to hand it to the branch box, or the pane becomes
one the row's walk cannot get past, the failure already recorded against
`caret`. Verified with real key presses: Alt+Right steps from the pane out to
the unit's search box and Alt+Left steps back. The top bar drops its + and
retitles the pill. The files panel offers Files alone, and a stored 'changes'
mode is coerced back, since `migrateUi` can still write one and `useChangesState`
fires the moment the mode is not 'files'.
No --session-id, and that was measured rather than assumed. With one session
per folder, transcriptDir(cwd) plus newest-transcript is exactly how every
worktree already works. It would only be needed for a second session in one
folder, and cannot simply be added for that: `claude --session-id <uuid>`
refuses an id that already exists ("Session ID ... is already in use"), so a
deterministic per-path id could resume but never start fresh.
Driven against a scratch instance with a real Claude, on a folder of three
checkouts plus a loose file and a node_modules: the warning dialog names the
three and offers only the folder; it opens as one window; a todo queued on it
was typed in by RUN NEXT and answered; sleeping and waking came back to the
same conversation; closing and reopening from Recently closed came back as a
folder. Changes, find and grep answer 200 where they answered 500.
Every test added here was checked by breaking the line it guards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.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.
Pointing the IDE at a directory that holds several checkouts -- four games
under ~/src, say -- refused it as "not a git repository" and then offered to
run
git initover it. Measured on a replica of that folder, that offer is atrap:
git add -Acommits each checkout as a gitlink (mode 160000) with no.gitmodules, and the parent is permanently dirty afterwards, showing
M carcade-game-minigolfevery time an agent commits inside one. The nestedrepos survive -- a nested .git stops git's upward walk -- but you are left with
a junk repository and the games still not open together.
Such a folder now opens as itself. It has no branches to cut, so it gets one
window rather than a row: one Claude session in the folder, with its todos, its
terminals and its files. The dialog says so before opening, and no longer
offers to initialise a repository where that would swallow the checkouts.
The unit is a synthetic worktree, and that is the whole of why this is small.
Everything downstream keys off a worktree id, so todos and RUN NEXT, the
top-bar pill, the amber indicator, Start Claude, sleep and wake, the layout
arithmetic and the files tree all work with no change whatever. With exactly
one unit per folder its id is
worktreeIdFor(root)like any other -- derived,not stored -- so "worktrees are not stored" still holds; the project is stored
and the unit falls out of it. The alternative, a reserved non-worktree unit
like the machine terminal's, was traced and rejected: every per-worktree
affordance is gated on
cell.worktree !== null, so it would have had no panelsat all, which is most of what was wanted.
What did need doing is the places that assume git, and three of them were 500s
rather than wrong words, because git exits 128 outside a repository:
worktreeChangesheld this file's one unwrappedgit status, so the Changesroute answered a git fatal in the body -- every three seconds, since it polls.
allFiles(git ls-files) andgrepFiles(git grep) are the search box,which is where the keyboard lands when the files panel opens. Arriving in a
folder's panel put the caret in a control that 500s. Both fall back to one
bounded readdir walk skipping .git and the generated directories; that skip
list is shared with the first-commit warning rather than copied, and it is
not cosmetic -- the folder this was built for is 476MB and three of its four
checkouts carry a node_modules, so a naive walk reads a third of a gigabyte
per keystroke. Measured through the real route: searching README returns the
three checkouts' and nothing under node_modules.
openProjectskips bothisGitRepoandrepoRooton the folder path, andboth matter:
isGitRepowalks up, so a folder sitting inside a checkoutanswers true and
repoRootthen normalises to that ancestor -- asking for<repo>/gameswould quietly register<repo>.describeProjectssuppressesdefaultBase, sinceresolveDefaultBaseis the one git helper that does notthrow outside a repository and falls through to HEAD, naming a base for
branches that cannot be cut.
In the row, the project pane drops the new-worktree form -- and has to take the
keyboard itself where it used to hand it to the branch box, or the pane becomes
one the row's walk cannot get past, the failure already recorded against
caret. Verified with real key presses: Alt+Right steps from the pane out tothe unit's search box and Alt+Left steps back. The top bar drops its + and
retitles the pill. The files panel offers Files alone, and a stored 'changes'
mode is coerced back, since
migrateUican still write one anduseChangesStatefires the moment the mode is not 'files'.
No --session-id, and that was measured rather than assumed. With one session
per folder, transcriptDir(cwd) plus newest-transcript is exactly how every
worktree already works. It would only be needed for a second session in one
folder, and cannot simply be added for that:
claude --session-id <uuid>refuses an id that already exists ("Session ID ... is already in use"), so a
deterministic per-path id could resume but never start fresh.
Driven against a scratch instance with a real Claude, on a folder of three
checkouts plus a loose file and a node_modules: the warning dialog names the
three and offers only the folder; it opens as one window; a todo queued on it
was typed in by RUN NEXT and answered; sleeping and waking came back to the
same conversation; closing and reopening from Recently closed came back as a
folder. Changes, find and grep answer 200 where they answered 500.
Every test added here was checked by breaking the line it guards.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
🤖 Generated with Claude Code