Skip to content

feat(app): isolate Code-side tab lists per project - #274

Closed
quickbeard wants to merge 1 commit into
mainfrom
feat/code-project-tabs
Closed

quickbeard wants to merge 1 commit into
mainfrom
feat/code-project-tabs

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Extends #239 to the other side. Code kept one tab strip for the whole side — every project of every connected server in one list — so a session on the local machine sat beside one on a WSL distro and one from an unrelated checkout. Work had already been split per (sandbox, mounted folder); Code is now split the same way, per (server, project), through the TabScope #239 introduced.

What changes for the user

  • Each project keeps its own tabs on Code, as it already did on Work. Tab shortcuts (Ctrl/Cmd + number, next/previous tab, close tab) act within the project on screen, so closing the last tab in a project no longer drops you into an unrelated one, and New tab starts in the project the strip is showing.
  • The strip shows one server at a time. While you are in one server's project, another server's tabs are not in the strip; opening one of its sessions brings them back. This is the same trade Work made in feat(app): isolate Work-side tab lists per mounted folder #239, and it is what makes "the same checkout on your machine and on a WSL distro" two lists rather than one.
  • Clicking a project in the Code sidebar still just filters Recents, unchanged — the strip follows whatever session you open, not what the sidebar highlights. Only Work navigates on a project click.
  • A Code server with no project open yet has nothing to scope to and shows the whole side, exactly as before.

How the scope resolves

useWorkspaceScope grows a server axis. Work could read the sidebar's selection alone because an effect in context/layout.tsx pins it to a sandbox — but nothing syncs home.selection.server to the route, and on Code the two come apart routinely: selecting a tab on a WSL server leaves the sidebar on the local one, and scoping to the sidebar would hide the tab that was just opened. So:

  • Server — serverForScope: the route's, then the sidebar's, each filtered to the side being shown. Work goes through the same resolution, where it settles the same mismatch between two sandboxes.
  • Project — scopeDirectory: the one on screen, then the sidebar's when the sidebar is on the scoped server (two servers can hold the same path), then a per-side fallback — the sandbox's own folder on Work, projects.list()[0] on Code, which is the fallback enter("code") already used.

Everything downstream was already scope-generic and narrows to Code projects with no change: tabsForScope, reorderWithinScope, the successor picked after a close, tab.prev/tab.next/tab.close, and openNewTab's leading folder branch. folderEntry's openSessions step stays Work-only, for the reason #239 gave — on Code that chain is reached from enter(side), where the extra step would change the side switch's behaviour.

Notes

  • No upstream file is touched: the logic is all in the fork-only packages/app/src/workspace/side.ts. The existing DIVERGENCES row is amended (retitled "Per-project tab strips") rather than a new one added, because it claimed the Code path was provably unchanged and that is no longer true. The CHANGELOG bullet from feat(app): isolate Work-side tab lists per mounted folder #239 is still under ## Unreleased and ended with "Code is unchanged", so it is rewritten as the net behaviour rather than contradicted by a second bullet.

Testing

  • bun run test in packages/app — 702 unit + 109 browser, all passing. 12 new cases in test-browser/workspace-side.test.ts cover serverForScope, scopeDirectory, and tabsForScope over Code servers (two projects on one server, the same path on two servers).
  • bunx playwright test — 137 passed, 2 failed (prompt-thinking-level, session-actions-menu:104). Both fail identically on the base commit with this branch stashed, so they are pre-existing and unrelated.
  • bun typecheck in packages/app — clean.

🤖 Generated with Claude Code

Extends #239 to the other side. Code kept one tab strip for the whole
side -- every project of every connected server in one list -- so a
session on the local machine sat beside one on a WSL distro and one from
an unrelated checkout. Work had already been split per (sandbox, mounted
folder); Code is now split the same way, per (server, project), and the
scope type it goes through is the one #239 introduced.

The resolution grows a server axis to make that safe. Work could read
the sidebar's selection alone because an effect in context/layout.tsx
pins it to a sandbox, but nothing syncs home.selection.server to the
route, and on Code the two come apart routinely: selecting a tab on a
WSL server leaves the sidebar on the local one, and scoping to the
sidebar would hide the tab that was just opened. serverForScope takes
the route's server first and the sidebar's second, both filtered to the
side being shown, which keeps "the strip always contains the session you
are looking at" true on a side with several servers connected at once.
Work goes through the same resolution, where it settles the same
mismatch between two sandboxes.

scopeDirectory is the chain #239 derived, with two changes. The
sidebar's directory only counts when the sidebar is on the scoped
server, since two servers can hold the same path -- a checkout the local
machine and a WSL distro both see. And the last resort is per side: the
sandbox's own folder on Work, projects.list()[0] on Code, which is the
fallback enter("code") already used. A Code server with no project open
resolves nothing and degrades to the side scope, which is what both
sides showed before.

Everything downstream was already scope-generic and narrows to Code
projects with no change: tabsForScope, reorderWithinScope, the successor
picked after a close, tab.prev/tab.next/tab.close, and openNewTab's
leading folder branch, which now starts a new tab in the Code project
the strip is showing. The Code sidebar keeps toggling Recents rather
than navigating, so folderEntry's openSessions step stays Work-only for
the reason #239 gave.

Logic stays in the fork-only src/workspace/side.ts; no upstream file is
touched, so the DIVERGENCES row is amended rather than added to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard force-pushed the feat/code-project-tabs branch from 270d26e to 0426227 Compare August 19, 2026 10:32
@quickbeard quickbeard closed this Aug 19, 2026
@quickbeard
quickbeard deleted the feat/code-project-tabs branch August 19, 2026 10:51
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