diff --git a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
index 11a98cae59..bb18bae50f 100644
--- a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
@@ -78,12 +78,7 @@
const conversation = conversationsStore.activeConversation;
if (conversation) {
- // an empty new-chat tab is unsaved, so drop it without confirmation
- if (conversationsStore.isTemporaryConversation(conversation.id)) {
- conversationsStore.deleteConversation(conversation.id);
- } else {
- showDeleteDialog = true;
- }
+ showDeleteDialog = true;
}
}
});
diff --git a/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabs.svelte b/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabs.svelte
index 9e433026a5..d37d88f924 100644
--- a/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabs.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabs.svelte
@@ -3,20 +3,24 @@
import { page } from '$app/state';
import * as Tooltip from '$lib/components/ui/tooltip';
import { cn } from '$lib/components/ui/utils';
- import { chatStore, conversationsStore, tabsStore } from '$lib/stores';
+ import { chatStore, conversationsStore, NEW_CHAT_TAB_ID, tabsStore } from '$lib/stores';
- let activeId = $derived(page.params.id);
+ let activeId = $derived(page.params.id ?? NEW_CHAT_TAB_ID);
let tabs = $derived(
tabsStore.openTabs.map((id) => ({
id,
- isNewChat: conversationsStore.isTemporaryConversation(id),
+ isNewChat: id === NEW_CHAT_TAB_ID,
name:
- conversationsStore.conversations.find((c) => c.id === id)?.name ??
- (conversationsStore.isTemporaryConversation(id) ? 'New chat' : 'Chat')
+ id === NEW_CHAT_TAB_ID
+ ? 'New chat'
+ : (conversationsStore.conversations.find((c) => c.id === id)?.name ?? 'Chat')
}))
);
+ // hide the New chat button when the new-chat tab is the only one open
+ let showNewChatButton = $derived(tabsStore.openTabs.some((id) => id !== NEW_CHAT_TAB_ID));
+
let loadingIds = $derived(new Set(chatStore.getAllLoadingChats()));
function handleClose(id: string) {
@@ -87,23 +91,25 @@
{/each}
-
-
- {#snippet child({ props })}
-
- {/snippet}
-
+ {#if showNewChatButton}
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
-
- New chat
-
-
+
+ New chat
+
+
+ {/if}
diff --git a/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationActions.svelte b/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationActions.svelte
index bf662d9ba5..b18b08549f 100644
--- a/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationActions.svelte
+++ b/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationActions.svelte
@@ -113,7 +113,7 @@
item.action === 'new-chat'
? () => {
onNewChat?.();
- conversationsStore.openNewChatTab();
+ conversationsStore.openNewChat();
}
: item.route
? () => {
@@ -167,7 +167,7 @@
item.action === 'new-chat'
? () => {
onNewChat?.();
- conversationsStore.openNewChatTab();
+ conversationsStore.openNewChat();
}
: item.route
? () => {
diff --git a/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts b/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts
index fe22f89465..4a2fd0af8c 100644
--- a/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts
+++ b/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts
@@ -33,7 +33,7 @@ export function useKeyboardShortcuts(callbacks: KeyboardShortcutsCallbacks) {
) {
event.preventDefault();
- conversationsStore.openNewChatTab();
+ conversationsStore.openNewChat();
}
if (event.shiftKey && isCmdOrCtrl && event.key === KeyboardKey.E_UPPER) {
diff --git a/tools/ui/src/lib/stores/chat/index.svelte.ts b/tools/ui/src/lib/stores/chat/index.svelte.ts
index e138d555c0..aab824fd71 100644
--- a/tools/ui/src/lib/stores/chat/index.svelte.ts
+++ b/tools/ui/src/lib/stores/chat/index.svelte.ts
@@ -167,8 +167,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
if (!activeConv) {
await conversationsStore.createConversation();
activeConv = conversationsStore.activeConversation;
- } else if (conversationsStore.isTemporaryConversation(activeConv.id)) {
- await conversationsStore.persistTemporaryConversation(activeConv.id);
}
if (!activeConv) return;
@@ -646,9 +644,6 @@ class ChatStore implements ChatStreamHost, ChatFlowsHost {
if (!activeConv) {
await conversationsStore.createConversation();
isNewConversation = true;
- } else if (conversationsStore.isTemporaryConversation(activeConv.id)) {
- await conversationsStore.persistTemporaryConversation(activeConv.id);
- isNewConversation = true;
}
const currentConv = conversationsStore.activeConversation;
diff --git a/tools/ui/src/lib/stores/conversations/index.svelte.ts b/tools/ui/src/lib/stores/conversations/index.svelte.ts
index 7ee4776574..7ff35a1806 100644
--- a/tools/ui/src/lib/stores/conversations/index.svelte.ts
+++ b/tools/ui/src/lib/stores/conversations/index.svelte.ts
@@ -21,8 +21,8 @@ import {
type ConversationsPreferencesHost
} from '$lib/stores/conversations/preferences.svelte';
import { settingsStore } from '$lib/stores/settings/index.svelte';
-import { tabsStore } from '$lib/stores/tabs.svelte';
-import { filterByLeafNodeId, findLeafNode, generateConversationTitle, uuid } from '$lib/utils';
+import { NEW_CHAT_TAB_ID, tabsStore } from '$lib/stores/tabs.svelte';
+import { filterByLeafNodeId, findLeafNode, generateConversationTitle } from '$lib/utils';
import { SvelteSet } from 'svelte/reactivity';
import { toast } from 'svelte-sonner';
@@ -39,13 +39,6 @@ class ConversationsStore implements ConversationsPreferencesHost {
/** Whether the store has been initialized */
isInitialized = $state(false);
- /**
- * Unsaved new-chat tabs. Each carries a temporary id used directly as the
- * route (`#/chat/`); it is persisted to the database - and moved to
- * `conversations` - only when the first message is sent.
- */
- temporaryConversations = $state([]);
-
/** Per-chat options (MCP overrides, reasoning effort, cwd), composed here. */
private _preferences = new ConversationPreferences(this);
@@ -289,7 +282,6 @@ class ConversationsStore implements ConversationsPreferencesHost {
this.clearActiveConversation();
this.conversations = [];
- this.temporaryConversations = [];
tabsStore.clear();
this.notifyConversationsDeleted(allIds);
@@ -307,20 +299,6 @@ class ConversationsStore implements ConversationsPreferencesHost {
* @param convId - The conversation ID to delete
*/
async deleteConversation(convId: string, options?: { deleteWithForks?: boolean }): Promise {
- // an unsaved new-chat tab is not in the DB; just drop it and close its tab
- if (this.isTemporaryConversation(convId)) {
- this.temporaryConversations = this.temporaryConversations.filter((c) => c.id !== convId);
-
- if (this.activeConversation?.id === convId) {
- this.clearActiveConversation();
- await tabsStore.close(convId, convId);
- } else {
- tabsStore.removeTabs([convId]);
- }
-
- return;
- }
-
try {
await DatabaseService.deleteConversation(convId, options);
@@ -396,17 +374,6 @@ class ConversationsStore implements ConversationsPreferencesHost {
ConversationTransferService.downloadConversationFile({ conv: conversation, messages });
}
- /**
- * Persist the active conversation if it is an unsaved new-chat tab.
- */
- async ensureActiveConversationPersisted(): Promise {
- const active = this.activeConversation;
-
- if (active && this.isTemporaryConversation(active.id)) {
- await this.persistTemporaryConversation(active.id);
- }
- }
-
/**
* Finds the index of a message in active messages.
*
@@ -522,11 +489,6 @@ class ConversationsStore implements ConversationsPreferencesHost {
return this.initPromise;
}
- /** True if the id refers to an unsaved new-chat tab */
- isTemporaryConversation(id: string): boolean {
- return this.temporaryConversations.some((c) => c.id === id);
- }
-
/**
* Loads a specific conversation and its messages
* @param convId - The conversation ID to load
@@ -534,17 +496,6 @@ class ConversationsStore implements ConversationsPreferencesHost {
*/
async loadConversation(convId: string): Promise {
try {
- // unsaved new-chat tab: present in memory only
- const temp = this.temporaryConversations.find((c) => c.id === convId);
-
- if (temp) {
- this.preferences.pendingCwd = null;
- this.activeConversation = temp;
- this.activeMessages = [];
-
- return true;
- }
-
const conversation = await DatabaseService.getConversation(convId);
if (!conversation) {
@@ -635,36 +586,14 @@ class ConversationsStore implements ConversationsPreferencesHost {
}
/**
- * Open a fresh new-chat tab (an unsaved conversation) and navigate to it.
- * Used by the "New chat" actions (sidebar, keyboard, tab bar, search) and
- * prompt/model deep-links.
+ * Start a fresh chat by navigating to the bare `#/` new-chat screen. The
+ * chat layout opens a new-chat tab for it when Conversation tabs are on.
*/
- async openNewChatTab(): Promise {
- const conversation = this.createTemporaryConversation();
+ async openNewChat(): Promise {
+ this.clearActiveConversation();
+ await goto(ROUTES.START);
- await goto(RouterService.chat(conversation.id));
-
- return conversation.id;
- }
-
- /**
- * Persist an unsaved new-chat tab to the database, keeping its id so the
- * route and tab stay stable. Called on the first message of a new chat.
- * @param convId - The temporary conversation id to persist
- */
- async persistTemporaryConversation(convId: string): Promise {
- const temp = this.temporaryConversations.find((c) => c.id === convId);
-
- if (!temp) return;
-
- // clone out of the $state proxy so IndexedDB serializes plain data
- const conversation = { ...temp };
-
- await DatabaseService.createConversationWithId(conversation);
-
- this.temporaryConversations = this.temporaryConversations.filter((c) => c.id !== convId);
- this.conversations = [conversation, ...this.conversations];
- this.activeConversation = conversation;
+ return NEW_CHAT_TAB_ID;
}
/**
@@ -813,30 +742,6 @@ class ConversationsStore implements ConversationsPreferencesHost {
*
*/
- /** Update an unsaved new-chat tab in place (not persisted to the DB) */
- updateTemporaryConversation(id: string, updates: Partial): void {
- this.temporaryConversations = this.temporaryConversations.map((c) =>
- c.id === id ? { ...c, ...updates } : c
- );
- }
-
- /** Build an unsaved new-chat conversation, baking in the pending cwd/effort */
- private createTemporaryConversation(): DatabaseConversation {
- const conversation: DatabaseConversation = {
- currNode: '',
- cwd: this.preferences.pendingCwd ?? undefined,
- id: uuid(),
- lastModified: Date.now(),
- name: 'New chat',
- reasoningEffort: this.preferences.pendingReasoningEffort
- };
-
- this.preferences.pendingCwd = null;
- this.temporaryConversations = [...this.temporaryConversations, conversation];
-
- return conversation;
- }
-
private notifyConversationsDeleted(convIds: string[]): void {
if (convIds.length === 0) return;
diff --git a/tools/ui/src/lib/stores/conversations/preferences.svelte.ts b/tools/ui/src/lib/stores/conversations/preferences.svelte.ts
index 3ff28773be..fea9286033 100644
--- a/tools/ui/src/lib/stores/conversations/preferences.svelte.ts
+++ b/tools/ui/src/lib/stores/conversations/preferences.svelte.ts
@@ -46,8 +46,6 @@ export interface ConversationsPreferencesHost {
activeConversation: DatabaseConversation | null;
conversations: DatabaseConversation[];
applyConversationUpdate(id: string, updates: Partial): void;
- isTemporaryConversation(id: string): boolean;
- updateTemporaryConversation(id: string, updates: Partial): void;
}
export class ConversationPreferences {
@@ -160,14 +158,6 @@ export class ConversationPreferences {
cwd: trimmed
});
- // unsaved new-chat tab: keep the pick in memory until it is persisted
- if (this.host.isTemporaryConversation(id)) {
- this.host.updateTemporaryConversation(id, { cwd: trimmed });
- this.pendingCwd = null;
-
- return;
- }
-
await DatabaseService.updateConversation(id, {
cwd: trimmed
});
@@ -221,13 +211,6 @@ export class ConversationPreferences {
mcpServerOverrides: overrides
});
- // unsaved new-chat tab: keep the override in memory until it is persisted
- if (this.host.isTemporaryConversation(id)) {
- this.host.updateTemporaryConversation(id, { mcpServerOverrides: overrides });
-
- return;
- }
-
await DatabaseService.updateConversation(id, {
mcpServerOverrides: overrides
});
@@ -252,13 +235,6 @@ export class ConversationPreferences {
reasoningEffort: effort
});
- // unsaved new-chat tab: keep the effort in memory until it is persisted
- if (this.host.isTemporaryConversation(id)) {
- this.host.updateTemporaryConversation(id, { reasoningEffort: effort });
-
- return;
- }
-
await DatabaseService.updateConversation(id, {
reasoningEffort: effort
});
diff --git a/tools/ui/src/lib/stores/index.ts b/tools/ui/src/lib/stores/index.ts
index 40ddd10bfd..2bf5039d5c 100644
--- a/tools/ui/src/lib/stores/index.ts
+++ b/tools/ui/src/lib/stores/index.ts
@@ -23,7 +23,7 @@ export { chatStore } from './chat/index.svelte';
export { draftMessagesStore } from './chat/drafts.svelte';
// CONVERSATION TABS
-export { tabsStore } from './tabs.svelte';
+export { NEW_CHAT_TAB_ID, tabsStore } from './tabs.svelte';
// CONTEXT STATS (active conversation context window usage)
export { contextStatsStore } from './chat/context-stats.svelte';
diff --git a/tools/ui/src/lib/stores/tabs.svelte.ts b/tools/ui/src/lib/stores/tabs.svelte.ts
index f9fc83ecae..6c37b37b1e 100644
--- a/tools/ui/src/lib/stores/tabs.svelte.ts
+++ b/tools/ui/src/lib/stores/tabs.svelte.ts
@@ -1,11 +1,10 @@
/**
* tabsStore - Reactive State Store for Browser-Style Conversation Tabs
*
- * Tracks which conversations and new-chat screens are open as tabs in the
- * chat layout, in order. Every tab - real conversation or unsaved new-chat
- * tab - is a `#/chat/` route; new-chat tabs simply carry a temporary
- * conversation id that is only persisted to the database once a message is
- * sent (see conversationsStore.temporaryConversations).
+ * Tracks which conversations and the new-chat screen are open as tabs in
+ * the chat layout, in order. Real conversation tabs are `#/chat/`
+ * routes; the new-chat tab is the bare `#/` route, represented here by the
+ * `NEW_CHAT_TAB_ID` sentinel (see {@link NEW_CHAT_TAB_ID}).
*
* **Architecture & Relationships:**
* - **conversationsStore**: owns conversation data; calls `removeTabs()` /
@@ -13,8 +12,8 @@
* so there is no circular dependency - tab names are resolved by the
* ChatTabs component from conversationsStore.
* - Tab order persists to localStorage and is pruned against the loaded
- * conversation list on init. Unsaved new-chat tabs are dropped on reload
- * (they are not in the database), which matches browser behavior.
+ * conversation list on init. The new-chat tab is dropped on reload (it is
+ * not a conversation), which matches browser behavior.
*/
import { browser } from '$app/environment';
@@ -23,16 +22,19 @@ import { CHAT_TABS_LOCALSTORAGE_KEY, ROUTES } from '$lib/constants';
import { RouterService } from '$lib/services/router.service';
import { untrack } from 'svelte';
+/** Sentinel tab id for the bare `#/` new-chat screen */
+export const NEW_CHAT_TAB_ID = 'new-chat';
+
class TabsStore {
- /** Ordered tab ids: conversation ids and temporary new-chat ids */
+ /** Ordered tab ids: conversation ids and the `NEW_CHAT_TAB_ID` sentinel */
openTabs = $state([]);
/** False until init() has read the persisted tabs; save() is a no-op before that */
private initialized = false;
- /** Navigate to a tab */
+ /** Navigate to a tab (the new-chat sentinel maps to the bare `#/` route) */
async activate(id: string): Promise {
- await goto(RouterService.chat(id));
+ await goto(id === NEW_CHAT_TAB_ID ? ROUTES.START : RouterService.chat(id));
}
/** Remove all tabs (e.g. after deleting all conversations) */
@@ -61,7 +63,7 @@ class TabsStore {
const target = (idx > 0 ? this.openTabs[idx - 1] : this.openTabs[0]) ?? null;
if (target) {
- await goto(RouterService.chat(target));
+ await goto(target === NEW_CHAT_TAB_ID ? ROUTES.START : RouterService.chat(target));
} else {
await goto(ROUTES.START);
}
diff --git a/tools/ui/src/routes/(chat)/+layout.svelte b/tools/ui/src/routes/(chat)/+layout.svelte
index 33769bc911..9f943626a9 100644
--- a/tools/ui/src/routes/(chat)/+layout.svelte
+++ b/tools/ui/src/routes/(chat)/+layout.svelte
@@ -1,23 +1,25 @@