Skip to content

Persist project and thread pinning in orchestration#1588

Open
juliusmarminge wants to merge 5 commits intomainfrom
t3code/project-pinning-priority
Open

Persist project and thread pinning in orchestration#1588
juliusmarminge wants to merge 5 commits intomainfrom
t3code/project-pinning-priority

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Mar 30, 2026

Summary

  • Moves project and thread pin state into the orchestration model instead of keeping it only in the web UI store.
  • Persists pinned through contracts, commands/events, projector state, projection repositories, snapshot queries, and a sqlite migration for existing databases.
  • Keeps pinned projects and threads ahead of unpinned ones in automatic sidebar sort modes, while leaving manual project ordering unchanged.
  • Updates the sidebar pin affordances and indicators for projects and threads, including the hover-action/focus fixes for thread archive/pin and project new-thread actions.

Testing

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run --cwd packages/contracts test src/orchestration.test.ts
  • bun run --cwd apps/server test src/orchestration/Layers/ProjectionSnapshotQuery.test.ts src/orchestration/decider.projectScripts.test.ts src/orchestration/projector.test.ts src/persistence/Layers/ProjectionRepositories.test.ts
  • bun run --cwd apps/web test src/uiStateStore.test.ts src/components/Sidebar.logic.test.ts src/store.test.ts src/components/ChatView.logic.test.ts src/orchestrationEventEffects.test.ts src/worktreeCleanup.test.ts

- Add project pin state persistence and toggle actions
- Show pin affordance in the sidebar and keep pinned projects first
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 98726b06-3b74-4275-9b82-d6664190f1a8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/project-pinning-priority

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:M 30-99 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Mar 30, 2026
- Sort pinned threads ahead of unpinned ones in auto-sort modes
- Persist thread pin state and surface pin/unpin actions in the sidebar
- Keep delete fallback selection aware of pinned threads
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unpinned items re-pinned on next server sync
    • Changed toggle functions to store false instead of deleting keys, and made syncProjects/syncThreads unconditionally store pinned state for all items, preventing the nullish coalescing fallback from reaching stale persisted sets.

Create PR

Or push these changes by commenting:

@cursor push 08e5181717
Preview (08e5181717)
diff --git a/apps/web/src/uiStateStore.test.ts b/apps/web/src/uiStateStore.test.ts
--- a/apps/web/src/uiStateStore.test.ts
+++ b/apps/web/src/uiStateStore.test.ts
@@ -136,6 +136,7 @@
     ]);
 
     expect(next.projectPinnedById).toEqual({
+      [oldProject1]: false,
       [recreatedProject2]: true,
     });
   });
@@ -232,7 +233,7 @@
 
     const unpinned = toggleProjectPinned(pinned, project1);
 
-    expect(unpinned.projectPinnedById).toEqual({});
+    expect(unpinned.projectPinnedById).toEqual({ [project1]: false });
   });
 
   it("toggleThreadPinned adds and removes pinned state", () => {
@@ -243,7 +244,7 @@
 
     const unpinned = toggleThreadPinned(pinned, thread1);
 
-    expect(unpinned.threadPinnedById).toEqual({});
+    expect(unpinned.threadPinnedById).toEqual({ [thread1]: false });
   });
 
   it("clearThreadUi removes visit state for deleted threads", () => {

diff --git a/apps/web/src/uiStateStore.ts b/apps/web/src/uiStateStore.ts
--- a/apps/web/src/uiStateStore.ts
+++ b/apps/web/src/uiStateStore.ts
@@ -211,9 +211,7 @@
       (previousProjectIdForCwd ? previousPinnedById[previousProjectIdForCwd] : undefined) ??
       persistedPinnedProjectCwds.has(project.cwd);
     nextExpandedById[project.id] = expanded;
-    if (pinned) {
-      nextPinnedById[project.id] = true;
-    }
+    nextPinnedById[project.id] = pinned;
     return {
       id: project.id,
       cwd: project.cwd,
@@ -307,8 +305,8 @@
     ) {
       nextThreadLastVisitedAtById[thread.id] = thread.seedVisitedAt;
     }
-    if (nextThreadPinnedById[thread.id] === undefined && persistedPinnedThreadIds.has(thread.id)) {
-      nextThreadPinnedById[thread.id] = true;
+    if (nextThreadPinnedById[thread.id] === undefined) {
+      nextThreadPinnedById[thread.id] = persistedPinnedThreadIds.has(thread.id);
     }
   }
   if (
@@ -388,7 +386,7 @@
 export function toggleThreadPinned(state: UiState, threadId: ThreadId): UiState {
   const nextThreadPinnedById = { ...state.threadPinnedById };
   if (nextThreadPinnedById[threadId]) {
-    delete nextThreadPinnedById[threadId];
+    nextThreadPinnedById[threadId] = false;
   } else {
     nextThreadPinnedById[threadId] = true;
   }
@@ -457,7 +455,7 @@
 export function toggleProjectPinned(state: UiState, projectId: ProjectId): UiState {
   const nextProjectPinnedById = { ...state.projectPinnedById };
   if (nextProjectPinnedById[projectId]) {
-    delete nextProjectPinnedById[projectId];
+    nextProjectPinnedById[projectId] = false;
   } else {
     nextProjectPinnedById[projectId] = true;
   }

You can send follow-ups to this agent here.

- Thread and project pin state now flows through decider, projector, snapshots, and SQL persistence
- Updates UI, contracts, and tests to cover pinned read models and mutations
@github-actions github-actions bot added size:L 100-499 changed lines (additions + deletions). and removed size:M 30-99 changed lines (additions + deletions). labels Mar 30, 2026
- Include project pin state in projection snapshots
- Show the pin indicator before project names in the sidebar
- Replace manual bit transforms with `Schema.BooleanFromBit`
- Keep project and thread projection mapping consistent
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Pin hover button overlaps thread meta on running threads
    • Unified the threadMetaClassName for running and non-running threads so both get the group-hover opacity-0 transition, preventing the pin button from overlapping visible thread metadata.

Create PR

Or push these changes by commenting:

@cursor push 6a48356137
Preview (6a48356137)
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx
--- a/apps/web/src/components/Sidebar.tsx
+++ b/apps/web/src/components/Sidebar.tsx
@@ -1439,9 +1439,7 @@
       const isConfirmingArchive = confirmingArchiveThreadId === thread.id && !isThreadRunning;
       const threadMetaClassName = isConfirmingArchive
         ? "pointer-events-none opacity-0"
-        : !isThreadRunning
-          ? "pointer-events-none transition-opacity duration-150 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0"
-          : "pointer-events-none";
+        : "pointer-events-none transition-opacity duration-150 group-hover/menu-sub-item:opacity-0 group-focus-within/menu-sub-item:opacity-0";
       const hoverActionClassName =
         "inline-flex size-5 cursor-pointer items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring";
       const hoverActionGroupClassName =

You can send follow-ups to this agent here.

<TooltipPopup side="top">Archive</TooltipPopup>
</Tooltip>
) : null}
</div>
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.

Pin hover button overlaps thread meta on running threads

Medium Severity

The hover action group (containing the pin button) now renders for running threads via !isConfirmingArchive, but threadMetaClassName for running threads is just "pointer-events-none" without the group-hover/menu-sub-item:opacity-0 fade. Previously, no hover actions appeared for running threads so this was fine, but now the pin button fades in on top of the visible thread metadata (status indicator, time-ago text), causing visual overlap that makes both elements hard to read or interact with.

Additional Locations (1)
Fix in Cursor Fix in Web

@juliusmarminge juliusmarminge changed the title Prioritize pinned projects in sidebar sorting Persist project and thread pinning in orchestration Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). 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