diff --git a/tools/server/public/index.html.gz b/tools/server/public/index.html.gz index 3d0991dde0f..2f41ea87d93 100644 Binary files a/tools/server/public/index.html.gz and b/tools/server/public/index.html.gz differ diff --git a/tools/server/webui/package-lock.json b/tools/server/webui/package-lock.json index 361144915f0..9460b62e9ef 100644 --- a/tools/server/webui/package-lock.json +++ b/tools/server/webui/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", + "acorn": "^8.15.0", "highlight.js": "^11.11.1", "mode-watcher": "^1.1.0", "pdfjs-dist": "^5.4.54", @@ -41,6 +42,7 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", + "@testing-library/svelte": "^5.2.9", "@types/node": "^24", "@vitest/browser": "^3.2.3", "@vitest/coverage-v8": "^3.2.3", @@ -939,7 +941,6 @@ "integrity": "sha512-oJrXtQiAXLvT9clCf1K4kxp3eKsQhIaZqxEyowkBcsvZDdZkbWrVmnGknxs5flTD0VGsxrxKgBCZty1EzoiMzA==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/helpers": "^0.5.0" } @@ -2161,7 +2162,6 @@ "integrity": "sha512-W9R51zUCd2iHOQBg/D93+bdpYv6kbtFx+kft5X8lPKQl6yEu0aKs9i5N5GyCASOhIApgx/tkqZIJ7vgM4cqrHA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ts-dedent": "^2.0.0", "type-fest": "~2.19" @@ -2245,7 +2245,6 @@ "integrity": "sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", @@ -2289,7 +2288,6 @@ "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "debug": "^4.4.1", @@ -2705,7 +2703,6 @@ "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -2762,6 +2759,46 @@ "dev": true, "license": "MIT" }, + "node_modules/@testing-library/svelte": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.3.1.tgz", + "integrity": "sha512-8Ez7ZOqW5geRf9PF5rkuopODe5RGy3I9XR+kc7zHh26gBiktLaxTfKmhlGaSHYUOTQE7wFsLMN9xCJVCszw47w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "9.x.x || 10.x.x", + "@testing-library/svelte-core": "1.0.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0", + "vite": "*", + "vitest": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@testing-library/svelte-core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/svelte-core/-/svelte-core-1.0.0.tgz", + "integrity": "sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "svelte": "^3 || ^4 || ^5 || ^5.0.0-next.0" + } + }, "node_modules/@testing-library/user-event": { "version": "14.6.1", "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", @@ -2873,7 +2910,6 @@ "integrity": "sha512-+0/4J266CBGPUq/ELg7QUHhN25WYjE0wYTPSQJn1xeu8DOlIOPxXxrNGiLmfAWl7HMMgWFWXpt9IDjMWrF5Iow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2940,7 +2976,6 @@ "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.0", "@typescript-eslint/types": "8.56.0", @@ -3177,7 +3212,6 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -3305,7 +3339,6 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -3376,7 +3409,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4094,7 +4126,8 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -4404,7 +4437,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -4465,7 +4497,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5672,7 +5703,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -8097,7 +8127,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8231,7 +8260,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -8248,7 +8276,6 @@ "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "prettier": "^3.0.0", "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" @@ -8480,7 +8507,6 @@ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8491,7 +8517,6 @@ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -8766,7 +8791,6 @@ "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -8877,7 +8901,6 @@ "integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -9172,7 +9195,6 @@ "integrity": "sha512-LwF0VZsT4qkgx66Ad/q0QgZZrU2a5WftaADDEcJ3bGq3O2fHvwWPlSZjM1HiXD4vqP9U5JiMqQkV1gkyH0XJkw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -9387,7 +9409,6 @@ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.48.3.tgz", "integrity": "sha512-w7QZ398cdNherTdiQ/v3SYLLGOO4948Jgjh04PYqtTYVohmBvbmFwLmo7pp8gp4/1tceRWfSTjHgjtfpCVNJmQ==", "license": "MIT", - "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", @@ -9633,7 +9654,6 @@ "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -9664,8 +9684,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.2.2", @@ -9942,7 +9961,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10336,7 +10354,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -10497,7 +10514,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -10819,7 +10835,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/tools/server/webui/package.json b/tools/server/webui/package.json index f5cdc9e47f0..10e023927e0 100644 --- a/tools/server/webui/package.json +++ b/tools/server/webui/package.json @@ -40,6 +40,7 @@ "@tailwindcss/forms": "^0.5.9", "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", + "@testing-library/svelte": "^5.2.9", "@types/node": "^24", "@vitest/browser": "^3.2.3", "@vitest/coverage-v8": "^3.2.3", @@ -80,6 +81,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", + "acorn": "^8.15.0", "highlight.js": "^11.11.1", "mode-watcher": "^1.1.0", "pdfjs-dist": "^5.4.54", diff --git a/tools/server/webui/playwright.config.ts b/tools/server/webui/playwright.config.ts index 26d3be535d1..9035d0e3bb6 100644 --- a/tools/server/webui/playwright.config.ts +++ b/tools/server/webui/playwright.config.ts @@ -2,7 +2,8 @@ import { defineConfig } from '@playwright/test'; export default defineConfig({ webServer: { - command: 'npm run build && http-server ../public -p 8181', + command: + 'npm run build && gzip -dc ../public/index.html.gz > ../public/index.html && http-server ../public -p 8181', port: 8181, timeout: 120000, reuseExistingServer: false diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 5977f1c8f1e..9c7911bcbd4 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -10,6 +10,7 @@ import { AgenticSectionType, AttachmentType, FileTypeText } from '$lib/enums'; import { formatJsonPretty } from '$lib/utils'; import { ATTACHMENT_SAVED_REGEX, NEWLINE_SEPARATOR } from '$lib/constants'; + import { getJavaScriptSourceArgument } from '$lib/services/tools/codeInterpreterSharedState'; import { parseAgenticContent, type AgenticSection } from '$lib/utils'; import type { DatabaseMessage, DatabaseMessageExtraImageFile } from '$lib/types/database'; import type { ChatMessageAgenticTimings, ChatMessageAgenticTurnStats } from '$lib/types/chat'; @@ -27,16 +28,35 @@ image?: DatabaseMessageExtraImageFile; }; + type ParsedAgenticSection = AgenticSection & { + parsedLines: ToolResultLine[]; + }; + + type InlineFlowGroup = { + kind: 'flow'; + flatIndices: number[]; + sections: ParsedAgenticSection[]; + }; + + type InlineTextGroup = { + kind: 'text'; + flatIndex: number; + section: ParsedAgenticSection; + }; + + type InlineRenderGroup = InlineFlowGroup | InlineTextGroup; + let { content, message, isStreaming = false, highlightTurns = false }: Props = $props(); let expandedStates: Record = $state({}); + let autoExpandedStates: Record = $state({}); const sections = $derived(parseAgenticContent(content)); const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean); const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean); // Parse toolResults with images only when sections or message.extra change - const sectionsParsed = $derived( + const sectionsParsed = $derived( sections.map((section) => ({ ...section, parsedLines: section.toolResult @@ -45,6 +65,85 @@ })) ); + function isToolSection(section: ParsedAgenticSection): boolean { + return ( + section.type === AgenticSectionType.TOOL_CALL || + section.type === AgenticSectionType.TOOL_CALL_PENDING || + section.type === AgenticSectionType.TOOL_CALL_STREAMING + ); + } + + function isReasoningSection(section: ParsedAgenticSection): boolean { + return ( + section.type === AgenticSectionType.REASONING || + section.type === AgenticSectionType.REASONING_PENDING + ); + } + + function isPendingFlowSection(section: ParsedAgenticSection): boolean { + return ( + section.type === AgenticSectionType.REASONING_PENDING || + section.type === AgenticSectionType.TOOL_CALL_PENDING || + section.type === AgenticSectionType.TOOL_CALL_STREAMING + ); + } + + function buildInlineRenderGroups( + parsedSections: ParsedAgenticSection[], + flatIndices: number[] + ): InlineRenderGroup[] { + const groups: InlineRenderGroup[] = []; + let currentFlowSections: ParsedAgenticSection[] = []; + let currentFlowIndices: number[] = []; + + const flushFlow = () => { + if (currentFlowSections.length === 0) { + return; + } + + groups.push({ + kind: 'flow', + sections: currentFlowSections, + flatIndices: currentFlowIndices + }); + currentFlowSections = []; + currentFlowIndices = []; + }; + + for (let i = 0; i < parsedSections.length; i++) { + const section = parsedSections[i]; + const flatIndex = flatIndices[i]; + + if (section.type === AgenticSectionType.TEXT) { + flushFlow(); + groups.push({ + kind: 'text', + section, + flatIndex + }); + continue; + } + + currentFlowSections.push(section); + currentFlowIndices.push(flatIndex); + } + + flushFlow(); + + return groups; + } + + function isFlowGroup(group: InlineRenderGroup): group is InlineFlowGroup { + return group.kind === 'flow'; + } + + const inlineGroups = $derived.by(() => + buildInlineRenderGroups( + sectionsParsed, + sectionsParsed.map((_, index) => index) + ) + ); + // Group flat sections into agentic turns // A new turn starts when a non-tool section follows a tool section const turnGroups = $derived.by(() => { @@ -78,35 +177,96 @@ return turns; }); - function getDefaultExpanded(section: AgenticSection): boolean { - if ( - section.type === AgenticSectionType.TOOL_CALL_PENDING || - section.type === AgenticSectionType.TOOL_CALL_STREAMING - ) { - return showToolCallInProgress; + const renderedGroups = $derived.by(() => { + if (highlightTurns && turnGroups.length > 1) { + return turnGroups.flatMap((turn) => buildInlineRenderGroups(turn.sections, turn.flatIndices)); } - if (section.type === AgenticSectionType.REASONING_PENDING) { - return showThoughtInProgress; - } + return inlineGroups; + }); + + const renderedFlowGroups = $derived.by(() => renderedGroups.filter(isFlowGroup)); + + function getDefaultExpanded(group: InlineFlowGroup): boolean { + return group.sections.some((section) => { + if ( + section.type === AgenticSectionType.TOOL_CALL_PENDING || + section.type === AgenticSectionType.TOOL_CALL_STREAMING + ) { + return showToolCallInProgress; + } + + if (section.type === AgenticSectionType.REASONING_PENDING) { + return showThoughtInProgress; + } + + return false; + }); + } - return false; + function shouldPreserveExpandedState(group: InlineFlowGroup): boolean { + const lastFlatIndex = Math.max(...group.flatIndices); + const hasInlineTextAfterGroup = sectionsParsed + .slice(lastFlatIndex + 1) + .some((section) => section.type === AgenticSectionType.TEXT); + + return ( + group.sections.some((section) => isReasoningSection(section)) && + group.sections.some((section) => isToolSection(section)) && + !hasInlineTextAfterGroup + ); } - function isExpanded(index: number, section: AgenticSection): boolean { - if (expandedStates[index] !== undefined) { - return expandedStates[index]; + function isExpanded(group: InlineFlowGroup): boolean { + const groupKey = group.flatIndices[0]; + + if (expandedStates[groupKey] !== undefined) { + return expandedStates[groupKey]; } - return getDefaultExpanded(section); + if (autoExpandedStates[groupKey] !== undefined) { + return autoExpandedStates[groupKey]; + } + + return getDefaultExpanded(group); } - function toggleExpanded(index: number, section: AgenticSection) { - const currentState = isExpanded(index, section); + function toggleExpanded(group: InlineFlowGroup) { + const groupKey = group.flatIndices[0]; + const currentState = isExpanded(group); - expandedStates[index] = !currentState; + expandedStates[groupKey] = !currentState; } + $effect(() => { + const activeGroupKeys = new Set(); + + for (const group of renderedFlowGroups) { + const groupKey = group.flatIndices[0]; + activeGroupKeys.add(groupKey); + + if (expandedStates[groupKey] !== undefined) { + continue; + } + + if (autoExpandedStates[groupKey] === undefined) { + autoExpandedStates[groupKey] = getDefaultExpanded(group); + continue; + } + + if (!shouldPreserveExpandedState(group)) { + autoExpandedStates[groupKey] = getDefaultExpanded(group); + } + } + + for (const key of Object.keys(autoExpandedStates)) { + const numericKey = Number(key); + if (!activeGroupKeys.has(numericKey)) { + delete autoExpandedStates[numericKey]; + } + } + }); + function parseToolResultWithImages( toolResult: string, extras?: DatabaseMessage['extra'] @@ -136,146 +296,149 @@ llm: stats.llm }; } - -{#snippet renderSection(section: (typeof sectionsParsed)[number], index: number)} - {#if section.type === AgenticSectionType.TEXT} -
- + function getToolArgumentsCode(section: ParsedAgenticSection): string | undefined { + if (!section.toolArgs) { + return undefined; + } + + return getJavaScriptSourceArgument(section.toolName, section.toolArgs); + } + + +{#snippet renderInlineToolSection(section: ParsedAgenticSection)} + {@const isPending = section.type === AgenticSectionType.TOOL_CALL_PENDING} + {@const isStreamingTool = section.type === AgenticSectionType.TOOL_CALL_STREAMING} + {@const ToolIcon = isPending + ? Loader2 + : isStreamingTool + ? isStreaming + ? Loader2 + : AlertTriangle + : Wrench} + {@const toolIconClass = isPending || (isStreamingTool && isStreaming) + ? 'h-4 w-4 animate-spin' + : isStreamingTool + ? 'h-4 w-4 text-yellow-500' + : 'h-4 w-4'} + +
+
+
+ + + {section.toolName || 'Tool call'} + +
+ + {#if isPending} + executing... + {:else if isStreamingTool && !isStreaming} + incomplete + {/if}
- {:else if section.type === AgenticSectionType.TOOL_CALL_STREAMING} - {@const streamingIcon = isStreaming ? Loader2 : AlertTriangle} - {@const streamingIconClass = isStreaming ? 'h-4 w-4 animate-spin' : 'h-4 w-4 text-yellow-500'} - {@const streamingSubtitle = isStreaming ? '' : 'incomplete'} - - toggleExpanded(index, section)} - > -
-
- Arguments: - - {#if isStreaming} - - {/if} -
- {#if section.toolArgs} - - {:else if isStreaming} -
- Receiving arguments... + + {#if section.toolArgs && section.toolArgs !== '{}'} + {@const toolArgumentsCode = getToolArgumentsCode(section)} +
Arguments
+ {#if toolArgumentsCode} +
+
{:else} -
- Response was truncated -
- {/if} -
- - {:else if section.type === AgenticSectionType.TOOL_CALL || section.type === AgenticSectionType.TOOL_CALL_PENDING} - {@const isPending = section.type === AgenticSectionType.TOOL_CALL_PENDING} - {@const toolIcon = isPending ? Loader2 : Wrench} - {@const toolIconClass = isPending ? 'h-4 w-4 animate-spin' : 'h-4 w-4'} - - toggleExpanded(index, section)} - > - {#if section.toolArgs && section.toolArgs !== '{}'} -
-
Arguments:
- + {/if} + {:else if isStreamingTool} +
Arguments
+ {#if isStreaming} +
+ Receiving arguments... +
+ {:else} +
+ Response was truncated
{/if} + {/if} + + {#if section.toolResult || isPending} +
Result
+ {#if section.toolResult} +
+ {#each section.parsedLines as line, i (i)} +
{line.text}
+ {#if line.image} + {line.image.name} + {/if} + {/each} +
+ {:else} +
+ Waiting for result... +
+ {/if} + {/if} +
+{/snippet} -
-
- Result: +{#snippet renderFlowGroup(group: InlineFlowGroup)} + {@const hasReasoning = group.sections.some((section) => isReasoningSection(section))} + {@const isPendingFlow = group.sections.some((section) => isPendingFlowSection(section))} + {@const groupTitle = hasReasoning ? (isPendingFlow && isStreaming ? 'Reasoning...' : 'Reasoning') : 'Tool call'} + {@const groupSubtitle = hasReasoning && isPendingFlow && !isStreaming ? 'incomplete' : undefined} + {@const groupIcon = hasReasoning ? Brain : Wrench} + + toggleExpanded(group)} + > +
+ {#each group.sections as section, groupIndex (group.flatIndices[groupIndex])} + {#if groupIndex > 0} +
+ {/if} - {#if isPending} - - {/if} -
- {#if section.toolResult} -
- {#each section.parsedLines as line, i (i)} -
{line.text}
- {#if line.image} - {line.image.name} - {/if} - {/each} -
- {:else if isPending} -
- Waiting for result... + {#if isReasoningSection(section)} +
+ {section.content}
+ {:else if isToolSection(section)} + {@render renderInlineToolSection(section)} {/if} -
-
- {:else if section.type === AgenticSectionType.REASONING} - toggleExpanded(index, section)} - > -
-
- {section.content} -
-
-
- {:else if section.type === AgenticSectionType.REASONING_PENDING} - {@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'} - {@const reasoningSubtitle = isStreaming ? '' : 'incomplete'} - - toggleExpanded(index, section)} - > -
-
- {section.content} -
-
-
+ {/each} +
+ +{/snippet} + +{#snippet renderGroup(group: InlineRenderGroup)} + {#if group.kind === 'text'} +
+ +
+ {:else} + {@render renderFlowGroup(group)} {/if} {/snippet} @@ -283,10 +446,11 @@ {#if highlightTurns && turnGroups.length > 1} {#each turnGroups as turn, turnIndex (turnIndex)} {@const turnStats = message?.timings?.agentic?.perTurn?.[turnIndex]} + {@const turnGroupsInline = buildInlineRenderGroups(turn.sections, turn.flatIndices)}
Turn {turnIndex + 1} - {#each turn.sections as section, sIdx (turn.flatIndices[sIdx])} - {@render renderSection(section, turn.flatIndices[sIdx])} + {#each turnGroupsInline as group, groupIndex (`${turnIndex}-${groupIndex}`)} + {@render renderGroup(group)} {/each} {#if turnStats}
@@ -306,8 +470,8 @@
{/each} {:else} - {#each sectionsParsed as section, index (index)} - {@render renderSection(section, index)} + {#each inlineGroups as group, groupIndex (groupIndex)} + {@render renderGroup(group)} {/each} {/if}
@@ -325,6 +489,42 @@ width: 100%; } + .agentic-inline-flow { + display: flex; + flex-direction: column; + gap: 0.875rem; + padding-top: 0.75rem; + } + + .agentic-flow-divider { + border-top: 1px solid hsl(var(--muted) / 0.75); + } + + .agentic-inline-tool { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.875rem; + border: 1px solid hsl(var(--border) / 0.75); + border-radius: 0.75rem; + background: hsl(var(--muted) / 0.35); + } + + .agentic-inline-tool-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + } + + .agentic-inline-label { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted-foreground); + } + .agentic-turn { position: relative; border: 1.5px dashed var(--muted-foreground); diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte index 44d59e2b360..844cf5c7adf 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettings.svelte @@ -5,6 +5,9 @@ AlertTriangle, Code, Monitor, + Sun, + Moon, + Wrench, ChevronLeft, ChevronRight, Database @@ -23,13 +26,15 @@ type SettingsSectionTitle, NUMERIC_FIELDS, POSITIVE_INTEGER_FIELDS, - SETTINGS_COLOR_MODES_CONFIG, SETTINGS_KEYS } from '$lib/constants'; import { setMode } from 'mode-watcher'; import { ColorMode } from '$lib/enums/ui'; import { SettingsFieldType } from '$lib/enums/settings'; import type { Component } from 'svelte'; + import '$lib/services/tools'; + import { getAllTools } from '$lib/services/tools'; + import type { ToolRegistration } from '$lib/services/tools/registry'; interface Props { onSave?: () => void; @@ -38,7 +43,13 @@ let { onSave, initialSection }: Props = $props(); - const settingSections: Array<{ + const THEME_OPTIONS = [ + { value: ColorMode.SYSTEM, label: 'System', icon: Monitor }, + { value: ColorMode.LIGHT, label: 'Light', icon: Sun }, + { value: ColorMode.DARK, label: 'Dark', icon: Moon } + ]; + + const baseSettingSections: Array<{ fields: SettingsFieldConfig[]; icon: Component; title: SettingsSectionTitle; @@ -51,7 +62,7 @@ key: SETTINGS_KEYS.THEME, label: 'Theme', type: SettingsFieldType.SELECT, - options: SETTINGS_COLOR_MODES_CONFIG + options: THEME_OPTIONS }, { key: SETTINGS_KEYS.API_KEY, label: 'API Key', type: SettingsFieldType.INPUT }, { @@ -323,13 +334,69 @@ // } ]; + let localConfig: SettingsConfigType = $state({ ...config() }); + + function getToolFields(cfg: SettingsConfigType): SettingsFieldConfig[] { + const toolGroups = new Map(); + + for (const tool of getAllTools()) { + const existing = toolGroups.get(tool.enableConfigKey); + if (existing) { + existing.push(tool); + } else { + toolGroups.set(tool.enableConfigKey, [tool]); + } + } + + return Array.from(toolGroups.values()).flatMap((tools) => { + const [primaryTool] = tools; + const enabled = Boolean(cfg[primaryTool.enableConfigKey]); + const mergedSettings = new Map(); + + for (const tool of tools) { + for (const setting of tool.settings ?? []) { + if (!mergedSettings.has(setting.key)) { + mergedSettings.set(setting.key, setting); + } + } + } + + return [ + { + key: primaryTool.enableConfigKey, + label: primaryTool.label, + type: SettingsFieldType.CHECKBOX, + help: primaryTool.description + }, + ...Array.from(mergedSettings.values()).map((setting) => ({ + ...setting, + disabled: !enabled + })) + ]; + }); + } + + let settingSections = $derived.by( + (): Array<{ + fields: SettingsFieldConfig[]; + icon: Component; + title: SettingsSectionTitle; + }> => [ + ...baseSettingSections, + { + title: SETTINGS_SECTION_TITLES.TOOLS, + icon: Wrench, + fields: getToolFields(localConfig) + } + ] + ); + let activeSection = $derived( initialSection ?? SETTINGS_SECTION_TITLES.GENERAL ); let currentSection = $derived( settingSections.find((section) => section.title === activeSection) || settingSections[0] ); - let localConfig: SettingsConfigType = $state({ ...config() }); let canScrollLeft = $state(false); let canScrollRight = $state(false); diff --git a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte index b9015c196c1..282ffc5a764 100644 --- a/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte +++ b/tools/server/webui/src/lib/components/app/chat/ChatSettings/ChatSettingsFields.svelte @@ -70,6 +70,7 @@ { // Update local config immediately for real-time badge feedback onConfigChange(field.key, e.currentTarget.value); @@ -111,6 +112,7 @@