Skip to content

feat(sidebar): inline SSH reconnect control on workspace cards - #12396

Open
AmethystLiang wants to merge 6 commits into
mainfrom
ui-ssh-reconnect-judge
Open

feat(sidebar): inline SSH reconnect control on workspace cards#12396
AmethystLiang wants to merge 6 commits into
mainfrom
ui-ssh-reconnect-judge

Conversation

@AmethystLiang

@AmethystLiang AmethystLiang commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Problem

A workspace whose SSH host dropped offered no way to reconnect from the sidebar. The card dimmed to 60% opacity and said nothing about why, and the only recovery paths were the terminal overlay (needs an open pane) or the status bar (needs the host list). The modal that used to front this — SshDisconnectedDialog — was a dead end: it interrupted, captured Enter at the window level, and still made the user hunt for the connect.

Change

An inline host control on the workspace card title row, plus four shared SSH modules the existing reconnect surfaces now delegate to.

New — src/renderer/src/ssh/

  • ssh-connection-recoverability.tsisConnectingSshStatus / canConnectSshStatus over two total Record<SshConnectionStatus, boolean> maps, so growing the 8-member union is a typecheck failure rather than a silent misclassification (an array + .includes() fails open).
  • ssh-connect-in-flight.ts — module-level target-id registry behind useSyncExternalStore, so a connect started on one surface suppresses the duplicate affordance on every other one.
  • ssh-connect-ui-timeout.tswithUiConnectTimeout; ssh.connect has no timeout of its own. Composer keeps its 20s budget; the reconnect surfaces pass SSH_RECONNECT_UI_TIMEOUT_MS = 180_000, which must exceed main's CREDENTIAL_TIMEOUT_MS = 120_000 (src/main/ipc/ssh-passphrase.ts:5) — an interactive passphrase alone can burn 120s before the 30s connect even starts.
  • ssh-connect-verb.ts — one verb table (auth-failed → Reconnect, error/reconnection-failed → Retry, else Connect) so four surfaces stop naming one click three ways.

New — WorktreeCardSshHostControl.tsx
A 16px pill in the title row. Connected (or runtime-owned, i.e. status null) renders a passive Server glyph with a tooltip; a removed host renders a passive muted ServerOff and offers nothing; a reconnectable status renders the verb pill with a destructive tint. aria-disabled + a handler guard while connecting (not disabled, which implies pointer-events-none and would drop clicks through to the card and kill the tooltip). Failure toasts and runs the STA-1468 metadata resync (listTargets first, then listRemovedTargetLabels).

DeletedSshDisconnectedDialog.tsx, its vi.mock in 14 specs, and its 11-key locale namespace across all 5 locale files. This deliberately removes its window-capture Enter handler (SshDisconnectedDialog.tsx:118-140 on main) — no replacement global key binding, by design.

Also removed — the card-root SSH opacity-60. CSS opacity composites the whole subtree, so it would have driven the control's destructive tint and spinner to an illegible alpha; the control now states the disconnected state in words instead.

Migrated to the shared modules (exported names unchanged, so no caller moved): TerminalSshReconnectOverlay, SshTargetStatusRow, SshTargetRow (its local mount-guard machinery is gone), SshStatusSegment, HostSectionHeaderMenu, NewWorkspaceComposerCard, new-workspace-ssh-gate, ssh-target-action-state, external-automation-source-availability, useIpcEvents.

Correctness notes

  • sshTargetRemoved guards on isRuntimeOwnedSshTargetId, matching sshStatus. ssh:listTargets filters runtime-owned targets out, so "absent from the target list" is not evidence of removal — without the guard every ephemeral-VM card reads "SSH host removed". Regression test included.
  • A live connected status outranks a stale removal tombstone in the branch order.
  • onKeyDown stops Enter/Space propagation, otherwise WorktreeList's container key handler steals activation.
  • The setSshConnectionState store mirror is kept: ssh.connect can resolve before the state-change IPC lands, and the deferred PTY reattach keys off the renderer store.

Testing

4 new module specs + WorktreeCardSshHostControl.test.tsx (~30 cases: aria-disabled semantics, second-click ignored, key containment, connected-beats-tombstone, verb table, tooltips, icon-only mode, store mirror, card-click isolation, resync ordering, runtime routing, sibling suppression, glyph/height stability). Full src/renderer suite: 2101 files / 19472 tests pass. typecheck, check:max-lines-ratchet, oxlint, oxfmt --check, and sync:localization-catalog all clean.

Live UI states

Captured against a running dev build by driving the renderer store (sshConnectionStates) for a real SSH target over CDP — not mocks. In each shot the middle card is a second, still-connected host, which is what sibling isolation looks like. This build runs the new card style, so these take the icon-only branch.

disconnected auth-failed
disconnected auth-failed
Quiet muted pill, ServerOff. No destructive tint — nothing has failed yet. Destructive tint; aria-label = "Reconnect SSH host openclaw — authentication failed".
connecting Host removed
connecting removed
Spinner, aria-busy="true", aria-disabled="true" — still hoverable, keeps its tooltip. Both cards on the host spin from the one shared registry. Passive muted glyph, no box, no tint, nothing to click. The box is what separates "act on this" from "settled fact".

Labeled (default, non-compact) card mode, forced by clearing the card-style setting. The locale here is Chinese, so this also shows the carried translation resolving live — 重新连接, not English — and makes the title-width residual concrete: the pill measures 47px and sits ahead of the title.

labeled

Measured across all five states the pill is 18px tall (h-4 + 1px border per side) and never changes height as the status changes, so the row does not shift.

Accepted residuals

  • The 11 net-new control strings are English-only; existing translations were carried onto the shared verb keys so nothing regressed to English.
  • In the default (non-compact) card mode at a 220px sidebar the labeled pill still consumes title width. Icon-only mode is used for compact and new-style cards.

Unmeasured pre-merge checks (from the design doc)

  • Title-truncation parity at the narrowest sidebar width, default card mode.
  • Destructive-token contrast for the pill in both light and dark themes.

Design doc and the model-comparison judgment that preceded it were kept out of the repo as process artifacts (artifacts/, untracked).

Made with Orca 🐋

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: acba32b2-a8d5-4046-bd54-8739b0f45be2

📥 Commits

Reviewing files that changed from the base of the PR and between bca40e6 and 6308098.

📒 Files selected for processing (3)
  • src/renderer/src/ssh/ssh-connect-in-flight.test.ts
  • src/renderer/src/ssh/ssh-connect-in-flight.ts
  • src/renderer/src/ssh/ssh-connect-verb.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/src/ssh/ssh-connect-verb.ts
  • src/renderer/src/ssh/ssh-connect-in-flight.test.ts

📝 Walkthrough

Walkthrough

The change centralizes SSH status classification, connection-action labels, UI timeouts, and per-target in-flight tracking. SSH connection entry points now suppress duplicate attempts and clear shared state after completion. Worktree cards replace the blocking disconnect dialog with inline SSH host controls. Status menus and teardown logic use shared helpers. Tests and translations cover the new controls, labels, connection states, and lifecycle behavior.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed and relevant, but it omits the required AI Review Report and Security Audit sections and does not use the template headings. Add the required AI Review Report, Security Audit, and Notes sections, and confirm the requested platform checks and testing items.
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. 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 summarizes the primary user-visible change: an inline SSH reconnect control on workspace cards.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

@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: Pro Plus

Run ID: f6792104-fec6-44eb-a1a2-c5d7170da307

📥 Commits

Reviewing files that changed from the base of the PR and between a69ca6d and 8334f04.

📒 Files selected for processing (44)
  • src/renderer/src/components/NewWorkspaceComposerCard.tsx
  • src/renderer/src/components/automations/external-automation-source-availability.ts
  • src/renderer/src/components/settings/ssh-target-action-state.ts
  • src/renderer/src/components/sidebar/AutoRenameFailedDialog.tsx
  • src/renderer/src/components/sidebar/HostSectionHeaderMenu.tsx
  • src/renderer/src/components/sidebar/SshDisconnectedDialog.tsx
  • src/renderer/src/components/sidebar/SshTargetRow.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.tsx
  • src/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCardSshHostControl.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx
  • src/renderer/src/components/status-bar/SshStatusSegment.tsx
  • src/renderer/src/components/status-bar/SshTargetStatusRow.tsx
  • src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsx
  • src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.tsx
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/hooks/useIpcEvents.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/new-workspace-ssh-gate.ts
  • src/renderer/src/ssh/ssh-connect-in-flight.test.ts
  • src/renderer/src/ssh/ssh-connect-in-flight.ts
  • src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts
  • src/renderer/src/ssh/ssh-connect-ui-timeout.ts
  • src/renderer/src/ssh/ssh-connect-verb.test.ts
  • src/renderer/src/ssh/ssh-connect-verb.ts
  • src/renderer/src/ssh/ssh-connection-recoverability.test.ts
  • src/renderer/src/ssh/ssh-connection-recoverability.ts
💤 Files with no reviewable changes (14)
  • src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx
  • src/renderer/src/components/sidebar/SshDisconnectedDialog.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts
  • src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx

Comment thread src/renderer/src/components/NewWorkspaceComposerCard.tsx Outdated
Comment thread src/renderer/src/hooks/useIpcEvents.ts
Replaces the blocking SshDisconnectedDialog with an inline pill in the
workspace card title row, and unifies the SSH connect vocabulary across the
sidebar card, terminal overlay, host-header menu, and status-bar row.

- new src/renderer/src/ssh/ modules: typechecker-total status predicates
  (recoverability), a shared in-flight connect registry, the promoted UI
  connect timeout, and the shared connect verb table
- WorktreeCardSshHostControl: one 16px pill shape for every state, icon-only
  in compact/new card modes, passive glyph for connected/null/removed hosts
- migrates the four duplicated status predicates to the shared module
- deletes SshDisconnectedDialog (and its window-capture Enter handler)
- reconnect surfaces get a 180s UI connect fence instead of the composer's
  20s: main allows 120s for an interactive passphrase before the 30s connect
  even starts, so the short cap toasted "timed out" and ran the stale-metadata
  resync against a host that was about to connect fine
- carries the existing es/ja/ko/zh translations onto the shared connect verbs
  (they were en-only, regressing four locales) and drops the dead
  SshDisconnectedDialog key namespace
- migrates the three remaining copies of the connecting predicate
  (SshStatusSegment, SshTargetRow, external-automation-source-availability)
- SshTargetStatusRow and SshTargetRow now join the shared in-flight registry,
  so a connect started on one surface disables the others immediately
- drops the per-card aria-live region that duplicated the button's own label
- guard sshTargetRemoved on isRuntimeOwnedSshTargetId: runtime-owned targets
  are filtered out of ssh:listTargets, so absence is not evidence of removal
  (every ephemeral-VM card otherwise read "SSH host removed")
- drop the card-root SSH dim: an ancestor opacity composited the control's
  destructive tint and spinner down to an illegible alpha
- aria-disabled instead of disabled while connecting, so the pill stays
  pointer-reachable for its tooltip and focus
- stop Enter/Space propagation so WorktreeList's container key handler does
  not steal activation
- prefer a live connection over a stale removal tombstone
- reclaim title width in labeled mode (no leading icon, no min-width floor)
- register composer connects in the shared in-flight registry
Replace manual begin/end pairs with trackSshConnect wrapper that holds
the lock for the full backend request duration. Previously, the lock
was released after the UI timeout fired, even though the backend was
still dialing — a second click on any surface for this host would
trigger a second connect and a duplicate credential prompt on
passphrase-gated targets. The wrapper survives unmount, unlike a
finally block in a component handler.
@AmethystLiang
AmethystLiang force-pushed the ui-ssh-reconnect-judge branch from 8334f04 to bca40e6 Compare August 4, 2026 04:45

@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: Pro Plus

Run ID: b1a1a163-33fc-4f65-bf93-39a2094d1d55

📥 Commits

Reviewing files that changed from the base of the PR and between 8334f04 and bca40e6.

📒 Files selected for processing (44)
  • src/renderer/src/components/NewWorkspaceComposerCard.tsx
  • src/renderer/src/components/automations/external-automation-source-availability.ts
  • src/renderer/src/components/settings/ssh-target-action-state.ts
  • src/renderer/src/components/sidebar/AutoRenameFailedDialog.tsx
  • src/renderer/src/components/sidebar/HostSectionHeaderMenu.tsx
  • src/renderer/src/components/sidebar/SshDisconnectedDialog.tsx
  • src/renderer/src/components/sidebar/SshTargetRow.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.tsx
  • src/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCardSshHostControl.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx
  • src/renderer/src/components/status-bar/SshStatusSegment.tsx
  • src/renderer/src/components/status-bar/SshTargetStatusRow.tsx
  • src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsx
  • src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.tsx
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/hooks/useIpcEvents.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/new-workspace-ssh-gate.ts
  • src/renderer/src/ssh/ssh-connect-in-flight.test.ts
  • src/renderer/src/ssh/ssh-connect-in-flight.ts
  • src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts
  • src/renderer/src/ssh/ssh-connect-ui-timeout.ts
  • src/renderer/src/ssh/ssh-connect-verb.test.ts
  • src/renderer/src/ssh/ssh-connect-verb.ts
  • src/renderer/src/ssh/ssh-connection-recoverability.test.ts
  • src/renderer/src/ssh/ssh-connection-recoverability.ts
💤 Files with no reviewable changes (14)
  • src/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsx
  • src/renderer/src/components/sidebar/SshDisconnectedDialog.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts
  • src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx
  • src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx
🚧 Files skipped from review as they are similar to previous changes (28)
  • src/renderer/src/components/sidebar/AutoRenameFailedDialog.tsx
  • src/renderer/src/components/status-bar/SshStatusSegment.tsx
  • src/renderer/src/components/terminal-pane/pty-connection.ts
  • src/renderer/src/components/automations/external-automation-source-availability.ts
  • src/renderer/src/ssh/ssh-connection-recoverability.test.ts
  • src/renderer/src/components/settings/ssh-target-action-state.ts
  • src/renderer/src/components/sidebar/HostSectionHeaderMenu.tsx
  • src/renderer/src/ssh/ssh-connection-recoverability.ts
  • src/renderer/src/lib/new-workspace-ssh-gate.ts
  • src/renderer/src/ssh/ssh-connect-ui-timeout.ts
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/components/NewWorkspaceComposerCard.tsx
  • src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsx
  • src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts
  • src/renderer/src/components/sidebar/SshTargetRow.tsx
  • src/renderer/src/components/sidebar/WorktreeCard.tsx
  • src/renderer/src/components/status-bar/SshTargetStatusRow.tsx
  • src/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.tsx
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsx
  • src/renderer/src/ssh/ssh-connect-in-flight.test.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/hooks/useIpcEvents.ts
  • src/renderer/src/ssh/ssh-connect-verb.test.ts
  • src/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsx
  • src/renderer/src/components/sidebar/WorktreeCardSshHostControl.tsx
  • src/renderer/src/i18n/locales/ko.json

Comment thread src/renderer/src/ssh/ssh-connect-in-flight.ts Outdated
A tracked request settling after its lock was cleared (via reset or
explicit end) must not unlock a newer connect on the same target. Lock
IDs ensure only the owning acquisition releases, preventing stale
settlements from clearing active locks.
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.

1 participant