diff --git a/tools/ui/.prettierignore b/tools/ui/.prettierignore index 635cf99c7e..ba47fbe165 100644 --- a/tools/ui/.prettierignore +++ b/tools/ui/.prettierignore @@ -19,3 +19,7 @@ test-results # Vendored third party sources, kept byte identical to upstream src/lib/vendors/ + +# Agent skills installed from upstream, kept as installed +.agents/ +skills-lock.json diff --git a/tools/ui/src/app.css b/tools/ui/src/app.css index f9b544bebc..ccf13fbec3 100644 --- a/tools/ui/src/app.css +++ b/tools/ui/src/app.css @@ -6,6 +6,33 @@ @custom-variant dark (&:is(.dark *)); +/* shadcn components key their orientation styles off these, toggle-group among them */ +@custom-variant data-horizontal (&[data-orientation='horizontal']); +@custom-variant data-vertical (&[data-orientation='vertical']); + +/* + * Tailwind v4 dropped the pointer cursor from buttons. Every control a user can + * press gets it back, and anything disabled keeps the cursor its own class asks for. + */ +@layer base { + button:not(:disabled), + [role='button']:not([data-disabled]), + [role='checkbox']:not([data-disabled]), + [role='combobox']:not([data-disabled]), + [role='menuitem']:not([data-disabled]), + [role='menuitemcheckbox']:not([data-disabled]), + [role='menuitemradio']:not([data-disabled]), + [role='option']:not([data-disabled]), + [role='radio']:not([data-disabled]), + [role='switch']:not([data-disabled]), + [role='tab']:not([data-disabled]), + label[for], + select, + summary { + cursor: pointer; + } +} + :root { --radius: 0.625rem; --background: oklch(1 0 0); @@ -140,7 +167,7 @@ } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground antialiased; scrollbar-width: thin; scrollbar-gutter: stable; } diff --git a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte index 962b6774fc..cc11851fa4 100644 --- a/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatScreen/ChatScreenForm.svelte @@ -3,7 +3,7 @@ import { page } from '$app/state'; import { ChatForm } from '$lib/components/app'; import { useDraftMessages } from '$lib/hooks/use-draft-messages.svelte'; - import { deviceStore } from '$lib/stores'; + import { deviceStore, uiStore } from '$lib/stores'; import { onMount } from 'svelte'; interface Props { @@ -33,6 +33,15 @@ }: Props = $props(); let chatFormRef: ChatForm | undefined = $state(undefined); + + // a dialog that closed onto the chat asks for the composer to take focus + $effect(() => { + if (!uiStore.consumeComposerFocus()) return; + + const frame = requestAnimationFrame(() => chatFormRef?.focus()); + + return () => cancelAnimationFrame(frame); + }); let formWrapperEl: HTMLDivElement | undefined = $state(); let chatId = $derived(page.params.id as string | undefined); diff --git a/tools/ui/src/lib/components/ui/button/button.svelte b/tools/ui/src/lib/components/ui/button/button.svelte index 4de7e87ae7..b5642098f1 100644 --- a/tools/ui/src/lib/components/ui/button/button.svelte +++ b/tools/ui/src/lib/components/ui/button/button.svelte @@ -27,7 +27,7 @@ outline: 'shadow-sm hover:text-accent-foreground hover:bg-muted-foreground/10 backdrop-blur-sm dark:border-input border', secondary: - 'bg-background dark:bg-muted-foreground/15 dark:text-secondary-foreground shadow-sm text-foreground hover:bg-muted-foreground/20 dark:hover:bg-muted-foreground/25', + 'bg-muted/30 dark:bg-muted-foreground/15 dark:text-secondary-foreground shadow-sm border-muted border text-foreground hover:bg-muted dark:hover:bg-muted-foreground/25', tertiary: 'bg-muted/60 dark:bg-muted/75 shadow-sm border border-border/30 text-foreground hover:bg-muted/80 dark:border-border/20 dark:hover:bg-muted' } diff --git a/tools/ui/src/routes/+layout.svelte b/tools/ui/src/routes/+layout.svelte index 5de4f82c1c..3e6a5a430f 100644 --- a/tools/ui/src/routes/+layout.svelte +++ b/tools/ui/src/routes/+layout.svelte @@ -2,7 +2,6 @@ import '../app.css'; import { browser } from '$app/environment'; import { goto } from '$app/navigation'; - import { base } from '$app/paths'; import { page } from '$app/state'; import { SidebarNavigation } from '$lib/components/app'; import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa'; @@ -31,6 +30,7 @@ versionStore } from '$lib/stores'; import { initStores } from '$lib/stores/init'; + import { apiUrl } from '$lib/utils/api-base'; import { ModeWatcher } from 'mode-watcher'; import { untrack } from 'svelte'; import { onMount } from 'svelte'; @@ -147,6 +147,11 @@ }); function checkApiKey() { + // the stored key authenticates the llama.cpp server serving this UI + if (!serverStore.capabilities.props) { + return; + } + const apiKey = settingsStore.config.apiKey; // Without a stored key there is nothing to re-validate here; the keyless @@ -167,7 +172,7 @@ [HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey.trim()}` }; - fetch(`${base}/props`, { headers }) + fetch(apiUrl('/props'), { headers }) .then((response) => { if (response.status === 401 || response.status === 403) { window.location.reload(); @@ -230,37 +235,17 @@ }); } - // Fetch router models when in router mode (for status and modalities) - // Wait for models to be loaded first, run only once - let routerModelsFetched = false; - - $effect(() => { - const isRouter = serverStore.isRouterMode; - const modelsCount = modelsStore.models.length; - - // Only fetch router models once when we have models loaded and in router mode - if (isRouter && modelsCount > 0 && !routerModelsFetched) { - routerModelsFetched = true; - - untrack(() => { - modelsStore.fetchRouterModels(); - }); - } - }); - - // Live model status and load progress via the /models/sse feed (router mode) + // Live model status and load progress via the /models/sse feed (router mode). + // The feed is kept for the session: switching to an external backend and back + // must not tear it down and reconnect on every tab switch. $effect(() => { if (!browser) return; - if (!serverStore.isRouterMode) return; + if (!serverStore.localIsRouter) return; untrack(() => { modelsStore.status.subscribe(); }); - - return () => { - modelsStore.status.unsubscribe(); - }; }); // Background MCP server health checks on app load.