Repository navigation
feat(app): split the sidebar into Code and Work - #225
Merged
Merged
Conversation
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>
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.
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:
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.nextare 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.titletooltip,goHomeselected or opened a draft. That is exactly the "goes to a new session" behaviour the tabs replace, so the button and its hiddenmod+bcommand 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 handedundefined.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.reorderWithinSidesplices the visible order back into the slots those tabs occupy and leaves the other side untouched.Web builds. Sandboxes are desktop-only. A
workside persisted on desktop would strand the web sidebar on a side that platform can never populate, souseWorkspaceSide()collapses to Code whenplatform.sandboxesis absent, and the tab bar does not render there at all.Verification
Nine unit tests in
test-browser/workspace-side.test.tscover 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 inpackages/app;bun typecheckclean inpackages/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