Skip to content

feat: sync sidebar layout across clients - #99

Merged
Jay1 merged 5 commits into
mainfrom
feature/server-sidebar-layout
Jul 19, 2026
Merged

feat: sync sidebar layout across clients#99
Jay1 merged 5 commits into
mainfrom
feature/server-sidebar-layout

Conversation

@Jay1

@Jay1 Jay1 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • persist canonical project order and pinned-thread membership/order on the JCode server
  • reconcile sidebar layout through typed commands, monotonic events, durable receipts, and transactional projections
  • replace browser-owned pin/project ordering with accessible optimistic drag-and-drop backed by server state
  • migrate legacy local ordering once while preserving device-local expansion and project names
  • cover stale events, rejected commands, lost responses, reconnects, concurrent initializers, entity lifecycle, reload, and restart persistence

Why

Pinned and project ordering were stored locally and continuously mirrored from thread metadata. During an unpin, a stale shell update could repin the thread immediately; ordering also diverged between computers connected to the same JCode server.

The server is now the single authority for both orders, so clients converge without compensating pin commands or local unions.

User impact

  • T3Code Integration Ideas and other threads can be unpinned reliably
  • pinned membership/order and manual project order synchronize between connected clients
  • pointer and keyboard drag interactions optimistically update, then roll back with targeted feedback on rejection
  • local sidebar expansion and project aliases remain device-local

Validation

  • contracts, server, and web TypeScript checks pass
  • focused contracts/server/web test suites pass
  • EventRouter Chromium integration suite: 18/18
  • real two-client loopback acceptance covered initialization races, cross-client moves, pin/reorder/unpin, rejection rollback, lost-response receipt recovery, reload, and same-database restart
  • final plan, code-quality, real-QA, security, and scope-fidelity reviews approved

Summary by CodeRabbit

  • New Features

    • Added server-synchronized sidebar layouts for project ordering and pinned-thread membership.
    • Added drag-and-drop and keyboard controls for rearranging projects and pinned threads.
    • Added optimistic updates, retry handling, rollback on rejected changes, and multi-session synchronization.
    • Added automatic migration of existing sidebar preferences with safeguards against stale data overwriting the new layout.
  • Bug Fixes

    • Improved sidebar event ordering, recovery, and consistency during reconnects and initialization races.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 39 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 94939f46-e30a-457d-bdda-83389de5c3ae

📥 Commits

Reviewing files that changed from the base of the PR and between ced0a1f and 0c370f5.

📒 Files selected for processing (3)
  • apps/server/src/orchestration/Layers/OrchestrationEngine.ts
  • apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts
  • apps/web/src/sidebarLayoutStore.test.ts
📝 Walkthrough

Walkthrough

Adds a server-owned sidebar layout aggregate for canonical project and pinned-thread ordering. The change spans contracts, event sourcing, SQLite projections, repair flows, client reconciliation, legacy migration, sidebar drag interactions, authorization, and extensive unit, integration, and browser coverage.

Changes

Sidebar layout architecture

Layer / File(s) Summary
Contracts and server domain
packages/contracts/src/*, apps/server/src/orchestration/*
Defines sidebar layout commands, events, snapshots, normalization, canonical pin handling, command decisions, projections, and snapshot fencing.
Persistence and repair
apps/server/src/persistence/*, apps/server/src/orchestration/Layers/*
Adds the singleton layout table and repository, persists denormalized pin membership, supports the sidebar aggregate in event storage, and extends projection rebuild and repair restoration.
Client reconciliation and migration
apps/web/src/sidebarLayout*.ts, apps/web/src/routes/__root.tsx, apps/web/src/storageKeyMigration.ts
Adds optimistic intent replay, ordered dispatch, shell-event routing, reconnect handling, and one-time migration from legacy sidebar storage.
Sidebar UI and browser coverage
apps/web/src/components/Sidebar.tsx, apps/web/src/components/Sidebar.logic.ts, apps/web/vitest.browser.config.ts
Uses canonical project and pinned-thread order, submits semantic move/pin intents, and adds pointer and keyboard drag interactions.
Validation and fixtures
apps/server/src/**/*.test.ts, apps/web/src/**/*.test.ts, docs/superpowers/specs/*
Adds contract, persistence, projection, repair, reconciliation, migration, authorization, browser, and design-spec coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Sidebar as Sidebar UI
  participant Store as sidebarLayoutStore
  participant RPC as Orchestration RPC
  participant Engine as OrchestrationEngine
  participant Projection as Sidebar layout projector
  participant Stream as Shell stream

  Sidebar->>Store: Enqueue project or pinned-thread intent
  Store->>RPC: Dispatch semantic command
  RPC->>Engine: Authorize and process command
  Engine->>Projection: Persist sidebar-layout.updated
  Projection-->>Engine: Return accepted sequence
  Engine-->>RPC: Return dispatch receipt
  RPC-->>Store: Return sequence
  Stream-->>Store: Deliver canonical layout update
  Store-->>Sidebar: Reconcile and render confirmed plus pending layout
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: syncing sidebar layout across clients.
Description check ✅ Passed The description covers summary, motivation, impact, and validation, but omits the template's UI Changes and Reviewer Notes sections.
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.

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.

@github-actions github-actions Bot added size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Jul 19, 2026
@Jay1
Jay1 marked this pull request as ready for review July 19, 2026 16:49

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/server/src/orchestration/Layers/OrchestrationEngine.ts (1)

389-465: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sidebar-layout decode failure discards the original cause.

When decoding the persisted projectOrder/pinnedThreadOrder fails, Effect.mapError swaps in a fresh OrchestrationCommandInternalError without setting cause, even though this error class supports an optional cause field elsewhere in the codebase. During a repair-flow failure this is the only signal an operator gets — losing the underlying ParseError detail makes diagnosing corrupted projection_sidebar_layout rows harder.

♻️ Suggested fix
             Effect.mapError(
-              () =>
+              (cause) =>
                 new OrchestrationCommandInternalError({
                   commandId: "repair-local-state",
                   commandType: ORCHESTRATION_WS_METHODS.repairState,
                   detail: "The rebuilt sidebar layout could not be decoded.",
+                  cause,
                 }),
             ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/orchestration/Layers/OrchestrationEngine.ts` around lines 389
- 465, Update the sidebar layout decode error handling in the `sidebarLayout`
construction to preserve the original decode error as the `cause` when creating
`OrchestrationCommandInternalError`. Keep the existing command metadata and
detail unchanged while passing through the `Effect.mapError` error value so the
underlying `ParseError` remains available.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/orchestration/Layers/OrchestrationEngine.ts`:
- Around line 50-52: Remove the local SIDEBAR_LAYOUT_PROJECTOR literal and reuse
ORCHESTRATION_PROJECTOR_NAMES.sidebarLayout from ProjectionPipeline.ts wherever
resetDerivedProjectionState or backupDerivedProjectionState needs the projector
name, preserving a single source of truth.

In `@apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts`:
- Around line 16-56: Ensure the test’s inserted event is deleted even when
assertions fail by wrapping the append, replay, and assertion effect in an
Effect.ensuring cleanup. Keep the existing DELETE for event_id
'evt-sidebar-layout-roundtrip' as the finalizer, and preserve the current
round-trip assertions and shared layer usage.

In `@apps/server/src/persistence/Layers/ProjectionRepositories.test.ts`:
- Around line 122-124: Remove the duplicated consecutive rows declaration in the
test’s “only the live member remains pinned” query, keeping a single const rows
assignment and its existing SQL template body.

In `@apps/web/src/sidebarLayoutStore.test.ts`:
- Around line 174-199: Update the test around acceptConfirmedLayout to match the
store’s actual revision-based gating: remove snapshotSequence from
olderReconnectSnapshotLayout and assert only the revision ordering, or move the
reconnect-fence scenario to the router/snapshot layer if that behavior is
intended to be tested.

In `@packages/contracts/src/orchestration.ts`:
- Around line 1102-1105: Extract a generic helper for the repeated
optional/null/decoding-default anchor schema pattern in
SidebarLayoutProjectMoveCommand, SidebarLayoutThreadPinCommand, and
SidebarLayoutPinnedThreadMoveCommand, then use it for each command’s
before-anchor field. Preserve the existing schema types and null decoding
default while keeping all three commands synchronized.

---

Outside diff comments:
In `@apps/server/src/orchestration/Layers/OrchestrationEngine.ts`:
- Around line 389-465: Update the sidebar layout decode error handling in the
`sidebarLayout` construction to preserve the original decode error as the
`cause` when creating `OrchestrationCommandInternalError`. Keep the existing
command metadata and detail unchanged while passing through the
`Effect.mapError` error value so the underlying `ParseError` remains available.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3aaf7692-28ce-4f93-8d74-9f9a5c46753f

📥 Commits

Reviewing files that changed from the base of the PR and between 1349514 and ced0a1f.

📒 Files selected for processing (65)
  • apps/server/src/main.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts
  • apps/server/src/orchestration/Layers/OrchestrationEngine.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts
  • apps/server/src/orchestration/Layers/ProjectionPipeline.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts
  • apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
  • apps/server/src/orchestration/Schemas.ts
  • apps/server/src/orchestration/Services/ProjectionPipeline.ts
  • apps/server/src/orchestration/commandInvariants.test.ts
  • apps/server/src/orchestration/decider.sidebarLayout.test.ts
  • apps/server/src/orchestration/decider.ts
  • apps/server/src/orchestration/projector.test.ts
  • apps/server/src/orchestration/projector.ts
  • apps/server/src/orchestration/sidebarLayout.test.ts
  • apps/server/src/orchestration/sidebarLayout.ts
  • apps/server/src/orchestration/sidebarLayoutDecider.ts
  • apps/server/src/persistence/Layers/OrchestrationCommandReceipts.test.ts
  • apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts
  • apps/server/src/persistence/Layers/OrchestrationEventStore.ts
  • apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
  • apps/server/src/persistence/Layers/ProjectionSidebarLayout.test.ts
  • apps/server/src/persistence/Layers/ProjectionSidebarLayout.ts
  • apps/server/src/persistence/Layers/ProjectionThreads.ts
  • apps/server/src/persistence/Migrations.ts
  • apps/server/src/persistence/Migrations/041_ProjectionSidebarLayout.test.ts
  • apps/server/src/persistence/Migrations/041_ProjectionSidebarLayout.ts
  • apps/server/src/persistence/Services/OrchestrationCommandReceipts.ts
  • apps/server/src/persistence/Services/ProjectionSidebarLayout.ts
  • apps/server/src/persistence/Services/ProjectionThreads.ts
  • apps/server/src/threadRetention.test.ts
  • apps/server/src/wsRpc.test.ts
  • apps/server/src/wsRpc.ts
  • apps/web/src/components/ChatView.browser.tsx
  • apps/web/src/components/EventRouter.browser.tsx
  • apps/web/src/components/FirstRunWizard.browser.tsx
  • apps/web/src/components/KeybindingsToast.browser.tsx
  • apps/web/src/components/Sidebar.logic.test.ts
  • apps/web/src/components/Sidebar.logic.ts
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/lib/desktopProjectRecovery.test.ts
  • apps/web/src/lib/threadCreatePromotion.test.ts
  • apps/web/src/pinnedThreadsStore.test.ts
  • apps/web/src/pinnedThreadsStore.ts
  • apps/web/src/routes/__root.tsx
  • apps/web/src/shellEventOrdering.test.ts
  • apps/web/src/shellEventOrdering.ts
  • apps/web/src/sidebarLayout.logic.test.ts
  • apps/web/src/sidebarLayout.logic.ts
  • apps/web/src/sidebarLayoutLegacyMigration.test.ts
  • apps/web/src/sidebarLayoutLegacyMigration.ts
  • apps/web/src/sidebarLayoutRouter.test.ts
  • apps/web/src/sidebarLayoutRouter.ts
  • apps/web/src/sidebarLayoutStore.test.ts
  • apps/web/src/sidebarLayoutStore.ts
  • apps/web/src/storageKeyMigration.test.ts
  • apps/web/src/storageKeyMigration.ts
  • apps/web/src/store.test.ts
  • apps/web/src/store.ts
  • apps/web/src/vite-env.d.ts
  • apps/web/vitest.browser.config.ts
  • docs/superpowers/specs/2026-07-17-server-owned-sidebar-layout-design.md
  • packages/contracts/src/baseSchemas.ts
  • packages/contracts/src/orchestration.test.ts
  • packages/contracts/src/orchestration.ts
💤 Files with no reviewable changes (2)
  • apps/web/src/pinnedThreadsStore.ts
  • apps/web/src/pinnedThreadsStore.test.ts

Comment thread apps/server/src/orchestration/Layers/OrchestrationEngine.ts Outdated
Comment thread apps/server/src/persistence/Layers/OrchestrationEventStore.test.ts
Comment on lines +122 to +124
// Then: only the live member remains pinned and every stale flag is cleared.
const rows = yield* sql<{ readonly threadId: string; readonly isPinned: number }>`
SELECT thread_id AS "threadId", is_pinned AS "isPinned"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Duplicate const rows = ... declaration breaks compilation.

The const rows = yield* sql<{ readonly threadId: string; readonly isPinned: number }>\`` line is duplicated back-to-back before the template literal body, producing a duplicate block-scoped declaration of rows` in the same test. This is a syntax/compile error, not just a style nit.

🐛 Proposed fix
       // Then: only the live member remains pinned and every stale flag is cleared.
       const rows = yield* sql<{ readonly threadId: string; readonly isPinned: number }>`
-      const rows = yield* sql<{ readonly threadId: string; readonly isPinned: number }>`
         SELECT thread_id AS "threadId", is_pinned AS "isPinned"
         FROM projection_threads
         WHERE thread_id IN (${staleId}, ${liveId}, ${deletedId})
         ORDER BY thread_id
       `;
📝 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
// Then: only the live member remains pinned and every stale flag is cleared.
const rows = yield* sql<{ readonly threadId: string; readonly isPinned: number }>`
SELECT thread_id AS "threadId", is_pinned AS "isPinned"
// Then: only the live member remains pinned and every stale flag is cleared.
const rows = yield* sql<{ readonly threadId: string; readonly isPinned: number }>`
SELECT thread_id AS "threadId", is_pinned AS "isPinned"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/persistence/Layers/ProjectionRepositories.test.ts` around
lines 122 - 124, Remove the duplicated consecutive rows declaration in the
test’s “only the live member remains pinned” query, keeping a single const rows
assignment and its existing SQL template body.

Comment thread apps/web/src/sidebarLayoutStore.test.ts
Comment on lines +1102 to +1105
beforeProjectId: Schema.optional(Schema.NullOr(ProjectId)).pipe(
Schema.withDecodingDefault(() => null),
),
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate "before-anchor" optional/null/default pattern across three commands.

SidebarLayoutProjectMoveCommand, SidebarLayoutThreadPinCommand, and SidebarLayoutPinnedThreadMoveCommand each repeat Schema.optional(Schema.NullOr(X)).pipe(Schema.withDecodingDefault(() => null)) for their anchor field. Extracting a small generic helper (e.g. optionalBeforeId = <A>(schema: Schema.Schema<A>) => Schema.optional(Schema.NullOr(schema)).pipe(Schema.withDecodingDefault(() => null))) would keep the three commands in sync if this normalization ever changes.

♻️ Proposed helper extraction
+const optionalBeforeId = <A, I, R>(schema: Schema.Schema<A, I, R>) =>
+  Schema.optional(Schema.NullOr(schema)).pipe(Schema.withDecodingDefault(() => null));
+
 const SidebarLayoutProjectMoveCommand = Schema.Struct({
   type: Schema.Literal("sidebar-layout.project.move"),
   commandId: CommandId,
   projectId: ProjectId,
-  beforeProjectId: Schema.optional(Schema.NullOr(ProjectId)).pipe(
-    Schema.withDecodingDefault(() => null),
-  ),
+  beforeProjectId: optionalBeforeId(ProjectId),
 });

Also applies to: 1111-1114, 1126-1129

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/contracts/src/orchestration.ts` around lines 1102 - 1105, Extract a
generic helper for the repeated optional/null/decoding-default anchor schema
pattern in SidebarLayoutProjectMoveCommand, SidebarLayoutThreadPinCommand, and
SidebarLayoutPinnedThreadMoveCommand, then use it for each command’s
before-anchor field. Preserve the existing schema types and null decoding
default while keeping all three commands synchronized.

@Jay1
Jay1 merged commit 7d8a300 into main Jul 19, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant