feat(web): drag pinned threads into your own order - #5581
Conversation
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Needs human review New feature adding drag-to-reorder for pinned threads, including database schema changes, new server capabilities, new UI interactions, and substantial new sorting logic across web and mobile platforms. Warrants human review due to scope. You can customize Macroscope's approvability policy. Learn more. |
|
Hey @t3dotgg — quick heads-up that I opened #5528 yesterday for the same request, after comparing against Codex’s pinned-task sidebar. That PR is rebased on current Happy for you to take whichever implementation or pieces fit best; the mobile actions and edge-case fixes/tests in #5528 may be useful even if #5581 is the preferred base. |
Pinned threads in the v2 sidebar can now be dragged into a custom order. Each pinned thread carries an optional fractional-index pinOrderKey stored on its own server, so a drag writes one key to one thread — the merged pinned list stays consistent across clients and across multiple connected servers without any server seeing the full list. - contracts: thread.pin.reorder command, thread.pin-reordered event, optional orderKey on thread.pin, pinOrderKey on thread shells, and a threadPinReorder capability flag for version skew - server: decider case (rejects unpinned, idempotent re-emission), projection column + migration 038, key cleared on unpin/settle - web: dnd-kit sortable on the pinned block (same pattern as v1 project reordering), optimistic order until the event round-trips, keyed threads sort first with keyless/legacy-server pins in creation order below - mobile: read-side sort by the same keys so both platforms render the order arranged on web Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le Move up/down Review fixes and cherry-picks from the parallel implementation (#5528): - The optimistic drag override now releases on ANY pinned-membership change (new pin, unpin, snooze/wake) instead of only exact sequence match, so a pin landing mid-drag can no longer freeze the override and launder a stale order into later drags. - attemptPin anchors the new pin's key against the DISPLAYED order, and every pin path (chat header, context menus, mobile) now sends a top-of-run orderKey, so the same action never places differently. - Pin-order key math moved to client-runtime (state/thread-sort) and is shared by web and mobile instead of mirrored. - Mobile: Move up / Move down actions in the pinned row menu, computed against the canonical arranged order so search or project scoping never disables or misdirects a move. - Docs: user page on organizing threads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
24ac159 to
246bf3f
Compare
[CLAUDE-FABLE-5] RESPONDING ON BEHALF OF THEORebased onto latest main and pushed a follow-up commit addressing the comparison review against #5528:
|
There was a problem hiding this comment.
Reviewed the Effect-side changes (contracts schemas, decider/projector cases, migration 038, client-runtime command atoms, and the web/mobile call sites) against the service conventions. Imports, Effect.fn/Effect.gen usage, and the new migration module follow the existing patterns, and no Layer.succeed/ManagedRuntime.make/runPromise misuse was introduced. One error-modeling violation found.
Posted via Macroscope — Effect Service Conventions
… across platforms Review-bot follow-ups: - Distinct ThreadPinReorderUnsupportedError so a reorder-capability gap no longer reports pinning guidance (Effect service conventions) - The optimistic drag override also releases when any pinOrderKey changes (our write confirming, or a concurrent client's reorder), closing the stuck-override window for same-membership remote reorders - Web now sorts ALL pinned threads with the shared keyed rule; server capability only gates dragging, so mixed-version fleets render one order on web and mobile - attemptPin defers to pinThread's all-shells head so a snoozed pin holding the smallest key can't strand a fresh pin mid-run - Materialization writes go out sequentially, stopping at the first failure (no rollback by design: each key write is a complete placement) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… tiebreak Remaining Macroscope findings on the Move up/down commit: - handleMenuAction and the tablet sidebar's renderListItem were missing the new move callbacks/inputs in their dependency arrays, so recycled rows could move the WRONG pinned thread (stale thread closure) or show stale enabled/disabled move actions - movePinnedThread now carries the same in-flight guard as snoozeThread: a second tap before the first write's event lands would plan from the same snapshot and silently collapse two moves into one - sortPinnedThreadsByOrderKey tiebreaks by id THEN environmentId — thread ids are only environment-unique, and the pinned block merges environments, so id alone could leave equal-key rows in stream-arrival order and diverge across clients Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Caution CodeRabbit couldn't update its existing comment. The review summary may be out of date. Error details |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
apps/web/src/hooks/useThreadActions.ts (1)
102-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueComment states
Nullbut the function returnsundefined.The doc comment at Line 103 says the helper returns "Null (keyless, ...)". The function returns
undefinedso the caller can omit the field via the spread at Line 563. Align the comment with the return type.📝 Proposed comment fix
/** Key that sorts before every arranged pinned thread, so a fresh pin lands - at the top of the run. Null (keyless, sorts with the legacy block) when + at the top of the run. Undefined (keyless, sorts with the legacy block) when key math can't produce one — pinning must never fail on placement. */🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/src/hooks/useThreadActions.ts` around lines 102 - 112, Update the documentation for topOfPinnedRunOrderKey to state that it returns undefined when key generation fails, matching its return type and caller behavior; leave the implementation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/server/src/orchestration/Layers/ProjectionPipeline.ts`:
- Line 615: Add focused projection-layer tests for the thread pin event handlers
covering `thread.pinned`, `thread.unpinned`, and `thread.pin-reordered`. Assert
absent `pinOrderKey` initializes correctly, legacy `thread.pinned` retains the
existing value, unpinning clears it, and reordering persists the updated value.
In `@apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts`:
- Line 321: Extend the tests around the existing snapshot and detail assertions
in ProjectionSnapshotQuery to cover a persisted non-null pinOrderKey. Add a
fixture with a specific order key, then assert every changed snapshot and detail
surface propagates that exact value while retaining the existing null-path
coverage.
In `@apps/web/src/components/SidebarV2.tsx`:
- Around line 411-429: Add keyboard reordering to SortablePinnedThreadRow by
registering KeyboardSensor with sortableKeyboardCoordinates in pinnedDndSensors,
exposing dnd-kit attributes and setActivatorNodeRef, and applying them to a
dedicated drag handle so the card’s existing handleKeyDown remains unaffected.
Update docs/user/thread-sidebar.md lines 7-9 to document both dragging and
keyboard-based reordering for web and desktop users.
---
Nitpick comments:
In `@apps/web/src/hooks/useThreadActions.ts`:
- Around line 102-112: Update the documentation for topOfPinnedRunOrderKey to
state that it returns undefined when key generation fails, matching its return
type and caller behavior; leave the implementation unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 315d0920-dd14-4208-8135-9af03ec689cf
📒 Files selected for processing (33)
apps/mobile/src/features/home/HomeRouteScreen.tsxapps/mobile/src/features/home/HomeScreen.tsxapps/mobile/src/features/home/useThreadListActions.tsapps/mobile/src/features/threads/ThreadNavigationSidebar.tsxapps/mobile/src/features/threads/thread-list-v2-items.tsxapps/mobile/src/features/threads/threadListV2.tsapps/server/src/environment/ServerEnvironment.tsapps/server/src/orchestration/Layers/ProjectionPipeline.tsapps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.tsapps/server/src/orchestration/Layers/ProjectionSnapshotQuery.tsapps/server/src/orchestration/Schemas.tsapps/server/src/orchestration/decider.pinned.test.tsapps/server/src/orchestration/decider.tsapps/server/src/orchestration/projector.tsapps/server/src/persistence/Layers/ProjectionThreads.tsapps/server/src/persistence/Migrations.tsapps/server/src/persistence/Migrations/038_ProjectionThreadsPinOrderKey.tsapps/server/src/persistence/Services/ProjectionThreads.tsapps/web/src/components/Sidebar.logic.test.tsapps/web/src/components/Sidebar.logic.tsapps/web/src/components/SidebarV2.tsxapps/web/src/hooks/useThreadActions.tsapps/web/src/state/entities.tsdocs/README.mddocs/user/thread-sidebar.mdpackages/client-runtime/src/operations/commands.tspackages/client-runtime/src/state/threadCommands.tspackages/client-runtime/src/state/threadDetail.tspackages/client-runtime/src/state/threadReducer.tspackages/client-runtime/src/state/threadSort.test.tspackages/client-runtime/src/state/threadSort.tspackages/contracts/src/environment.tspackages/contracts/src/orchestration.ts
… projections CodeRabbit follow-ups: - projector: full pin order key lifecycle — fresh pin stores the key, legacy thread.pinned events without the field preserve it, reorder persists the new slot, unpin clears it - snapshot query: thread-1 fixture now carries a persisted pinned_at + pin_order_key, asserting the non-null path end to end through SQLite hydration into both the thread and shell surfaces - docs: topOfPinnedRunOrderKey comment says undefined, matching the type Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopts pingdotgg#5581 (drag pinned threads into your own order) on top of the pingdotgg#5372 merge already on this branch. Pinned rows now sort by the user's arranged pinOrderKey on every platform, which is deliberately independent of the fork's thread-grouping preference: that preference still orders the active rows, but a pin is an explicit arrangement and upstream documents the sort as identical everywhere so mixed-version fleets cannot disagree. Server capability gates dragging only, never the sort. SidebarV2 keeps the fork's environment/ownership filters around the new pinned block and gains upstream's reorderablePinnedKeys. Welds the auto-merge produced, all caught by typecheck or tests: - useThreadActions.ts: upstream's ThreadPinReorderUnsupportedError landed inside the fork's clearPerThreadClientState, leaving it unclosed. - threadListV2.ts: two `for (const thread of ...)` headers stacked on one loop body; the pinned block takes upstream's order-key sort. - useThreadListActions.ts: duplicate threadEnvironment import plus local copies of helpers this fork imports from threadActionMessages. - HomeRouteScreen: upstream's single-environment onEnvironmentChange handler does not exist in this fork's multi-select screen. - projector.pinned.test.ts indexed state.threads[0]; this fork's read model keys threads by id in a HashMap. - Six migration-ledger fixtures pinned the tail at 37; upstream's 038 ProjectionThreadsPinOrderKey extends it. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
Adopts pingdotgg#5581 (drag pinned threads into your own order) on top of the pingdotgg#5372 merge already on this branch. Pinned rows now sort by the user's arranged pinOrderKey on every platform, which is deliberately independent of the fork's thread-grouping preference: that preference still orders the active rows, but a pin is an explicit arrangement and upstream documents the sort as identical everywhere so mixed-version fleets cannot disagree. Server capability gates dragging only, never the sort. SidebarV2 keeps the fork's environment/ownership filters around the new pinned block and gains upstream's reorderablePinnedKeys. Welds the auto-merge produced, all caught by typecheck or tests: - useThreadActions.ts: upstream's ThreadPinReorderUnsupportedError landed inside the fork's clearPerThreadClientState, leaving it unclosed. - threadListV2.ts: two `for (const thread of ...)` headers stacked on one loop body; the pinned block takes upstream's order-key sort. - useThreadListActions.ts: duplicate threadEnvironment import plus local copies of helpers this fork imports from threadActionMessages. - HomeRouteScreen: upstream's single-environment onEnvironmentChange handler does not exist in this fork's multi-select screen. - projector.pinned.test.ts indexed state.threads[0]; this fork's read model keys threads by id in a HashMap. - Six migration-ledger fixtures pinned the tail at 37; upstream's 038 ProjectionThreadsPinOrderKey extends it. Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>
## What's Changed * fix(mobile): reconnects no longer shift the thread list by @t3dotgg in pingdotgg/t3code#5372 * feat(web): drag pinned threads into your own order by @t3dotgg in pingdotgg/t3code#5581 **Full Changelog**: pingdotgg/t3code@v0.0.33-nightly.20260807.1025...v0.0.33-nightly.20260807.1026 Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.33-nightly.20260807.1026
Catches the fork up on 101 upstream commits (2026-08-03..08-07, through v0.0.32), and adopts upstream's thread pinning and pinned-thread ordering in place of the fork's own manual thread ordering. Ordering: upstream's pingdotgg#5312 and pingdotgg#5581 implement pinning plus drag ordering server-side -- pinned/pinOrderKey columns, fractional base-26 order keys, synced across devices, with mobile move up/down. The fork's version was client-only (uiStateStore.threadOrder), single-device, and its two follow-up fixes existed to patch problems the key-based design does not have (dense array rewrites, stale ranks, undraggable new rows). Upstream's supersedes it on every axis the fork cared about except ordering unpinned threads and multi-select drag, both of which we are choosing to drop. So a124e82, 907e62f and cda5f09 are dropped: SidebarV2, Sidebar.logic, and uiStateStore take upstream's side wholesale, and the design docs for the fork feature go with them. thread.clear survives the SidebarV2 rewrite, re-applied onto upstream's extracted buildThreadActionMenuItems. Its capability is optional on the menu state rather than a plain boolean, so upstream's new chat-header menu does not silently inherit a destructive action nobody wired up there. Migrations: upstream claimed 036-038, so the fork's 036_MemoryAndDrive is renumbered to 039 -- the number is the applied-order key, so it moves rather than collides. Its test migrated to 36 and now migrates to 39. Editor discovery: upstream added its own Clock-based discovery cache, which replaces the fork's Effect.cachedWithTTL from a1e7a48. Same defect fixed the same way; theirs is the maintained one. Note this lowers the cached TTL from 5 minutes to upstream's 60s -- still no per-request PATH sweep, which was the actual bug. Effect beta.103 (upstream 2b1d4fe) tightened two things the fork's own tests tripped on: McpServer.layerHttp now requires `protocols`, and MCP sessions reject requests that omit the mcp-protocol-version header, so the memory toolkit listing test needed both. The VCS upstream-fetch constants from 81e99bb survive untouched; upstream did not go near them.
Pinned threads were stuck in creation order — pinning says "keep this on top" but you couldn't say which one goes first. Now you can drag pinned cards in the v2 sidebar into whatever order you want, and that order holds everywhere: every client, every platform, even with multiple servers connected at once.
How it syncs
Each pinned thread stores an optional fractional-index
pinOrderKey(lexorank-style string) on its own server, next topinnedAt. Clients sort the merged pinned list by plain string comparison of keys, so:pinnedAtalready does. Concurrent drags of the same thread are last-write-wins; equal keys fall back to a deterministic id tiebreak.threadPinReordercapability: threads on older servers aren't draggable and keep today's creation order below the arranged run.Changes
thread.pin.reordercommand,thread.pin-reorderedevent, optionalorderKeyonthread.pin,pinOrderKeyon thread shells, capability flagThreads pinned before this ships stay keyless and get a key materialized on their first drag — no backfill. A pinned thread's key survives snooze, so it wakes back into its exact slot.
Tests: fractional key math (incl. 100-deep insertion churn), reorder planning, pinned sort, decider cases. Full typecheck/lint/test suites green across contracts, client-runtime, server, web, and mobile.
Built by Claude Fable 5 via Claude Code.
🤖 Generated with Claude Code
Note
Medium Risk
Touches orchestration, persistence, and multi-client ordering with optimistic UI; failures are mostly localized, but partial multi-write materialization and version skew across environments need careful rollout.
Overview
Adds user-arranged pinned thread order synced through the server using fractional
pinOrderKeystrings, gated by a newthreadPinReordercapability.Backend & contracts: New
thread.pin.reorder/thread.pin-reordered, optionalorderKeyon pin,pin_order_keycolumn (migration 038), and decider/projector rules (fresh pins accept a key, re-pins preserve placement, unpin clears the key).Shared client logic:
thread-sorthelpers (pinOrderKeyBetween,planPinnedReorder,planPinnedMove,sortPinnedThreadsByOrderKey) so web and mobile render the same merged pinned order; keyless legacy pins stay below keyed ones.Web: dnd-kit drag on the pinned block with optimistic order until events land; new pins get a key at the top of the arranged run.
Mobile: Move up / Move down in the thread menu (canonical order, not filtered list), with in-flight guards like snooze.
User docs added for organizing pinned threads.
Reviewed by Cursor Bugbot for commit cec4a57. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add drag-to-reorder pinned threads in the sidebar
pinOrderKeyfractional index on pinned threads, persisted via a newthread.pin.reordercommand,thread.pin-reorderedevent, and DB migration addingpin_order_keytoprojection_threads.@dnd-kit/sortablewith optimistic reordering, released when canonical server state converges or membership changes.planPinnedReorderandpinOrderKeyBetweenutilities inthreadSort.tsminimize key writes (single write when neighbors are keyed, full spread otherwise).threadPinReordercapability flag gates all reorder UI; environments that don't advertise it show pins in legacy creation-time order without drag handles.Macroscope summarized cec4a57.
Summary by CodeRabbit