chat: fix the deep-linked prompt, the tab width and the tab shortcuts

The chat start page creates the conversation and hands the prompt over
to the chat route, which still sees it in the query string. Sending it
on both sides queues the second copy as a pending message, which shows
up as a stray user bubble once the answer lands and vanishes on reload
since it never reaches the database.

The tab bar takes the max width of the collapsed sidebar while it is
expanded, and the other way round.

The tab list is pruned against a snapshot of the loaded conversations,
so a conversation created while that list is still loading loses its
tab even though the route just opened it. The active tab then falls out
of the list and the cycling shortcut jumps to an edge on every keypress
instead of moving one tab over. Tabs synced from the route are kept as
they are, only the persisted ones are pruned.

The rich chat input claims ctrl or alt with shift and an arrow for its
badge-aware word jump, which now belongs to the tab cycling shortcut.
Holding shift hands the key combination over, the plain word jump is
unchanged.

The close-tab shortcut consumes the event before checking whether the
setting is on, and the logo background loses its importance flag.
This commit is contained in:
Pascal
2026-08-22 15:10:48 +02:00
committed by GitHub
parent e4c77b8610
commit 128dd1fa73
8 changed files with 18 additions and 17 deletions
@@ -625,7 +625,7 @@
}
if (rootElement && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
const isWordJump = (event.altKey || event.ctrlKey) && !event.metaKey;
const isWordJump = (event.altKey || event.ctrlKey) && !event.metaKey && !event.shiftKey;
const isPlainLeft =
event.key === 'ArrowLeft' && !event.altKey && !event.ctrlKey && !event.metaKey;
@@ -73,9 +73,7 @@
});
const { handleKeydown } = useKeyboardShortcuts({
deleteActiveConversation: () => {
const conversation = conversationsStore.activeConversation;
if (conversation) {
if (conversationsStore.activeConversation) {
showDeleteDialog = true;
}
}
@@ -35,7 +35,7 @@
let loadingIds = $derived(new Set(chatStore.getAllLoadingChats()));
function handleClose(id: string) {
void tabsStore.close(id, activeId ?? null);
void tabsStore.close(id, activeId);
}
function handleStop(id: string, event: MouseEvent) {
@@ -56,7 +56,7 @@
$effect(() => {
const currentIds = new Set(tabs.map((t) => t.id));
const addedIds = tabs.filter((t) => !previousTabIds.has(t.id)).map((t) => t.id);
const hasAddedTab = tabs.some((t) => !previousTabIds.has(t.id));
previousTabIds = currentIds;
@@ -65,7 +65,7 @@
previousActiveId = activeId;
// scroll when the active tab changes (a click) or when a new tab is added
if (addedIds.length === 0 && !activeChanged) return;
if (!hasAddedTab && !activeChanged) return;
// wait for the new tab to be laid out before scrolling to it
void tick().then(() => {
@@ -73,7 +73,7 @@
`[${UI_DATA_ATTRS.ACTIVE_TAB}]`
);
if (el && activeId) {
if (el) {
carousel.scrollToCenter(el);
}
});
@@ -82,8 +82,8 @@
<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.COLLAPSED_SIDEBAR
: CHAT_TABS_MAX_WIDTH.EXPANDED_SIDEBAR}"
? CHAT_TABS_MAX_WIDTH.EXPANDED_SIDEBAR
: CHAT_TABS_MAX_WIDTH.COLLAPSED_SIDEBAR}"
aria-label="Open conversations"
>
<div class="relative">
@@ -343,7 +343,7 @@
iconSize="h-4.5 w-4.5 md:h-4 md:w-4"
class="{uiStore.isSidebarExpanded
? 'bg-muted! md:bg-foreground/5!'
: 'bg-transparent'} md:h-9 md:w-9 h-10 w-10 rounded-full md:hover:bg-foreground/10! pointer-events-auto"
: 'bg-transparent!'} md:h-9 md:w-9 h-10 w-10 rounded-full md:hover:bg-foreground/10! pointer-events-auto"
href={uiStore.isSidebarExpanded ? ROUTES.START : undefined}
onclick={uiStore.isSidebarExpanded ? undefined : toggleExpandedMode}
tooltip={uiStore.isSidebarExpanded ? undefined : 'Open Sidebar'}
@@ -54,11 +54,11 @@ export function useKeyboardShortcuts(callbacks: KeyboardShortcutsCallbacks) {
// act so the synchronous navigation does not cascade-close every tab
if (event.defaultPrevented) return;
event.preventDefault();
// close-tab only makes sense with conversation tabs enabled
if (!settingsStore.config.conversationTabs) return;
event.preventDefault();
const activeId =
page.params.id ?? (page.route.id === '/(chat)' ? NEW_CHAT_TAB_ID : undefined);
+4 -1
View File
@@ -88,7 +88,10 @@ class TabsStore {
// still kept so a reload on `#/` does not drop the tab the user is on
const isLive = (id: string) => validIds.includes(id) || id === NEW_CHAT_TAB_ID;
const persisted = this.load().filter(isLive);
const extras = this.openTabs.filter((id) => isLive(id) && !persisted.includes(id));
// tabs already in openTabs come from the live route, so they stay as they
// are: `validIds` is a snapshot and a conversation created while the list
// was loading is not in it
const extras = this.openTabs.filter((id) => !persisted.includes(id));
this.openTabs = [...persisted, ...extras];
this.initialized = true;
+1 -1
View File
@@ -1,4 +1,4 @@
import { SidebarAction } from '$lib/enums';
import type { SidebarAction } from '$lib/enums';
import type { Component } from 'svelte';
/**
+2 -2
View File
@@ -61,10 +61,10 @@
}
}
// ?q= sends an immediate prompt in a fresh conversation
// ?q= creates the conversation, the chat route sends the prompt once the
// conversation id is in the URL
if (qParam !== null) {
await conversationsStore.createConversation();
await chatStore.sendMessage(qParam);
clearUrlParams();
} else if (modelParam) {
clearUrlParams();