From a69de44bc3d6088eb7108c9130f6efff5a24cd8a Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Mon, 28 Sep 2026 16:24:43 +0200 Subject: [PATCH] ui : polish the shell The secondary button gets its own look back and the chat add button its own light surface. Pressable elements get a pointer cursor again, the font rendering smooths on the app shell, the root layout resolves its props probe through the providers' api url, and the agent skills stay out of prettier's way. Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- tools/ui/.prettierignore | 4 ++ tools/ui/src/app.css | 29 ++++++++++++++- .../app/chat/ChatScreen/ChatScreenForm.svelte | 11 +++++- .../lib/components/ui/button/button.svelte | 2 +- tools/ui/src/routes/+layout.svelte | 37 ++++++------------- 5 files changed, 54 insertions(+), 29 deletions(-) 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.