Skip to content

fix(remote): restore offline workspaces and reconnect in place - #890

Merged
xintaofei merged 5 commits into
spacering-net:mainfrom
ker2xu:fix/remote-workspace-offline-recovery
Oct 9, 2026
Merged

xintaofei merged 5 commits into
spacering-net:mainfrom
ker2xu:fix/remote-workspace-offline-recovery

Conversation

@ker2xu

@ker2xu ker2xu commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Saved remote windows depended on server health during startup, so an outage could remove them from the next saved session. This restores their local shells while offline and allows connection recovery without remounting the workspace.

  • Restore saved remote workspace shells without an online health prerequisite.
  • Preserve explicit closes during restoration, including a close racing with the final focus update.
  • Show connection progress and disconnection accurately, with an in-place Reconnect now action that preserves the mounted workspace and unsent drafts during live reconnection.
  • Refresh failed initial reads after recovery, keep readiness waiters across repeated disconnects, clean up late listeners and remove stopped proxy tasks before resubscription.
  • Treat failed folder snapshots as unknown and reserve the expired-credentials flow for confirmed HTTP authentication failures.

Validation

  • Standalone frontend regressions: 75 tests passed across five files.
  • Standalone Rust regressions: 20 workspace tests and 2 proxy cleanup tests passed.
  • Frontend lint/static build and Rust clippy for all targets with test-utils passed.
  • Native macOS checks on the combined build covered offline relaunch, explicit-close persistence, expired credentials and in-place reconnection.
  • Combining this change with the separate Dock and local-entry changes reproduces the previously validated source tree exactly.
  • GitHub Test workflow concluded action_required with zero jobs executed; CI validation is pending.

Known issues

  • Native macOS validation found unresolved remote folder-draft loss on cold restart.
  • Both full desktop Rust runs during validation had 4,977 passed, 1 failed and 2 ignored. Failures occurred in GitLab/Gitea localhost HTTP tests; the cause remains unconfirmed.

Related work

Complements #840, which proposes automatic heartbeat, wake probes, continued retries and sleep/wake transcript recovery.

Related to #887, a broader draft combining workspace activation, restoration, heartbeats and continued retries. This change includes a manual retry fallback after the existing retry cutoff.

The separate Dock activation PR adds adjacent session methods and tests in workspace_windows.rs. Rebasing after either change merges needs a text-conflict resolution that retains both sets. The combined resolution was checked against the native-validated source tree.

The local-workspace entry is proposed separately in #891.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It changes cross-runtime restoration and connection lifecycle behavior while validation still reports an unresolved draft-loss case and an unexplained Rust test failure.

0 open findings

What changed in this PR

Restores remote desktop workspaces during outages and adds in-place connection recovery while preserving workspace state and drafts.

Changes:

  • Restores saved remote windows without health preflight.
  • Tracks connection state and supports manual reconnection.
  • Prevents failed folder reads from pruning offline drafts.
File Description
src/​stores/​app-workspace-store.ts Marks folders hydrated only after successful reads.
src/​stores/​app-workspace-store.test.ts Tests folder hydration failure and recovery.
src/​stores/​app-workspace-store-offline.test.tsx Tests offline draft preservation.
src/​lib/​transport/​types.ts Clarifies reconnect and authorization contracts.
src/​lib/​transport/​remote-desktop-transport.ts Adds connection state, recovery, and in-place retry.
src/​lib/​transport/​remote-desktop-transport.test.ts Covers transport lifecycle and recovery behavior.
src/​contexts/​remote-connection-gate.test.tsx Tests mounted-workspace recovery behavior.
src/​contexts/​remote-connection-context.tsx Integrates remote connection status handling.
src/​components/​connection/​remote-connection-status.tsx Adds reconnect status and action UI.
src/​components/​connection/​remote-connection-status.test.tsx Tests status transitions and cleanup.
src-tauri/​src/​commands/​workspace_windows.rs Restores offline windows and preserves explicit closes.
src-tauri/​src/​commands/​remote_workspace.rs Documents probe-free saved-window restoration.
src-tauri/​src/​commands/​remote_proxy.rs Removes stopped proxy tasks before retry notification.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

xintaofei and others added 3 commits October 9, 2026 08:45
The event socket proxy cannot tell a handshake the server refused with
HTTP 401 from a server that is down: every failure was a string, and after
three of them the task sent `__unauthorized__`. The frontend in this branch
reads that signal as "retries stopped", so a revoked or rotated token was
shown as "Connection lost / Reconnect now", and each retry failed three
more times without ever reaching the expired screen, unless some HTTP call
happened to run. PRs spacering-net#840 and spacering-net#887 also give `__unauthorized__` exactly the
meaning "refused token", which this frontend would have shown as retryable.

Classify the handshake instead. A 401 ends the task at once with
`__unauthorized__`, which reaches `onUnauthorized()` and the expired screen,
the same as a 401 on an HTTP call. The three-failure cutoff now sends its
own `__retries_exhausted__`, which keeps the workspace mounted and offers
the in-place retry. `onUnauthorized` goes back to meaning only a refused
token, so the gate no longer filters by source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Reconnect now" moves the transport to "reconnecting" synchronously, and
the status pill hides every "reconnecting" period for its 4 s grace. So the
click made the pill vanish, and against a server that still refuses, the
proxy gives up after about 3 s (fail, 1 s, fail, 2 s, fail): the pill came
back with the same button and no progress was ever shown.

A retry the user asked for now skips the grace, for as long as that retry
runs. The flag is armed only when the transport actually started a retry,
and cleared as soon as the retry ends, so a later outage still gets its
grace window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nothing

Four retry tests passed with `reconnectNow()` replaced by a no-op: the
synthetic `__ready__` they inject resolves the waiters and reattaches the
stream whether or not the retry resubscribed, and in the destroy case
`destroy()` itself consumed the pending unsubscribe the test meant for the
retry. Assert that the retry reached its commands: the failing one before
it gave up, a fresh subscribe before the ready, and the unsubscribe the
retry, not `destroy()`, is waiting on. All six retry tests now fail when
`reconnectNow()` is a no-op.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xintaofei

Copy link
Copy Markdown
Collaborator

codeg work task 291 is done — #890 (8 files, +247/-60).

@xintaofei
xintaofei merged commit 9e59216 into spacering-net:main Oct 9, 2026
7 checks passed
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.

3 participants