Skip to content

Persist project and thread pinning in orchestration#1588

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

Persist project and thread pinning in orchestration#1588
juliusmarminge wants to merge 8 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

Note

Medium Risk
Touches cross-cutting orchestration contracts, event/command handling, and SQLite projections/migrations; mistakes could break snapshot decoding or existing DBs, but the change is mostly additive with defaults/nulls and test coverage updates.

Overview
Adds first-class pinning persistence for projects and threads by introducing pinnedAt on orchestration read models and projection tables, and wiring a pinned boolean through project.create/thread.create plus project.meta.update/thread.meta.update so the decider emits pinnedAt timestamps (or null) in created/meta-updated events.

Updates the server projection pipeline, snapshot query decoding/SQL selects, and projection repositories to store/load pinned_at, including a new SQLite migration (019_ProjectionPins) that adds the pinned_at column to projection_projects and projection_threads.

Updates the web app types/store mapping and sidebar behavior to use orchestration-provided pin state: pinned projects/threads sort ahead of unpinned (with pinned items ordered by most-recent pinnedAt), adds pin/unpin context menu actions, and renders a global pinned-threads section; tests and fixtures are updated to include pinned/pinnedAt throughout.

Written by Cursor Bugbot for commit c8e8668. This will update automatically on new commits. Configure here.

Note

Persist project and thread pinning state across orchestration, sidebar, and database

  • Adds pinnedAt fields to OrchestrationProject and OrchestrationThread contracts and a pinned boolean to create/meta-update commands, with defaults of null/false for backward compatibility.
  • The decider emits pinnedAt timestamps on project.created and thread.created events and toggles them in project.meta-updated and thread.meta-updated when a pinned flag is present.
  • A new database migration (019_ProjectionPins.ts) adds pinned_at columns to projection_projects and projection_threads; persistence and snapshot query layers read and write these columns.
  • The sidebar gains pin/unpin context menu actions for threads and projects, renders a new global pinned-threads section at the top, shows a pin badge on pinned projects, and excludes pinned threads from per-project lists.
  • Sidebar sort order now places pinned items first, ordered by pinnedAt recency, before existing timestamp/name tie-breakers.

Macroscope summarized c8e8668.

- 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: c1d83d46-eb41-4a80-bcf0-1a1446bbb2b4

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.

@juliusmarminge juliusmarminge changed the title Prioritize pinned projects in sidebar sorting Persist project and thread pinning in orchestration Mar 30, 2026
- Render pinned threads above projects
- Remove pin indicators from project rows
- Keep archive and status actions working in the shared row renderer
- replace boolean pin state with pinnedAt timestamps
- update orchestration, persistence, and sidebar sorting
- Drop legacy pinned columns from projection tables
- Consolidate pin timestamp migration and update projection writes
- Fix sidebar pin icon hover state
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