allozaur/feat/chat slash commands (#26716)

* base : slash-command/misc foundation - model icon and focus-selector constants

* feat : slash-command picker and command parsing helpers

* refactor : wire command and @-mention pickers into the chat form

* ui : improve model selector keyboard navigation and load/dismiss

* feat: Unify markdown/raw-text rendering under one setting with migration

* fix: Misc fixes - tool-call subtitle, assistant wrap, progress guards

* feat: Clamp and style numeric settings inputs from registry bounds
This commit is contained in:
Aleksander Grygier
2026-08-07 20:20:01 +02:00
committed by GitHub
parent f8e30266d2
commit 6de1b63473
41 changed files with 1425 additions and 506 deletions
@@ -1,8 +1,9 @@
<script lang="ts">
import { ChevronDown, Loader2, Package } from '@lucide/svelte';
import { ChevronDown, Loader2 } from '@lucide/svelte';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
import { KeyboardKey, ServerModelStatus } from '$lib/enums';
import { MODEL_SELECTOR_ICON } from '$lib/constants';
import { useModelsSelector } from '$lib/hooks/use-models-selector.svelte';
import { modelsStore, routerModels } from '$lib/stores/models.svelte';
import { modelLoadFraction } from '$lib/utils';
@@ -35,7 +36,7 @@
}: Props = $props();
let isOpen = $state(false);
let highlightedIndex = $state<number>(-1);
let highlightedId = $state<string | null>(null);
const ms = useModelsSelector({
currentModel: () => currentModel,
@@ -43,15 +44,77 @@
onModelChange: () => onModelChange,
onOpenChange: (open) => {
isOpen = open;
highlightedIndex = -1;
highlightedId = null;
}
});
$effect(() => {
void ms.searchTerm;
highlightedIndex = -1;
highlightedId = null;
});
// Focus the dropdown's search box without scrolling the page. bits-ui
// auto-focuses the opened content by default, which can yank the page
// scroll; we prevent that on the Content and refocus the search here.
$effect(() => {
if (!isOpen) return;
requestAnimationFrame(() => {
const search = document.querySelector<HTMLElement>(
'[data-slot="dropdown-menu-content"] input'
);
search?.focus({ preventScroll: true });
});
});
// Keyboard navigation follows the on-screen row order, not the flat option list order.
let visualOrder = $derived.by(() => {
const order: string[] = [];
for (const item of ms.groupedFilteredOptions.loaded) order.push(item.option.id);
for (const item of ms.groupedFilteredOptions.favorites) order.push(item.option.id);
for (const group of ms.groupedFilteredOptions.available) {
for (const item of group.items) order.push(item.option.id);
}
return order;
});
let highlightedIndex = $derived(highlightedId ? visualOrder.indexOf(highlightedId) : -1);
function moveHighlight(direction: 1 | -1) {
const len = visualOrder.length;
if (len === 0) {
highlightedId = null;
return;
}
let index = highlightedIndex;
if (index === -1) {
index = direction === 1 ? 0 : len - 1;
} else {
index = (index + direction + len) % len;
}
highlightedId = visualOrder[index];
}
// Alt+Enter only unloads and keeps the dropdown open.
async function handleModelKeyAction(modelId: string, unload: boolean) {
if (!unload) {
void ms.handleSelect(modelId);
return;
}
const model = routerModels().find((m) => m.id === modelId);
const status = model?.status?.value as ServerModelStatus | undefined;
if (status === ServerModelStatus.LOADING) return;
await modelsStore.unloadModel(modelId);
}
export function open() {
ms.handleOpenChange(true);
}
@@ -61,33 +124,17 @@
if (event.key === KeyboardKey.ARROW_DOWN) {
event.preventDefault();
if (ms.filteredOptions.length === 0) return;
if (highlightedIndex === -1 || highlightedIndex === ms.filteredOptions.length - 1) {
highlightedIndex = 0;
} else {
highlightedIndex += 1;
}
moveHighlight(1);
} else if (event.key === KeyboardKey.ARROW_UP) {
event.preventDefault();
if (ms.filteredOptions.length === 0) return;
if (highlightedIndex === -1 || highlightedIndex === 0) {
highlightedIndex = ms.filteredOptions.length - 1;
} else {
highlightedIndex -= 1;
}
moveHighlight(-1);
} else if (event.key === KeyboardKey.ENTER) {
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < ms.filteredOptions.length) {
const option = ms.filteredOptions[highlightedIndex];
ms.handleSelect(option.id);
} else if (ms.filteredOptions.length > 0) {
highlightedIndex = 0;
if (highlightedId) {
void handleModelKeyAction(highlightedId, event.altKey);
} else if (visualOrder.length > 0) {
highlightedId = visualOrder[0];
}
}
}
@@ -109,7 +156,7 @@
]}
style="max-width: min(calc(100cqw - 10rem), 20rem)"
>
<Package class="h-3.5 w-3.5 shrink-0" />
<MODEL_SELECTOR_ICON class="h-3.5 w-3.5 shrink-0" />
</span>
{:else}
<p class="text-xs text-muted-foreground">No models available.</p>
@@ -150,7 +197,7 @@
]}
disabled={disabled || ms.updating}
>
<Package class="h-3.5 w-3.5 shrink-0" />
<MODEL_SELECTOR_ICON class="h-3.5 w-3.5 shrink-0" />
{#if selectedOption}
<ModelId
@@ -186,6 +233,7 @@
<DropdownMenu.Content
align="end"
class="w-full max-w-[100vw] pt-0 sm:w-max sm:max-w-[calc(100vw-2rem)]"
onOpenAutoFocus={(event) => event.preventDefault()}
>
<DropdownMenuSearchable
searchValue={ms.searchTerm}
@@ -217,9 +265,9 @@
{/if}
{#snippet modelOption(item: ModelItem, hideOrgName: boolean)}
{@const { option, flatIndex } = item}
{@const { option } = item}
{@const isSelected = currentModel === option.model || ms.activeId === option.id}
{@const isHighlighted = flatIndex === highlightedIndex}
{@const isHighlighted = option.id === highlightedId}
{@const isFav = ms.isFavorite(option.model)}
<ModelsSelectorOption
@@ -230,11 +278,11 @@
{hideOrgName}
onSelect={ms.handleSelect}
onInfoClick={ms.handleInfoClick}
onMouseEnter={() => (highlightedIndex = flatIndex)}
onMouseEnter={() => (highlightedId = option.id)}
onKeyDown={(event) => {
if (event.key === KeyboardKey.ENTER || event.key === KeyboardKey.SPACE) {
event.preventDefault();
ms.handleSelect(option.id);
void handleModelKeyAction(option.id, event.altKey);
}
}}
/>
@@ -275,7 +323,7 @@
onclick={() => ms.handleOpenChange(true)}
disabled={disabled || ms.updating}
>
<Package class="h-3.5 w-3.5 shrink-0" />
<MODEL_SELECTOR_ICON class="h-3.5 w-3.5 shrink-0" />
{#if selectedOption}
<ModelId
@@ -62,9 +62,10 @@
<div
class={[
'group relative flex w-full items-center gap-2 rounded-sm p-2 text-left text-sm transition focus:outline-none',
'cursor-pointer hover:bg-muted focus:bg-muted',
(isSelected || isHighlighted) && 'bg-accent text-accent-foreground',
!(isSelected || isHighlighted) && 'hover:bg-accent hover:text-accent-foreground',
'cursor-pointer',
isSelected && 'bg-accent/50 text-accent-foreground',
isHighlighted && 'bg-accent',
!isSelected && !isHighlighted && 'hover:bg-muted',
isLoaded ? 'text-popover-foreground' : 'text-muted-foreground'
]}
role="option"