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:
@@ -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
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user