ui: Agentic Content UX improvements (#25450)

* feat: Add shimmer text animation for processing state indicators

* feat: Redesign CollapsibleContentBlock component with improved UX

* feat: Add conditional setting display support with dependsOn field

* feat: Add showAgenticTurnStats setting for per-turn statistics

* feat: Update ChatMessageAgenticContent with improved UI and new features

* feat: Enhance file read tool UI/UX

* feat: Refine styling of collapsible content and code preview blocks

* feat: add terminal variant to CollapsibleContentBlock

* feat: add built-in tools UI registry

* feat: extract ChatMessageReasoningBlock and ChatMessageToolCallBlock

* refactor: simplify ChatMessageAgenticContent to use extracted blocks

* fix: correct markdown content block margin spacing

* fix: reorganize SettingsChatFields layout and reset button positioning

* fix: use direct map access in agentic store session methods

* refactor: remove reasoning preview/throttle system from CollapsibleContentBlock

* feat: add auto-scroll to reasoning block and remove showThoughtInProgress

* feat: add ChatMessageToolCallDateTime component and support for new tool types

* feat: improve auto-scroll reliability in reasoning block with RAF coalescing and MutationObserver

* feat: show MCP server favicon for tools without a built-in icon

* feat: add search-results parsing utilities and tests

* feat: add ChatMessageToolCallSearchResults component

* feat: integrate search results rendering into ChatMessageAgenticContent

* feat: display tool call input alongside output in ChatMessageToolCallBlock

* style: use muted foreground color in reasoning block content

* chore: Format

* feat: Refine reasoning block layout and make pending thoughts display configurable

* feat: Stream tool call code blocks with auto-scroll and handle partial JSON

* feat: add streaming permission gate infrastructure

* feat: wire permission gate into the agentic loop

* fix: bail out on abort and skip already-approved tool calls

* fix: clear partial tool calls on abort and savePartialResponse

* test: cover partial tool call cleanup end-to-end

* refactor: Remove streaming permission gate logic

* fix: Correct autoscroll and streaming gates for tool calls and reasoning blocks

* refactor: Chat Message Assistant componentization

* fix: Show health metadata for disabled MCP servers and promote connections on enable

* fix: Inherit global enabled state for missing MCP per-chat overrides

* refactor: Cleanup

* refactor: Split ChatMessageToolCallBlock into dedicated components

* feat: Add live streaming and auto-scroll for tool execution output

* feat: Add line numbers and change markers to file edit diffs

* chore: Formatting

* feat: Add type definitions and utilities for recommended MCP servers

* feat: Add recommended MCP servers configuration and storage key

* feat: Add McpServerCardCompact component for recommended servers

* feat: Add recommended servers section to Add New Server dialog

* feat: Update McpServerForm to support authorization requirements

* feat: Add select-none classes for text selection prevention

* feat: Add recommended MCP server icon assets

* refactor: Store dismissed MCP recommendations as a boolean flag

* feat: Render tool results as JSON or Markdown based on detected content type

* feat: UI improvement

* feat: Render search block early and update heading to show execution state

* fix: Prevent non-web-search tools from triggering the search UI block

* refactor: Cleanup

* refactor: Extract hardcoded icon size classes into shared constants

* refactor: Extract hardcoded tool result separator into a shared constant

* refactor: Tool Calls UI/logic

* refactor: Cleanup

* refactor: Cleanup

* refactor: Cleanup
This commit is contained in:
Aleksander Grygier
2026-07-15 20:31:45 +02:00
committed by GitHub
parent 3b53219361
commit 32beb244f5
146 changed files with 5960 additions and 1053 deletions
@@ -0,0 +1,151 @@
<script lang="ts">
import { Lightbulb } from '@lucide/svelte';
import { CollapsibleContentBlock, MarkdownContent } from '$lib/components/app';
import { AgenticSectionType } from '$lib/enums';
import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
import type { DatabaseMessageExtra } from '$lib/types';
import type { AgenticSection } from '$lib/utils';
interface Props {
section: AgenticSection;
open: boolean;
isStreaming: boolean;
renderThinkingAsMarkdown: boolean;
hasReasoningError?: boolean;
attachments?: DatabaseMessageExtra[];
onToggle?: () => void;
}
let {
section,
open,
isStreaming,
renderThinkingAsMarkdown,
hasReasoningError = false,
attachments,
onToggle
}: Props = $props();
const REASONING_HEADER = 'Reasoning';
const REASONING_HEADER_PENDING = 'Reasoning...';
const REASONING_SUBTITLE_ERROR = 'Error';
const REASONING_SUBTITLE_CANCELLED = 'Cancelled';
const isPending = $derived(section.type === AgenticSectionType.REASONING_PENDING);
const title = $derived(isPending && isStreaming ? REASONING_HEADER_PENDING : REASONING_HEADER);
const subtitle = $derived.by(() => {
if (isPending && !isStreaming) {
return hasReasoningError ? REASONING_SUBTITLE_ERROR : REASONING_SUBTITLE_CANCELLED;
}
if (section.wasInterrupted) {
return hasReasoningError ? REASONING_SUBTITLE_ERROR : REASONING_SUBTITLE_CANCELLED;
}
return isStreaming ? '' : undefined;
});
const shimmerTitle = $derived(isPending && isStreaming);
let scrollEl: HTMLDivElement | undefined = $state();
const SCROLL_BOTTOM_THRESHOLD_PX = REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX;
let userScrolledUp = $state(false);
let lastScrollTop = 0;
let pendingFrame: number | null = null;
function isAtBottom(): boolean {
if (!scrollEl) return false;
return (
scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
SCROLL_BOTTOM_THRESHOLD_PX
);
}
function scrollToBottomOnFrame() {
if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
pendingFrame = requestAnimationFrame(() => {
pendingFrame = null;
// User may scroll between scheduling and paint.
if (scrollEl && !userScrolledUp) {
scrollEl.scrollTop = scrollEl.scrollHeight;
}
});
}
function handleScrollEvent() {
if (!scrollEl) return;
const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
if (isScrollingUp && !isAtBottom()) {
userScrolledUp = true;
} else if (isAtBottom()) {
userScrolledUp = false;
}
lastScrollTop = scrollEl.scrollTop;
}
$effect(() => {
void section.content;
if (!scrollEl || !isPending || !isStreaming) return;
scrollToBottomOnFrame();
});
$effect(() => {
// Layout shifts that don't change section.content (markdown re-parse,
// syntax-highlight settle, image loads).
if (!scrollEl || !isPending || !isStreaming) return;
const observer = new MutationObserver(() => scrollToBottomOnFrame());
observer.observe(scrollEl, {
childList: true,
subtree: true,
characterData: true
});
return () => observer.disconnect();
});
$effect(() => {
// Pin to bottom at the start of each round.
if (!isPending) {
userScrolledUp = false;
lastScrollTop = 0;
}
});
</script>
<CollapsibleContentBlock
{open}
class="my-2"
icon={Lightbulb}
iconClass="h-3.5 w-3.5"
{title}
{subtitle}
{shimmerTitle}
{onToggle}
>
<div
bind:this={scrollEl}
class="reasoning-content"
class:is-streaming={isPending}
onscroll={handleScrollEvent}
>
{#if renderThinkingAsMarkdown}
<MarkdownContent content={section.content} class="text-muted-foreground" {attachments} />
{:else}
<div
class="text-[13px] leading-relaxed wrap-break-word whitespace-pre-wrap text-muted-foreground"
>
{section.content}
</div>
{/if}
</div>
</CollapsibleContentBlock>
<style>
.reasoning-content.is-streaming {
max-height: 28rem;
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-gutter: stable;
padding-right: 0.25rem;
}
</style>