Skip to content

feat(app): split the sidebar into Code and Work - #225

Merged
quickbeard merged 4 commits into
mainfrom
workspace-code-work
Aug 13, 2026
Merged

quickbeard merged 4 commits into
mainfrom
workspace-code-work

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Sandboxes were just another row in a list of servers, so the isolated environment and the local machine were one undifferentiated pile. The sidebar now opens on two tabs above Projects:

  • Code — every non-sandbox server: the local sidecar, WSL, remote, SSH.
  • Work — the sandboxes.

Per the agreed answers: Code holds all non-sandbox servers, Work groups every sandbox the way Code groups servers today, and each side keeps its own session tabs.

What each side owns

Projects, pinned, recents and the home page all already derived from layout.home.selection.server, so a side is a filter over that existing mechanism, not a second implementation — every section below the tabs is the same component reading a side-filtered server list.

Session tabs follow: the titlebar strip, its drag-reorder and tab.prev/tab.next are all scoped to the visible side, so a sandbox tab never shows while you are in Code and Ctrl+Tab never steps onto a tab you cannot see.

Navigation

Clicking a tab goes to that side's last session, recorded per side by an effect on route() in the layout context (in the context rather than the sidebar so it keeps working while the sidebar is collapsed). Fallbacks, in order: an open draft for that side, a new draft on its first project, then the home screen when the side has nothing to open yet. A recorded session whose server has since disappeared — a removed sandbox — falls through rather than navigating to a dead route.

The Home button is gone. Worth being precise about what it was: despite the label and the home.title tooltip, goHome selected or opened a draft. That is exactly the "goes to a new session" behaviour the tabs replace, so the button and its hidden mod+b command are removed. The actual home screen is unaffected and still reachable from the search button and the sidebar's View all.

The Work gate

Switching to Work with no sandbox configured opens the existing DialogAddSandbox — reused, not reimplemented — and only switches if a sandbox actually exists when the dialog closes. Cancelling leaves you on Code.

A sandbox joins the server list only once it is ready, so Work can legitimately be empty while one is starting or after one failed. That needed a new empty state, because every branch under it indexes servers[0] and would previously have been handed undefined.

Two things that could have gone wrong quietly

Filtered reorder. tabs.reorder() rejects any key list that is not the complete set, so a drag inside a filtered strip would have been a silent no-op. reorderWithinSide splices the visible order back into the slots those tabs occupy and leaves the other side untouched.

Web builds. Sandboxes are desktop-only. A work side persisted on desktop would strand the web sidebar on a side that platform can never populate, so useWorkspaceSide() collapses to Code when platform.sandboxes is absent, and the tab bar does not render there at all.

Verification

Nine unit tests in test-browser/workspace-side.test.ts cover the pure logic: side classification, the server and tab splits, selection preservation across switches, the reorder merge and its partial-list guard, and the web collapse.

bun typecheck + test:unit (657) + test:browser (66) clean in packages/app; bun typecheck clean in packages/desktop.

Not visually verified. I could not drive the Electron UI here, so the layout of the tab strip itself — spacing above the Projects header, the selected-tab treatment — wants a look in the running app. The logic is tested; the pixels are not.

🤖 Generated with Claude Code

Quickbeard and others added 4 commits August 13, 2026 17:14
Sandboxes were just another row in a list of servers, so the isolated
environment and the local machine were one undifferentiated pile. The sidebar
now opens on two tabs: Code, every non-sandbox server (local sidecar, WSL,
remote, SSH), and Work, the sandboxes.

Each side keeps its own projects, pinned and recent sessions, and its own
session tabs — the titlebar strip, its reorder and tab.prev/tab.next all
follow the visible side. Clicking a tab returns you to that side's last
session rather than opening a draft, falling back to a draft and then to the
home screen when a side has no history and nothing to open.

The Home button goes with it: it opened a draft, not a home screen, which is
exactly the behaviour the tabs replace. Its mod+b command goes too.

Switching to Work with no sandbox reuses DialogAddSandbox rather than growing
a second one, and only switches if a sandbox actually gets created — cancel
leaves you on Code. A side can now be empty (a sandbox joins the server list
only once it is ready), so the sidebar gains an empty state; every branch
under it indexes servers[0].

Logic lives in the fork-only workspace module and is unit tested; the upstream
files carry wiring only, and every section below the tabs is the same
component reading a filtered list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deleting a sandbox from Settings while the Work side was open killed the
renderer with "Permission server not found: sandbox:<name>".

createMemo computes eagerly, so permissionState ran the moment
useSessionTabAvatarState did. For a tab drawn after its server had already
disappeared that is during construction — before the sync()/connection()
guards downstream get a chance — and ensureServerState throws on a key it
cannot find. #219 guarded the notification lookup on the same hook but left
this one, which is reached earlier.

Gate it on connection() too, and let effectiveSide fall back to Code once the
last sandbox is deleted so the sidebar is not left sitting on a side with
nothing in it. An unloaded sandbox list stays "unknown" rather than "none",
or every launch would bounce off Work before the list arrives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Creating the first sandbox from the Work gate left the app on Code until the
tabs were toggled by hand. Three things had to line up and none of them did.

The switch checked the sandbox list after the dialog closed, but that list
arrives on its own IPC event and is still empty for a beat after add()
resolves. DialogAddSandbox now reports the sandbox it created and the gate
switches on that.

effectiveSide collapsed Work to Code whenever the sandbox list was empty,
which is the same stale read from the other direction — and also fires on
every launch before the list loads. An empty Work side is legible on its own;
WorkspaceEmpty says so and offers to create one.

Selection was corrected only at the moment of a switch, but a sandbox joins
the server list only once it has started, so a sandbox created seconds earlier
was never selected: the projects list showed the sandbox while Pinned and
Recents still read the local server. Correcting it from an effect covers the
sandbox arriving late, which is what toggling the tabs by hand was really
doing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deleting the only sandbox left the user on a Work side with nothing in it,
still looking at a session belonging to the server that had just gone away.

Driven from the removal itself rather than from watching the sandbox list.
That list updates over IPC a beat after the delete resolves, so a reactive
"no sandboxes means go to Code" rule reads as true both before the list has
loaded and in the moment after the first sandbox is created — which is what
made the earlier attempt at this bounce the user off Work twice over. Asking
"does removing this key empty the side?" against the list as it stands before
the removal has no such window.

It runs beside the tab cleanup, ahead of the awaited platform removal, so the
switch lands immediately rather than after a delete that takes as long as
erasing a disk. Reuses the same enter() as selecting the tab, so Code lands on
its last session rather than wherever the deleted sandbox left the route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 4cbe095 into main Aug 13, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the workspace-code-work branch August 13, 2026 11:46
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