Skip to content

feat(sidebar): ui.setCollapsed changes one section's collapse without undoing another device - #30044

Merged
Jinwoo-H merged 9 commits into
mainfrom
jinwoo/sidebar-collapse-per-key
Oct 17, 2026
Merged

Jinwoo-H merged 9 commits into
mainfrom
jinwoo/sidebar-collapse-per-key

Conversation

@Jinwoo-H

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

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 4 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​186 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​185
Prod 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​106 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​105

ELI5

The sidebar remembers which sections you folded shut. Today, when the desktop and its phone each fold a section, each one sends its whole list of folded sections, so the second one can quietly undo the first. This adds a way to say "fold just this one section" so two devices stop overwriting each other. Nothing uses it yet.

Dark: the method exists only in a build with ORCA_SIDEBAR_INPUTS=main (ORCA_SIDEBAR_INPUTS_IS_MAIN, #30050). Shipped builds have the switch off and keep exactly main's behaviour and method set.

What Changed

  • New RPC method ui.setCollapsed({ key, collapsed, opId: { writer, seq } }). It folds or unfolds one section in the saved collapsedGroups and leaves every other section alone. It saves through the same UI-state write as ui.set, so the desktop window and phones get the same change events as they do today.
  • Ordering: for each section, the last change to arrive wins. Main keeps the highest seq each writer has applied to each section, in a bounded in-memory map. When a writer resends an op that already landed, or an older op arrives after its newer one, main does nothing and returns applied: false with the section's current value. In both cases the op is finished, so the client can drop its pending entry even if the value differs. Example: phone A folds a section, phone B unfolds it, then A reconnects and resends its fold. B's unfold stays.
  • The method is added to the list of phone methods the paired Mac answers itself, the same as ui.set. Regenerated the RPC catalog.
  • Behind the switch, following src/shared/sidebar-inputs-switch.ts:
    • The method sits in SIDEBAR_INPUT_METHODS, which is registered only when the switch is on.
    • The ordering memory is the runtime field collapseWriteOrder, which is null when the switch is off.
    • The phone route and the mobile allowlist entry are added only when the switch is on.
    • The RPC catalog is generated with the switch on, so it lists the method, the same as the existing sidebarInputs.* entries.

Before and after for users: none. The window and the phone still use the whole-list ui.set. Switching them over is a later PR.

Why

The owner decided that for each section, the last change wins. Resending the desired value is not enough to make retries safe: a late retry would still undo another device's newer change. Each writer numbers its own changes, so main can tell a stale or repeated op from a new one without comparing clocks across devices.

Alternatives considered:

  • A compare-and-set on a revision number would turn a normal two-device toggle into a rejection, which goes against last-write-wins.
  • A durable operation log is more than a view setting needs. Only an op from before an Orca restart could slip past the in-memory map.

Who may call it: the same callers as ui.set. collapsedGroups is not one of the fields each side of a pairing keeps for itself, so a different scope here would make the two writes disagree. Separating views per device is owned by a later PR.

Mixed versions:

  • The new method is additive. Old pages keep the whole-list write and lose changes exactly as they do today.
  • A whole-list write and a per-section change mixed together are no worse than two whole-list writes. Every per-section change touches only its own section.
  • An older host answers a phone's call with forbidden, because the mobile allowlist check runs before method lookup. Other old callers get method-not-found. Future callers must treat both answers as "fall back to ui.set".

Known limits:

  • The ordering map lives in memory. After an Orca restart, a retry of an op applied before the restart can apply again.
  • If a writer's first send never reached main, its later retry still overrides another device's change that landed in between. That is last-arrival-wins, as decided.
  • A client must mint a new writer id whenever its counter restarts.

Linked Issue

Part of the sidebar list redesign (design PR 10, per-section collapse).

Visual Proof

N/A. No caller uses the new method, so nothing visible changes.

Testing

  • Automated tests added: src/main/runtime/rpc/methods/client-ui-set-collapsed.test.ts runs the real dispatcher and method. It covers desktop and phone folding different sections, both changing the same section, a retry after another writer's newer change, an older op arriving after a newer one from the same writer, and mixing with an old client's whole-list ui.set.
  • pnpm tc, client-ui, mobile allowlist, phone-method routing and caller-scope tests, RPC catalog check, changed-code quality gate, focus-read ratchet.
  • With the switch off:
    • The off-census confirms that no gated method is registered and the ordering field is null.
    • The routing census confirms that no route for ui.setCollapsed exists.
  • Bundle comparison with the switch off, against main at 6fcc183, built in the same lane path. The preload is byte-identical. After normalizing chunk hashes and the build commit, the renderer is identical too. Main has no ui.setCollapsed string, handler or route, and the field folds to collapseWriteOrder=null. Two unused definitions remain in a shared chunk: the ordering class and the params schema expression, because the bundler keeps them. Nothing calls either one. All other changes are minifier renames.
  • Hidden-app e2e tests/e2e/sidebar-collapse-per-key.spec.ts, run with ORCA_BACKGROUND_LAUNCH=1, headless, CDP only:
    • Switch on: passed. The window folds one section through its own whole-list write, the method folds another, and both stay. Another writer then unfolds it, and phone A's retry of its earlier fold returns applied: false and leaves the newer change in place. sidebar-review-facts also passed in the same run.
    • Switch off: the call returns method_not_found.
    • Positive control, not committed: making the retry check always fail turned the switch-on spec red.

Review

An Opus review of the diff found no ordering or eviction bugs. Applied from it: the old-host forbidden answer is now documented, the rule for minting a new writer id is spelled out, the caller scope now matches ui.set instead of being narrowed, and the eviction code is simpler.

A second Opus review covered the gating. It found nothing reachable with the switch off. Applied from it:

  • The off-census now covers every method in SIDEBAR_INPUT_METHODS.
  • The e2e skips at the describe level, before the app launches.
  • The switch-off check asserts method_not_found.
  • Removed an unused mock.

Agent skill upstream boundary

  • Not applicable.

Notes

Covers SSH, folder workspaces and paired servers: collapse keys are opaque strings, and the phone's call is answered by its paired Mac, the same as ui.set. The change is the same on every platform.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • N/A with reason for visuals
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (typecheck and targeted tests local; CI covers the rest)

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The change adds the ui.setCollapsed request contract and per-writer sequence tracking for section-specific updates. The RPC is enabled only when sidebar inputs are enabled and is not added to mobile RPC routes. Tests cover update ordering, retries, caller contexts, legacy whole-set writes, and enabled and switch-off builds.

Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to eb964

The new RPC is gated and unused, so no visible behavior changes. One routing test would fail in builds with sidebar inputs enabled; fix it 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 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 10 files. 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 primary change: adding per-section collapse updates that prevent one device from undoing another device's change.
Description check Passed The description is complete and focused. It explains the user impact, implementation, ordering behavior, feature gating, compatibility limits, testing, review findings, and checklist status. It does n…
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 complete and focused. It explains the user impact, implementation, ordering behavior, feature gating, compatibility limits, testing, review findings, and checklist status. It does not provide a direct issue reference in the Linked Issue section, but the remaining required sections are substantially complete.

  • 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: a7f6dd45-4b54-4f5f-97d8-808db5be571c
📥 Commits

Reviewing files that changed from the base of the PR and between 609f342 and f933a6b.

⛔ Files ignored due to path filters: 1
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing: 6
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/client-ui-set-collapsed.test.ts
  • src/main/runtime/rpc/methods/client-ui.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-mobile-paired-desktop-methods.ts
  • src/main/runtime/workspace-view-collapse-writes.ts
  • src/shared/rpc-contract/workspace-view-collapse-params.ts

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

const runtime = fake as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: CLIENT_UI_METHODS })
let requestId = 0
const call = async (method: string, params: unknown, context: object = ownPhone) => {

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Give both test contexts a named dispatch-options type.

Both context: object parameters fail the reported anti-slop lint check. The static-analysis job cannot pass with these declarations. Use a named type derived from RpcDispatcher.dispatch for both parameters.

Proposed change
+type TestContext = NonNullable<Parameters<RpcDispatcher['dispatch']>[1]>
+
-  const call = async (method: string, params: unknown, context: object = ownPhone) => {
+  const call = async (method: string, params: unknown, context: TestContext = ownPhone) => {
...
-    context: object = ownPhone
+    context: TestContext = ownPhone

Also applies to: 37-37

🧰 Tools
🪛 GitHub Actions: PR 30044 | source 092111eb53c479e1c81f67d188acc427dde85e37 | workflow 092111eb53c479e1c81f67d188acc427dde85e37 | unit full / 14_static analysis and typecheck.txt

[error] 28-28: Lint check failed: Parameter context uses the broad object type. Accept a named owner type; parse external input at its boundary before calling this function.

🪛 GitHub Actions: PR 30044 | source 092111eb53c479e1c81f67d188acc427dde85e37 | workflow 092111eb53c479e1c81f67d188acc427dde85e37 | unit full / static analysis and typecheck

[error] 28-28: Oxlint anti-slop check failed: Parameter context uses the broad object type. Accept a named owner type and parse external input at its boundary. Command: bun --smol node_modules/oxlint/bin/oxlint --config config/oxlint-anti-slop.json src config tests mobile --deny-warnings.

Source: Pipeline failures

@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: 379270b7-739e-4b40-9b19-b0320648c7d5
📥 Commits

Reviewing files that changed from the base of the PR and between f933a6b and eb96461.

⛔ Files ignored due to path filters: 1
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing: 7
  • src/main/runtime/orca-runtime.ts
  • src/main/runtime/rpc/methods/client-ui-set-collapsed.test.ts
  • src/main/runtime/rpc/methods/sidebar-inputs.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.test.ts
  • src/main/runtime/runtime-rpc/runtime-rpc-mobile-paired-desktop-methods.ts
  • src/main/runtime/sidebar-inputs/sidebar-inputs-switch-off.test.ts
  • tests/e2e/sidebar-collapse-per-key.spec.ts

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

})

it('routes no per-section collapse write while the sidebar-inputs switch is off', () => {
expect(MOBILE_RPC_METHOD_ROUTES.has('ui.setCollapsed')).toBe(false)

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 | 🟠 Major | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

rg -n 'PAIRED_DESKTOP_METHODS|MOBILE_RPC_METHOD_ROUTES' src/main/runtime/runtime-rpc
sed -n '1,45p' src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.test.ts
sed -n '100,120p' src/main/runtime/runtime-rpc/runtime-rpc-mobile-paired-desktop-methods.ts

Repository: stablyai/orca

Length of output: 4000


🏁 Script executed:

set -eu
printf '%s\n' '--- test ---'
nl -ba src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.test.ts | sed -n '1,40p'
printf '%s\n' '--- paired methods ---'
nl -ba src/main/runtime/runtime-rpc/runtime-rpc-mobile-paired-desktop-methods.ts | sed -n '1,16p;100,125p'
printf '%s\n' '--- switch bindings ---'
rg -n -F -- 'ORCA_SIDEBAR_INPUTS_IS_MAIN' src/main/runtime/runtime-rpc src/main | head -40
printf '%s\n' '--- routing imports and map ---'
nl -ba src/main/runtime/runtime-rpc/runtime-rpc-mobile-method-routing.ts | sed -n '1,12p;208,220p'

Repository: stablyai/orca

Length of output: 6066


Make the routing assertion conditional on the build switch.

When ORCA_SIDEBAR_INPUTS_IS_MAIN is true, the route exists. Update the test to compare route presence with the switch value.

Suggested fix
 import { PAIRED_DESKTOP_METHODS } from './runtime-rpc-mobile-paired-desktop-methods'
+import { ORCA_SIDEBAR_INPUTS_IS_MAIN } from '../../../shared/sidebar-inputs-switch'
 
 describe('mobile RPC method routing census', () => {
@@
-  it('routes no per-section collapse write while the sidebar-inputs switch is off', () => {
-    expect(MOBILE_RPC_METHOD_ROUTES.has('ui.setCollapsed')).toBe(false)
+  it('routes the per-section collapse write according to the sidebar-inputs switch', () => {
+    expect(MOBILE_RPC_METHOD_ROUTES.has('ui.setCollapsed')).toBe(
+      ORCA_SIDEBAR_INPUTS_IS_MAIN
+    )
   })

…se-per-key

# Conflicts:
#	src/main/runtime/rpc/methods/sidebar-inputs.ts
#	src/main/runtime/sidebar-inputs/sidebar-inputs-switch-off.test.ts
@Jinwoo-H
Jinwoo-H merged commit b389854 into main Oct 17, 2026
40 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