Skip to content

fix(sidebar): stop a removed folder workspace from leaving a blank row - #26376

Merged
OrcaWin merged 3 commits into
stablyai:mainfrom
bbingz:fix/22712-removed-folder-workspace-ghost-row
Oct 11, 2026
Merged

OrcaWin merged 3 commits into
stablyai:mainfrom
bbingz:fix/22712-removed-folder-workspace-ghost-row

Conversation

@bbingz

@bbingz bbingz commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

ELI5

In a plain-folder project, deleting a workspace that still had a terminal open could leave a blank card in the sidebar, named after the project. Deleting that card did nothing. Now the workspace goes away and stays away.

What Changed

Before: in a folder (non-git) project, right-click a workspace with an open terminal and choose Delete Workspace. A blank card named after the project took its place. Deleting the card again left it there.

After: the workspace is removed and no card is left behind. Workspaces that still exist save their activity, unread state, and names exactly as before.

Mechanism: a workspace in a folder project has no checkout of its own. It exists only as its metadata record, and the sidebar lists every record whose id carries the ::workspace:<uuid> suffix. Deleting it removes that record in the main process. The renderer keeps the workspace in its own list until its teardown runs after the delete returns. If the terminal's PTY binds again in that window, the renderer stamps "last activity" on the workspace through the worktrees:updateMeta IPC. The store's write treated the unknown id as new and created a default record with an empty name. That empty record is the blank card, and the sidebar shows the project name in place of the empty name. A later delete removed it, but the next late write created it again.

A new shared check in folder-workspace-meta-presence.ts answers one question: is this id a folder-workspace instance of any folder project with that id, and does it have no record on the host being written? Two write paths now use it:

  • worktrees:updateMeta, the renderer's path, skips the write and returns null.
  • worktree.set, the runtime path used by the CLI and paired clients, fails with selector_not_found, like any other missing workspace. This covers a write whose workspace lookup started before the delete and finished after it.

Every other write is unchanged:

  • writes to a folder workspace that exists still apply
  • the project's root workspace is never refused
  • git worktrees are not affected, because git, not the record, decides that they exist
  • new folder workspaces are still created through their own creation path, which does not use this IPC

Both renderer callers ignore the IPC's return value, and the preload type now says it can be null.

Why

The record is the only thing that makes a folder workspace exist, so the safe rule is: a background update must never create one. I considered three places for the fix:

  1. The renderer: skip the activity stamp while the workspace is being deleted. Several renderer paths write passive metadata, including activity, unread, and clearing unread. Each would need its own gate, and a missed path brings the ghost back.
  2. The store's write function: refuse to create any record without identity fields. Creation, discovery backfill, and listing repair all use that same function, so this would change far more callers than the bug needs.
  3. The two metadata write boundaries: one check where every late renderer write arrives, and the same check in the runtime's worktree.set. Together they cover every caller that writes on behalf of an open workspace.

I chose option 3.

The issue also lists other state the deleted workspace leaves behind, such as stranded terminals and session tabs. Related issues track those, for example #15909 and #14474. This PR only fixes the blank card that cannot be removed.

Linked Issue

Fixes #22712

Visual Proof

N/A. The fix stops a sidebar row from appearing. It adds nothing to the screen. The new test reproduces the blank row through the real removal and listing handlers. I did not launch the app.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Verified on macOS. I did not launch the app, and I did not test on Windows, Linux, or SSH.

  • worktrees-create-metadata-persistence.test.ts, repro: a folder workspace is deleted through worktrees:remove. Then a late activity write and a late host-qualified unread write arrive through worktrees:updateMeta, and worktrees:list is checked. Without the fix, the list contains the deleted workspace again with an empty name, an empty comment, a new instance id, and no creation time. That matches the record in the issue. With the fix, the workspace stays gone.
  • Same file: a write that names an SSH host is refused when another owner of the same project id sits at a different path. It fails without the fix.
  • Same file, regression: an activity write to a folder workspace that still exists is saved, and its name is kept.
  • runtime-managed-worktree-metadata.test.ts: a worktree.set whose lookup returned a deleted folder workspace fails with selector_not_found and writes nothing. It fails without the fix. A write to a folder workspace that exists still lands.
  • vitest run over src/main/ipc/worktrees/, src/main/ipc/worktrees-*.test.ts, the runtime metadata tests, first-work-folder-rename.test.ts, folder-workspace-pty-identity.test.ts, and orca-runtime-linked-issue-live-meta.integration.test.ts: 54 files and 490 tests pass.
  • tsc --noEmit for config/tsconfig.node.json and config/tsconfig.tc.web.json, oxlint, oxfmt --check, and pnpm run check:code-quality:changed: all clean.

AI Disclosure

Written with Claude Code (Claude Opus 5.5) and cross-reviewed with OpenAI Codex CLI.

Review

Cross-reviewed with OpenAI Codex CLI in two read-only passes. The first pass found two gaps, and both are fixed with tests:

  • The first version looked up the project with a lookup that returns only one owner of a project id. When the same id was registered at a different path on another host, the check missed the workspace. It now matches any folder owner of that id.
  • The runtime's worktree.set could still write after a workspace lookup that began before the delete. It now runs the same check before writing.

The second pass reported no actionable findings.

  • Cross-platform: no OS-specific code. The check compares ids and looks up records.
  • SSH/remote: the check reads the record for the host being written, so a workspace on an SSH host is judged by that host's record. It matches every owner of a project id, so same-id projects on different hosts and paths are handled. Paired clients get the same selector_not_found error they already get for any missing workspace.
  • Agents and integrations: the first-agent-message rename and folder-workspace creation write to the store directly, not through this IPC. They are unchanged.
  • Performance: one repo lookup and one record lookup per metadata write.
  • Security: no new input path. The check only refuses writes.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

Mobile is not affected because the mobile app writes metadata through the runtime, not this IPC. Several open PRs edit the same handler, the preload type, or the runtime metadata update for unrelated features, for example #18676, #24956, #17251, #22723, and #26101. None of them changes how a missing folder-workspace record is handled. Their hunks sit next to this one, so rebasing them may need a trivial merge.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

https://claude.ai/code/session_01XZXEdbC4RKp288k2gBzLUW

@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

We couldn't safely recover the incremental review. No full review was started, and the last reviewed checkpoint was preserved. Retry later, or explicitly request a full review by commenting @coderabbitai full review.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change adds a helper that identifies removed folder-workspace instances using repository identity and metadata presence. The IPC metadata handler returns null for those instances. The runtime metadata update rejects them with selector_not_found. Tests cover removed instances, same-ID repositories at different paths, and updates to existing instances. The API return type now allows null.


Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to d40d7

The change prevents deleted folder workspaces from reappearing as blank rows. A rejected runtime update may still leave stale lineage in a narrow race, and one test may not catch a host-specific write. Both are small follow-ups.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR satisfies the blank-row requirement in #22712. isRemovedFolderWorkspaceInstance blocks late worktrees:updateMeta writes and rejects late runtime worktree.set writes. Added tests cover rem… Implement and test the terminal and session-state cleanup required by #22712, including removal of terminal bindings and workspace session references. If those outcomes are intentionally separate defects, split or revise the issue requireme…
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 5 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary fix: preventing a removed folder workspace from leaving a blank sidebar row.
Description check ✅ Passed The description is complete and directly covers the user impact, implementation, rationale, linked issue, visual proof, testing, scope, compatibility considerations, and review notes. It also reports …
Out of Scope Changes check ✅ Passed The changed production code is limited to the shared folder-workspace presence check and the two metadata write boundaries named by #22712. The added tests verify the late-write regression and preserv…

Full details: Linked Issues check

Explanation

The PR satisfies the blank-row requirement in #22712. isRemovedFolderWorkspaceInstance blocks late worktrees:updateMeta writes and rejects late runtime worktree.set writes. Added tests cover removal, host-qualified writes, existing folder workspaces, and runtime writes. However, #22712 also requires removal of the deleted workspace's terminals and session state. This PR only prevents metadata recreation and explicitly leaves those states unchanged.

Resolution

Implement and test the terminal and session-state cleanup required by #22712, including removal of terminal bindings and workspace session references. If those outcomes are intentionally separate defects, split or revise the issue requirements so the linked target matches the reduced scope.


  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8cf5fa85-be9a-49a1-a4b3-701d09099b0b
📥 Commits

Reviewing files that changed from the base of the PR and between 369b202 and d40d7831717bbc9b51d8428b7d2cac216f357cf4.

📒 Files selected for processing (7)
  • src/main/folder-workspace-meta-presence.ts
  • src/main/ipc/worktrees-create-metadata-persistence.test.ts
  • src/main/ipc/worktrees/metadata/register-worktree-metadata-handlers.ts
  • src/main/runtime/runtime-managed-worktree-metadata.test.ts
  • src/main/runtime/runtime-managed-worktree-metadata.ts
  • src/main/runtime/runtime-store-contract.ts
  • src/preload/api/worktree-api.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.

updates: { lastActivityAt: 2 }
})

expect(table[workspaceId]).toBeUndefined()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert that host-qualified writes are refused.

The shared table tracks setWorktreeMeta, but both host-qualified updates use setWorktreeMetaForHost. This assertion therefore passes even if the SSH update reaches its setter and recreates the metadata row in a real store. Assert that setWorktreeMetaForHost was not called in both removed-workspace cases, or back that setter with host-specific table state. (github.com)

Comment on lines +96 to +97
if (isRemovedFolderWorkspaceInstance(args.store, worktree.id, executionHostId)) {
throw new Error('selector_not_found')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Check workspace presence before changing lineage.

If removal finishes after resolveWorktree, a worktree.set request with lineage.noParent or lineage.parentWorktree changes lineage before this check throws selector_not_found. The rejected request can leave stale lineage. Resolve the parent first when necessary, then check presence before any lineage mutation. Keep the check after the asynchronous resolution. (github.com)

bbingz and others added 3 commits October 11, 2026 01:34
A folder-project workspace exists only as its worktreeMeta row. When it is
deleted while a terminal is still bound, the renderer keeps the row until its
teardown runs, and a PTY reattach in that window stamps lastActivityAt through
worktrees:updateMeta. setWorktreeMeta then minted a default row with an empty
name, which the sidebar listed under the project name and which came back after
every delete. The metadata IPC now refuses to create a row for a folder
workspace instance that no longer exists.

Fixes stablyai#22712

Claude-Session: https://claude.ai/code/session_01XZXEdbC4RKp288k2gBzLUW
Move the existence check into a shared predicate that matches any same-id
folder owner, so an owner at a different path on another host cannot bypass
it, and apply it to the runtime's worktree.set path, where a resolution
snapshot taken before the removal still returns the workspace.

Claude-Session: https://claude.ai/code/session_01XZXEdbC4RKp288k2gBzLUW
@OrcaWin
OrcaWin force-pushed the fix/22712-removed-folder-workspace-ghost-row branch from d40d783 to b2c176a Compare October 11, 2026 08:40
@OrcaWin
OrcaWin merged commit b4aec8a into stablyai:main Oct 11, 2026
29 checks passed
@OrcaWin

OrcaWin commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator

Thank you @bbingz! This was a sharp diagnosis and a nicely contained fix, and the regression tests made it easy to confirm the bug on main. I rebased onto main and added one small test-only commit (a typed fixture parameter for a lint rule that landed after your last push). Merged. Much appreciated.

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.

[Bug]: Deleting a folder-project workspace from the sidebar while a terminal is attached leaves an unremovable blank row named after the project

2 participants