From 1ae4576fe3eee610f9f2f87ade7bfc434d365eda Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 17 Aug 2026 14:01:40 +0200 Subject: [PATCH] ui : route new-chat entry points through tabs The sidebar New chat item, Cmd+Shift+O, the search page and the arrow-key fallback now open a new-chat tab instead of navigating to the ?new_chat URL, which is removed. New chat is no longer a special route but a tab like any other conversation. --- .../SidebarNavigationActions.svelte | 46 ++++++++++++------- .../ui/src/lib/constants/routes.constants.ts | 4 -- tools/ui/src/lib/constants/ui.constants.ts | 2 +- .../hooks/use-keyboard-shortcuts.svelte.ts | 5 +- tools/ui/src/lib/types/navigation.d.ts | 2 + tools/ui/src/routes/+layout.svelte | 2 +- tools/ui/src/routes/search/+page.svelte | 7 ++- 7 files changed, 38 insertions(+), 30 deletions(-) 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 dc333134bf..bf662d9ba5 100644 --- a/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationActions.svelte +++ b/tools/ui/src/lib/components/app/navigation/SidebarNavigation/SidebarNavigationActions.svelte @@ -12,7 +12,7 @@ SIDEBAR_ACTIONS_ITEMS } from '$lib/constants'; import { TooltipSide } from '$lib/enums'; - import { deviceStore } from '$lib/stores'; + import { conversationsStore, deviceStore } from '$lib/stores'; import type { Component } from 'svelte'; import { onMount } from 'svelte'; import { circIn } from 'svelte/easing'; @@ -109,14 +109,20 @@ {@const isActive = isItemActive(item)} {@const isSearchOnMobile = item.icon === Search && deviceStore.isMobile} {@const itemHref = isSearchOnMobile ? ROUTES.SEARCH : item.route} - {@const itemOnClick = item.route - ? () => { - onNewChat?.(); - goto(item.route!); - } - : isSearchOnMobile - ? undefined - : onSearchClick} + {@const itemOnClick = + item.action === 'new-chat' + ? () => { + onNewChat?.(); + conversationsStore.openNewChatTab(); + } + : item.route + ? () => { + onNewChat?.(); + goto(item.route!); + } + : isSearchOnMobile + ? undefined + : onSearchClick} {@const itemTransition = { delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0, duration: ICON_STRIP_TRANSITION_DURATION, @@ -157,14 +163,20 @@ {#each SIDEBAR_ACTIONS_ITEMS as item, i (item.tooltip)} {@const isActive = isItemActive(item)} {@const isSearchOnMobile = item.icon === Search && deviceStore.isMobile} - {@const itemOnClick = item.route - ? () => { - onNewChat?.(); - goto(item.route!); - } - : isSearchOnMobile - ? undefined - : onSearchClick} + {@const itemOnClick = + item.action === 'new-chat' + ? () => { + onNewChat?.(); + conversationsStore.openNewChatTab(); + } + : item.route + ? () => { + onNewChat?.(); + goto(item.route!); + } + : isSearchOnMobile + ? undefined + : onSearchClick} {@const itemTransition = { delay: !initialized ? i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER : 0, duration: ICON_STRIP_TRANSITION_DURATION, diff --git a/tools/ui/src/lib/constants/routes.constants.ts b/tools/ui/src/lib/constants/routes.constants.ts index e4cce77399..d46acfd780 100644 --- a/tools/ui/src/lib/constants/routes.constants.ts +++ b/tools/ui/src/lib/constants/routes.constants.ts @@ -4,8 +4,6 @@ export const URL_PARAMS = { LOAD: 'load', /** Model to select. */ MODEL: 'model', - /** Start a new chat. */ - NEW_CHAT: 'new_chat', /** Prompt to send on arrival. */ QUERY: 'q' } as const; @@ -15,8 +13,6 @@ export const ROUTES = { CHAT: '#/chat', /** MCP servers. */ MCP_SERVERS: '#/mcp-servers', - /** New chat — root with new chat query param. */ - NEW_CHAT: `?${URL_PARAMS.NEW_CHAT}=true#/`, /** Search — mobile-only full-page conversation search. */ SEARCH: '#/search', /** Settings base — for dynamic settings URLs use RouterService. */ diff --git a/tools/ui/src/lib/constants/ui.constants.ts b/tools/ui/src/lib/constants/ui.constants.ts index feae08742f..48af703d70 100644 --- a/tools/ui/src/lib/constants/ui.constants.ts +++ b/tools/ui/src/lib/constants/ui.constants.ts @@ -55,7 +55,7 @@ export const ICON_STRIP_TRANSITION_DELAY_MULTIPLIER = 50; export const MAX_HEIGHT_CODE_BLOCK = '22rem'; export const SIDEBAR_ACTIONS_ITEMS: DesktopIconStripItem[] = [ - { icon: SquarePen, keys: ['shift', 'cmd', 'o'], route: ROUTES.NEW_CHAT, tooltip: 'New chat' }, + { action: 'new-chat', icon: SquarePen, keys: ['shift', 'cmd', 'o'], tooltip: 'New chat' }, { icon: Search, keys: ['cmd', 'k'], tooltip: 'Search' }, { activeRouteId: '/mcp-servers', 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 ce394c383d..fe22f89465 100644 --- a/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts +++ b/tools/ui/src/lib/hooks/use-keyboard-shortcuts.svelte.ts @@ -1,6 +1,5 @@ -import { goto } from '$app/navigation'; -import { ROUTES } from '$lib/constants'; import { KeyboardKey } from '$lib/enums'; +import { conversationsStore } from '$lib/stores'; interface KeyboardShortcutsCallbacks { activateSearchMode?: () => void; @@ -34,7 +33,7 @@ export function useKeyboardShortcuts(callbacks: KeyboardShortcutsCallbacks) { ) { event.preventDefault(); - goto(ROUTES.NEW_CHAT); + conversationsStore.openNewChatTab(); } if (event.shiftKey && isCmdOrCtrl && event.key === KeyboardKey.E_UPPER) { diff --git a/tools/ui/src/lib/types/navigation.d.ts b/tools/ui/src/lib/types/navigation.d.ts index 357e060dc7..c344d81503 100644 --- a/tools/ui/src/lib/types/navigation.d.ts +++ b/tools/ui/src/lib/types/navigation.d.ts @@ -7,6 +7,8 @@ export interface DesktopIconStripItem { icon: Component; tooltip: string; route?: string; + /** Custom action handled by the sidebar, e.g. opening a new-chat tab */ + action?: 'new-chat'; activeRouteId?: string; activeRoutePrefix?: string; activeUrlIncludes?: string; diff --git a/tools/ui/src/routes/+layout.svelte b/tools/ui/src/routes/+layout.svelte index f87bbe26a2..fa0424c8e0 100644 --- a/tools/ui/src/routes/+layout.svelte +++ b/tools/ui/src/routes/+layout.svelte @@ -96,7 +96,7 @@ if (targetIdx >= 0 && targetIdx < allConvs.length) { goto(RouterService.chat(allConvs[targetIdx].id)); } else { - goto(ROUTES.NEW_CHAT); + conversationsStore.openNewChatTab(); } } diff --git a/tools/ui/src/routes/search/+page.svelte b/tools/ui/src/routes/search/+page.svelte index a882e55529..0ab492085a 100644 --- a/tools/ui/src/routes/search/+page.svelte +++ b/tools/ui/src/routes/search/+page.svelte @@ -3,7 +3,6 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; import { SearchInput, SidebarNavigationSearchResults } from '$lib/components/app'; - import { ROUTES } from '$lib/constants'; import { RouterService } from '$lib/services/router.service'; import { chatStore, conversationsStore, deviceStore } from '$lib/stores'; @@ -21,10 +20,10 @@ }); // Search page is intended for mobile; on desktop the sidebar already exposes - // in-place search, so bounce back to a chat. + // in-place search, so bounce back to a new-chat tab. $effect(() => { if (browser && !deviceStore.isMobile) { - goto(ROUTES.NEW_CHAT, { replaceState: true }); + conversationsStore.openNewChatTab(); } }); @@ -66,7 +65,7 @@ if (history.length > 1) { history.back(); } else { - goto(ROUTES.NEW_CHAT); + conversationsStore.openNewChatTab(); } }