Skip to content

Add drag-to-group conversations feature#2153

Closed
gary149 wants to merge 4 commits intomainfrom
claude/drag-conversations-grouping-GpwPJ
Closed

Add drag-to-group conversations feature#2153
gary149 wants to merge 4 commits intomainfrom
claude/drag-conversations-grouping-GpwPJ

Conversation

@gary149
Copy link
Collaborator

@gary149 gary149 commented Feb 26, 2026

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

  • New ConversationGroup type to represent groups with name and collapse state
  • New ConvGroupSidebar type for sidebar rendering with embedded conversations
  • Added optional groupId field to Conversation type
  • Added optional groupId field to ConvSidebar type
  • New conversationGroups MongoDB collection with indexes on userId and sessionId

API Endpoints

  • GET /api/v2/conversation-groups — List user's groups with embedded conversation data
  • POST /api/v2/conversation-groups — Create new group from conversation IDs
  • PATCH /api/v2/conversation-groups/[id] — Update group name or collapse state
  • DELETE /api/v2/conversation-groups/[id] — Delete group and ungroup conversations
  • PATCH /api/v2/conversation-groups/[id]/conversations — Add/remove conversations from group
  • POST /api/v2/conversation-groups/[id]/generate-name — Generate group name via task model
  • Modified GET /api/v2/conversations to exclude grouped conversations

UI Components

  • New NavConversationGroup.svelte — Group header with expand/collapse, rename, and delete actions
  • New DragOverlay.svelte — Floating ghost element during drag operations
  • New DragState store — Manages drag state across components
  • Modified NavConversationItem.svelte — Added pointer event handlers for drag initiation (desktop and 500ms long-press mobile)
  • Modified NavMenu.svelte — Integrated groups into time-section rendering, coordinates drag operations
  • Modified +layout.svelte — Added group state management and CRUD handlers

Group Name Generation

  • New groupName.ts text generation module that uses the task model to generate concise 2-4 word group names from conversation titles

Interaction Design

  • Desktop: Drag initiated after 5px pointer movement, creates floating ghost element with visual feedback
  • Mobile: Long-press (500ms) initiates drag with haptic feedback
  • Drop targets highlight with blue ring and visual indicators
  • Groups positioned by their max conversation updatedAt within time sections
  • Auto-delete empty groups when last conversation is removed
  • Drag-out to empty space removes conversation from group

Notable Implementation Details

  • Uses Pointer Events (not HTML5 DnD) for cross-platform consistency and fine-grained control
  • Groups are injected into time sections alongside ungrouped conversations, sorted by updatedAt
  • Group's updatedAt is computed as the max of its member conversations' updatedAt values
  • Name generation is async and non-blocking — returns "New Group" placeholder immediately
  • Conversations reference their group via groupId rather than groups storing conversation IDs
  • Supports both user-provided and AI-generated group names
  • Grouped conversations are excluded from the main conversations endpoint pagination

https://claude.ai/code/session_01P5DLYUSsirA261YA2sUnhc

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
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +193 to +197
if (sourceGroupId) {
// First remove from current group, then create new group
onremoveFromGroup?.({ groupId: sourceGroupId, convId });
}
oncreateGroup?.([convId, dropTarget.id]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +95 to +97
await collections.conversations.updateMany(
{ _id: { $in: convObjectIds }, ...authCondition(locals) },
{ $set: { groupId: groupDoc._id } }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

claude and others added 2 commits February 26, 2026 10:28
…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.
@gary149 gary149 closed this Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants