Persist project and thread pinning in orchestration#1588
Persist project and thread pinning in orchestration#1588juliusmarminge wants to merge 5 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.
| <TooltipPopup side="top">Archive</TooltipPopup> | ||
| </Tooltip> | ||
| ) : null} | ||
| </div> |
There was a problem hiding this comment.
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.



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.ts