+
+
+ Context
+ ·
+
+ {formatParameters(gauge.contextUsed)}
+ / {gauge.contextTotal !== null ? formatParameters(gauge.contextTotal) : '-'}
+
+
+
+ {#if gauge.activeModelId !== null && !gauge.isActiveModelLoaded}
+
+ {:else if showProgressBar}
+
+
+
+
+ {gauge.contextPercent}% used
+
+
+ {formatParameters((gauge.contextTotal ?? 0) - gauge.contextUsed)} remaining
+
+
+ {:else}
+
No context info available
+ {/if}
+
+ {#if gauge.hasAnyUsage}
+
+ {/if}
+
+
+{/if}
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
index 560bf73ab..8e8a14ac3 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
@@ -7,7 +7,6 @@
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
import { REASONING_TAGS } from '$lib/constants/agentic';
import { MessageRole, AttachmentType, AgenticSectionType } from '$lib/enums';
- import { fadeInView } from '$lib/actions/fade-in-view.svelte';
import {
ChatMessageAssistant,
ChatMessageUser,
@@ -328,7 +327,7 @@
}
-
+
{#if message.role === MessageRole.SYSTEM}
{/if}
+
+
diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserPending.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserPending.svelte
index 58b3a42e0..1cc79fe6b 100644
--- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserPending.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserPending.svelte
@@ -1,6 +1,5 @@
- import { onMount } from 'svelte';
- import { beforeNavigate, afterNavigate } from '$app/navigation';
import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
import { setChatActionsContext } from '$lib/contexts';
import { MessageRole } from '$lib/enums';
@@ -35,9 +33,6 @@
let { messages = [], onUserAction, onMessagesReady }: Props = $props();
let allConversationMessages = $state
([]);
- let isVisible = $state(false);
- let previousConversationId = $state(null);
- let previousRouteId = $state(null);
const currentConfig = config();
@@ -123,26 +118,10 @@
}
}
- // Track conversation changes to trigger transition even on same route
+ // Refresh messages whenever the active conversation changes
$effect(() => {
- const conversation = activeConversation();
- const currentId = conversation?.id ?? null;
-
- if (currentId !== previousConversationId && previousConversationId !== null) {
- // Conversation changed - trigger fade out/in
- isVisible = false;
- requestAnimationFrame(() => {
- refreshAllMessages();
- previousConversationId = currentId;
- requestAnimationFrame(() => {
- isVisible = true;
- });
- });
- } else {
- previousConversationId = currentId;
- if (conversation) {
- refreshAllMessages();
- }
+ if (activeConversation()) {
+ refreshAllMessages();
}
});
@@ -152,23 +131,6 @@
onMessagesReady?.(displayMessages.length);
});
- onMount(() => {
- requestAnimationFrame(() => {
- isVisible = true;
- });
- });
-
- beforeNavigate((navigation) => {
- isVisible = false;
- previousRouteId = navigation.from?.route.id ?? null;
- });
-
- afterNavigate(() => {
- requestAnimationFrame(() => {
- isVisible = true;
- });
- });
-
let siblingInfoByMessageId = $derived(buildSiblingInfoMap(allConversationMessages));
let displayMessages = $derived.by(() => {
@@ -272,11 +234,7 @@
});
-
+
{#each displayMessages as { message, toolMessages, isLastAssistantMessage, isLastUserMessage, nextAssistantMessage, siblingInfo } (message.id)}
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
import { AlertTriangle, Loader2, RefreshCw } from '@lucide/svelte';
- import { fadeInView } from '$lib/actions/fade-in-view.svelte';
import * as Alert from '$lib/components/ui/alert';
import { serverError, serverLoading, serverStatus, serverStore } from '$lib/stores/server.svelte';
@@ -10,10 +9,7 @@
{#if hasError}
-
+
{#if isLoadingModel}
diff --git a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
index 8ac7f9448..2c17f4234 100644
--- a/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
+++ b/tools/ui/src/lib/components/app/content/MarkdownContent/MarkdownContent.svelte
@@ -78,7 +78,6 @@
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import type { DatabaseMessageExtra } from '$lib/types/database';
import { config } from '$lib/stores/settings.svelte';
- import { fadeInView } from '$lib/actions/fade-in-view.svelte';
interface Props {
attachments?: DatabaseMessageExtra[];
@@ -828,7 +827,7 @@
: ''}"
>
{#each renderedBlocks as block (block.id)}
-
+
{@html block.html}
{/each}
diff --git a/tools/ui/src/lib/constants/context-gauge-popup.ts b/tools/ui/src/lib/constants/context-gauge-popup.ts
new file mode 100644
index 000000000..fe2e6a1b5
--- /dev/null
+++ b/tools/ui/src/lib/constants/context-gauge-popup.ts
@@ -0,0 +1,8 @@
+// Half of the card width, matching the w-64 class on the card.
+export const CONTEXT_GAUGE_CARD_HALF_WIDTH_PX = 128;
+// Minimum distance kept between the card and the form edges.
+export const CONTEXT_GAUGE_EDGE_MARGIN_PX = 8;
+// Gap between the top of the dial and the bottom edge of the card.
+export const CONTEXT_GAUGE_DIAL_GAP_PX = 8;
+// Grace delay before closing, letting the pointer travel from dial to card.
+export const CONTEXT_GAUGE_CLOSE_GRACE_MS = 150;
diff --git a/tools/ui/src/lib/constants/index.ts b/tools/ui/src/lib/constants/index.ts
index 68b5992c2..a80e0cb63 100644
--- a/tools/ui/src/lib/constants/index.ts
+++ b/tools/ui/src/lib/constants/index.ts
@@ -12,6 +12,7 @@ export * from './recommended-mcp-servers';
export * from './storage';
export * from './attachment-menu';
export * from './auto-scroll';
+export * from './context-gauge-popup';
export * from './binary-detection';
export * from './built-in-tools';
export * from './cache';
diff --git a/tools/ui/src/lib/hooks/use-auto-scroll.svelte.ts b/tools/ui/src/lib/hooks/use-auto-scroll.svelte.ts
index 8107f08d0..f2ad50dff 100644
--- a/tools/ui/src/lib/hooks/use-auto-scroll.svelte.ts
+++ b/tools/ui/src/lib/hooks/use-auto-scroll.svelte.ts
@@ -84,11 +84,11 @@ export class AutoScrollController {
}
/**
- * Scrolls the container to the bottom.
+ * Scrolls the container to the bottom instantly.
*/
- scrollToBottom(behavior: ScrollBehavior = 'smooth'): void {
+ scrollToBottom(): void {
if (this._disabled || !this._container) return;
- this._container.scrollTo({ top: this._container.scrollHeight, behavior });
+ this._container.scrollTop = this._container.scrollHeight;
}
/**
diff --git a/tools/ui/src/lib/stores/context-gauge-popup.svelte.ts b/tools/ui/src/lib/stores/context-gauge-popup.svelte.ts
new file mode 100644
index 000000000..441edb3ac
--- /dev/null
+++ b/tools/ui/src/lib/stores/context-gauge-popup.svelte.ts
@@ -0,0 +1,90 @@
+// Shared state for the context gauge popup. The dial and the card live in
+// different DOM subtrees, so open state and placement are coordinated here.
+// centerX and bottom place the card just above the dial: both are measured
+// once at open time, relative to the closest form ancestor; the dial and
+// the card share that positioning frame, so the values stay exact for the
+// whole time the card is open.
+// Mouse pointers open on hover with a short grace delay to travel from
+// dial to card; touch pointers toggle on tap.
+import {
+ CONTEXT_GAUGE_CARD_HALF_WIDTH_PX,
+ CONTEXT_GAUGE_CLOSE_GRACE_MS,
+ CONTEXT_GAUGE_DIAL_GAP_PX,
+ CONTEXT_GAUGE_EDGE_MARGIN_PX
+} from '$lib/constants';
+
+let closeTimer: ReturnType
| undefined;
+let lastPointerType = '';
+
+export const gaugePopup = $state({ open: false, centerX: 0, bottom: 0 });
+
+function openFrom(trigger: HTMLElement): void {
+ clearTimeout(closeTimer);
+ const frame = trigger.closest('form');
+ if (frame) {
+ const frameRect = frame.getBoundingClientRect();
+ const triggerRect = trigger.getBoundingClientRect();
+ const centerX = triggerRect.left + triggerRect.width / 2 - frameRect.left;
+ const min = CONTEXT_GAUGE_CARD_HALF_WIDTH_PX + CONTEXT_GAUGE_EDGE_MARGIN_PX;
+ const max = frameRect.width - CONTEXT_GAUGE_CARD_HALF_WIDTH_PX - CONTEXT_GAUGE_EDGE_MARGIN_PX;
+ gaugePopup.centerX = Math.min(Math.max(centerX, min), Math.max(min, max));
+ gaugePopup.bottom = frameRect.bottom - triggerRect.top + CONTEXT_GAUGE_DIAL_GAP_PX;
+ }
+ gaugePopup.open = true;
+}
+
+function toggleFrom(trigger: HTMLElement): void {
+ if (gaugePopup.open) {
+ clearTimeout(closeTimer);
+ gaugePopup.open = false;
+ } else {
+ openFrom(trigger);
+ }
+}
+
+export function gaugePopupClose(): void {
+ clearTimeout(closeTimer);
+ gaugePopup.open = false;
+}
+
+export function gaugeTriggerPointerDown(event: PointerEvent): void {
+ lastPointerType = event.pointerType;
+}
+
+export function gaugeTriggerClick(event: MouseEvent): void {
+ if (lastPointerType !== 'touch') return;
+ toggleFrom(event.currentTarget as HTMLElement);
+}
+
+export function gaugeTriggerKeydown(event: KeyboardEvent): void {
+ if (event.key !== 'Enter' && event.key !== ' ') return;
+ event.preventDefault();
+ toggleFrom(event.currentTarget as HTMLElement);
+}
+
+export function gaugeTriggerEnter(event: PointerEvent): void {
+ if (event.pointerType !== 'mouse') return;
+ openFrom(event.currentTarget as HTMLElement);
+}
+
+export function gaugeTriggerLeave(event: PointerEvent): void {
+ if (event.pointerType !== 'mouse') return;
+ scheduleClose();
+}
+
+export function gaugeCardEnter(event: PointerEvent): void {
+ if (event.pointerType !== 'mouse') return;
+ clearTimeout(closeTimer);
+}
+
+export function gaugeCardLeave(event: PointerEvent): void {
+ if (event.pointerType !== 'mouse') return;
+ scheduleClose();
+}
+
+function scheduleClose(): void {
+ clearTimeout(closeTimer);
+ closeTimer = setTimeout(() => {
+ gaugePopup.open = false;
+ }, CONTEXT_GAUGE_CLOSE_GRACE_MS);
+}
diff --git a/tools/ui/src/lib/utils/viewport.ts b/tools/ui/src/lib/utils/viewport.ts
deleted file mode 100644
index 9e9b7aff3..000000000
--- a/tools/ui/src/lib/utils/viewport.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * Check if an element is within the current viewport.
- */
-export function isElementInViewport(node: HTMLElement): boolean {
- const rect = node.getBoundingClientRect();
- return (
- rect.top < window.innerHeight &&
- rect.bottom > 0 &&
- rect.left < window.innerWidth &&
- rect.right > 0
- );
-}