Persist project and thread pinning in orchestration#1588
Persist project and thread pinning in orchestration#1588juliusmarminge wants to merge 8 commits intomainfrom
Conversation
- Add project pin state persistence and toggle actions - Show pin affordance in the sidebar and keep pinned projects first
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
- 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
There was a problem hiding this comment.
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
falseinstead of deleting keys, and made syncProjects/syncThreads unconditionally store pinned state for all items, preventing the nullish coalescing fallback from reaching stale persisted sets.
- Changed toggle functions to store
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
- 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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
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.
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.
- 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


Summary
pinnedthrough contracts, commands/events, projector state, projection repositories, snapshot queries, and a sqlite migration for existing databases.Testing
bun fmtbun lintbun typecheckbun run --cwd packages/contracts test src/orchestration.test.tsbun 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.tsbun 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.tsNote
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
pinnedAton orchestration read models and projection tables, and wiring apinnedboolean throughproject.create/thread.createplusproject.meta.update/thread.meta.updateso the decider emitspinnedAttimestamps (ornull) 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 thepinned_atcolumn toprojection_projectsandprojection_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 includepinned/pinnedAtthroughout.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
pinnedAtfields toOrchestrationProjectandOrchestrationThreadcontracts and apinnedboolean to create/meta-update commands, with defaults ofnull/falsefor backward compatibility.pinnedAttimestamps onproject.createdandthread.createdevents and toggles them inproject.meta-updatedandthread.meta-updatedwhen apinnedflag is present.pinned_atcolumns toprojection_projectsandprojection_threads; persistence and snapshot query layers read and write these columns.pinnedAtrecency, before existing timestamp/name tie-breakers.Macroscope summarized c8e8668.