diff --git a/src/lib/components/CloseTabConfirmModal.svelte b/src/lib/components/CloseTabConfirmModal.svelte new file mode 100644 index 0000000..42b9bb8 --- /dev/null +++ b/src/lib/components/CloseTabConfirmModal.svelte @@ -0,0 +1,107 @@ + + + + +{#if isOpen} +
e.key === " " && onCancel()} + > +
e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + role="dialog" + aria-labelledby="confirm-title" + aria-describedby="confirm-message" + tabindex="-1" + > +
+
+
+ + + +
+
+

+ Close Connected Tab? +

+

+ The tab "{tabName}" is currently connected to Claude. Are you sure you want to close + it? This will disconnect the session. +

+
+
+ +
+ + +
+
+
+
+{/if} + + diff --git a/src/lib/components/ConversationTabs.svelte b/src/lib/components/ConversationTabs.svelte index 96864aa..71fcc26 100644 --- a/src/lib/components/ConversationTabs.svelte +++ b/src/lib/components/ConversationTabs.svelte @@ -3,49 +3,35 @@ import { onMount } from "svelte"; import type { Conversation } from "$lib/stores/conversations"; import { SvelteMap } from "svelte/reactivity"; + import CloseTabConfirmModal from "./CloseTabConfirmModal.svelte"; - let conversations: Map = new Map(); - let activeConversationId: string | null = null; - let editingTabId: string | null = null; - let editingName = ""; + // Use store subscriptions with $ syntax + const conversations = $derived(claudeStore.conversations); + const activeConversationId = $derived(claudeStore.activeConversationId); - // Track which conversation actually has the Claude connection - let connectedConversationId: string | null = null; + let editingTabId = $state(null); + let editingName = $state(""); // Track last seen message count for each conversation let lastSeenMessageCount = new SvelteMap(); - claudeStore.conversations.subscribe((convs) => { - conversations = convs; + // Confirmation modal state + let showConfirmModal = $state(false); + let tabToClose = $state(null); + let tabToCloseName = $state(""); - // Update the last seen count for the active conversation - if (activeConversationId) { - const activeConv = convs.get(activeConversationId); + // Update last seen count when active conversation changes + $effect(() => { + if ($activeConversationId) { + const activeConv = $conversations.get($activeConversationId); if (activeConv) { - lastSeenMessageCount.set(activeConversationId, activeConv.terminalLines.length); + lastSeenMessageCount.set($activeConversationId, activeConv.terminalLines.length); + // Trigger reactivity + lastSeenMessageCount = lastSeenMessageCount; } } }); - claudeStore.activeConversationId.subscribe((id) => { - activeConversationId = id; - }); - - // Find the connected conversation - $: { - let foundConnected = false; - for (const [id, conv] of conversations) { - if (conv.connectionStatus === "connected" || conv.connectionStatus === "connecting") { - connectedConversationId = id; - foundConnected = true; - break; - } - } - if (!foundConnected) { - connectedConversationId = null; - } - } - function createNewTab() { claudeStore.createConversation(); } @@ -57,7 +43,7 @@ await claudeStore.switchConversation(id); // Mark messages as seen when switching to this tab - const conv = conversations.get(id); + const conv = $conversations.get(id); if (conv) { lastSeenMessageCount.set(id, conv.terminalLines.length); // Trigger reactivity @@ -67,11 +53,35 @@ function deleteTab(id: string, event: MouseEvent) { event.stopPropagation(); - if (conversations.size > 1) { - claudeStore.deleteConversation(id); + if ($conversations.size > 1) { + const conversation = $conversations.get(id); + if (conversation && conversation.connectionStatus === "connected") { + // Show confirmation modal for connected tabs + tabToClose = id; + tabToCloseName = conversation.name; + showConfirmModal = true; + } else { + // Close disconnected tabs immediately + claudeStore.deleteConversation(id); + } } } + function confirmCloseTab() { + if (tabToClose) { + claudeStore.deleteConversation(tabToClose); + } + showConfirmModal = false; + tabToClose = null; + tabToCloseName = ""; + } + + function cancelCloseTab() { + showConfirmModal = false; + tabToClose = null; + tabToCloseName = ""; + } + function startEditing(id: string, name: string, event: MouseEvent) { event.stopPropagation(); editingTabId = id; @@ -105,7 +115,7 @@ } function hasUnreadMessages(id: string, conversation: Conversation): boolean { - if (id === activeConversationId) return false; // Active tab never has unread + if (id === $activeConversationId) return false; // Active tab never has unread const lastSeen = lastSeenMessageCount.get(id) || 0; return conversation.terminalLines.length > lastSeen; } @@ -137,15 +147,24 @@ // Ctrl/Cmd + W: Close current tab else if ((event.ctrlKey || event.metaKey) && event.key === "w") { event.preventDefault(); - if (activeConversationId && conversations.size > 1) { - claudeStore.deleteConversation(activeConversationId); + if ($activeConversationId && $conversations.size > 1) { + const conversation = $conversations.get($activeConversationId); + if (conversation && conversation.connectionStatus === "connected") { + // Show confirmation modal for connected tabs + tabToClose = $activeConversationId; + tabToCloseName = conversation.name; + showConfirmModal = true; + } else { + // Close disconnected tabs immediately + claudeStore.deleteConversation($activeConversationId); + } } } // 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 tabs = Array.from($conversations.keys()); + const currentIndex = tabs.findIndex((id) => id === $activeConversationId); if (currentIndex !== -1) { const nextIndex = (currentIndex + 1) % tabs.length; claudeStore.switchConversation(tabs[nextIndex]); @@ -154,8 +173,8 @@ // 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 tabs = Array.from($conversations.keys()); + const currentIndex = tabs.findIndex((id) => id === $activeConversationId); if (currentIndex !== -1) { const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length; claudeStore.switchConversation(tabs[prevIndex]); @@ -171,17 +190,17 @@
- {#each Array.from(conversations.entries()) as [id, conversation] (id)} + {#each Array.from($conversations.entries()) as [id, conversation] (id)}
switchTab(id)} onkeydown={(e) => handleTabKeydown(id, e)} role="tab" tabindex={0} - aria-selected={id === activeConversationId} + aria-selected={id === $activeConversationId} > {#if editingTabId === id}
startEditing(id, conversation.name, e)} role="button" tabindex={-1} > {conversation.name} - {#if id !== activeConversationId && id === connectedConversationId} - - (connected) - - {/if} {#if hasUnreadMessages(id, conversation)}
{/if}
{/if} - {#if conversations.size > 1} + {#if $conversations.size > 1}
+ +