Skip to content

fix(sidebar): render workspace recency order on the server to stop refresh reshuffle - #8281

Closed
waleedlatif1 wants to merge 2 commits into
stagingfrom
fix/sidebar-workspace-order-flash
Closed

waleedlatif1 wants to merge 2 commits into
stagingfrom
fix/sidebar-workspace-order-flash

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Workspace visit order lived only in localStorage, so the org sidebar's SSR and hydration rendered the Workspaces list newest-first and then re-sorted after hydration — non-pinned workspaces visibly reshuffled on every refresh (pins come from the DB, so they stayed put)
  • Mirror the 20 most recent workspace ids into a workspace_recency cookie whenever visit history is saved (same pattern as sidebar_collapsed)
  • Org layout reads the cookie and passes it through WorkspaceRecencyProvider; useWorkspaceOrder sorts by it during server render and hydration, then hands off to localStorage with the same order
  • Workspace layout is untouched: its switcher only renders the list inside the closed-by-default dropdown, so it never flashed
  • Query cache order is untouched, so other workspace pickers keep server order
  • Declared the cookie in the cookie policy
  • Browsers without the cookie yet pick it up on their next workspace visit

Type of Change

  • Bug fix

Testing

  • Updated hydration test asserts SSR renders the visit order and hydration makes zero DOM changes (fails on the old hook)
  • Unit tests for cookie serialize/parse/cap/tamper and ordering
  • lint, type-check, check:audits (49), docs-manifest:check pass

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
docs Skipped Skipped Sep 25, 2026 12:29am UTC

Request Review

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 8 files

Confidence score: 3/5

  • In apps/sim/app/o/[organizationId]/layout.tsx, useWorkspaceOrder switches after hydration from the cookie’s 20 IDs to an uncapped localStorage map, so workspace order can reshuffle after refresh; keep the client handoff limited to the same 20 IDs or otherwise preserve the cookie order.
  • In apps/sim/lib/workspaces/recency-cookie.ts, invalid workspace IDs can consume the 20-item cap and push valid recent workspaces out of the cookie; filter invalid IDs before applying the cap.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/lib/workspaces/recency-cookie.ts">

<violation number="1" location="apps/sim/lib/workspaces/recency-cookie.ts:17">
P2: Filter invalid workspace IDs before applying the 20-item cap. Otherwise malformed client-side entries can displace valid recent workspaces from the cookie and reintroduce the refresh reshuffle this helper is intended to prevent.</violation>
</file>

<file name="apps/sim/app/o/[organizationId]/layout.tsx">

<violation number="1" location="apps/sim/app/o/[organizationId]/layout.tsx:87">
P2: The provider makes SSR use only the cookie’s 20 IDs, but `useWorkspaceOrder` switches to the uncapped localStorage map after hydration. Keep the client handoff limited to the same 20 IDs, or otherwise preserve the cookie order, to prevent older tracked and untracked workspaces from visibly reshuffling again.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

export function serializeWorkspaceRecency(visits: Record<string, number>): string {
return Object.entries(visits)
.sort((a, b) => b[1] - a[1])
.slice(0, MAX_RECENT_WORKSPACES)

@cubic-dev-ai cubic-dev-ai Bot Sep 25, 2026 •

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.

P2: Filter invalid workspace IDs before applying the 20-item cap. Otherwise malformed client-side entries can displace valid recent workspaces from the cookie and reintroduce the refresh reshuffle this helper is intended to prevent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/workspaces/recency-cookie.ts, line 17:

<comment>Filter invalid workspace IDs before applying the 20-item cap. Otherwise malformed client-side entries can displace valid recent workspaces from the cookie and reintroduce the refresh reshuffle this helper is intended to prevent.</comment>

<file context>
@@ -0,0 +1,45 @@
+export function serializeWorkspaceRecency(visits: Record<string, number>): string {
+  return Object.entries(visits)
+    .sort((a, b) => b[1] - a[1])
+    .slice(0, MAX_RECENT_WORKSPACES)
+    .map(([id]) => id)
+    .filter((id) => WORKSPACE_ID_PATTERN.test(id))
</file context>
Fix with cubic

>
{children}
</WorkspaceChrome>
<WorkspaceRecencyProvider recentWorkspaceIds={recentWorkspaceIds}>

@cubic-dev-ai cubic-dev-ai Bot Sep 25, 2026 •

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.

P2: The provider makes SSR use only the cookie’s 20 IDs, but useWorkspaceOrder switches to the uncapped localStorage map after hydration. Keep the client handoff limited to the same 20 IDs, or otherwise preserve the cookie order, to prevent older tracked and untracked workspaces from visibly reshuffling again.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/app/o/[organizationId]/layout.tsx, line 87:

<comment>The provider makes SSR use only the cookie’s 20 IDs, but `useWorkspaceOrder` switches to the uncapped localStorage map after hydration. Keep the client handoff limited to the same 20 IDs, or otherwise preserve the cookie order, to prevent older tracked and untracked workspaces from visibly reshuffling again.</comment>

<file context>
@@ -81,12 +84,14 @@ export default async function OrganizationLayout({
-              >
-                {children}
-              </WorkspaceChrome>
+              <WorkspaceRecencyProvider recentWorkspaceIds={recentWorkspaceIds}>
+                <WorkspaceChrome
+                  sidebar={<OrganizationSidebar />}
</file context>
Fix with cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cubic-dev-ai review this PR

@cubic-dev-ai

cubic-dev-ai Bot commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review this PR

@waleedlatif1 I have started the AI code review. It will take a few minutes to complete.

@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The PR is not ready to merge because organization sidebars can still reshuffle on refresh when visit history exceeds the cookie’s cap.

Findings

  1. P1 Older visits still reshuffle ▶
  2. P2 Recency cookie survives sign-out ▶
  3. P2 Tied visits can swap ▶
  4. P2 Valid IDs lose recency ▶

Summary

The PR mirrors recent workspace IDs into a cookie and supplies them to the organization sidebar during server rendering, aiming to prevent a hydration-time reorder.

  • Adds cookie serialization, parsing, ordering, and hydration tests.
  • Documents the cookie in the cookie policy.
  • The 20-ID cookie cap does not cover all retained visit history.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  V[Workspace visit] --> L[Full localStorage history]
  V --> C[Cookie: latest 20 IDs]
  C --> S[Organization server render]
  S --> H[Hydration]
  L --> P[Post-hydration ordering]
  H --> P
Loading

Reviews (2) · Last reviewed commit: "fix(sidebar): scope recency cookie to th..."

export function serializeWorkspaceRecency(visits: Record<string, number>): string {
return Object.entries(visits)
.sort((a, b) => b[1] - a[1])
.slice(0, MAX_RECENT_WORKSPACES)

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.

P1 Older visits still reshuffle When a viewer has visited more than 20 accessible workspaces, the cookie records only the newest 20. The server leaves the remaining workspaces in creation order, but hydration switches to localStorage, which ranks every visit. Those older rows can move on refresh—the reshuffle this change is meant to prevent.

/** Also mirrors the head of the history into a cookie so server renders use the same order. */
private static save(map: Record<string, number>): void {
if (BrowserStorage.setItem(WorkspaceRecencyStorage.KEY, map)) {
document.cookie = `${WORKSPACE_RECENCY_COOKIE}=${serializeWorkspaceRecency(map)}; path=/; max-age=31536000; samesite=lax`

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.

P1 Visit order survives sign-out Sign-out clears the localStorage visit history, but not this year-long cookie. If another account uses the same browser and can access some of the same workspaces, its sidebar can display those workspaces in the previous account’s visit order. Clear or scope the cookie at the identity boundary.


const MAX_RECENT_WORKSPACES = 20
const SEPARATOR = '.'
const WORKSPACE_ID_PATTERN = /^[\w-]{1,64}$/

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.

P2 Valid IDs lose recency If an accessible legacy or imported workspace has an ID outside this 64-character, word-or-hyphen pattern, the cookie omits it even though the workspace ID contract accepts strings up to 128 characters. The server then cannot render its visit position, so its row can move after hydration. Filtering only after the 20-item cap can also leave usable cookie slots empty.

export function serializeWorkspaceRecency(visits: Record<string, number>): string {
return Object.entries(visits)
.sort((a, b) => b[1] - a[1])
.slice(0, MAX_RECENT_WORKSPACES)

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.

P1 Older visits still reshuffle When someone has visited more than 20 workspaces, the cookie omits older visits but localStorage keeps them. The organization sidebar can therefore render those workspaces in creation order on refresh, then move them into visit order after hydration—the reshuffle this PR aims to prevent.

/** Also mirrors the head of the history into a cookie so server renders use the same order. */
private static save(map: Record<string, number>): void {
if (BrowserStorage.setItem(WorkspaceRecencyStorage.KEY, map)) {
document.cookie = `${WORKSPACE_RECENCY_COOKIE}=${serializeWorkspaceRecency(map)}; path=/; max-age=31536000; samesite=lax`

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.

P2 Recency cookie survives sign-out Sign-out and identity-switch cleanup remove the localStorage visit history, but not this year-long cookie. Subsequent requests still carry the previous account’s workspace IDs, and signing back into that account can restore its old sidebar order without a new visit.

/** Most-recent-first workspace ids from a visit-time map, ready for `document.cookie`. */
export function serializeWorkspaceRecency(visits: Record<string, number>): string {
return Object.entries(visits)
.sort((a, b) => b[1] - a[1])

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.

P2 Tied visits can swap If two visits receive the same Date.now() timestamp, cookie serialization keeps their order from the visit map, while the client keeps their order from the workspace list. When those orders differ, the rows swap after hydration, leaving a small version of the refresh reshuffle.

@cubic-dev-ai cubic-dev-ai 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.

1 existing issue remains and 3 new issues found across 8 files

Confidence score: 3/5

  • In apps/sim/lib/workspaces/recency-cookie.ts, client-side ordering can diverge from the 20 IDs in the cookie, so older visits may reshuffle untracked workspaces after hydration; keep both ordering paths aligned.
  • In apps/sim/lib/workspaces/recency-cookie.ts, encoding may drop valid workspace IDs up to the 128-character limit, leaving server order incomplete and allowing hydration to reshuffle rows; preserve IDs within the stated contract.
  • In apps/sim/lib/core/utils/browser-storage.ts, workspace_recency can survive sign-out and expose one account’s visit order to another account; clear or scope it at sign-out.
  • In apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx, the policy promises to revise its “Last updated” date when cookies change; ensure the date is updated when that commitment applies.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx">

<violation number="1" location="apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx:166">
P3: The policy's own text commits to revising the "Last updated" date when the cookies we set change: "We update this policy when the cookies we set change, and we revise the 'Last updated' date above whenever we do." This PR adds a new `workspace_recency` cookie to the inventory (it is genuinely set, with `max-age=31536000` in `apps/sim/lib/core/utils/browser-storage.ts`, so the "1 year" retention is accurate) without bumping `lastUpdated`, which still reads 'September 17, 2026'. Update `lastUpdated` to the publication date so the policy stays consistent with its own freshness statement.</violation>
</file>

<file name="apps/sim/lib/workspaces/recency-cookie.ts">

<violation number="1" location="apps/sim/lib/workspaces/recency-cookie.ts:11">
P2: Preserve valid workspace IDs up to the 128-character contract limit when encoding the cookie; this pattern drops them from the server order and lets hydration reshuffle those rows.</violation>
</file>

<file name="apps/sim/lib/core/utils/browser-storage.ts">

<violation number="1" location="apps/sim/lib/core/utils/browser-storage.ts:151">
P2: Clear or scope `workspace_recency` at sign-out so another account cannot inherit this account’s workspace visit order.</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

Fix all with cubic | Re-trigger cubic


const MAX_RECENT_WORKSPACES = 20
const SEPARATOR = '.'
const WORKSPACE_ID_PATTERN = /^[\w-]{1,64}$/

@cubic-dev-ai cubic-dev-ai Bot Sep 25, 2026 •

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.

P2: Preserve valid workspace IDs up to the 128-character contract limit when encoding the cookie; this pattern drops them from the server order and lets hydration reshuffle those rows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/workspaces/recency-cookie.ts, line 11:

<comment>Preserve valid workspace IDs up to the 128-character contract limit when encoding the cookie; this pattern drops them from the server order and lets hydration reshuffle those rows.</comment>

<file context>
@@ -0,0 +1,45 @@
+
+const MAX_RECENT_WORKSPACES = 20
+const SEPARATOR = '.'
+const WORKSPACE_ID_PATTERN = /^[\w-]{1,64}$/
+
+/** Most-recent-first workspace ids from a visit-time map, ready for `document.cookie`. */
</file context>
Fix with cubic

/** Also mirrors the head of the history into a cookie so server renders use the same order. */
private static save(map: Record<string, number>): void {
if (BrowserStorage.setItem(WorkspaceRecencyStorage.KEY, map)) {
document.cookie = `${WORKSPACE_RECENCY_COOKIE}=${serializeWorkspaceRecency(map)}; path=/; max-age=31536000; samesite=lax`

@cubic-dev-ai cubic-dev-ai Bot Sep 25, 2026 •

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.

P2: Clear or scope workspace_recency at sign-out so another account cannot inherit this account’s workspace visit order.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/lib/core/utils/browser-storage.ts, line 151:

<comment>Clear or scope `workspace_recency` at sign-out so another account cannot inherit this account’s workspace visit order.</comment>

<file context>
@@ -141,8 +145,10 @@ export class WorkspaceRecencyStorage {
+  /** Also mirrors the head of the history into a cookie so server renders use the same order. */
   private static save(map: Record<string, number>): void {
     if (BrowserStorage.setItem(WorkspaceRecencyStorage.KEY, map)) {
+      document.cookie = `${WORKSPACE_RECENCY_COOKIE}=${serializeWorkspaceRecency(map)}; path=/; max-age=31536000; samesite=lax`
       window.dispatchEvent(new Event(WorkspaceRecencyStorage.CHANGE_EVENT))
     }
</file context>
Fix with cubic

'1 year',
],
[
'workspace_recency',

@cubic-dev-ai cubic-dev-ai Bot Sep 25, 2026 •

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.

P3: The policy's own text commits to revising the "Last updated" date when the cookies we set change: "We update this policy when the cookies we set change, and we revise the 'Last updated' date above whenever we do." This PR adds a new workspace_recency cookie to the inventory (it is genuinely set, with max-age=31536000 in apps/sim/lib/core/utils/browser-storage.ts, so the "1 year" retention is accurate) without bumping lastUpdated, which still reads 'September 17, 2026'. Update lastUpdated to the publication date so the policy stays consistent with its own freshness statement.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/sim/app/(landing)/cookie-policy/cookie-policy-content.tsx, line 166:

<comment>The policy's own text commits to revising the "Last updated" date when the cookies we set change: "We update this policy when the cookies we set change, and we revise the 'Last updated' date above whenever we do." This PR adds a new `workspace_recency` cookie to the inventory (it is genuinely set, with `max-age=31536000` in `apps/sim/lib/core/utils/browser-storage.ts`, so the "1 year" retention is accurate) without bumping `lastUpdated`, which still reads 'September 17, 2026'. Update `lastUpdated` to the publication date so the policy stays consistent with its own freshness statement.</comment>

<file context>
@@ -162,6 +162,12 @@ export const COOKIE_POLICY_CONFIG: LegalPageConfig = {
             '1 year',
           ],
+          [
+            'workspace_recency',
+            'Sim',
+            'Remembers which workspaces you opened most recently, so the sidebar lists them in that order without reshuffling on load.',
</file context>
Fix with cubic

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Superseded: recency is moving server-side (per-user visit table returned with the workspace list) instead of a cookie mirror. Replacement PR incoming.

This branch was previously deployed

1 inactive deployment
Preview — 19484677 Deployed Sep 25, 2026 by vercel[bot]
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