Skip to content

A folder of repositories opens as one Claude session - #91

Merged
andrin-n-dream merged 1 commit into
masterfrom
folder-project
Oct 2, 2026
Merged

andrin-n-dream merged 1 commit into
masterfrom
folder-project

Conversation

@andrin-n-dream

Copy link
Copy Markdown
Contributor

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

🤖 Generated with Claude Code

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>
@andrin-n-dream
andrin-n-dream merged commit 40d49a7 into master Oct 2, 2026
1 check passed
@andrin-n-dream
andrin-n-dream deleted the folder-project branch October 2, 2026 14:21
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