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
@@ -28,7 +28,7 @@ import {
NEW_TO_DEPRECATED_MAP
} from '$lib/constants';
import { LEGACY_AGENTIC_REGEX, LEGACY_REASONING_TAGS } from '$lib/constants/agentic';
import { SETTINGS_KEYS } from '$lib/constants/settings-registry';
import { SETTINGS_KEYS } from '$lib/constants/settings-keys';
import { MessageRole } from '$lib/enums';
// Types
+3 -3
View File
@@ -1,5 +1,5 @@
import {
NEWLINE_SEPARATOR,
NEWLINE,
SANDBOX_EMPTY_OUTPUT,
SANDBOX_OUTPUT_MAX_CHARS,
SANDBOX_TIMEOUT_MS_DEFAULT,
@@ -29,10 +29,10 @@ function formatReply(reply: SandboxReply): ToolExecutionResult {
lines.push(`=> ${String(reply.result)}`);
}
let content = lines.join(NEWLINE_SEPARATOR);
let content = lines.join(NEWLINE);
if (!content) content = SANDBOX_EMPTY_OUTPUT;
if (content.length > SANDBOX_OUTPUT_MAX_CHARS) {
content = `${content.slice(0, SANDBOX_OUTPUT_MAX_CHARS)}${NEWLINE_SEPARATOR}${SANDBOX_TRUNCATION_NOTICE}`;
content = `${content.slice(0, SANDBOX_OUTPUT_MAX_CHARS)}${NEWLINE}${SANDBOX_TRUNCATION_NOTICE}`;
}
return { content, isError: reply.error != null };
@@ -1,3 +1,6 @@
import { base } from '$app/paths';
import { getJsonHeaders } from '$lib/utils/api-headers';
import { parseSseJsonStream, type SseJsonEvent } from '$lib/utils/sse';
import { apiFetch } from '$lib/utils';
import { API_TOOLS } from '$lib/constants';
import { ToolResponseField } from '$lib/enums';
@@ -37,4 +40,91 @@ export class ToolsService {
return { content: JSON.stringify(result), isError: false };
}
/**
* Stream a built-in tool's output chunks from the server. The server
* `POST /tools` endpoint with `{stream: true}` emits `data: {"chunk": "..."}`
* events followed by a terminal `data: {"done": true}` (optionally with
* `error`). Yields the chunk string for each partial event.
*
* The terminal event's `error` field, if present, is yielded as a final
* synthetic chunk prefixed with an error marker so the accumulated content
* already carries the failure context for the caller.
*
* Throws synchronously if the server rejects the request (e.g. tool does
* not support streaming, or 4xx/5xx response). The HTTP fetch goes through
* a minimal text/event-stream reader since the chat SSE parser in
* chat.service.ts embeds extra resume logic that is unnecessary here.
*/
static async *streamTool(
toolName: string,
params: Record<string, unknown>,
signal?: AbortSignal
): AsyncGenerator<ToolStreamEvent> {
const headers = getJsonHeaders();
const response = await fetch(`${base}${API_TOOLS.EXECUTE}`, {
method: 'POST',
headers,
body: JSON.stringify({ tool: toolName, params, stream: true }),
signal
});
if (!response.ok || !response.body) {
const detail = await formatNonOkResponse(response);
throw new Error(detail);
}
const iterator = parseSseJsonStream<ToolServerEvent>(response, signal);
while (true) {
const next: IteratorResult<SseJsonEvent<ToolServerEvent>> = await iterator.next();
if (next.done) return;
const event = next.value.data;
if (event.chunk !== undefined) {
yield { chunk: event.chunk, done: false };
}
if (event.done) {
yield { chunk: null, done: true, error: event.error };
return;
}
}
}
}
/**
* One event from streaming a tool's output.
* - During execution: `chunk` is a non-empty text fragment, `done: false`.
* - On terminal event: `done: true`, `error` populated if the call failed,
* and `chunk` is null.
*/
export interface ToolStreamEvent {
chunk: string | null;
done: boolean;
error?: string;
}
/** Wire shape of one SSE event from `POST /tools?stream=true`. */
interface ToolServerEvent {
chunk?: string;
done?: boolean;
error?: string;
}
async function formatNonOkResponse(response: Response): Promise<string> {
const status = `${response.status} ${response.statusText}`.trim();
try {
const errBody = (await response.clone().json()) as { error?: string; message?: string };
if (errBody?.error) return `${status}: ${errBody.error}`;
if (errBody?.message) return `${status}: ${errBody.message}`;
} catch (error) {
console.error('[tools] Non-JSON error response, falling back to raw text:', error);
try {
const text = await response.text();
if (text.trim()) return `${status}: ${text.trim()}`;
} catch (error) {
console.error('[tools] Failed to read error response as text:', error);
}
}
return status || `HTTP ${response.status}`;
}