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'}"
>
{/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}