Repository navigation
feat(app): unmount a Work folder from its own menu, and colour it as destructive - #256
Merged
Merged
Conversation
quickbeard
force-pushed
the
feat/unmount-project-menu
branch
from
August 17, 2026 10:48
07fd2e9 to
df89d02
Compare
…destructive
A project row on Work is a folder mounted into a sandbox, but its menu
offered Close, which only took the row out of this window's sidebar. The
folder stayed mounted, stayed listed under Folders..., and the row could
come back on the next launch -- the removal gesture that looked most like
removal did the least, while the real one was buried in a dialog.
The row's trailing separator and item now come from the fork-only
ProjectRemovalMenuItems, so pages/home.tsx keeps one mount point instead
of a branch:
- a plain server renders upstream's Close, unchanged;
- an extra mounted folder gets Unmount, in the same danger colour the
server and session menus use, which removes it from SandboxConfig.mounts
and then drops the row;
- the folder the sandbox was created with offers neither. It is fixed for
the sandbox's life, and a disabled item that can never be enabled says
less than no item at all;
- a row whose folder is already gone -- unmounted from the dialog instead,
which deliberately leaves the row behind -- keeps Close, or it could
never be cleared away.
The row is dropped with projects.remove(), not the menu's close(): the
folder is leaving the sandbox, which is the internal, non-user removal
that store documents. close() would also record it as user-closed and the
seeding effect honours that, so remounting the folder would leave the user
with no project for it.
Also colours the sandbox Delete item (Settings -> Servers) red, which the
generic and WSL server rows already were.
Driven against the real app over CDP: Work rows offer Unmount in
rgb(241, 116, 113) and the sandbox's own folder offers nothing, Code still
offers Close, unmounting takes the folder out of the config and the row out
of the sidebar, and remounting brings both back. That run also caught the
IPC rejecting Solid store proxies ("An object could not be cloned"), so the
mount list is rebuilt as plain objects before it crosses.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o the local server A restart -- mounting or unmounting a folder -- takes a sandbox out of the server list for a second or two. Session tabs already waited for it (TargetServerRoute renders ServerAway), but the draft route did not: its providers took `conn` as undefined and resolved to whichever server is selected instead, so a New session tab belonging to a sandbox rendered the local machine's composer. The sidebar said Work and the screen was Code -- and sending the prompt would have run it on the user's own machine. ResolvedDraftRoute gets the same guard, so both server-scoped routes wait in the same way and come back on their own. ServerAway gains a LoaderV2 over its copy: this state always ends by itself, so it should read as waiting rather than as something to act on. Its description now says "this tab" rather than "this session" — the draft route shares the screen and has no session yet. Verified in the app with a sandbox draft open: while the sandbox is away the pane is the waiting screen, spinner and all, with no composer; the composer returns on its own, still on the sandbox, once it is back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…estarts The waiting screen fixed the main pane, but the rest of the window still treated a restarting sandbox as one that had gone. Two more places: home-model's "keep the selection on a server that still exists" effect moved it to the local server, and every focused* memo falls back to the current server anyway — so the Work sidebar showed the local machine's projects, pinned and recents for the second or two the sandbox was away, then switched back. A retained server now holds the selection, and the sections render empty instead of someone else's sessions: the Projects area already says the sandbox is not running yet, and an empty list beside that reads as waiting. `projects`, `recentlyClosed` and `indexedSessions` answer before their fallbacks rather than through them, because each falls back to the current server by a different route. useWorkspaceScope lost its sandbox with the connection, so the tab strip widened from one folder to every folder of the sandbox and then narrowed again. The scope memo keeps its previous value while the server it names is retained. Verified in the app across a stop/start with Work selected: the strip holds at three tabs (it went to six), pinned and recents stay empty (they listed the local machine's sessions), and Code is unchanged -- its own pinned and recents still populate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block inherited `pb-42` from the session-not-found screen it was modelled on. That padding is there to clear the composer underneath, but this screen replaces the whole route and has nothing below it, so the offset only pushed the block above the middle -- about 80px on a full window. Measured after: content centre and pane centre are the same pixel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The faint third line was the routing key (`sandbox:<name>`), not a name anyone chose: the connection that would carry a display name is exactly what is missing while the server is away, so there was nothing to resolve it against. An internal id shown to someone waiting a second or two is noise, and the tab they are looking at already says which server it is. The prop goes with it — both call sites passed the key only for this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The block was centred vertically but sat hard against the sidebar. The shell's <main> is `flex flex-col items-start`, so a child that does not claim the width shrinks to its content -- 496px here, the description's max-w-md plus padding -- and centres inside that instead of the pane. The session page claims the width the same way; this one did not. Measured after: content centre and pane centre are the same pixel on both axes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quickbeard
force-pushed
the
feat/unmount-project-menu
branch
from
August 17, 2026 11:06
df89d02 to
6f4217c
Compare
…s ready The retained-server guard covered a sandbox that is restarting, but not a sandbox that does not exist yet: addSandbox puts it in the controller's state only after create resolves, so while one is being created the Work side has no server at all and the selection is still whatever Code left behind. Every focused* fallback then resolved to the current server and the sidebar filled with the local machine's projects, pinned and recents. The guard is now "nothing the selected server owns can be on screen", which covers both: the selection belongs to the side that is not showing, or it is on this side and away. The main area had the same gap from the other end. enter() returns early when the side has no connected server, which left the previous side's session on screen -- composer and all -- under a sidebar that had already switched. It navigates home instead, the destination the no-project branch below it already used. Verified in the app: on Work with its sandbox down and the selection on a Code project, the sidebar shows the "not running" message with Pinned and Recents empty, and the pane shows the home screen's "Nothing here yet" rather than the Code session. Code is unchanged, and Work's own content comes back when the sandbox does. 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.
Follows the investigation of the Close item on a Work project. A project row there is a folder mounted into a sandbox, but Close only took the row out of this window's sidebar: the folder stayed mounted, stayed listed under Folders…, and the row could come back on the next launch. The gesture that looked most like removal did the least, while the real one was buried in a dialog.
What the menu does now
HomeProjectRow's trailing separator and item come from the fork-onlyProjectRemovalMenuItems, sopages/home.tsxkeeps one mount point instead of a branch:The sandbox's own project folder is fixed for its life, so a disabled item that can never be enabled says less than no item at all — deleting the sandbox is still the way to be rid of that one. The last row is the one the Folders… dialog deliberately leaves behind (sessions may reference it); without Close it could never be cleared away.
Unmount removes the folder from
SandboxConfig.mountsviaapi.sandboxes.updateMounts, then drops the row withprojects.remove(), notclose()— the folder is leaving the sandbox, which is the "internal, non-user removal" that store documents.close()would also record it as user-closed, andsrc/sandbox/projects.tshonours that, so remounting the folder would leave the user with no project for it.Also colours the sandbox Delete (Settings → Servers) red, which the generic and WSL server rows already were (#251).
Testing
packages/app: 691 unit + 97 browser tests pass, typecheck clean.projectRemovalis a pure function with cover for each row above, including the two "primary not known yet" cases.Driven against the real desktop app over CDP, since this is a menu:
codev-landing-pageandcodev-hub: New session / Edit project / Clear notifications / Unmount atrgb(241, 116, 113)with--menu-v2-fg: var(--v2-state-fg-danger). No Close, and no "Reveal in Finder" (a sandbox is notServerConnection.local, so that was already correct).codev-backend(the sandbox's own folder): no removal item.rgb(241, 116, 113).config.mounts2 → 1, sandbox restarts, row gone, remaining rows correct. Re-mounting the folder brings both the mount and its project row back.That run is also what caught a bug I would have shipped:
ipcRenderer.invokerejected the mount list with "An object could not be cloned" becauseconfig.mountsare Solid store proxies. The list is now rebuilt as plain objects before it crosses (the dialog is accidentally safe — it builds its list from strings). The sandbox's mounts were restored to their original pair afterwards.Notes
pages/home.tsx): one import, one element. DIVERGENCES.md row added, including the warning not to pass the row's close handler as the unmount path.sandbox.mounts.unmountinen+es.🤖 Generated with Claude Code