Skip to content

[bug] "Remove from sidebar" wipes the workspace's whole pane layout and orphans its running agents — one click, no confirm, no undo #5989

Open

Description

@2solarmax

Where did this happen?

Desktop app (macOS)

Version

1.17.0

What happened?

Hovering a workspace row in the sidebar reveals a small − button (tooltip: "Remove from sidebar"). One click on it, with no confirmation and no undo:

  1. Replaces the workspace's entire paneLayout with an empty one. Every tab and split in that workspace is discarded from local state.
  2. Tears down the pane runtimes — releases every terminal runtime and destroy()s every browser pane.
  3. Is not recoverable. Re-adding the workspace to the sidebar only flips isHidden back to false; paneLayout is never restored. You get the workspace back, empty.

Meanwhile the PTYs behind those terminals are not killed — terminalRuntimeRegistry.release() explicitly does not tell host-service to kill the PTY. So any Claude Code / Codex sessions that were running keep running, headless, with no pane referencing them. (Corrected — see comment below: they are recoverable via the "N background terminal sessions" button, which I missed when filing. The layout loss below stands; the session loss does not.)

The comment that justifies skipping a confirmation dialog is this one, in RemoveFromSidebarMount.tsx:

Unpinning is reversible — the workspace itself is not deleted and can be re-pinned from the Workspaces page or sidebar — so it runs immediately with no confirmation dialog.

That premise is true of the workspace record and false of everything in it. The same flow's tombstone path wipes paneLayout and disposes the runtimes. The action is documented as reversible and implemented as destructive.

Code path (verified at 97e0446, and byte-identical in the shipped 1.17.0 app.asar)

DashboardSidebarExpandedWorkspaceRow.tsx:296-315 — the − button, onClick → onRemoveFromSidebarClick(), no guard.

RemoveFromSidebarMount.tsx:20-29 — fires immediately on intent:

navigateAwayFromWorkspace(target.workspaceId);
hideWorkspaceInSidebar(target.workspaceId, target.projectId);
clear();

sidebarMutations.ts:56-65 — tombstoneSidebarWorkspaceRecord:

cleanupPaneRuntimes([existing]);
collections.v2WorkspaceLocalState.update(workspaceId, (draft) => {
  draft.sidebarState.isHidden = true;
  draft.paneLayout = createEmptyPaneLayout();   // <-- every tab and split, gone
});

useDashboardSidebarState.ts:180-188 — cleanupWorkspacePaneRuntimes:

for (const terminalId of extractPaneIds(rows, getTerminalRuntimeId))
  terminalRuntimeRegistry.release(terminalId);
for (const browserId of extractPaneIds(rows, getBrowserRuntimeId))
  browserRuntimeRegistry.destroy(browserId);

terminal-runtime-registry.ts:400-404 — why the agents survive but become unreachable:

Release the renderer-side terminal runtime only. This detaches the xterm
view and closes the WebSocket, but it does not tell host-service to kill
the underlying PTY.

useDashboardSidebarState.ts:131-167 — ensureSidebarWorkspaceRecord, the re-add path. It sets isHidden = false and nothing else. There is no branch that rebuilds paneLayout, so the layout is unrecoverable once tombstoned.

The target itself

  • The − is size-3.5 (14px), rendered inside invisible … group-hover:visible, so it only exists while the pointer is over the row.
  • On a main workspace row it is the only hover action, and it sits immediately right of the ⌘N shortcut hint (see screenshot).
  • It carries the same visual weight as the non-destructive row affordances next to it — nothing marks it as the one that discards state.

Why I'm writing this up in detail

I lost a full working set this way in one mis-click: multiple tabs and splits with long-running agents in them, no dialog, no undo, no restore path. It cost me enough trust in the app that I've moved my daily terminal elsewhere for now. I'd rather see this fixed than leave, hence the writeup rather than a one-liner.

Steps to reproduce

  1. Open a workspace and build up real state in it: several tabs, a couple of splits, a few agents running.
  2. Move the pointer over that workspace's row in the sidebar.
  3. Click the − that appears on the right of the row.
  4. The workspace disappears from the sidebar. No dialog, no toast, no undo.
  5. Re-add it from the Workspaces page. It comes back with an empty layout — every tab and split is gone.
  6. ps on the host: the agent processes from step 1 are still running, with no pane in the app bound to them.

Logs, screenshots, or recordings

Remove from sidebar

The − next to ⌘1 is the button. Repo and workspace names are redacted.

Suggested fixes

Roughly in order of value:

  1. Don't destroy contents on hide. Keep paneLayout on the tombstone row and restore it in ensureSidebarWorkspaceRecord. Hiding then means what the comment already claims it means: a visibility flag. This alone removes the data loss.
  2. If the layout genuinely has to be dropped, gate it. Either an AlertDialog (already the pattern in DeleteWorkspaceDialog) or an undo toast — sonner's action: { label, onClick } is already used in ~9 places in this codebase.
  3. Say what is at stake in that confirm, e.g. "3 agents are still running in this workspace" / "12 tabs will be closed". The counts are already available on the row.
  4. Reconsider the affordance. A bare hover-revealed 14px icon button, sitting next to a keyboard-shortcut hint, is a low bar for an irreversible action. The context menu would be a safer home for it.

Related: the running-agents chip on the same row has a similar problem — filed separately as #5990.

Happy to send a PR for (1) + (2) if that would help.

Activity

  1. 2solarmax commented on Jul 27, 2026

    @2solarmax
    Author

    Correction from me (the reporter): one claim in the original report was wrong, and it lowers the severity.

    I wrote that the orphaned sessions have "no UI anywhere in the app to reattach to them." That is false. BackgroundTerminalsButton does exactly that:

    • it queries terminal.countBackgroundSessions / terminal.listSessions and computes getBackgroundTerminalSessions(sessions, attachedTerminalIds) — every live session in the workspace not currently bound to a pane;
    • it renders an archive-icon button labelled N background terminal session(s) whenever that count is non-zero;
    • handleAdopt(terminalId) → focusOrAddTerminalPane(store, terminalId) puts one back into a pane.

    So after a "Remove from sidebar", re-adding the workspace shows that button with every still-running session behind it, and they can be adopted back one at a time. I grepped for orphan / reattach / recover session and concluded the feature was absent; it is called "background sessions" and "adopt". That is on me, and I'd rather flag it than let the report stand on a wrong premise.

    What this changes: the running agents are recoverable. This is not the total loss I described.

    What still stands, unchanged:

    1. draft.paneLayout = createEmptyPaneLayout() still discards the workspace's tab and split structure irreversibly. Adoption rebuilds sessions one at a time into a fresh layout; it does not restore which pane was where, or the splits. On a workspace with a dozen panes that is still real, unrecoverable work.
    2. Browser panes are still destroy()ed with no equivalent of adoption.
    3. It is still a single unconfirmed click on a hover-revealed 14px control, with no undo and no toast — nothing tells you the layout just went away, or that the button exists to get the sessions back. Discoverability is the recovery path here, and the destructive action says nothing about it.

    Adjusted ask: fix (1) in the original list — keep paneLayout on the tombstone row and restore it in ensureSidebarWorkspaceRecord — is still the right fix and now the only one strictly needed. Failing that, an undo toast that says what happened would close most of the gap, since the recovery mechanism already exists and just isn't surfaced at the moment it becomes relevant.

    Verified against the shipped 1.17.0 app.asar, same as the original report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions