Hide offline machines from the Work sidebar - #941
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Warning Review limit reached
Next review available in: 45 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (10)
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 |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d881c540eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return useMemo(() => { | ||
| const foreignRows = rows | ||
| // Offline machines are dropped here, once, for everything the sidebar sees. | ||
| const reachableRows = selectReachableCrossMachineRows(rows); |
There was a problem hiding this comment.
Hide the active remote binding after it goes offline
When the current project tab is itself bound to the machine that disconnects, this filter does not remove that machine's sidebar rows: buildCrossMachineLaneRows hard-codes every active-binding lane as online: true, and SessionListPane renders the active store's lanes and sessions directly rather than from foreignRows. As a result, after the six-second grace period, an offline active remote project still shows all of its cached, unusable Work rows, contrary to the new behavior; reachability must also gate the active remote binding's local-list inputs.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Verified against the code — this is a pre-existing gap owned by a different mechanism, not something this PR introduced or should fix here.
The factual claims are right: buildCrossMachineLaneRows hard-codes online: true for active-binding rows, and SessionListPane renders the bound machine's lanes from the lanes prop, not foreignRows. But that was equally true before this PR: the old dimmed={!row.online} and the "${machineName} is offline" disabledReason both lived inside the visibleForeignRows.map(...) block and were gated on options?.foreignRow, which is only ever passed for foreign rows. The bound machine has never had offline treatment in the Work sidebar, dimmed or hidden.
"My tab's machine dropped" already has an owner: AppShell renders a Reconnecting/Disconnected notice with the transport error and a Retry now button (AppShell.tsx:1167-1211), and the TopBar tab gets a red border plus a (Disconnected) title suffix. The design is deliberately keep-the-project-open-and-retry — evictProjectDataCaches/closeProject are reachable only from the user-initiated Disconnect flow in TopBar.tsx:1803.
Routing the bound machine through the 6s reachability grace would be the wrong gate and worse UX. It debounces union membership for machines you are only observing; the bound machine is the tab's execution context, so a second 6s-latency owner would race the retry loop and the preserved workViewByProject restore path. And hiding those rows empties the primary list, which falls through to the generic "No sessions — Start a new session above" empty state (SessionListPane.tsx:2166) — a lie, with no reconnect affordance, while the only explanation sits in a corner toast.
If it's worth doing, the right shape is an inline Work-sidebar disconnected state (banner + retry) driven by the tab binding's connection state — separate, deliberate work rather than widening selectReachableCrossMachineRows.
The stale disabledBusy doc comment this surfaced (SessionCard.tsx:442, still said "offline rows") is fixed.
| const generation = runtime.generation; | ||
| void remoteRuntime.getConnectionSnapshot?.() | ||
| .then((snapshot) => { | ||
| if (generation !== runtime.generation) return; |
There was a problem hiding this comment.
Preserve the initial snapshot across refresh generations
If the initial getConnectionSnapshot() IPC call takes longer than the 400 ms refresh coalescing window, the already-scheduled runRefresh() increments runtime.generation, causing this guard to discard the valid snapshot even though the sync is still mounted and in the same scope. With runtime.connections left empty, subsequent five-second refreshes cannot discover or load any foreign machines, and the snapshot-change subscription need not emit while connections remain stable; use a teardown/scope-specific token rather than the refresh generation for this check.
Useful? React with 👍 / 👎.
`attach()` captured `runtime.generation` to discard a first `getConnectionSnapshot()` that resolved after teardown. Wrong token: every `runRefresh` bumps `generation` too, and one is scheduled 400ms after attach. A snapshot slower than that coalescing window was therefore dropped while the sync was still mounted in the same scope — leaving `runtime.connections` empty, so no foreign machine was ever discovered and reachability never updated. The snapshot subscription need not fire again while connections are stable, so the sidebar could sit permanently empty. Adds a `lifecycle` counter bumped only by teardown and scope change, which is what the guard actually meant. Covered by a test that asserts the machine is still hidden after the grace window when the first snapshot lands late — it fails against the generation guard. Also corrects a `disabledBusy` doc comment that still described offline rows. Reported by Codex (P2) on #941. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bdd6d13047
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Offline machines are dropped here, once, for everything the sidebar sees. | ||
| const reachableRows = selectReachableCrossMachineRows(rows); | ||
| const foreignRows = reachableRows | ||
| .filter((row) => !row.isActiveBinding) |
There was a problem hiding this comment.
Hide the active remote binding when it disconnects
When the current tab is bound to a remote machine that remains disconnected beyond the grace period, this filtering still cannot remove that machine's primary Work rows. buildCrossMachineLaneRows hard-codes active-binding rows as online, while the active lanes and sessions are rendered through the primary lists rather than foreignRows; the remote project also intentionally stays open while reconnecting, so its cached chats remain visible and actionable. Reachability therefore needs to gate the active binding's primary rows as well as this foreign union.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Same finding as the earlier thread on this file — see #941 (comment) for the full trace. Short version: the bound machine has never had offline treatment in the Work sidebar (the old dimming and is offline overlay were both gated on options?.foreignRow), so this is neither introduced nor regressed here. That case is owned by the AppShell Reconnecting/Disconnected notice with its Retry button, and routing it through the 6s union grace would empty the primary list into a "No sessions — Start a new session above" state that is both wrong and unactionable. Deliberately out of scope.
| // Reachability is not recomputed here: `targets` is the read list, and the | ||
| // connection snapshot is the single source of truth for what is visible. |
There was a problem hiding this comment.
Reapply reachability after a late machine read
If a refresh starts while a machine is connected, the machine disconnects, and its pending callAction read resolves after the six-second grace window, applyReachability first marks the retained slice offline but readMachine subsequently merges it with online: true. Because reachability is no longer recomputed here, the queued refresh has no connected target to read and leaves that stale online flag in place indefinitely unless another connection snapshot happens to arrive, putting the offline rows back in the sidebar.
Useful? React with 👍 / 👎.
| runtime.generation += 1; | ||
| runtime.lifecycle += 1; |
There was a problem hiding this comment.
Preserve the initial snapshot across overlapping scope changes
When ref-counted consumers overlap during a project-scope transition before the initial getConnectionSnapshot() resolves, incrementing lifecycle here invalidates the only pending snapshot. Since the existing subscription keeps refCount above one, attach() is not called again for the new scope, so no replacement snapshot is requested; with runtime.connections still empty, every periodic refresh discovers zero foreign machines until an unrelated connection-change event occurs.
Useful? React with 👍 / 👎.
Two follow-ups from Codex on #941, both real. A lane/session read that was already in flight when its machine dropped merged back with `online: true`. Because reachability is now only recomputed from the connection snapshot, nothing hid the machine again until an unrelated snapshot happened to fire — so a machine the grace window had already removed could reappear and stay. The read now confirms reachability but never asserts it: `online` is set only when the machine is still connected as of the newest snapshot, and otherwise omitted so the snapshot path's verdict stands. A scope change also bumped `lifecycle`, discarding an in-flight first `getConnectionSnapshot()`. Connections are machine-global, so a scope change does not make that snapshot stale — and because ref-counted consumers overlap across a project-tab transition, `refCount` never reaches zero and no second read is issued. Dropping it left `runtime.connections` empty for good. Scope changes now only reset the grace deadlines, which genuinely are scope-bound. Both are covered by tests that fail against the previous behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a51c510a7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // Reachability is not recomputed here: `targets` is the read list, and the | ||
| // connection snapshot is the single source of truth for what is visible. |
There was a problem hiding this comment.
Exclude connected machines that no longer host the repository
When a retained machine reconnects after this repository has been removed or changed on that machine, the connection snapshot still marks the target connected, so applyReachability restores its stale slice to online: true. However, the targets predicate immediately above excludes that machine because repoMatch !== "matched", and this change no longer updates visibility from targets; consequently its old lanes and chats remain visible indefinitely without ever being refreshed, and their actions use a stale project binding. Keep repository eligibility separate from connection grace, and hide or remove retained slices that are absent from the current matched target set.
Useful? React with 👍 / 👎.
An offline machine used to keep every lane and chat it had last reported, dimmed, with an "X IS OFFLINE" overlay on each card. On a multi-machine setup that filled the Work list with rows that looked live but could not be acted on — every action against them failed. Offline machines now leave the sidebar entirely: their lanes, chats, and machine markers are hidden, and their branches stop counting toward "same branch elsewhere" on lanes that ARE reachable. The filter lives in one place, `selectReachableCrossMachineRows` inside `useCrossMachineLaneUnion`, so nothing downstream can reintroduce them — `CrossMachineUnion` no longer exposes an unfiltered `rows` escape hatch. Removal is debounced by a 6s reconnect grace window. `connect()` publishes `connecting` before every automatic redial and a single failed liveness ping publishes `error`, so hiding on the first non-connected snapshot would make a websocket blip or a sleep/wake yank a machine's whole lane group out of the list and animate it back a second later. Dimming absorbed that; removal does not. Reconnects apply instantly; drops only count once they persist, measured from the first drop rather than the latest snapshot. Reachability derives from connection state alone — `runRefresh`'s narrower target list decides what to read, not what is visible. The store slice is still retained on disconnect: the push-divergence guard needs a dropped machine's last-known branch state, and an open chat on a machine that drops keeps its tab and selection. Also removes the offline plumbing that is now unreachable in the foreign lane context menu. Its captured `online` flag was a right-click-time snapshot, so it read "online" in the one case it appeared to cover — the machine dropping while the menu is open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`test-push-relay` started failing on every PR at 08:00:05Z today, on code identical to main. The Attention fixture hardcoded `expiresAt: "2026-07-29T08:00:05.000Z"`, and the snapshot query filters `expires_at > now` against the REAL clock — so the moment that literal lapsed, every snapshot came back empty and the suite went red on a wall-clock boundary rather than on a code change. Anchors the whole class to `Date.now()`: the item expiry fixture, the device lease default, and the expired/active pair in the prune test (whose "active" row was set to lapse at 12:00Z today, three hours behind the first). Rows that are meant to be lapsed now use an explicitly past anchor instead of a date that happens to be in the past. Inert literals — `occurredAt`, `updatedAt`, and explicit `now:` arguments — are left alone; they are inputs, not filters, and fixed values keep those tests deterministic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`attach()` captured `runtime.generation` to discard a first `getConnectionSnapshot()` that resolved after teardown. Wrong token: every `runRefresh` bumps `generation` too, and one is scheduled 400ms after attach. A snapshot slower than that coalescing window was therefore dropped while the sync was still mounted in the same scope — leaving `runtime.connections` empty, so no foreign machine was ever discovered and reachability never updated. The snapshot subscription need not fire again while connections are stable, so the sidebar could sit permanently empty. Adds a `lifecycle` counter bumped only by teardown and scope change, which is what the guard actually meant. Covered by a test that asserts the machine is still hidden after the grace window when the first snapshot lands late — it fails against the generation guard. Also corrects a `disabledBusy` doc comment that still described offline rows. Reported by Codex (P2) on #941. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two follow-ups from Codex on #941, both real. A lane/session read that was already in flight when its machine dropped merged back with `online: true`. Because reachability is now only recomputed from the connection snapshot, nothing hid the machine again until an unrelated snapshot happened to fire — so a machine the grace window had already removed could reappear and stay. The read now confirms reachability but never asserts it: `online` is set only when the machine is still connected as of the newest snapshot, and otherwise omitted so the snapshot path's verdict stands. A scope change also bumped `lifecycle`, discarding an in-flight first `getConnectionSnapshot()`. Connections are machine-global, so a scope change does not make that snapshot stale — and because ref-counted consumers overlap across a project-tab transition, `refCount` never reaches zero and no second read is issued. Dropping it left `runtime.connections` empty for good. Scope changes now only reset the grace deadlines, which genuinely are scope-bound. Both are covered by tests that fail against the previous behavior. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A machine that reconnects after this repository has been removed or renamed on it stayed visible forever: the connection snapshot still reported it connected, so reachability kept it online, while `runRefresh`'s narrower target predicate dropped it, so its rows were never refreshed again. Permanently visible, permanently stale — the exact state this PR set out to remove. Both now come from `resolveEligibleMachines()`: connected AND still hosting this repository, excluding This Mac and the tab's own binding. Having reachability and the read list share one definition is what makes "if it can't be refreshed, it isn't shown" true by construction rather than by coincidence. Reported by Codex (P2) on #941. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8a51c51 to
298341a
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. 🎉 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
A machine used to leave the Work sidebar entirely on any connection blip. The reconnect grace window was 6s while a single connect candidate is allowed 10s and candidates are dialed in sequence, so every wifi hiccup and every sleep/wake yanked a machine's whole lane and chat group out of the list and animated it back a moment later. Users read that as "my machines disappear". Presence is now three verdicts instead of two, decided in one place: - LIVE: connected and still hosting this repository. - DIMMED: its lanes and chats stay on screen, collapsed and inert, with the offline form of the machine marker naming it and every card reading "<machine> is offline". A drop earns this only once a reconnect attempt has run to completion and failed - `connecting` observed while dropped, then a non-connected state - plus a 45s floor, with a 120s ceiling for a dial that never finishes and an immediate verdict for an `idle` target that will not redial at all. `lastAttemptedAt` cannot answer this on its own: a failed RPC over an established connection stamps it too, and that is the event most drops start with. - FORGOTTEN: the only case that deletes rows. A target gone from the connection snapshot, a connected machine that positively reports the repository missing (the #941 fix, preserved), or 24 hours unreachable. A machine we ARE connected to but cannot re-prove the repository on keeps its last verdict. Absence of proof is not proof of absence: a project list that has not caught up after a reconnect must not read as "the repo is gone". The foreign-lane context menu's machine-bound actions are now disabled from live store state rather than a flag captured at right-click time, which was a lie in the one case it looked like it covered. The same file also owned an undisclosed poller. Every ~5.4s, per connected foreign machine, it fired `lane.list` with `includeStatus` (a git status and a worktree probe per lane, plus a state-snapshot row written per lane, on the other machine) alongside `session.list` - and only the Work tab being selected gated it, not whether the window was visible at all. It now stops entirely while the window is hidden and refreshes once on the way back; chats are re-read every 10s and lanes on their own 30s cadence, with an immediate lane read when a chat names a lane that machine has never reported. For one connected foreign machine that is 22.2 to 7.7 calls/min visible, and to zero hidden. Also fixes a latent wedge found while testing: a refresh outlives its own runtime, and bookkeeping from a torn-down run left `refreshInFlight` set for whoever mounted next, which then scheduled nothing at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ound load (#947) * fix(work): dim machines that drop instead of vanishing them A machine used to leave the Work sidebar entirely on any connection blip. The reconnect grace window was 6s while a single connect candidate is allowed 10s and candidates are dialed in sequence, so every wifi hiccup and every sleep/wake yanked a machine's whole lane and chat group out of the list and animated it back a moment later. Users read that as "my machines disappear". Presence is now three verdicts instead of two, decided in one place: - LIVE: connected and still hosting this repository. - DIMMED: its lanes and chats stay on screen, collapsed and inert, with the offline form of the machine marker naming it and every card reading "<machine> is offline". A drop earns this only once a reconnect attempt has run to completion and failed - `connecting` observed while dropped, then a non-connected state - plus a 45s floor, with a 120s ceiling for a dial that never finishes and an immediate verdict for an `idle` target that will not redial at all. `lastAttemptedAt` cannot answer this on its own: a failed RPC over an established connection stamps it too, and that is the event most drops start with. - FORGOTTEN: the only case that deletes rows. A target gone from the connection snapshot, a connected machine that positively reports the repository missing (the #941 fix, preserved), or 24 hours unreachable. A machine we ARE connected to but cannot re-prove the repository on keeps its last verdict. Absence of proof is not proof of absence: a project list that has not caught up after a reconnect must not read as "the repo is gone". The foreign-lane context menu's machine-bound actions are now disabled from live store state rather than a flag captured at right-click time, which was a lie in the one case it looked like it covered. The same file also owned an undisclosed poller. Every ~5.4s, per connected foreign machine, it fired `lane.list` with `includeStatus` (a git status and a worktree probe per lane, plus a state-snapshot row written per lane, on the other machine) alongside `session.list` - and only the Work tab being selected gated it, not whether the window was visible at all. It now stops entirely while the window is hidden and refreshes once on the way back; chats are re-read every 10s and lanes on their own 30s cadence, with an immediate lane read when a chat names a lane that machine has never reported. For one connected foreign machine that is 22.2 to 7.7 calls/min visible, and to zero hidden. Also fixes a latent wedge found while testing: a refresh outlives its own runtime, and bookkeeping from a torn-down run left `refreshInFlight` set for whoever mounted next, which then scheduled nothing at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(attention): cut idle Attention cadence and make a hung refresh recover Attention kept three background pollers running regardless of whether anything was on screen, and one RPC path could pin the UI on "syncing" for ten minutes. - `callAttention` inherited the runtime client's 10-minute default while every other sync-domain call uses 30s. It now passes the sync-domain budget, and `callSync` takes the timeout as an option so no other caller changes. - `refreshAttentionSnapshot` deduped on a module-level promise that a wedged call never settled, so every later refresh returned the same dead promise and the UI never left "syncing". A 45s renderer backstop - deliberately above the 30s main-process budget, so real host errors still win - now lands in the existing degraded/retry path and clears the dedupe. - The notch helper polled every 15s from the moment it spawned, including with the screen locked or asleep. It now reconciles its cadence: 15s only while it has an anchored surface and the screen is awake, 60s otherwise, driven by `powerMonitor` lock/suspend in main. - The presence POST ran at a fixed 30s. Hidden windows now report at 120s and send immediately on the way back; `blur` still reports the foreground change at once, so nothing is learned later than before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(work): close presence and lane-cadence gaps found in review Six real defects, four of them in the new code: - A remount re-brightened a machine that was already dimmed. Leaving Work and coming back tears the shared runtime down, taking its drop records with it while the store slice survives, so the next snapshot derived a fresh drop and held the machine live for another floor — group re-expanded, actions re-enabled. The verdict now survives: a dimmed machine is only ever re-brightened by becoming eligible again, and its retention deadline is re-anchored to its last successful read. - The catch-up lane read fired every tick instead of once. `session.list` does not filter on lane status while `lane.list` asks for `includeArchived: false`, so a chat on an archived lane is permanently unresolvable and demanded a fresh `includeStatus` read forever — more expensive than before the cadence existed. Lane ids a completed read did not explain are now remembered until the next one, and both read paths share the helper that owns the rule. - Removal on "the repository is gone" believed a folder-name mismatch. The scope's origin is re-resolved from the bound machine and can be transiently null, and `repoMatchFor` will say "missing" off a name alone — so a healthy machine's rows could be deleted while the bound machine blipped. Removal now requires an origin to prove it by. - A connected machine whose repository could not be re-proven stayed bright and was never read again: eligible for display, ineligible for refresh. It now dims on the same floor. It is still not removed — absence of proof is not proof of absence — but it stops claiming to be live. - The lane cadence could be stamped by a read that resolved after a scope change, suppressing the new scope's first lane read. - The Attention backstop was below the budget it was meant to clear: 15s relay request plus one 401 retry plus the 30s local fallback is 60s, so a 45s race could discard a slow-but-successful snapshot. Raised to 75s. Structural, from the same review: the hold/dim rule is one deadline instead of a double negative plus a matching ternary; `MachineConnectivity` carries the machine option rather than a nullable copy of one field, and the eligible list is derived from it instead of re-deriving; the machine marker moved out of the 2.3k-line session list; the offline-has-no-live-work rule is one predicate instead of two inverted copies. `resume` no longer claims the screen is awake while it is still locked, and a respawned notch helper no longer inherits the previous child's surface. The presence-cadence test now advances the clock instead of asserting that a timer was scheduled, so it would catch a chain that fires once and never re-arms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: describe the presence, cadence, and Attention rules as they now stand Commit 1's doc pass predated the review fixes, so it still said a connected machine that cannot re-prove the repository keeps its last verdict, and still claimed the cross-machine union never polls. Corrects both, and documents what had no coverage at all: the notch helper's surface- and screen-driven cadence, the presence POST's visible/hidden split, and the two bounds on an Attention snapshot read. Also adds a regression test for the one contract from the review round that had none: a catch-up lane read that resolves after a scope change must not stamp the cadence, or the new scope goes a full cadence with no lane list and therefore no rows at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The problem
An offline machine kept every lane and chat it had last reported, dimmed, with an
X IS OFFLINEoverlay on each card. On a multi-machine setup that filled the Work list with rows that looked live but could not be acted on — every action against them failed.What changed
Offline machines now leave the Work sidebar entirely: their lanes, chats, and machine markers are hidden, and their branches stop counting toward "same branch elsewhere" on lanes that are reachable.
The filter lives in exactly one place —
selectReachableCrossMachineRows, insideuseCrossMachineLaneUnion— so nothing downstream can reintroduce them.CrossMachineUnionno longer exposes an unfilteredrowsescape hatch, andCrossMachineLaneMarkerno longer has anonlinefield (there is no offline form of the marker any more).Reconnect grace window
Removal is debounced by 6s.
connect()publishesconnectingbefore every automatic redial, and a single failed 30s liveness ping publisheserror— so hiding on the first non-connected snapshot would make a websocket blip or a sleep/wake yank a machine's whole lane group out of the list and animate it back a second later. Dimming absorbed that; removal does not.runRefresh's narrower target list decides what to read, not what is visible.What is deliberately unchanged
AttentionCenterstill shows its "last-known state" offline banner. Different surface, explanatory by design, and out of scope here.Cleanup
Removed the offline plumbing in the foreign lane context menu, now unreachable. Its captured
onlineflag was a right-click-time snapshot, so it read "online" in the one case it appeared to cover — the machine dropping while the menu is open.Testing
SessionListPane.test.tsx— an offline machine's lane group, chats, session card, marker, machine name, and "is offline" copy are all absentcrossMachineLanes.test.ts— an invisible offline machine must not tip a reachable lane into "same branch elsewhere"; the store slice is retained while hidden; two grace-window tests (blip keeps rows, persisted drop hides at 6s from the first drop, second drop gets a full window)renderer/components/terminals+renderer/state; CI-mirrored shard 1/8 green (1298 tests)Docs updated:
ARCHITECTURE.md,features/terminals-and-sessions/{README,ui-surfaces}.md,features/remote-runtime/README.md.No CLI, TUI, iOS, or analytics surface is affected — these types are renderer-local and
apps/desktop/src/shared/**is untouched.🤖 Generated with Claude Code