Repository navigation
ui: Chat Conversation Tabbed navigation #27263
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ServeurpersoCom
merged 32 commits into
master
from
allozaur/feat/chat-conversation-tabs
Aug 23, 2026
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
19eb0cb
ui : add browser-style conversation tabs store
allozaur 4389efa
ui : add temporary new-chat tabs
allozaur f46de6b
ui : render conversation tab bar in chat layout
allozaur 36a12a3
ui : route new-chat entry points through tabs
allozaur ed33fab
ui : track sidebar expanded state in a shared ui store
allozaur e0312d6
chat : add opt-in conversation tabs setting
allozaur 190f170
chat : add browser-style conversation tabs with a new-chat screen
allozaur c7ef9f7
chat : turn tab bar into a horizontally scrollable carousel
allozaur 8b9cd32
chat : restyle the scroll-to-bottom button to match tab styling
allozaur 975c22e
chat : add close-tab keyboard shortcut
allozaur 338468c
chat : soften tab bar fade and dim inactive tabs
allozaur 3dcff15
feat: Add stop button to tabs
allozaur 096dbaa
refactor: Componentize
allozaur d900ec7
ui : fix carousel scrollability detection
allozaur 6568871
ui : add unified ScrollCarousel component
allozaur fed4740
ui : migrate carousels to ScrollCarousel
allozaur 495fe44
ui : improve chat tabs carousel UX
allozaur 6622549
refactor: Naming
allozaur 26da5a3
chat : add keyboard shortcut to jump between conversation tabs
allozaur 7b67452
chat : make the whole tab item act as a link
allozaur 1b183b4
chat : adjust tab bar width and use a shared offset variable
allozaur 0ed2ffb
chat : account for the tab bar offset in the assistant min-height
allozaur b4c672c
refactor: Post-review fixes
allozaur e5af81c
ui : restore deep links on the chat start page
allozaur 4a0a228
ui : fix tab item link nesting and centralize tab constants
allozaur 707e7da
ui : tidy scroll carousel hook and keep mobile header arrows on
allozaur 890b459
ui : keep the new-chat tab across reloads and fall back on close
allozaur 89776eb
ui : don't block startup on the conversation list
allozaur 01d0f13
ui: fix routing nits
allozaur 54b74f2
chore: Update doc comments
allozaur e4c77b8
refactor: Mark fire-and-forget openNewChat calls as `void`
allozaur 128dd1f
chat: fix the deep-linked prompt, the tab width and the tab shortcuts
ServeurpersoCom File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
136 changes: 136 additions & 0 deletions
136
tools/ui/src/lib/components/app/chat/ChatTabs/ChatTabs.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| <script lang="ts"> | ||
| import ChatTabsItem from './ChatTabsItem.svelte'; | ||
| import ChatTabsNewChatButton from './ChatTabsNewChatButton.svelte'; | ||
| import { page } from '$app/state'; | ||
| import { ScrollCarousel } from '$lib/components/app'; | ||
| import { | ||
| CHAT_TABS_MAX_WIDTH, | ||
| NEW_CHAT_LABEL, | ||
| NEW_CHAT_TAB_ID, | ||
| UI_DATA_ATTRS, | ||
| UNNAMED_CHAT_LABEL | ||
| } from '$lib/constants'; | ||
| import { useScrollCarousel } from '$lib/hooks/use-scroll-carousel.svelte'; | ||
| import { chatStore, conversationsStore, tabsStore, uiStore } from '$lib/stores'; | ||
| import { tick } from 'svelte'; | ||
|
|
||
| const carousel = useScrollCarousel(); | ||
|
|
||
| let activeId = $derived(page.params.id ?? NEW_CHAT_TAB_ID); | ||
|
|
||
| let tabs = $derived( | ||
| tabsStore.openTabs.map((id) => ({ | ||
| id, | ||
| isNewChat: id === NEW_CHAT_TAB_ID, | ||
| name: | ||
| id === NEW_CHAT_TAB_ID | ||
| ? NEW_CHAT_LABEL | ||
| : (conversationsStore.conversations.find((c) => c.id === id)?.name ?? UNNAMED_CHAT_LABEL) | ||
| })) | ||
| ); | ||
|
|
||
| // hide the New chat button while a new-chat tab is already open | ||
| let showNewChatButton = $derived(!tabsStore.openTabs.includes(NEW_CHAT_TAB_ID)); | ||
|
|
||
| let loadingIds = $derived(new Set(chatStore.getAllLoadingChats())); | ||
|
|
||
| function handleClose(id: string) { | ||
| void tabsStore.close(id, activeId); | ||
| } | ||
|
|
||
| function handleStop(id: string, event: MouseEvent) { | ||
| event.stopPropagation(); | ||
| void chatStore.stopGenerationForChat(id); | ||
| } | ||
|
|
||
| function handleAuxClick(id: string, event: MouseEvent) { | ||
| // middle-click closes, like browser tabs | ||
| if (event.button === 1) { | ||
| event.preventDefault(); | ||
| handleClose(id); | ||
| } | ||
| } | ||
|
|
||
| let previousTabIds = new Set<string>(); | ||
| let previousActiveId: string | null = null; | ||
|
|
||
| $effect(() => { | ||
| const currentIds = new Set(tabs.map((t) => t.id)); | ||
| const hasAddedTab = tabs.some((t) => !previousTabIds.has(t.id)); | ||
|
|
||
| previousTabIds = currentIds; | ||
|
|
||
| const activeChanged = activeId !== previousActiveId; | ||
|
|
||
| previousActiveId = activeId; | ||
|
|
||
| // scroll when the active tab changes (a click) or when a new tab is added | ||
| if (!hasAddedTab && !activeChanged) return; | ||
|
|
||
| // wait for the new tab to be laid out before scrolling to it | ||
| void tick().then(() => { | ||
| const el = carousel.scrollContainer?.querySelector<HTMLElement>( | ||
| `[${UI_DATA_ATTRS.ACTIVE_TAB}]` | ||
| ); | ||
|
|
||
| if (el) { | ||
| carousel.scrollToCenter(el); | ||
| } | ||
| }); | ||
| }); | ||
| </script> | ||
|
|
||
| <nav | ||
| class="group sticky pl-1 top-0 z-10 hidden md:block chat-tabs-fade transition-[padding] duration-200 ease-in-out pt-3.25 {uiStore.isSidebarExpanded | ||
| ? CHAT_TABS_MAX_WIDTH.EXPANDED_SIDEBAR | ||
| : CHAT_TABS_MAX_WIDTH.COLLAPSED_SIDEBAR}" | ||
| aria-label="Open conversations" | ||
| > | ||
| <div class="relative"> | ||
| <ScrollCarousel | ||
| class="h-10" | ||
| containerClass="flex h-10 min-w-0 items-center" | ||
| innerClass="items-center gap-1.25" | ||
| {carousel} | ||
| > | ||
| {#each tabs as tab (tab.id)} | ||
| <ChatTabsItem | ||
| {tab} | ||
| isActive={tab.id === activeId} | ||
| isLoading={loadingIds.has(tab.id)} | ||
| onActivate={(id) => tabsStore.activate(id)} | ||
| onClose={handleClose} | ||
| onStop={handleStop} | ||
| onAuxClick={handleAuxClick} | ||
| /> | ||
| {/each} | ||
|
|
||
| {#if showNewChatButton} | ||
| <ChatTabsNewChatButton onclick={() => void conversationsStore.openNewChat()} /> | ||
| {/if} | ||
| </ScrollCarousel> | ||
|
|
||
| <div | ||
| class="pointer-events-none absolute inset-y-0 left-0 z-[5] w-8 bg-gradient-to-r from-background to-transparent transition-opacity {carousel.canScrollLeft | ||
| ? 'opacity-100' | ||
| : 'opacity-0'}" | ||
| ></div> | ||
| <div | ||
| class="pointer-events-none absolute inset-y-0 right-0 z-[5] w-8 bg-gradient-to-l from-background to-transparent transition-opacity {carousel.canScrollRight | ||
| ? 'opacity-100' | ||
| : 'opacity-0'}" | ||
| ></div> | ||
| </div> | ||
| </nav> | ||
|
|
||
| <style> | ||
| .chat-tabs-fade { | ||
| background: linear-gradient( | ||
| to bottom, | ||
| color-mix(in srgb, var(--background) 100%, transparent) 25%, | ||
| color-mix(in srgb, var(--background) 80%, transparent) 50%, | ||
| color-mix(in srgb, var(--background) 40%, transparent) 75%, | ||
| transparent 100% | ||
| ); | ||
| } | ||
| </style> | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.