Commit b040213
fix(frontend): make disconnect indicator clickable to reconnect (#9410)
## 📝 Summary
Closes #9385
**This pull request was primarily authored by a coding agent.**
### Problem
When marimo is accessed over an SSH-forwarded tunnel (or any flaky
link), the kernel WebSocket drops on tunnel reset or laptop sleep. The
browser shows the broken-link indicator in the top-left, and the only
recovery is a full page reload — disruptive because it resets scroll
position, cell focus, and any unsaved client state, despite the kernel
surviving the WS drop in edit mode and the server already supporting
session resume on a fresh connection
(`marimo/_server/api/endpoints/ws/ws_session_connector.py`).
The frontend was forcing the reload because of two compounding issues:
1. **One-shot reconnect gate**:
`useMarimoKernelConnection.shouldTryReconnecting` only resets on a
successful `onOpen`. After partysocket's 10-retry budget is exhausted,
the gate stays closed and `ws.reconnect()` is never called again from
our code.
2. **Upstream partysocket bug**: in `partysocket@1.1.10`, `_connectLock`
leaks in the `_connect()` max-retries early-return — even if the gate
were reset, `ws.reconnect()` would silently no-op because the lock is
permanently held. Fixed upstream in
[partykit#322](cloudflare/partykit#322),
released as partysocket 1.1.13.
3. **Stuck spinner**: the `onClose` default branch always sets state to
`CONNECTING`, even after partysocket has exhausted its retry budget —
the spinner outlives the actual retry loop with no escape.
The broken-link icon also gives the visual impression of being clickable
but is a no-op `<div>` today.
### What changed
This is two commits — a dependency bump (precondition) followed by the
frontend fix.
#### `chore(frontend): bump partysocket 1.1.10 → 1.1.13`
Patch-version bump pulling in the upstream `_connectLock` fix. No
API-surface changes — verified that `retryCount`, `reconnect()`, and
`maxRetries` signatures are identical between 1.1.10 and 1.1.13. The
lockfile diff is exactly 6 lines, all about partysocket.
#### `fix(frontend): make disconnect indicator clickable to reconnect`
- **Disconnect indicator becomes a real `<button>`** (`status.tsx`).
Native button gives Enter/Space activation and accessibility; previously
it was a `<div>` with a tooltip. When the connection is in a recoverable
CLOSED state, clicking calls a new `reconnect()` exposed by
`useMarimoKernelConnection`. When the connection is `CLOSED` for any
other reason, the button is `disabled`.
- **`reconnect()` probes the runtime first** via the existing
`runtimeManager.isHealthy()` (`/health`). If the server is genuinely
unreachable (e.g. the SSH tunnel is dead), we transition straight back
to `CLOSED + KERNEL_DISCONNECTED` in ~1s instead of burning
partysocket's full retry budget on the silent click. The probe also
resets the one-shot reconnect gate so subsequent automatic retries work.
- **Surface `CLOSED` when partysocket has given up.** In the `onClose`
default branch, when `ws.retryCount >= MAX_RETRIES`, transition to
`CLOSED + KERNEL_DISCONNECTED` instead of `CONNECTING`. This makes the
disconnect icon reappear and become clickable again, so the user can
manually retry without reloading. Without this, the spinner stayed up
forever even after partysocket had stopped trying.
- **Refactor**: the inline switch on `event.reason` in `onClose` is
extracted into a pure `classifyCloseEvent` helper (`close-handler.ts`)
so the regression behavior can be unit-tested. The hook is now thin
glue: classify → setConnection → optionally close transport or call
`tryReconnecting`.
- **Tests**: `close-handler.test.ts` (15 cases) covers each terminal
reason plus the retry-budget-exhaustion path. `status.test.tsx` (3
cases) covers the click → callback wiring and the disabled state.
### Heads-up for review
- **`runtimeManager.isHealthy()` has a side effect** (`setDOMBaseUri()`
on success). It was previously only triggered during initial startup;
now it can fire on each manual reconnect. Almost certainly a no-op in
practice (URL hasn't changed) but flagging in case you want it gated.
- **`IConnectionTransport.retryCount` was added** to the transport
interface so the close handler can detect partysocket give-up. This
leaks a partysocket-specific concept into a transport-agnostic
interface; an alternative would be to have the transport emit a
synthetic close event with a "gave-up" reason. Open to that direction if
preferred — current approach was chosen for minimal diff.
- **No automatic-behavior changes** for users on a healthy connection.
The only paths that exercise the new code are user-initiated clicks and
the previously-stuck post-give-up case.
- **No new user-facing strings.** `"kernel not found"` / tooltip text
reuse existing copy; only new string is the tooltip suffix `"— click to
reconnect"`.
## 📋 Pre-Review Checklist
<!-- These checks need to be completed before a PR is reviewed -->
- [ ] For large changes, or changes that affect the public API: this
change was discussed or approved through an issue, on
[Discord](https://marimo.io/discord?ref=pr), or the community
[discussions](https://github.com/marimo-team/marimo/discussions) (Please
provide a link if applicable).
- [x] Any AI generated code has been reviewed line-by-line by the human
PR author, who stands by it.
- [x] Video or media evidence is provided for any visual changes
(optional). <!-- PR is more likely to be merged if evidence is provided
for changes made -->
## ✅ Merge Checklist
- [x] I have read the [contributor
guidelines](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md).
- [x] Documentation has been updated where applicable, including
docstrings for API changes.
- [x] Tests have been added for the changes made.
I have read the CLA Document and I hereby sign the CLA
---------
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>1 parent 3e688b3 commit b040213
13 files changed
Lines changed: 599 additions & 76 deletions
File tree
- frontend
- src
- components/editor
- header
- __tests__
- core
- websocket
- __tests__
- transports
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
123 | | - | |
| 123 | + | |
124 | 124 | | |
125 | 125 | | |
126 | 126 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| 26 | + | |
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
31 | | - | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
32 | 38 | | |
33 | 39 | | |
34 | 40 | | |
| |||
Lines changed: 108 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
11 | 15 | | |
12 | 16 | | |
13 | 17 | | |
14 | 18 | | |
15 | 19 | | |
16 | | - | |
| 20 | + | |
| 21 | + | |
17 | 22 | | |
18 | 23 | | |
19 | 24 | | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
20 | 30 | | |
21 | 31 | | |
22 | 32 | | |
| |||
28 | 38 | | |
29 | 39 | | |
30 | 40 | | |
31 | | - | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
32 | 46 | | |
33 | 47 | | |
34 | 48 | | |
| |||
37 | 51 | | |
38 | 52 | | |
39 | 53 | | |
40 | | - | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
47 | 81 | | |
48 | 82 | | |
49 | 83 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
79 | 79 | | |
80 | 80 | | |
81 | 81 | | |
82 | | - | |
| 82 | + | |
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
| 150 | + | |
150 | 151 | | |
151 | 152 | | |
152 | 153 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| |||
84 | 84 | | |
85 | 85 | | |
86 | 86 | | |
| 87 | + | |
87 | 88 | | |
88 | 89 | | |
89 | 90 | | |
| |||
0 commit comments