feat(sidebar): inline SSH reconnect control on workspace cards - #12396
feat(sidebar): inline SSH reconnect control on workspace cards#12396AmethystLiang wants to merge 6 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThe 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)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (44)
src/renderer/src/components/NewWorkspaceComposerCard.tsxsrc/renderer/src/components/automations/external-automation-source-availability.tssrc/renderer/src/components/settings/ssh-target-action-state.tssrc/renderer/src/components/sidebar/AutoRenameFailedDialog.tsxsrc/renderer/src/components/sidebar/HostSectionHeaderMenu.tsxsrc/renderer/src/components/sidebar/SshDisconnectedDialog.tsxsrc/renderer/src/components/sidebar/SshTargetRow.tsxsrc/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.tsxsrc/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsxsrc/renderer/src/components/sidebar/WorktreeCardSshHostControl.tsxsrc/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsxsrc/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.tssrc/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsxsrc/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsxsrc/renderer/src/components/status-bar/SshStatusSegment.tsxsrc/renderer/src/components/status-bar/SshTargetStatusRow.tsxsrc/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsxsrc/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.tsxsrc/renderer/src/components/terminal-pane/pty-connection.tssrc/renderer/src/hooks/useIpcEvents.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/new-workspace-ssh-gate.tssrc/renderer/src/ssh/ssh-connect-in-flight.test.tssrc/renderer/src/ssh/ssh-connect-in-flight.tssrc/renderer/src/ssh/ssh-connect-ui-timeout.test.tssrc/renderer/src/ssh/ssh-connect-ui-timeout.tssrc/renderer/src/ssh/ssh-connect-verb.test.tssrc/renderer/src/ssh/ssh-connect-verb.tssrc/renderer/src/ssh/ssh-connection-recoverability.test.tssrc/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
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.
8334f04 to
bca40e6
Compare
There was a problem hiding this comment.
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
📒 Files selected for processing (44)
src/renderer/src/components/NewWorkspaceComposerCard.tsxsrc/renderer/src/components/automations/external-automation-source-availability.tssrc/renderer/src/components/settings/ssh-target-action-state.tssrc/renderer/src/components/sidebar/AutoRenameFailedDialog.tsxsrc/renderer/src/components/sidebar/HostSectionHeaderMenu.tsxsrc/renderer/src/components/sidebar/SshDisconnectedDialog.tsxsrc/renderer/src/components/sidebar/SshTargetRow.tsxsrc/renderer/src/components/sidebar/WorktreeCard.affiliate-list-mode.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.compact-ports-hover-independence.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.lineage.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.ssh-reconnect-prompt.test.tsxsrc/renderer/src/components/sidebar/WorktreeCard.tsxsrc/renderer/src/components/sidebar/WorktreeCardSshHostControl.test.tsxsrc/renderer/src/components/sidebar/WorktreeCardSshHostControl.tsxsrc/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsxsrc/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.tssrc/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsxsrc/renderer/src/components/sidebar/WorktreeList.status-lane-lineage-drop.test.tsxsrc/renderer/src/components/status-bar/SshStatusSegment.tsxsrc/renderer/src/components/status-bar/SshTargetStatusRow.tsxsrc/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.test.tsxsrc/renderer/src/components/terminal-pane/TerminalSshReconnectOverlay.tsxsrc/renderer/src/components/terminal-pane/pty-connection.tssrc/renderer/src/hooks/useIpcEvents.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/renderer/src/lib/new-workspace-ssh-gate.tssrc/renderer/src/ssh/ssh-connect-in-flight.test.tssrc/renderer/src/ssh/ssh-connect-in-flight.tssrc/renderer/src/ssh/ssh-connect-ui-timeout.test.tssrc/renderer/src/ssh/ssh-connect-ui-timeout.tssrc/renderer/src/ssh/ssh-connect-verb.test.tssrc/renderer/src/ssh/ssh-connect-verb.tssrc/renderer/src/ssh/ssh-connection-recoverability.test.tssrc/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
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.
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.ts—isConnectingSshStatus/canConnectSshStatusover two totalRecord<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 behinduseSyncExternalStore, so a connect started on one surface suppresses the duplicate affordance on every other one.ssh-connect-ui-timeout.ts—withUiConnectTimeout;ssh.connecthas no timeout of its own. Composer keeps its 20s budget; the reconnect surfaces passSSH_RECONNECT_UI_TIMEOUT_MS = 180_000, which must exceed main'sCREDENTIAL_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.tsxA 16px pill in the title row. Connected (or runtime-owned, i.e. status
null) renders a passiveServerglyph with a tooltip; a removed host renders a passive mutedServerOffand offers nothing; a reconnectable status renders the verb pill with a destructive tint.aria-disabled+ a handler guard while connecting (notdisabled, which impliespointer-events-noneand would drop clicks through to the card and kill the tooltip). Failure toasts and runs the STA-1468 metadata resync (listTargetsfirst, thenlistRemovedTargetLabels).Deleted —
SshDisconnectedDialog.tsx, itsvi.mockin 14 specs, and its 11-key locale namespace across all 5 locale files. This deliberately removes its window-capture Enter handler (SshDisconnectedDialog.tsx:118-140onmain) — 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
sshTargetRemovedguards onisRuntimeOwnedSshTargetId, matchingsshStatus.ssh:listTargetsfilters 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.connectedstatus outranks a stale removal tombstone in the branch order.onKeyDownstopsEnter/Spacepropagation, otherwiseWorktreeList's container key handler steals activation.setSshConnectionStatestore mirror is kept:ssh.connectcan 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). Fullsrc/renderersuite: 2101 files / 19472 tests pass.typecheck,check:max-lines-ratchet,oxlint,oxfmt --check, andsync:localization-catalogall 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-connectedhost, which is what sibling isolation looks like. This build runs the new card style, so these take the icon-only branch.disconnectedauth-failedServerOff. No destructive tint — nothing has failed yet.aria-label= "Reconnect SSH host openclaw — authentication failed".connectingaria-busy="true",aria-disabled="true"— still hoverable, keeps its tooltip. Both cards on the host spin from the one shared registry.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.
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
Unmeasured pre-merge checks (from the design doc)
Design doc and the model-comparison judgment that preceded it were kept out of the repo as process artifacts (
artifacts/, untracked).Made with Orca 🐋