Skip to content

fix(workspaces): awaken explicitly reopened empty workspaces with a blank terminal - #22016

Closed
mmarabel wants to merge 4 commits into
stablyai:mainfrom
mmarabel:mmarabel/no-terminal-awaken
Closed

mmarabel wants to merge 4 commits into
stablyai:mainfrom
mmarabel:mmarabel/no-terminal-awaken

Conversation

@mmarabel

Copy link
Copy Markdown
Contributor

ELI5

Closing the last tab in a workspace puts it to rest, which is fine. But clicking that emptied workspace to wake it back up did nothing — no terminal appeared, so the workspace stayed dead until you opened the new-tab menu and created one by hand. Now an explicit re-open seeds one blank terminal so the workspace wakes up. Nothing old is resumed; it is a fresh shell.

What Changed

Before: activating an emptied workspace (e.g. an SSH-backed worktree whose host had not answered workspace sync) left the center empty/landing with no surface. The async activation gate could verify the host held no live PTYs, or fail to answer at all, and in both cases no terminal was seeded.

After: the user clicks the empty workspace and one blank terminal appears, making the workspace usable immediately.

Mechanism:

  • Added a gateVerifiedEmpty flag to the initial-terminal options. When the activation gate already censused the owning host and found no live PTYs, an explicit empty-workspace reseed may proceed even while workspace-sync hydration still reports unverifiable. live (runtime-owned) hosts are still never seeded client-side — the host owns creation there.
  • A blocked gate (census could not answer) now falls back to the local authority check, so none-authority workspaces still reseed instead of stranding an explicitly opened empty workspace. unverifiable stays unseeded without the gate's positive empty verdict (STA-4658).

Why

Stranded-with-no-surface is worse than one fresh shell for an explicit user gesture. Seeding a fresh shell does not fork a second writer onto a live agent transcript (no resume happens on this path); the resumed-vs-fresh distinction the reporter asked for is preserved. Alternatives considered: seeding unconditionally on activation (would reintroduce per-launch duplicates per STA-4658 and the runtime double-seed in #17681), and leaving empty workspaces empty (the direction of #21753 / #17682 / PRs #18513 and #19591) — rejected here because the reported bug is the workspace never waking at all.

Linked Issue

Fixes #22015

Visual Proof

N/A — no screenshots or video attached. Verified via automated tests below; manual UI capture not run in this environment.

Testing

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

New/updated tests:

  • workspace-terminal-host-authority.test.ts: gate-verified empty seeds an explicitly reopened empty SSH (unverifiable) workspace; live runtime workspace never seeds even when gate-verified.
  • worktree-activation-emptied-workspace-reseed.test.ts: a blocked gate still reseeds a local empty workspace.

Runs: targeted activation suites (119 tests) and store suites (103 tests) pass; pnpm tc passes; check:code-quality:changed passes with 0 findings. Full suite and build left to CI.

Platforms actually tested: none manually (unit tests only, Linux host).

Review

Self-reviewed. Cross-platform: change is platform-neutral (store logic, no shell/path/shortcut/menu code). SSH/remote/local: SSH unverifiable now seeds only after the gate verified the host empty; live runtime never seeds client-side; local behavior unchanged. Agents/integrations: no agent-specific logic; fresh shell only, no resume/clone of sessions. Performance: one extra boolean through an activation-only path, no hot-path work. Security: no new inputs, IPC, or permissions; creates a normal local tab like the manual New Terminal flow.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Security, cross-platform (Linux, Windows, Mac), remote SSH, mobile, backwards compatibility, performance considered above. Known tension: open items #21753, #17682, #11108 and PRs #18513/#19591 prefer keeping empty workspaces empty on selection; this PR takes the opposite stance for explicitly re-opened workspaces that would otherwise have no surface at all.

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)

…lank terminal

Clicking an emptied workspace left it with no surface: the async
activation gate verified the host empty (or could not answer), but the
seeder still refused on an unverifiable workspace-sync verdict, and a
blocked gate never reseeded at all.

Let a gate-verified empty reseed proceed for unverifiable hosts, and
fall back to the local authority check on blocked gates. Live
(runtime-owned) workspaces still never seed client-side, and startup
hydration still honours the closed-last-tab tombstone.

Fixes stablyai#22015
@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 7815dde6-2d9c-4ffb-b67f-74390a36d200

📥 Commits

Reviewing files that changed from the base of the PR and between 7f8ceff and 7264781.

📒 Files selected for processing (3)
  • src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts
  • src/renderer/src/lib/worktree-agent-activation-gate.test.ts
  • src/renderer/src/lib/worktree-agent-activation-gate.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts
  • src/renderer/src/lib/worktree-agent-activation-gate.ts
  • src/renderer/src/lib/worktree-agent-activation-gate.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.


📝 Walkthrough

Walkthrough

The change renames the gate option to gateApprovedReseed and adds isDirectSshWorkspace. An unverifiable workspace reseeds only when it is a direct SSH workspace and the gate approves reseeding. Live and runtime-unnameable workspaces remain unseeded. PTY census failures now use blocked-census-unavailable, which permits reseeding only when no sleeping agent sessions exist. Tests cover these outcomes.

Priority: ➖ Normal

Severity of issue fixed: Medium

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: explicitly reopened empty workspaces receive a blank terminal.
Description check ✅ Passed The description includes all required sections, links issue #22015, explains the user-facing behavior and implementation, documents testing and review considerations, and identifies CI coverage. It ha…
Linked Issues check ✅ Passed The PR satisfies the coding requirements in [#22015]. Explicit activation reseeds an empty direct-SSH workspace when the gate approves the produced-no-surface path. The census-unavailable fallback req…
Out of Scope Changes check ✅ Passed The production changes and tests support [#22015]. The gate outcome, direct-SSH predicate, sleeping-session guard, runtime exclusions, and option rename implement or verify the explicit-reopen behavio…
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@pullfrog pullfrog 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.

Important

gateVerifiedEmpty widens the seed exception to every unverifiable workspace, but unverifiable also covers a runtime host whose owner cannot be named. That path now seeds a client terminal beside the host's own — the #15556 double-write the PR body says cannot happen. The first inline comment has the reproduction.

Reviewed changes

  • gateVerifiedEmpty option — a new InitialTerminalOptions flag threaded through reseedGatedEmptyWorkspace into ensureWorktreeHasInitialTerminal.
  • empty authorizes a seed — gateAndReseedEmptyWorkspace now passes gateVerifiedEmpty: true for the empty outcome, extending shouldAutoCreate beyond hostAuthority === 'none' to unverifiable.
  • blocked reseeds — the blocked outcome falls back to reseedGatedEmptyWorkspace without the flag, so only none-authority workspaces seed.
  • Tests — a direct-SSH unverifiable predicate test and a local blocked reseed test.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/renderer/src/lib/worktree-initial-terminal-seeding.ts Outdated
Comment thread src/renderer/src/lib/worktree-activation-gated-empty-reseed.ts Outdated
Comment thread src/renderer/src/lib/workspace-terminal-host-authority.test.ts
Comment thread src/renderer/src/lib/worktree-activation-store-contract.ts Outdated
…cked fallback

Address review findings on the empty-workspace awakening:

- A runtime host that cannot be named also reports unverifiable, and the
  gate reports empty there via its unscoped fallback. Gate-approved
  reseeds now require an explicit direct-SSH workspace check, so the
  client never seeds beside a paired host's own terminal (stablyai#15556).
- A blocked gate also covers unrestored sessions and inconsistent
  structured ownership, where the renderer does not yet know what
  surfaces exist. The blocked fallback now only fires with no known
  sleeping sessions, instead of adding the stray shell the deferral was
  avoiding.
- Rename gateVerifiedEmpty to gateApprovedReseed: empty is the gate's
  produced-no-surface directive, not proof the host holds nothing.
- Add an activation-level SSH test through the mocked empty gate (the
  wiring the unit test alone did not guard), a runtime-unnameable
  exclusion test, and blocked with/without sleeping tests.
@mmarabel

Copy link
Copy Markdown
Contributor Author

Addressed the pullfrog review findings in c987d22:

  1. Gate-approved reseed widened to every unverifiable (runtime-unnameable double-write): fixed. The exception now requires isDirectSshWorkspace() — an explicit SSH host-kind plus non-folder check living next to the authority verdicts so the two cannot drift. Runtime-unnameable hosts still report unverifiable but are excluded, preserving the [Bug]: Paired remote client + host desktop window both auto-create the initial terminal — workspace activation spawns two empty terminals #15556 guard. New test: does not seed a runtime-unnameable workspace even for a gate-approved reseed (verified it fails with the kind-check removed).

  2. blocked fallback too broad: narrowed. The fallback now only fires with no known sleeping sessions; with sleeping sessions the workspace is left alone so no stray shell lands beside sessions the deferral was protecting. New tests cover both halves.

  3. Missing activation-level wiring guard: added seeds an explicitly reopened empty SSH workspace through a mocked empty gate — goes through activateAndRevealWorktree with a mocked empty gate on an unverifiable SSH worktree. Verified it fails when the flag argument is dropped from gateAndReseedEmptyWorkspace.

  4. Flag name/doc overstated what empty proves: renamed gateVerifiedEmpty → gateApprovedReseed, doc now states empty is the gate's produced-no-surface directive (including the detached-relay fallback and declined-adoption paths), not proof the host holds nothing.

@pullfrog pullfrog 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.

Important

The blocked fallback is narrower than before, but it still reseeds a local shell when the gate blocked on inconsistent structured ownership — the stray terminal the deferral exists to prevent. The new guard only covers sleeping sessions; a reproduction is in the inline comment.

Reviewed changes

  • Gate-approved reseed scoped to direct-SSH — gateVerifiedEmpty became gateApprovedReseed, and shouldAutoCreate now also requires isDirectSshWorkspace, so a runtime host whose owner cannot be named stays unseeded (#15556).
  • blocked fallback narrowed — the fallback now runs only when workspaceHasSleepingAgentSessions is false; a workspace with a sleeping agent stays deferred.
  • Activation-level wiring coverage — a mocked empty gate test now drives gateAndReseedEmptyWorkspace, so dropping the flag fails the suite.
  • New regression tests — runtime-unnameable + gate-approved reseed stays unseeded; blocked + sleeping sessions leaves the row empty.

ℹ️ Nitpicks

  • worktree-activation-store-contract.ts:85 says gateApprovedReseed is "Never set for live or runtime-unnameable hosts", but the empty branch passes true for every host kind and only the predicate ignores it there. "Never honored for" would match the code.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/renderer/src/lib/worktree-activation-gated-empty-reseed.ts
Follow-up to the gate-approved reseed: a plain blocked gate also covers
unrestored sessions and inconsistent structured ownership, where the
renderer does not yet know what surfaces exist. Seeding there plants a
stray shell beside a chat that appears moments later (e.g. an ownerless
agent-session the runtime is about to project).

Expose the census cause from the gate as blocked-census-unavailable and
reseed only for it (still gated on no known sleeping sessions). Plain
blocked never reseeds. Also fixes the contract doc nitpick: the empty
branch passes the flag for every host kind; the predicate honors it only
for direct-SSH.
@mmarabel

Copy link
Copy Markdown
Contributor Author

Follow-up in 7f8ceff addressing the second review round:

  • blocked fallback still seeded on structured-ownership causes: fixed as suggested by exposing the cause from the gate. The PTY-census throw now resolves a distinct blocked-census-unavailable outcome; all other blocked causes (unready restore, structured read failures, ownerless structured sessions, unbindable TUI terminals) never reseed. The fallback additionally still requires no known sleeping sessions. New gate test (reports census-unavailable when the PTY census itself throws) plus reseed tests for blocked-census-unavailable → seeds and plain blocked without sleeping → stays empty (your ownerless-agent-session repro shape).
  • Doc nitpick: fixed — the flag doc now reads "Never honored for ... The empty branch passes the flag for every host kind; the predicate honors it only for direct-SSH."

Verified: gate/activation suites green (76 + 68 tests across the touched areas), pnpm tc clean, changed-files quality gate clean.

@pullfrog pullfrog 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.

ℹ️ No critical issues — one test-coverage gap and a stale comment inline.

Reviewed changes

  • Split blocked into two verdicts — the gate now returns blocked-census-unavailable only when deps.listSessions() itself throws; every other blocked cause (restore never ready, structured read throwing, inconsistent structured ownership, unbound live PTY) stays plain blocked.
  • Narrowed the reseed fallback — gateAndReseedEmptyWorkspace now enters the fallback only for blocked-census-unavailable, so a plain blocked leaves the workspace empty instead of planting the stray shell beside a chat about to appear. Both prior review threads are addressed by this.
  • Tests — a gate test asserts the census-throw flavor, and an activation-level test asserts plain blocked leaves the row empty.

ℹ️ Nitpicks

  • worktree-activation-emptied-workspace-reseed.test.ts:211 — the comment "blocked means the census could not answer, not that the workspace has a surface" now describes the opposite of the split; that sentence belongs to blocked-census-unavailable.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Comment thread src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts Outdated

@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: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 51d53795-a83a-48a2-994e-02791e431031

📥 Commits

Reviewing files that changed from the base of the PR and between c987d22 and 7f8ceff.

📒 Files selected for processing (5)
  • src/renderer/src/lib/worktree-activation-emptied-workspace-reseed.test.ts
  • src/renderer/src/lib/worktree-activation-gated-empty-reseed.ts
  • src/renderer/src/lib/worktree-activation-store-contract.ts
  • src/renderer/src/lib/worktree-agent-activation-gate.test.ts
  • src/renderer/src/lib/worktree-agent-activation-gate.ts

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

Comment thread src/renderer/src/lib/worktree-agent-activation-gate.ts
A rejected PTY census with structured owners on record now stays plain
blocked instead of becoming the reseedable flavor: TUI terminals need
the census to bind, and skipping it would also blind adoption to
coexisting live shells. Only a census with no structured context may
reseed. Covers the sleeping-guard mock with the census-unavailable
flavor and fixes the stale split comment.
@mmarabel

Copy link
Copy Markdown
Contributor Author

Follow-up in 7264781 covering the latest pullfrog + CodeRabbit notes:

  • Sleeping-guard coverage (pullfrog): the sleeping test now mocks blocked-census-unavailable so it actually enters the guarded branch (verified the guard deletion fails it), and plain-blocked keeps its own no-op test.
  • Stale split comment (pullfrog nitpick): the census-unavailable test comment now describes that flavor instead of blocked.
  • Structured owners vs census failure (CodeRabbit): this one changed the implementation. A rejected census with structured owners on record now stays plain blocked — TUI terminals need the census to bind, and skipping it would additionally blind adoption to coexisting live shells (I briefly tried returning structured pre-census and reverted it for exactly that reason; the existing does not spawn before a structured chat tab hydrates seam test pins the census call). Only a census with zero structured context yields blocked-census-unavailable. New gate tests for native-on-record and TUI-on-record rejections, both mutation-checked.

Verified: 177 tests across gate/activation/seam/reactivation/store suites green, pnpm tc clean, changed-files quality gate clean.

@pullfrog pullfrog 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.

✅ No new issues found.

Reviewed changes

  • Census-throw path fails closed on structured owners — when deps.listSessions() rejects and a structured runtime inventory is on record, the gate now returns plain blocked instead of the reseedable blocked-census-unavailable, so a shell cannot land beside a structured surface the runtime already knows about.
  • New gate tests — a native-only structured inventory resolves structured (no seed) while the census still runs, and a rejected census with native or TUI owners on record stays blocked.
  • Prior coverage gap closed — the sleeping-workspace activation test now mocks blocked-census-unavailable rather than blocked, so the fallback's workspaceHasSleepingAgentSessions guard is actually exercised, and the stale blocked comment was corrected.

I also checked whether the guard should test structured (which includes store-only unifiedTabsByWorktree agent-session tabs) instead of structuredInventory. It should not: a store-only structured tab already suppresses seeding through renderableTabCount > 0 in shouldAutoCreateInitialTerminal, so structuredInventory is precisely the runtime-knows/renderer-has-not-projected case that needs the conservative verdict.

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@mmarabel

Copy link
Copy Markdown
Contributor Author

This goes the opposite way from #21753, #17682 and #19591, which ask that selecting an emptied workspace leave it empty. It's scoped to direct-SSH and unverifiable workspaces, where today the click does nothing at all, and it never seeds runtime-owned (#18513) or startup-restored workspaces. Before this gets a full review, could a maintainer decide whether an explicit click on an emptied workspace should seed one blank shell?

@AmoabaKelvin

Copy link
Copy Markdown
Collaborator

@mmarabel thanks for filing and putting this together. Your diagnosis was right: the activation census already knows the host has nothing running, and seeding was ignoring it.

#23213 lands the same core idea in a smaller change, and you're credited as a co-author on the commit.

The one place it deliberately differs is the blocked fallback. When the census can't reach the host, #23213 doesn't seed. A failed census is the "we couldn't ask" case the STA-4658 guard exists for, so a fresh shell only appears once the host has actually answered.

Closing this in favour of #23213. If you think the blocked case needs covering, happy to pick that up there or in a follow-up.

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]: Clicking an emptied workspace does not awaken it with a blank terminal

2 participants