Skip to content

Hide offline machines from the Work sidebar - #941

Merged
arul28 merged 5 commits into
mainfrom
ade/start-skill-attacched-screenshot-shows-eec1a1c0
Jul 29, 2026
Merged

Hide offline machines from the Work sidebar#941
arul28 merged 5 commits into
mainfrom
ade/start-skill-attacched-screenshot-shows-eec1a1c0

Conversation

@arul28

@arul28 arul28 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

The problem

An offline machine kept 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.

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, inside useCrossMachineLaneUnion — so nothing downstream can reintroduce them. CrossMachineUnion no longer exposes an unfiltered rows escape hatch, and CrossMachineLaneMarker no longer has an online field (there is no offline form of the marker any more).

Reconnect grace window

Removal is debounced by 6s. connect() publishes connecting before every automatic redial, and a single failed 30s 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.
  • The window is measured from the first drop, not the latest snapshot.
  • Reachability derives from connection state alone — runRefresh's narrower target list decides what to read, not what is visible.

What is deliberately unchanged

  • 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, selection, and lane.
  • AttentionCenter still 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 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.

Testing

  • SessionListPane.test.tsx — an offline machine's lane group, chats, session card, marker, machine name, and "is offline" copy are all absent
  • crossMachineLanes.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)
  • typecheck clean; 603 tests across 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

@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 29, 2026 9:30am

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@arul28, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 45 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b3d6b68b-795b-433f-8b15-d618a2a029af

📥 Commits

Reviewing files that changed from the base of the PR and between 2bc8eb1 and 298341a.

⛔ Files ignored due to path filters (4)
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/remote-runtime/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/README.md is excluded by !docs/**
  • docs/features/terminals-and-sessions/ui-surfaces.md is excluded by !docs/**
📒 Files selected for processing (10)
  • apps/desktop/src/renderer/components/terminals/ForeignLaneContextMenu.tsx
  • apps/desktop/src/renderer/components/terminals/SessionCard.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.test.tsx
  • apps/desktop/src/renderer/components/terminals/SessionListPane.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.test.tsx
  • apps/desktop/src/renderer/components/terminals/useWorkLaneContextMenu.tsx
  • apps/desktop/src/renderer/state/appStore.ts
  • apps/desktop/src/renderer/state/crossMachineLanes.test.ts
  • apps/desktop/src/renderer/state/crossMachineLanes.ts
  • apps/push-relay/test/attention.test.ts

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.

@arul28 arul28 changed the title hide-offline-machine-sessions -> main Hide offline machines from the Work sidebar Jul 29, 2026
@arul28

arul28 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

arul28 added a commit that referenced this pull request Jul 29, 2026
`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>
@arul28

arul28 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 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".

Comment on lines +1215 to 1218
// Offline machines are dropped here, once, for everything the sidebar sees.
const reachableRows = selectReachableCrossMachineRows(rows);
const foreignRows = reachableRows
.filter((row) => !row.isActiveBinding)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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.

Comment on lines +845 to +846
// Reachability is not recomputed here: `targets` is the read list, and the
// connection snapshot is the single source of truth for what is visible.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +1065 to +1066
runtime.generation += 1;
runtime.lifecycle += 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

arul28 added a commit that referenced this pull request Jul 29, 2026
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>
@arul28

arul28 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 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".

Comment on lines +858 to +859
// Reachability is not recomputed here: `targets` is the read list, and the
// connection snapshot is the single source of truth for what is visible.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

arul28 and others added 5 commits July 29, 2026 05:25
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>
@arul28
arul28 force-pushed the ade/start-skill-attacched-screenshot-shows-eec1a1c0 branch from 8a51c51 to 298341a Compare July 29, 2026 09:30
@arul28

arul28 commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 298341a07b

ℹ️ 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".

@arul28
arul28 merged commit 381bb65 into main Jul 29, 2026
3 checks passed
@arul28
arul28 deleted the ade/start-skill-attacched-screenshot-shows-eec1a1c0 branch July 29, 2026 09:43
arul28 added a commit that referenced this pull request Jul 29, 2026
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>
arul28 added a commit that referenced this pull request Jul 29, 2026
…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>
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