generated from nhcarrigan/template
feat: naomi did too much at once #53
@@ -0,0 +1,107 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
tabName: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const { isOpen, tabName, onConfirm, onCancel }: Props = $props();
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
onConfirm();
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
onclick={onCancel}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === " " && onCancel()}
|
||||
>
|
||||
<div
|
||||
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-md w-full"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-labelledby="confirm-title"
|
||||
aria-describedby="confirm-message"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-yellow-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 id="confirm-title" class="text-lg font-semibold text-gray-100 mb-1">
|
||||
Close Connected Tab?
|
||||
</h3>
|
||||
<p id="confirm-message" class="text-sm text-gray-400">
|
||||
The tab "{tabName}" is currently connected to Claude. Are you sure you want to close
|
||||
it? This will disconnect the session.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6 justify-end">
|
||||
<button
|
||||
onclick={onCancel}
|
||||
class="px-4 py-2 text-sm font-medium text-gray-300 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onclick={onConfirm}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors"
|
||||
>
|
||||
Close Tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
[role="dialog"] {
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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<string, Conversation> = 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<string | null>(null);
|
||||
let editingName = $state("");
|
||||
|
||||
// Track last seen message count for each conversation
|
||||
let lastSeenMessageCount = new SvelteMap<string, number>();
|
||||
|
||||
claudeStore.conversations.subscribe((convs) => {
|
||||
conversations = convs;
|
||||
// Confirmation modal state
|
||||
let showConfirmModal = $state(false);
|
||||
let tabToClose = $state<string | null>(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 @@
|
||||
<div
|
||||
class="terminal-tabs flex items-center gap-1 px-2 py-1 bg-[var(--bg-secondary)] border-b border-[var(--border-color)]"
|
||||
>
|
||||
{#each Array.from(conversations.entries()) as [id, conversation] (id)}
|
||||
{#each Array.from($conversations.entries()) as [id, conversation] (id)}
|
||||
<div
|
||||
class="tab-item group relative flex items-center px-3 py-1.5 rounded-t cursor-pointer transition-all
|
||||
{id === activeConversationId
|
||||
{id === $activeConversationId
|
||||
? 'bg-[var(--bg-terminal)] text-[var(--text-primary)] border-t border-l border-r border-[var(--border-color)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-terminal)]/50'}"
|
||||
onclick={() => switchTab(id)}
|
||||
onkeydown={(e) => handleTabKeydown(id, e)}
|
||||
role="tab"
|
||||
tabindex={0}
|
||||
aria-selected={id === activeConversationId}
|
||||
aria-selected={id === $activeConversationId}
|
||||
>
|
||||
{#if editingTabId === id}
|
||||
<input
|
||||
@@ -196,37 +215,26 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="w-2 h-2 rounded-full {getConnectionStatusColor(conversation.connectionStatus)}"
|
||||
title="Connection: {conversation.connectionStatus}{id !== connectedConversationId &&
|
||||
connectedConversationId
|
||||
? ' (Another tab is connected)'
|
||||
: ''}"
|
||||
title="Connection: {conversation.connectionStatus}"
|
||||
></div>
|
||||
<span
|
||||
class="text-sm pr-6 max-w-[150px] truncate"
|
||||
class="text-sm pr-2 max-w-[150px] truncate"
|
||||
ondblclick={(e) => startEditing(id, conversation.name, e)}
|
||||
role="button"
|
||||
tabindex={-1}
|
||||
>
|
||||
{conversation.name}
|
||||
</span>
|
||||
{#if id !== activeConversationId && id === connectedConversationId}
|
||||
<span
|
||||
class="text-xs text-[var(--text-tertiary)]"
|
||||
title="This tab has the Claude connection"
|
||||
>
|
||||
(connected)
|
||||
</span>
|
||||
{/if}
|
||||
{#if hasUnreadMessages(id, conversation)}
|
||||
<div
|
||||
class="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-blue-500 animate-pulse"
|
||||
class="absolute -top-1 -right-1 w-2 h-2 rounded-full bg-blue-500 animate-pulse pointer-events-none"
|
||||
title="New messages"
|
||||
></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if conversations.size > 1}
|
||||
{#if $conversations.size > 1}
|
||||
<button
|
||||
onclick={(e) => deleteTab(id, e)}
|
||||
class="absolute right-1 top-1/2 -translate-y-1/2 w-4 h-4 flex items-center justify-center rounded hover:bg-[var(--bg-secondary)] opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
@@ -268,6 +276,13 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<CloseTabConfirmModal
|
||||
isOpen={showConfirmModal}
|
||||
tabName={tabToCloseName}
|
||||
onConfirm={confirmCloseTab}
|
||||
onCancel={cancelCloseTab}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.terminal-tabs {
|
||||
min-height: 36px;
|
||||
|
||||
Reference in New Issue
Block a user