diff --git a/src/lib/components/ConversationTabs.svelte b/src/lib/components/ConversationTabs.svelte index 34c9d87..75fe9e1 100644 --- a/src/lib/components/ConversationTabs.svelte +++ b/src/lib/components/ConversationTabs.svelte @@ -12,6 +12,25 @@ let editingTabId = $state(null); let editingName = $state(""); + // Tab order for pointer-drag reordering + let tabOrder = $state([]); + let draggedId = $state(null); + let dragOverId = $state(null); + let dragStartX = 0; + let isDragging = false; + let wasDragged = false; + let tabsRef = $state(null); + + // Keep tabOrder in sync with conversations map (add new, remove deleted) + $effect(() => { + const currentIds = Array.from($conversations.keys()); + const validIds = tabOrder.filter((id) => currentIds.includes(id)); + const newIds = currentIds.filter((id) => !tabOrder.includes(id)); + if (validIds.length !== tabOrder.length || newIds.length > 0) { + tabOrder = [...validIds, ...newIds]; + } + }); + // Track last seen message count for each conversation let lastSeenMessageCount = new SvelteMap(); @@ -138,6 +157,63 @@ } } + async function handleTabClick(id: string) { + if (wasDragged) { + wasDragged = false; + return; + } + await switchTab(id); + } + + function handlePointerDown(event: PointerEvent, id: string) { + if (editingTabId === id) return; + draggedId = id; + dragStartX = event.clientX; + isDragging = false; + wasDragged = false; + + function onMove(e: PointerEvent) { + if (!isDragging && Math.abs(e.clientX - dragStartX) > 5) { + isDragging = true; + } + if (!isDragging || !tabsRef) return; + const tabs = tabsRef.querySelectorAll("[data-tab-id]"); + dragOverId = null; + for (const tab of tabs) { + const rect = tab.getBoundingClientRect(); + if (e.clientX >= rect.left && e.clientX <= rect.right) { + const tabId = tab.dataset.tabId; + if (tabId && tabId !== id) { + dragOverId = tabId; + } + break; + } + } + } + + function onUp() { + if (isDragging && draggedId && dragOverId && draggedId !== dragOverId) { + const order = [...tabOrder]; + const fromIndex = order.indexOf(draggedId); + const toIndex = order.indexOf(dragOverId); + order.splice(fromIndex, 1); + order.splice(toIndex, 0, draggedId); + tabOrder = order; + wasDragged = true; + } + draggedId = null; + dragOverId = null; + isDragging = false; + window.removeEventListener("pointermove", onMove); + window.removeEventListener("pointerup", onUp); + window.removeEventListener("pointercancel", onUp); + } + + window.addEventListener("pointermove", onMove); + window.addEventListener("pointerup", onUp); + window.addEventListener("pointercancel", onUp); + } + // Keyboard shortcuts onMount(() => { function handleGlobalKeydown(event: KeyboardEvent) { @@ -165,21 +241,19 @@ // Ctrl/Cmd + Tab: Next tab else if ((event.ctrlKey || event.metaKey) && event.key === "Tab" && !event.shiftKey) { event.preventDefault(); - const tabs = Array.from($conversations.keys()); - const currentIndex = tabs.findIndex((id) => id === $activeConversationId); + const currentIndex = tabOrder.findIndex((id) => id === $activeConversationId); if (currentIndex !== -1) { - const nextIndex = (currentIndex + 1) % tabs.length; - claudeStore.switchConversation(tabs[nextIndex]); + const nextIndex = (currentIndex + 1) % tabOrder.length; + claudeStore.switchConversation(tabOrder[nextIndex]); } } // Ctrl/Cmd + Shift + Tab: Previous tab else if ((event.ctrlKey || event.metaKey) && event.key === "Tab" && event.shiftKey) { event.preventDefault(); - const tabs = Array.from($conversations.keys()); - const currentIndex = tabs.findIndex((id) => id === $activeConversationId); + const currentIndex = tabOrder.findIndex((id) => id === $activeConversationId); if (currentIndex !== -1) { - const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length; - claudeStore.switchConversation(tabs[prevIndex]); + const prevIndex = (currentIndex - 1 + tabOrder.length) % tabOrder.length; + claudeStore.switchConversation(tabOrder[prevIndex]); } } } @@ -190,15 +264,22 @@
- {#each Array.from($conversations.entries()) as [id, conversation] (id)} + {#each tabOrder + .filter((id) => $conversations.has(id)) + .map((id) => ({ id, conversation: $conversations.get(id)! })) as { id, conversation } (id)}
switchTab(id)} + : 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-terminal)]/50'} + {dragOverId === id && draggedId !== id ? 'drag-over' : ''} + {draggedId === id ? 'dragging' : ''}" + onpointerdown={(e) => handlePointerDown(e, id)} + onclick={() => handleTabClick(id)} onkeydown={(e) => handleTabKeydown(id, e)} role="tab" tabindex={0} @@ -211,7 +292,7 @@ onblur={saveTabName} onkeydown={handleKeydown} onclick={(e) => e.stopPropagation()} - class="bg-transparent border-b border-[var(--border-color)] outline-none px-0 py-0 text-sm w-32" + class="bg-transparent border-b border-[var(--border-color)] outline-none px-0 py-0 text-sm w-32 select-text" /> {:else}
@@ -296,5 +377,20 @@ .tab-item { min-width: 100px; + cursor: grab; + touch-action: none; + user-select: none; + } + + .tab-item:active { + cursor: grabbing; + } + + .drag-over { + border-left: 2px solid var(--accent-primary); + } + + .dragging { + opacity: 0.4; }