diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index fe01dd781f..a17c36b3c0 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -2649,7 +2649,10 @@ const initNewChat = async () => { loading = true; - if (!embedded && ($showTabs === 'comments' || $showTabs === 'side' || typeof $showTabs === 'object')) + if ( + !embedded && + ($showTabs === 'comments' || $showTabs === 'side' || typeof $showTabs === 'object') + ) showTabs.set(false); const signal = startChatLoad(); modelDefaults = null; @@ -3874,6 +3877,10 @@ }; let terminalTabs: ChatTabs; + let fullWidthTabs = false; + let mainChatActive = false; + let tabTitleEditor: import('svelte').Snippet | undefined; + let tabActions: import('svelte').Snippet | undefined; const getChatId = async (_history: typeof history) => { let _chatId = $chatId; @@ -3954,7 +3961,9 @@ messageInput?.openComments(); return { status: 'rejected' }; } - const sideCommand = String(userPrompt).trim().match(/^\/(?:side|btw)(?:\s+([\s\S]*))?$/i); + const sideCommand = String(userPrompt) + .trim() + .match(/^\/(?:side|btw)(?:\s+([\s\S]*))?$/i); if (sideCommand && !sideChatAllowed) { toast.error($i18n.t('You do not have permission to use side chat.')); return { status: 'rejected' }; @@ -5290,8 +5299,18 @@ > {/if} -
-
+
+
{#if loading}
@@ -5327,6 +5346,9 @@
{:else} 0} {onSelect} {onInsertToNote} @@ -5698,6 +5720,11 @@ {#if !embedded} tab.id === mainChatTabId)) { + tabs = setFullWidthTabs(tabs, fullWidth, title); + if (fullWidth) void tick().then(() => document.getElementById(domId(mainChatTabId))?.focus()); + } + const toggleLayout = async () => { + if (panelTarget) releasePanel(panelTarget); + expanded = !expanded; + localStorage.setItem('chatTabsLayout', expanded ? 'full' : 'side'); + await tick(); + if (!fullWidth) document.getElementById('chat-tabs-layout-toggle')?.focus(); + }; let largeScreen = false; let dragged = false; @@ -169,6 +190,7 @@ }; const tabIcon = (kind: PanelTab['kind']) => ({ + chat: 'chat-lines', files: 'folder', file: 'page', terminal: 'terminal', @@ -183,14 +205,18 @@ const domId = (id: string) => `chat-panel-${encodeURIComponent(id)}`; $: activeTabId = tabs.activeId; $: activeTab = tabs.tabs.find((tab) => tab.id === tabs.activeId); - $: terminalContext = fileContexts[tabs.activeId] ?? activeTab?.context ?? null; + $: contextTab = + activeTab?.kind === 'chat' + ? tabs.tabs.find((tab) => tab.id === tabs.history.at(-1)) + : activeTab; + $: terminalContext = (contextTab && fileContexts[contextTab.id]) ?? contextTab?.context ?? null; // Resolve before reactive Files-tab creation, which can run later in the same update. $: gitConnection = mounted && $config?.features?.enable_tool_servers && - (tabs.tabs.find((tab) => tab.id === tabs.activeId)?.connection ?? + (contextTab?.connection ?? resolveTerminalConnection( - tabs.tabs.find((tab) => tab.id === tabs.activeId)?.terminalId ?? $selectedTerminalId, + contextTab?.terminalId ?? $selectedTerminalId, $terminalServers ?? [], directTerminals, localStorage.token ?? '' @@ -254,7 +280,7 @@ terminalId: selectedConnection.selector, connection: { ...selectedConnection }, untitled: true, - context: activeTab?.terminalId === selectedConnection.selector ? terminalContext : undefined + context: contextTab?.terminalId === selectedConnection.selector ? terminalContext : undefined }); }; const openFeature = (kind: 'artifacts' | 'preview' | 'voice' | 'comments' | 'side') => { @@ -277,7 +303,9 @@ const newTerminal = () => { if (!terminalEnabled || !selectedConnection) return; let terminalNumber = 1; - while (tabs.tabs.some((tab) => tab.kind === 'terminal' && tab.terminalNumber === terminalNumber)) { + while ( + tabs.tabs.some((tab) => tab.kind === 'terminal' && tab.terminalNumber === terminalNumber) + ) { terminalNumber++; } openPanel({ @@ -343,6 +371,7 @@ }); }; const closeTab = async (id: string) => { + if (id === mainChatTabId) return; if (closing.has(id)) return; closing.add(id); try { @@ -397,7 +426,7 @@ // Restore the DOM before Svelte reconciles its keyed tabs. from.insertBefore(item, nextSibling?.parentNode === from ? nextSibling : null); if (newDraggableIndex != null) - tabs = moveTab(tabs, item.dataset.panelTab!, newDraggableIndex); + tabs = moveTab(tabs, item.dataset.panelTab!, newDraggableIndex + (fullWidth ? 1 : 0)); dragged = false; } }); @@ -492,7 +521,11 @@ $: showPreviewTab = !!$embed && $embed.chatId === chatId; $: availableViews = [ { kind: 'files', label: $i18n.t('Files'), available: showFilesTab }, - { kind: 'side', label: $i18n.t('Side chat'), available: canSideChat && !!chatId && hasMessages }, + { + kind: 'side', + label: $i18n.t('Side chat'), + available: canSideChat && !!chatId && hasMessages + }, { kind: 'artifacts', label: $i18n.t('Artifacts'), available: showArtifactsTab }, { kind: 'preview', label: $i18n.t('Preview'), available: showPreviewTab }, { kind: 'voice', label: $i18n.t('Voice'), available: $showCallOverlay }, @@ -529,7 +562,7 @@ const hadTabs = tabs.tabs.length > 0; for (const tab of tabs.tabs) { // An open side chat owns its model and context; changing the parent must not discard it. - if (tab.kind === 'side') continue; + if (tab.kind === 'side' || tab.kind === 'chat') continue; const available = tab.terminalId ? !!$selectedTerminalId && terminalAccessible(tab.terminalId) : availableViews.some((view) => view.kind === tab.kind && view.available); @@ -591,7 +624,7 @@ $: if (chatId !== tabsChatId) { if (tabsChatId || !chatId) { - tabs = emptyTabs(); + tabs = setFullWidthTabs(emptyTabs(), fullWidth, title); sideChatNumber = 0; hasOpenedTabs = false; dirtyFiles = {}; @@ -602,10 +635,12 @@ } $: if ($showTabs === true && !tabs.tabs.length) { const options = [ - ...availableViews.filter((view) => view.available).map((view) => () => { - if (view.kind === 'files') openFiles(); - else openFeature(view.kind as 'artifacts' | 'preview' | 'voice' | 'comments' | 'side'); - }), + ...availableViews + .filter((view) => view.available) + .map((view) => () => { + if (view.kind === 'files') openFiles(); + else openFeature(view.kind as 'artifacts' | 'preview' | 'voice' | 'comments' | 'side'); + }), ...(terminalFilesAvailable ? [newFile] : []), ...(terminalEnabled ? [newTerminal] : []), ...(browserEnabled ? [openBrowser] : []), @@ -623,7 +658,8 @@ ($user.permissions?.chat?.side_chat ?? false)) ) && $sideChatRequest?.parentId === chatId - ) sideChatRequest.set(null); + ) + sideChatRequest.set(null); $: if ($showTabs === 'files' && showFilesTab) openFiles(); $: if ($showTabs === 'side' && chatId && hasMessages) { if (canSideChat) openFeature('side'); @@ -715,6 +751,8 @@ const mediaQuery = window.matchMedia('(min-width: 768px)'); mediaQuery.addEventListener('change', handleMediaQuery); handleMediaQuery(mediaQuery); + expanded = localStorage.getItem('chatTabsLayout') === 'full'; + if (expanded && largeScreen && !$showTabs) showTabs.set(true); let isDestroyed = false; @@ -788,11 +826,13 @@
-
+
{#each tabs.tabs as tab (tab.id)}
selectTab(tab.id)} + class="panel-tab flex cursor-pointer items-center gap-1.5 ps-2.5 pe-2 h-7 rounded-lg text-sm font-normal whitespace-nowrap shrink-0 transition {tabs.activeId === tab.id ? 'bg-gray-100/40 dark:bg-gray-800/25 text-gray-700 dark:text-gray-200' : 'text-gray-500 dark:text-gray-400 hover:bg-gray-100/30 dark:hover:bg-gray-800/20 hover:text-gray-600 dark:hover:text-gray-300'}" > + {#if tab.kind === 'chat'} + {@render tabTitleEditor?.()} + {@render tabActions?.()} + {:else}{/if}
{/each}
@@ -906,6 +959,19 @@ {/if}
+ {#if largeScreen} + + {/if}
-
- {#each tabs.tabs as tab (tab.id)} +
+ {#each tabs.tabs.filter((tab) => tab.kind !== 'chat') as tab (tab.id)}
{/if}
- {#if gitConnection && tabs.tabs.length} - openFile(path, gitConnection.selector)} - onOpenDirectory={(path) => openFiles(gitConnection.selector, path)} - /> - {/if} +
+ {#if gitConnection && tabs.tabs.length} + openFile(path, gitConnection.selector)} + onOpenDirectory={(path) => openFiles(gitConnection.selector, path)} + /> + {/if} +
{#if !largeScreen} @@ -1096,7 +1167,7 @@ {:else}
+ {#if fullWidth} + +
+ {/if} {/if}