fix(sidebar): render workspace recency order on the server to stop refresh reshuffle - #8281
waleedlatif1 wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
There was a problem hiding this comment.
2 issues found across 8 files
Confidence score: 3/5
- In
apps/sim/app/o/[organizationId]/layout.tsx,useWorkspaceOrderswitches 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) |
There was a problem hiding this comment.
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>
| > | ||
| {children} | ||
| </WorkspaceChrome> | ||
| <WorkspaceRecencyProvider recentWorkspaceIds={recentWorkspaceIds}> |
There was a problem hiding this comment.
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>
…in the cookie policy
|
@cubic-dev-ai review this PR |
@waleedlatif1 I have started the AI code review. It will take a few minutes to complete. |
|
| export function serializeWorkspaceRecency(visits: Record<string, number>): string { | ||
| return Object.entries(visits) | ||
| .sort((a, b) => b[1] - a[1]) | ||
| .slice(0, MAX_RECENT_WORKSPACES) |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
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}$/ |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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` |
There was a problem hiding this comment.
| /** 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]) |
There was a problem hiding this comment.
There was a problem hiding this comment.
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_recencycan 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}$/ |
There was a problem hiding this comment.
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>
| /** 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` |
There was a problem hiding this comment.
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>
| '1 year', | ||
| ], | ||
| [ | ||
| 'workspace_recency', |
There was a problem hiding this comment.
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>
|
Superseded: recency is moving server-side (per-user visit table returned with the workspace list) instead of a cookie mirror. Replacement PR incoming. |
Summary
workspace_recencycookie whenever visit history is saved (same pattern assidebar_collapsed)WorkspaceRecencyProvider;useWorkspaceOrdersorts by it during server render and hydration, then hands off to localStorage with the same orderType of Change
Testing
Checklist
🤖 Generated with Claude Code