Skip to content

feat(sidebar): main keeps one live set of paired-server rows, checked against the window in dev and test - #30027

Merged
Jinwoo-H merged 3 commits into
mainfrom
jinwoo/sidebar-server-rows
Oct 17, 2026
Merged

Jinwoo-H merged 3 commits into
mainfrom
jinwoo/sidebar-server-rows

Conversation

@Jinwoo-H

@Jinwoo-H Jinwoo-H commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​621 $\color{#cf222e}{\Huge{\mathbf{−}}}$​7 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​614
Prod 20 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​829 $\color{#cf222e}{\Huge{\mathbf{−}}}$​184 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​645

ELI5

The phone and desktop workspace lists should come from one source: Orca's background process ("main"). For workspaces on a paired Orca server, main used to fetch the server's list only when a phone asked, and the desktop window fetched its own copy separately. Now main keeps one live copy of every paired server's workspace rows by itself, even with no window open. The phone reads that copy. In dev and test builds the window checks main's copy against its own and counts any disagreement. Nothing visible changes.

What Changed

No visible change. The phone gets the same rows as before.

  • One live set of server rows in main (src/main/runtime/sidebar-inputs/paired-server-rows-producer.ts).
    • Main opens its own change-event stream to each server it is in contact with. This does not depend on the window.
    • It refetches a server's rows when the server comes into contact, whenever the server announces a workspace or project change, and after a reconnect that may have missed events. It also refetches when a phone asks, and every 30 s while a window shows the sidebar (the fallback the owner approved).
    • If a change arrives during a fetch, one more fetch runs afterwards, so no change is missed.
    • A fetch that answers replaces the rows, so deleted workspaces disappear.
    • A server that loses contact keeps its last rows, marked unverifiable, never treated as gone. Only removing or re-pairing the server drops them. A late answer from before a re-pair is discarded.
  • The phone catalog keeps no copy of its own (mobile-relay-host-catalog.ts). It reads main's set, refreshing first as it did before.
  • Ids include the host. Rows are keyed runtime:<server>|<workspace id>, so a local workspace and a server workspace with the same id can't collide. The activity comparison from feat(sidebar): main produces per-workspace activity facts, checked against the window in dev and test #29980 now also leaves out an id that both a local and a server workspace use, because the window keys activity by bare id and can't tell which host it belongs to.
  • Server header facts. Main also publishes each server's label, health, compatibility, and the desktop's own device id on that server (used by "Hide other devices"). One shared function, describePairedServerHosts, computes them for the phone catalog, main's set, and the window's comparison.
  • Read path: sidebarInputs.pairedServerRows, a local, host-owned method.
  • Shadow comparison (dev and test only): two new domains, paired-server-rows and paired-server-hosts.
    • Rows are compared only for servers main currently reaches, and only for projects the window has already listed. The window lists a new project later than main does, and that lag is not a disagreement.
    • The shadow mechanism's readWindow now receives main's read, so a domain can limit the window's side to what main covers.

Older servers. Main still uses worktree.ps and runtime.clientEvents.subscribe, which every server already supports. Missing optional fields get defaults (for example, a missing workspaceKind reads as a regular git workspace), and an unknown workspace kind falls back the same way instead of being rejected. A server that sends no change events is still covered by the 30 s fallback and by phone reads.

Why

This is step 5d of the sidebar plan. It settles the first must-fix in the design review, "One normalizer is not one live input source": one aggregate in main, initial population, subscriptions that work without a window, deletions removed, and loss of contact reported as unverifiable.

Alternatives considered:

  • Keep fetching on demand and add a cache in front. Rejected: the rows would still age differently for the phone and the desktop, which is the problem this fixes.
  • Reuse the window's subscriptions (runtime-environment-subscription-handlers.ts). Rejected: they are tied to the renderer's lifetime, and a desktop without a window still has to serve its phone.
  • Gate change-event fetches on the list being shown, too. Tried and reverted: a desktop with no window would never fill in or refresh its rows until a phone asked. Only the 30 s fallback is gated on the list being shown.

Linked Issue

Part of the sidebar refactor plan (step 5d). No issue.

Visual Proof

N/A. Nothing visible changes. The comparison is development tooling and does not run in production builds.

Testing

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

  • Producer unit tests:

    • population once the change stream is ready
    • a refetch on a change, with deleted workspaces dropped
    • one more fetch for a change that lands mid-fetch
    • an unverifiable server that keeps its rows (lost contact or no answer), and reads unverifiable after contact returns until a fetch answers
    • a new watch after the old one ends with its transport, even with no close notice
    • a re-paired or removed server's rows dropped, including a late answer
    • the 30 s fallback runs only while the list shows
    • host-qualified keys, and defaults for older servers
  • Catalog unit tests updated. One race now reads better: a phone request made just before a re-pair gets the new server's rows instead of none, and still never the old server's.

  • Shadow unit tests: the window-side reader, and the scoping that leaves out an id both hosts use.

  • e2e on macOS with a hidden window (ORCA_BACKGROUND_LAUNCH=1):

    • The new spec paired-server-rows-shadow runs a desktop paired to an orca serve host. Workspaces created on the server itself reach main with no phone involved. Pinning and deleting on the server follow, and the rows read unverifiable (and are kept) while the server process is down, then live again.
    • expectNoSidebarShadowMismatches was added to that spec and to paired-mobile-desktop-mirror-workspace-visibility.
    • Those two plus paired-mobile-desktop-mirror and the three sidebar specs from feat(sidebar): main produces per-workspace activity facts, checked against the window in dev and test #29980 pass, run twice each (14/14), with zero disagreements.
  • Positive controls, not committed: changing a server row's name only in the window's store, and clearing the window's server status, were each caught by their domain.

  • Gates:

    • Passing: pnpm tc, oxlint, check:code-quality:changed, the focus-read ratchet, the runtime-electron ratchet, the rpc params catalog check, and the census tests over ALL_RPC_METHODS.
    • Environmental failures: under bun, desktop-browser-bridge-wiring (no node:sqlite) and three socket integration suites time out. The same catalog integration test also fails in a lane at origin/main. All of them pass under Node.

Review

Opus elegance and bug review, two rounds, all findings applied:

  • a watch left dead after a transport replacement
  • rows dropped while a managed server is reached through its SSH host
  • rows reading live before they were re-verified
  • an extra fetch on joined reads
  • the demand gating, which departed from the brief

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH and remote: paired servers only, through the desktop's existing connection.
  • WSL hosts stay excluded (they are never relayed to phones).
  • Folder workspaces on servers are normalized and compared too.
  • Deferred to later steps: phone-subscription demand for the fallback (the feed step), and a refresh the moment the list starts showing (the next 30 s tick covers it today).

…-rows

# Conflicts:
#	src/main/runtime/rpc/methods/sidebar-inputs.ts
#	src/renderer/src/lib/sidebar-shadow/sidebar-shadow-comparison.ts
#	src/renderer/src/lib/sidebar-shadow/workspace-activity-shadow.ts
#	src/renderer/src/main.tsx
#	src/shared/rpc-contract/rpc-params-catalog.generated.ts
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The runtime now tracks paired-server worktree rows, refreshes them after host changes, and exposes snapshots through a sidebar RPC. The relay catalog reads rows from the shared producer. The renderer adds paired-server row and host shadow comparisons, and workspace activity now excludes IDs that also have server ownership. Tests cover producer behavior, catalog re-pairing, renderer comparisons, and end-to-end row updates.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to d0b79

A newly contacted server may show no workspace rows for up to 30 seconds, or longer while the sidebar is hidden. Fetching on contact should address this before merge.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 27 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly identifies the main change: a single live paired-server row set maintained by the main process and checked against the window. It is specific and related to the changeset.
Description check Passed The description is detailed and covers the required explanation, changes, rationale, testing, visual proof, review notes, compatibility considerations, and checklist. It does not link an issue, but it…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is detailed and covers the required explanation, changes, rationale, testing, visual proof, review notes, compatibility considerations, and checklist. It does not link an issue, but it explains that the change is part of an existing sidebar refactor plan.

Full details: Docstring Coverage

Explanation

Docstring coverage is 47.06% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 27 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cf9d97c2-4642-45cd-9ca6-2bb6ab7db380
📥 Commits

Reviewing files that changed from the base of the PR and between 3813104 and d0b79ee.

⛔ Files ignored due to path filters: 1
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing: 28
  • src/main/ipc/runtime-environment-mobile-relay-hosts.ts
  • src/main/ipc/runtime-environment-status-owner.ts
  • src/main/runtime/mobile-desktop-relay/mobile-desktop-relay-hosts.ts
  • src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.integration.test.ts
  • src/main/runtime/mobile-desktop-relay/mobile-desktop-relay.test.ts
  • src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.integration.test.ts
  • src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.test.ts
  • src/main/runtime/mobile-desktop-relay/mobile-relay-host-catalog.ts
  • src/main/runtime/mobile-desktop-relay/mobile-relay-host-descriptions.ts
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/sidebar-inputs.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-pairing.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-shutdown.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-state.ts
  • src/main/runtime/sidebar-inputs/paired-server-rows-producer.test.ts
  • src/main/runtime/sidebar-inputs/paired-server-rows-producer.ts
  • src/renderer/src/assets/mobile-page.css
  • src/renderer/src/components/native-chat/use-native-chat-paste-lifetime.ts
  • src/renderer/src/lib/sidebar-shadow/paired-server-rows-shadow.test.ts
  • src/renderer/src/lib/sidebar-shadow/paired-server-rows-shadow.ts
  • src/renderer/src/lib/sidebar-shadow/sidebar-shadow-comparison.ts
  • src/renderer/src/lib/sidebar-shadow/workspace-activity-shadow.test.ts
  • src/renderer/src/lib/sidebar-shadow/workspace-activity-shadow.ts
  • src/renderer/src/main.tsx
  • src/shared/paired-server-hosts.ts
  • src/shared/sidebar-list/paired-server-rows.ts
  • tests/e2e/paired-mobile-desktop-mirror-workspace-visibility.spec.ts
  • tests/e2e/paired-server-rows-shadow.spec.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Comment on lines +123 to +138
for (const entry of this.servers.values()) {
const inContact = shown.get(entry.environmentId)?.health === 'available'
if (inContact !== entry.inContact) {
entry.inContact = inContact
entry.confirmed = false
this.revision += 1
}
// Why current(): a replaced transport can end the stream without telling it.
if (inContact && !entry.watch?.current()) {
entry.watch?.close()
this.watch(hosts, entry)
} else if (!inContact && entry.watch) {
entry.watch.close()
entry.watch = null
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '108,155p' src/main/runtime/sidebar-inputs/paired-server-rows-producer.ts
sed -n '205,251p' src/main/runtime/sidebar-inputs/paired-server-rows-producer.ts

Repository: stablyai/orca

Length of output: 3274


🏁 Script executed:

sed -n '1,235p' src/main/runtime/sidebar-inputs/paired-server-rows-producer.ts

Repository: stablyai/orca

Length of output: 8678


Fetch rows when contact begins.

When inContact changes to true, sync() starts the watch but does not call refresh(). A subscription can close without an onChange callback, and the fallback requires demand. The server can therefore remain without rows after contact. Add the refresh call without adding another revision increment. The existing refresh coalescing and refreshAgain handling prevent a fetch loop.

Suggested fix
         entry.inContact = inContact
         entry.confirmed = false
         this.revision += 1
+        if (inContact) {
+          void this.refresh(entry.environmentId)
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const entry of this.servers.values()) {
const inContact = shown.get(entry.environmentId)?.health === 'available'
if (inContact !== entry.inContact) {
entry.inContact = inContact
entry.confirmed = false
this.revision += 1
}
// Why current(): a replaced transport can end the stream without telling it.
if (inContact && !entry.watch?.current()) {
entry.watch?.close()
this.watch(hosts, entry)
} else if (!inContact && entry.watch) {
entry.watch.close()
entry.watch = null
}
}
for (const entry of this.servers.values()) {
const inContact = shown.get(entry.environmentId)?.health === 'available'
if (inContact !== entry.inContact) {
entry.inContact = inContact
entry.confirmed = false
this.revision += 1
if (inContact) {
void this.refresh(entry.environmentId)
}
}
// Why current(): a replaced transport can end the stream without telling it.
if (inContact && !entry.watch?.current()) {
entry.watch?.close()
this.watch(hosts, entry)
} else if (!inContact && entry.watch) {
entry.watch.close()
entry.watch = null
}
}

@Jinwoo-H
Jinwoo-H merged commit 03cefcb into main Oct 17, 2026
39 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.

1 participant