Conversation
Allow users to drag one conversation on top of another to create a named group. Groups appear inline within time sections (Today, This week, etc.) positioned by their most-recently-updated conversation. Group names are auto-generated via the task model using conversation titles. - New ConversationGroup collection with CRUD API endpoints - Pointer events-based drag & drop (desktop: 5px threshold, mobile: 500ms long-press) - Ghost overlay, blue ring drop targets, opacity feedback during drag - Collapsible groups with inline rename, delete, and membership management - Auto-delete empty groups when last conversation is removed - Grouped conversations excluded from paginated sidebar list https://claude.ai/code/session_01P5DLYUSsirA261YA2sUnhc
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af58985717
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
src/lib/components/NavMenu.svelte
Outdated
| if (sourceGroupId) { | ||
| // First remove from current group, then create new group | ||
| onremoveFromGroup?.({ groupId: sourceGroupId, convId }); | ||
| } | ||
| oncreateGroup?.([convId, dropTarget.id]); |
There was a problem hiding this comment.
Await source-group removal before creating a new group
In the drag-to-conversation path, onremoveFromGroup and oncreateGroup are fired back-to-back without waiting, so network completion order is nondeterministic. removeFromGroup in +layout.svelte re-inserts the dragged conversation into the ungrouped list when it resolves, which means dragging a conversation out of a group onto another conversation can leave that chat duplicated (new group + ungrouped) when the remove request finishes after the create request.
Useful? React with 👍 / 👎.
| await collections.conversations.updateMany( | ||
| { _id: { $in: convObjectIds }, ...authCondition(locals) }, | ||
| { $set: { groupId: groupDoc._id } } |
There was a problem hiding this comment.
Delete emptied source groups when reassigning conversations
Creating a group reassigns groupId for the selected conversations but never cleans up any previous groups those conversations were moved out of. If one of those source groups had only that conversation, it becomes an orphaned empty group, and GET /api/v2/conversation-groups will still return it because it reads all group documents regardless of member count.
Useful? React with 👍 / 👎.
| export const PATCH: RequestHandler = async ({ locals, params, request }) => { | ||
| requireAuth(locals); | ||
|
|
||
| const groupId = new ObjectId(params.id); |
There was a problem hiding this comment.
Validate group id format before constructing ObjectId
This route constructs new ObjectId(params.id) directly, which throws on malformed IDs and surfaces as a 500 instead of a client error. The existing conversations API validates IDs first and returns 400; these new group routes should do the same to avoid turning simple bad input (or mistyped URLs) into server errors.
Useful? React with 👍 / 👎.
…idation - Fix P1 race condition: add sourceGroupId param to POST create and PATCH membership endpoints so source group cleanup happens atomically on the server instead of via two separate fire-and-forget client calls - Fix P2 orphaned groups: after reassigning conversations to a new group, detect and delete any source groups that became empty - Fix P2 ObjectId validation: add ObjectId.isValid() checks before new ObjectId() in all 4 conversation-groups API route files, returning 400 instead of unhandled 500 on invalid IDs https://claude.ai/code/session_01P5DLYUSsirA261YA2sUnhc
The <a> element's default draggable behavior was intercepting pointer events, preventing the custom drag-to-group interaction from activating.
Summary
Implements a drag-to-group conversations feature that allows users to organize conversations into named groups by dragging one conversation onto another. Groups appear inline within time sections in the sidebar, positioned by their most recently updated conversation. Group names are auto-generated using the task model. The feature is available to all instances and supports both desktop (pointer events) and mobile (long-press) interactions.
Key Changes
Data Model
ConversationGrouptype to represent groups with name and collapse stateConvGroupSidebartype for sidebar rendering with embedded conversationsgroupIdfield toConversationtypegroupIdfield toConvSidebartypeconversationGroupsMongoDB collection with indexes on userId and sessionIdAPI Endpoints
GET /api/v2/conversation-groups— List user's groups with embedded conversation dataPOST /api/v2/conversation-groups— Create new group from conversation IDsPATCH /api/v2/conversation-groups/[id]— Update group name or collapse stateDELETE /api/v2/conversation-groups/[id]— Delete group and ungroup conversationsPATCH /api/v2/conversation-groups/[id]/conversations— Add/remove conversations from groupPOST /api/v2/conversation-groups/[id]/generate-name— Generate group name via task modelGET /api/v2/conversationsto exclude grouped conversationsUI Components
NavConversationGroup.svelte— Group header with expand/collapse, rename, and delete actionsDragOverlay.svelte— Floating ghost element during drag operationsDragStatestore — Manages drag state across componentsNavConversationItem.svelte— Added pointer event handlers for drag initiation (desktop and 500ms long-press mobile)NavMenu.svelte— Integrated groups into time-section rendering, coordinates drag operations+layout.svelte— Added group state management and CRUD handlersGroup Name Generation
groupName.tstext generation module that uses the task model to generate concise 2-4 word group names from conversation titlesInteraction Design
Notable Implementation Details
groupIdrather than groups storing conversation IDshttps://claude.ai/code/session_01P5DLYUSsirA261YA2sUnhc