Skip to content

feat: copy popover, optimistic sub-chats, worktree cleanup, sortable sidebar, draggable tabs#23

Merged
aletc1 merged 3 commits into
devfrom
claude/modest-pascal-9de8c3
Apr 18, 2026
Merged

feat: copy popover, optimistic sub-chats, worktree cleanup, sortable sidebar, draggable tabs#23
aletc1 merged 3 commits into
devfrom
claude/modest-pascal-9de8c3

Conversation

@aletc1
Copy link
Copy Markdown
Owner

@aletc1 aletc1 commented Apr 18, 2026

Summary

Bundles six related UX/cleanup improvements plus a merge of dev:

  • F1 Copy button in the text-selection popover
  • F2 Optimistic sub-chat creation (instant UI, RPC in background, rollback on failure)
  • F3 Worktree directory cleanup (rmdir guard, project-delete cascade, startup orphan scan)
  • F4 Native HTML5 DnD on tab bar + grab cursor
  • F5 Sortable sub-chats sidebar via @dnd-kit + grip handle on hover
  • F6 Cached file_stats_* columns on sub_chats + auto-delete empty sub-chats on tab close / app quit

Test plan

F1 — Copy button in selection popover

  • Select text in an assistant message → popover shows Add to context + Copy. Click Copy → CopyIcon morphs into CheckIcon for ~2s. Paste in another app → matches.
  • Select text inside a diff or tool-edit block → popover shows Add to context + Copy + Reply. Copy still works.
  • Selection in file-viewer → popover does not appear (uses native context menu).

F2 — Optimistic sub-chat creation

  • Local mode: click + 5× rapidly → all 5 tabs appear instantly with no waiting spinner.
  • Reload window → all 5 sub-chats persisted (DB write succeeded in background).
  • Sandbox mode: + still uses lazy creation; first message persists the row.
  • Failure path: temporarily throw inside chats.createSubChat server-side → tab appears, then disappears with toast "Failed to create chat".

F3 — Worktree cleanup

  • Create a chat → ~/.21st/worktrees/<slug>/<folder>/ exists. Delete the chat → directory is gone.
  • Create 3 chats in one project, then delete the project → all 3 worktree dirs gone, parent slug dir gone if empty.
  • Pre-stage an orphan: mkdir ~/.21st/worktrees/fake/orphan && touch ~/.21st/worktrees/fake/orphan/x. Restart the app → log line [WorktreeCleanup] Scanned N worktree dirs, removed M orphans shows the orphan was removed (~5s after start).
  • Tamper test: in db:studio, set a chat's worktreePath to /tmp/test. Delete the chat → /tmp/test is not removed (path-prefix guard rejects).
  • Symlink test: ln -s ~ ~/.21st/worktrees/<slug>/<folder>/escape. Delete the chat → home dir contents intact (Node fs.rm only unlinks the symlink, doesn't follow it).

F4 — Native DnD on tab bar

  • Switch to tabs mode (close the Chats sidebar via the « icon). Open ≥ 2 sub-chats.
  • Hover a tab → cursor shows grab. Hover a split-pair tab (with border) → cursor stays pointer (drag disabled).
  • Click+drag a tab horizontally → insertion marker appears on left/right edge of hovered tab. Release → tabs reorder.
  • Reload window → order persisted via openSubChatIds localStorage.
  • While dragging, a tab being dragged shows reduced opacity.

F5 — Sortable sidebar with @dnd-kit

  • Switch to sidebar mode. Hover a row → grip icon (⋮⋮) fades in on the left edge; cursor shows grab.
  • Drag a row up/down within its section → reorders. Reload → persisted.
  • Pinned and unpinned sections sort independently — dragging across sections is not supported.
  • Split-pair rows (with border) → grip icon is hidden, cursor is pointer (drag disabled).
  • Single click without drag still switches the chat (4px activation distance).

F6 — File stats cache + auto-delete empty

  • Create a sub-chat with no messages → close its tab → row is deleted from DB (verify via bun run db:studio).
  • Create a sub-chat → send a message → close tab → row preserved.
  • Create a sub-chat → quit app via Cmd+Q → reopen → empty unnamed sub-chats are gone, named/messaged ones survive.
  • File-stats numbers (additions/deletions/file count) in the sidebar match what they were pre-change.
  • Make a new edit via Claude → sidebar stats update on next refresh.
  • Streaming-in-flight: close a tab while it's mid-stream → final write still lands (delete is gated on useStreamingStatusStore.isStreaming(id)).

Regression smoke

  • bun run dev boots without console errors.
  • Existing context-menu actions on sub-chats still work (rename, archive, pin, split).
  • Multi-window: open a 2nd window, create a chat there — no cross-window deletion.
  • No new TypeScript errors: bun run ts:check (or npx tsc --noEmit) is no worse than baseline.

Migration

A new Drizzle migration drizzle/0008_shiny_hydra.sql adds three columns to sub_chats. Auto-runs on app start; no manual step needed.

aletc1 and others added 3 commits April 18, 2026 15:31
…up, sortable sidebar, draggable tabs

Bundles six related improvements (F1-F6):
- F1: Copy button in text-selection popover
- F2: Optimistic sub-chat creation with rollback on RPC failure
- F3: Worktree directory cleanup (path-prefix guard, project-delete cascade, startup orphan scan)
- F4: Native HTML5 DnD on the tab bar with insertion marker
- F5: Sortable sidebar rows via @dnd-kit
- F6: Cached file_stats columns + auto-delete empty sub-chats on tab close and app quit

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	bun.lock
#	package.json
#	src/main/lib/trpc/routers/projects.ts
Drag affordances were invisible — users couldn't tell which rows/tabs
were draggable. Sidebar rows now show a hover-revealed GripVertical icon
on the left edge plus cursor-grab on hover. Tab pills show cursor-grab
when draggable. Split-pair items keep cursor-pointer (they're locked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aletc1 aletc1 merged commit 22a5685 into dev Apr 18, 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.

1 participant