@@ -59,7 +72,9 @@
- {reasoningContent ?? ''}
+
+ {reasoningContent ?? ''}
+
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageTool.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageTool.svelte
new file mode 100644
index 0000000000..c2e3ec44c7
--- /dev/null
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageTool.svelte
@@ -0,0 +1,44 @@
+
+
+
+
Tool • Calculator
+ {#if parsed && typeof parsed === 'object'}
+ {#if parsed.expression}
+
Expression
+
+ {parsed.expression}
+
+ {/if}
+ {#if parsed.result !== undefined}
+
Result
+
+ {parsed.result}
+
+ {/if}
+ {:else}
+
+ {message.content}
+
+ {/if}
+
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
index 2e5f57cb61..9e02ccfe74 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
@@ -4,6 +4,12 @@
import { conversationsStore, activeConversation } from '$lib/stores/conversations.svelte';
import { config } from '$lib/stores/settings.svelte';
import { getMessageSiblings } from '$lib/utils';
+ import { SvelteSet } from 'svelte/reactivity';
+ import type {
+ ApiChatCompletionToolCall,
+ ChatMessageSiblingInfo,
+ DatabaseMessage
+ } from '$lib/types';
interface Props {
class?: string;
@@ -13,6 +19,11 @@
let { class: className, messages = [], onUserAction }: Props = $props();
+ // Prefer live store messages; fall back to the provided prop (e.g. initial render/tests).
+ const sourceMessages = $derived(
+ conversationsStore.activeMessages.length ? conversationsStore.activeMessages : messages
+ );
+
let allConversationMessages = $state
([]);
const currentConfig = config();
@@ -37,31 +48,195 @@
}
});
- let displayMessages = $derived.by(() => {
- if (!messages.length) {
- return [];
- }
+ type ToolSegment =
+ | { kind: 'thinking'; content: string }
+ | { kind: 'tool'; toolCalls: ApiChatCompletionToolCall[]; parentId: string };
+ type CollectedToolMessage = {
+ toolCallId?: string | null;
+ parsed: { expression?: string; result?: string; duration_ms?: number };
+ };
+ type AssistantDisplayMessage = DatabaseMessage & {
+ _toolParentIds?: string[];
+ _segments?: ToolSegment[];
+ _toolMessagesCollected?: CollectedToolMessage[];
+ _actionTargetId?: string;
+ };
+ type DisplayEntry = {
+ message: DatabaseMessage | AssistantDisplayMessage;
+ siblingInfo: ChatMessageSiblingInfo;
+ };
+
+ let displayMessages = $derived.by((): DisplayEntry[] => {
+ // Force reactivity on message field changes (important for streaming updates)
+ const signature = sourceMessages
+ .map(
+ (m) =>
+ `${m.id}-${m.role}-${m.parent ?? ''}-${m.timestamp ?? ''}-${m.thinking ?? ''}-${
+ m.toolCalls ?? ''
+ }-${m.content ?? ''}`
+ )
+ .join('|');
+ // signature is unused but ensures Svelte tracks the above fields
+ void signature;
+
+ if (!sourceMessages.length) return [];
// Filter out system messages if showSystemMessage is false
const filteredMessages = currentConfig.showSystemMessage
- ? messages
- : messages.filter((msg) => msg.type !== 'system');
+ ? sourceMessages
+ : sourceMessages.filter((msg) => msg.type !== 'system');
- return filteredMessages.map((message) => {
- const siblingInfo = getMessageSiblings(allConversationMessages, message.id);
+ const visited = new SvelteSet();
+ const result: DisplayEntry[] = [];
+ const getChildren = (parentId: string, role?: string) =>
+ filteredMessages.filter((m) => m.parent === parentId && (!role || m.role === role));
+
+ const normalizeToolParsed = (
+ value: unknown
+ ): { expression?: string; result?: string; duration_ms?: number } | null => {
+ if (!value || typeof value !== 'object') return null;
+ const obj = value as Record;
return {
- message,
- siblingInfo: siblingInfo || {
- message,
- siblingIds: [message.id],
+ expression: typeof obj.expression === 'string' ? obj.expression : undefined,
+ result: typeof obj.result === 'string' ? obj.result : undefined,
+ duration_ms: typeof obj.duration_ms === 'number' ? obj.duration_ms : undefined
+ };
+ };
+
+ for (const msg of filteredMessages) {
+ if (visited.has(msg.id)) continue;
+ // Don't render tools directly, but keep them for collection; skip marking visited here
+
+ // Skip tool messages (rendered inline)
+ if (msg.role === 'tool') continue;
+
+ if (msg.role === 'assistant') {
+ // Collapse consecutive assistant/tool chains into one display message
+ const toolParentIds: string[] = [];
+ const thinkingParts: string[] = [];
+ const toolCallsCombined: ApiChatCompletionToolCall[] = [];
+ const segments: ToolSegment[] = [];
+ const toolMessagesCollected: CollectedToolMessage[] = [];
+ const toolCallIds = new SvelteSet();
+
+ let currentAssistant: DatabaseMessage | undefined = msg;
+
+ while (currentAssistant) {
+ visited.add(currentAssistant.id);
+ toolParentIds.push(currentAssistant.id);
+
+ if (currentAssistant.thinking) {
+ thinkingParts.push(currentAssistant.thinking);
+ segments.push({ kind: 'thinking', content: currentAssistant.thinking });
+ }
+ let thisAssistantToolCalls: ApiChatCompletionToolCall[] = [];
+ if (currentAssistant.toolCalls) {
+ try {
+ const parsed: unknown = JSON.parse(currentAssistant.toolCalls);
+ if (Array.isArray(parsed)) {
+ for (const tc of parsed as ApiChatCompletionToolCall[]) {
+ if (tc?.id && toolCallIds.has(tc.id)) continue;
+ if (tc?.id) toolCallIds.add(tc.id);
+ toolCallsCombined.push(tc);
+ thisAssistantToolCalls.push(tc);
+ }
+ }
+ } catch {
+ // ignore malformed
+ }
+ }
+ if (thisAssistantToolCalls.length) {
+ segments.push({
+ kind: 'tool',
+ toolCalls: thisAssistantToolCalls,
+ parentId: currentAssistant.id
+ });
+ }
+
+ const toolChildren = getChildren(currentAssistant.id, 'tool');
+ for (const t of toolChildren) {
+ visited.add(t.id);
+ // capture parsed tool message for inline use
+ try {
+ const parsedUnknown: unknown = t.content ? JSON.parse(t.content) : null;
+ const normalized = normalizeToolParsed(parsedUnknown);
+ toolMessagesCollected.push({
+ toolCallId: t.toolCallId,
+ parsed: normalized ?? { result: t.content }
+ });
+ } catch {
+ const p = { result: t.content };
+ toolMessagesCollected.push({ toolCallId: t.toolCallId, parsed: p });
+ }
+ }
+
+ // Assume at most one assistant child chained after tools
+ let nextAssistant = toolChildren
+ .map((t) => getChildren(t.id, 'assistant')[0])
+ .find((a) => a !== undefined);
+
+ // Also allow direct assistant->assistant continuation (no intervening tool)
+ if (!nextAssistant) {
+ nextAssistant = getChildren(currentAssistant.id, 'assistant')[0];
+ }
+
+ if (nextAssistant) {
+ currentAssistant = nextAssistant;
+ continue;
+ }
+ break;
+ }
+
+ const siblingInfo: ChatMessageSiblingInfo = getMessageSiblings(
+ allConversationMessages,
+ msg.id
+ ) || {
+ message: msg,
+ siblingIds: [msg.id],
currentIndex: 0,
totalSiblings: 1
- }
+ };
+
+ const mergedAssistant: AssistantDisplayMessage = {
+ ...(currentAssistant ?? msg),
+ content: currentAssistant?.content ?? '',
+ thinking: thinkingParts.filter(Boolean).join('\n\n'),
+ toolCalls: toolCallsCombined.length ? JSON.stringify(toolCallsCombined) : '',
+ _toolParentIds: toolParentIds,
+ _segments: segments,
+ _actionTargetId: msg.id,
+ _toolMessagesCollected: toolMessagesCollected
+ };
+
+ result.push({ message: mergedAssistant, siblingInfo });
+ continue;
+ }
+
+ // user/system messages
+ const siblingInfo: ChatMessageSiblingInfo = getMessageSiblings(
+ allConversationMessages,
+ msg.id
+ ) || {
+ message: msg,
+ siblingIds: [msg.id],
+ currentIndex: 0,
+ totalSiblings: 1
};
- });
+ result.push({ message: msg, siblingInfo });
+ }
+
+ return result;
});
+ function getToolParentIdsForMessage(msg: DisplayEntry['message']): string[] | undefined {
+ return (msg as AssistantDisplayMessage)._toolParentIds;
+ }
+
+ function getDisplayKeyForMessage(msg: DisplayEntry['message']): string {
+ return (msg as AssistantDisplayMessage)._actionTargetId ?? msg.id;
+ }
+
async function handleNavigateToSibling(siblingId: string) {
await conversationsStore.navigateToSibling(siblingId);
}
@@ -121,11 +296,12 @@
- {#each displayMessages as { message, siblingInfo } (message.id)}
+ {#each displayMessages as { message, siblingInfo } (getDisplayKeyForMessage(message))}
`m-${++idCounter}`;
+
+const makeMsg = (partial: Partial): DatabaseMessage => ({
+ id: uid(),
+ convId: 'c1',
+ role: 'assistant',
+ type: 'text',
+ parent: '-1',
+ content: '',
+ thinking: '',
+ toolCalls: '',
+ timestamp: Date.now(),
+ children: [],
+ ...partial
+});
+
+describe('ChatMessages reasoning streaming', () => {
+ it('renders consecutive reasoning chunks around a tool call inline without refresh', async () => {
+ const user = makeMsg({ role: 'user', type: 'text', content: 'hi', id: 'u1' });
+ const assistant1 = makeMsg({ id: 'a1', thinking: 'reasoning-step-1' });
+
+ conversationsStore.activeMessages = [user, assistant1];
+
+ const { container } = render(ChatMessages, {
+ props: { messages: conversationsStore.activeMessages }
+ });
+
+ await waitFor(() => {
+ expect(container.textContent || '').toContain('reasoning-step-1');
+ });
+
+ // Tool call arrives
+ conversationsStore.updateMessageAtIndex(1, {
+ toolCalls: JSON.stringify([
+ {
+ id: 'call-1',
+ type: 'function',
+ function: { name: 'calculator', arguments: '{"expression":"1+1"}' }
+ }
+ ])
+ });
+
+ // Tool message + continued assistant
+ const tool = makeMsg({
+ id: 't1',
+ role: 'tool',
+ type: 'tool',
+ parent: 'a1',
+ content: JSON.stringify({ result: '2' }),
+ toolCallId: 'call-1'
+ });
+ const assistant2 = makeMsg({
+ id: 'a2',
+ parent: 't1',
+ thinking: 'reasoning-step-2',
+ content: 'final-answer'
+ });
+ conversationsStore.addMessageToActive(tool);
+ conversationsStore.addMessageToActive(assistant2);
+
+ await waitFor(() => {
+ const text = container.textContent || '';
+ expect(text).toContain('reasoning-step-1');
+ expect(text).toContain('reasoning-step-2');
+ expect(text).toContain('final-answer');
+ });
+
+ cleanup();
+ });
+});
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
index 57a2edac58..30501b2ec1 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatScreen/ChatScreen.svelte
@@ -38,6 +38,8 @@
let disableAutoScroll = $derived(Boolean(config().disableAutoScroll));
let autoScrollEnabled = $state(true);
let chatScrollContainer: HTMLDivElement | undefined = $state();
+ // Always hand ChatMessages a fresh array so mutations in the store trigger rerenders/merges
+ const liveMessages = $derived.by(() => [...conversationsStore.activeMessages]);
let dragCounter = $state(0);
let isDragOver = $state(false);
let lastScrollTop = $state(0);
@@ -345,6 +347,14 @@
scrollInterval = undefined;
}
});
+
+ // Keep view pinned to bottom across message merges while auto-scroll is enabled.
+ $effect(() => {
+ void liveMessages;
+ if (!disableAutoScroll && autoScrollEnabled) {
+ queueMicrotask(() => scrollChatToBottom('instant'));
+ }
+ });
{#if isDragOver}
@@ -369,7 +379,7 @@
>
{
if (!disableAutoScroll) {
userScrolledUp = false;
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 4ec9b478fd..562e3ea1ce 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
@@ -9,7 +9,8 @@
Moon,
ChevronLeft,
ChevronRight,
- Database
+ Database,
+ Wrench
} from '@lucide/svelte';
import {
ChatSettingsFooter,
@@ -20,6 +21,8 @@
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { setMode } from 'mode-watcher';
import type { Component } from 'svelte';
+ import '$lib/services/tools'; // ensure built-in tools register
+ import { getAllTools } from '$lib/services/tools';
interface Props {
onSave?: () => void;
@@ -27,259 +30,286 @@
let { onSave }: Props = $props();
- const settingSections: Array<{
- fields: SettingsFieldConfig[];
- icon: Component;
- title: string;
- }> = [
- {
- title: 'General',
- icon: Settings,
- fields: [
- {
- key: 'theme',
- label: 'Theme',
- type: 'select',
- options: [
- { value: 'system', label: 'System', icon: Monitor },
- { value: 'light', label: 'Light', icon: Sun },
- { value: 'dark', label: 'Dark', icon: Moon }
- ]
- },
- { key: 'apiKey', label: 'API Key', type: 'input' },
- {
- key: 'systemMessage',
- label: 'System Message',
- type: 'textarea'
- },
- {
- key: 'pasteLongTextToFileLen',
- label: 'Paste long text to file length',
- type: 'input'
- },
- {
- key: 'copyTextAttachmentsAsPlainText',
- label: 'Copy text attachments as plain text',
- type: 'checkbox'
- },
- {
- key: 'enableContinueGeneration',
- label: 'Enable "Continue" button',
- type: 'checkbox',
- isExperimental: true
- },
- {
- key: 'pdfAsImage',
- label: 'Parse PDF as image',
- type: 'checkbox'
- },
- {
- key: 'askForTitleConfirmation',
- label: 'Ask for confirmation before changing conversation title',
- type: 'checkbox'
- }
- ]
- },
- {
- title: 'Display',
- icon: Monitor,
- fields: [
- {
- key: 'showMessageStats',
- label: 'Show message generation statistics',
- type: 'checkbox'
- },
- {
- key: 'showThoughtInProgress',
- label: 'Show thought in progress',
- type: 'checkbox'
- },
- {
- key: 'keepStatsVisible',
- label: 'Keep stats visible after generation',
- type: 'checkbox'
- },
- {
- key: 'autoMicOnEmpty',
- label: 'Show microphone on empty input',
- type: 'checkbox',
- isExperimental: true
- },
- {
- key: 'renderUserContentAsMarkdown',
- label: 'Render user content as Markdown',
- type: 'checkbox'
- },
- {
- key: 'disableAutoScroll',
- label: 'Disable automatic scroll',
- type: 'checkbox'
- },
- {
- key: 'alwaysShowSidebarOnDesktop',
- label: 'Always show sidebar on desktop',
- type: 'checkbox'
- },
- {
- key: 'autoShowSidebarOnNewChat',
- label: 'Auto-show sidebar on new chat',
- type: 'checkbox'
- }
- ]
- },
- {
- title: 'Sampling',
- icon: Funnel,
- fields: [
- {
- key: 'temperature',
- label: 'Temperature',
- type: 'input'
- },
- {
- key: 'dynatemp_range',
- label: 'Dynamic temperature range',
- type: 'input'
- },
- {
- key: 'dynatemp_exponent',
- label: 'Dynamic temperature exponent',
- type: 'input'
- },
- {
- key: 'top_k',
- label: 'Top K',
- type: 'input'
- },
- {
- key: 'top_p',
- label: 'Top P',
- type: 'input'
- },
- {
- key: 'min_p',
- label: 'Min P',
- type: 'input'
- },
- {
- key: 'xtc_probability',
- label: 'XTC probability',
- type: 'input'
- },
- {
- key: 'xtc_threshold',
- label: 'XTC threshold',
- type: 'input'
- },
- {
- key: 'typ_p',
- label: 'Typical P',
- type: 'input'
- },
- {
- key: 'max_tokens',
- label: 'Max tokens',
- type: 'input'
- },
- {
- key: 'samplers',
- label: 'Samplers',
- type: 'input'
- }
- ]
- },
- {
- title: 'Penalties',
- icon: AlertTriangle,
- fields: [
- {
- key: 'repeat_last_n',
- label: 'Repeat last N',
- type: 'input'
- },
- {
- key: 'repeat_penalty',
- label: 'Repeat penalty',
- type: 'input'
- },
- {
- key: 'presence_penalty',
- label: 'Presence penalty',
- type: 'input'
- },
- {
- key: 'frequency_penalty',
- label: 'Frequency penalty',
- type: 'input'
- },
- {
- key: 'dry_multiplier',
- label: 'DRY multiplier',
- type: 'input'
- },
- {
- key: 'dry_base',
- label: 'DRY base',
- type: 'input'
- },
- {
- key: 'dry_allowed_length',
- label: 'DRY allowed length',
- type: 'input'
- },
- {
- key: 'dry_penalty_last_n',
- label: 'DRY penalty last N',
- type: 'input'
- }
- ]
- },
- {
- title: 'Import/Export',
- icon: Database,
- fields: []
- },
- {
- title: 'Developer',
- icon: Code,
- fields: [
- {
- key: 'showToolCalls',
- label: 'Show tool call labels',
- type: 'checkbox'
- },
- {
- key: 'disableReasoningFormat',
- label: 'Show raw LLM output',
- type: 'checkbox'
- },
- {
- key: 'custom',
- label: 'Custom JSON',
- type: 'textarea'
- }
- ]
- }
- // TODO: Experimental features section will be implemented after initial release
- // This includes Python interpreter (Pyodide integration) and other experimental features
- // {
- // title: 'Experimental',
- // icon: Beaker,
- // fields: [
- // {
- // key: 'pyInterpreterEnabled',
- // label: 'Enable Python interpreter',
- // type: 'checkbox'
- // }
- // ]
- // }
- ];
+ let localConfig: SettingsConfigType = $state({ ...config() });
+
+ function getToolFields(cfg: SettingsConfigType): SettingsFieldConfig[] {
+ return getAllTools().flatMap((tool) => {
+ const enableField: SettingsFieldConfig = {
+ key: tool.enableConfigKey,
+ label: tool.label,
+ type: 'checkbox',
+ help: tool.description
+ };
+
+ const enabled = Boolean(cfg[tool.enableConfigKey]);
+ const settingsFields = (tool.settings ?? []).map((s) => ({
+ ...s,
+ disabled: !enabled
+ }));
+
+ return [enableField, ...settingsFields];
+ });
+ }
+
+ const settingSections = $derived.by(
+ (): Array<{
+ fields: SettingsFieldConfig[];
+ icon: Component;
+ title: string;
+ }> => [
+ {
+ title: 'General',
+ icon: Settings,
+ fields: [
+ {
+ key: 'theme',
+ label: 'Theme',
+ type: 'select',
+ options: [
+ { value: 'system', label: 'System', icon: Monitor },
+ { value: 'light', label: 'Light', icon: Sun },
+ { value: 'dark', label: 'Dark', icon: Moon }
+ ]
+ },
+ { key: 'apiKey', label: 'API Key', type: 'input' },
+ {
+ key: 'systemMessage',
+ label: 'System Message',
+ type: 'textarea'
+ },
+ {
+ key: 'pasteLongTextToFileLen',
+ label: 'Paste long text to file length',
+ type: 'input'
+ },
+ {
+ key: 'copyTextAttachmentsAsPlainText',
+ label: 'Copy text attachments as plain text',
+ type: 'checkbox'
+ },
+ {
+ key: 'enableContinueGeneration',
+ label: 'Enable "Continue" button',
+ type: 'checkbox',
+ isExperimental: true
+ },
+ {
+ key: 'pdfAsImage',
+ label: 'Parse PDF as image',
+ type: 'checkbox'
+ },
+ {
+ key: 'askForTitleConfirmation',
+ label: 'Ask for confirmation before changing conversation title',
+ type: 'checkbox'
+ }
+ ]
+ },
+ {
+ title: 'Display',
+ icon: Monitor,
+ fields: [
+ {
+ key: 'showMessageStats',
+ label: 'Show message generation statistics',
+ type: 'checkbox'
+ },
+ {
+ key: 'showThoughtInProgress',
+ label: 'Show thought in progress',
+ type: 'checkbox'
+ },
+ {
+ key: 'keepStatsVisible',
+ label: 'Keep stats visible after generation',
+ type: 'checkbox'
+ },
+ {
+ key: 'autoMicOnEmpty',
+ label: 'Show microphone on empty input',
+ type: 'checkbox',
+ isExperimental: true
+ },
+ {
+ key: 'renderUserContentAsMarkdown',
+ label: 'Render user content as Markdown',
+ type: 'checkbox'
+ },
+ {
+ key: 'disableAutoScroll',
+ label: 'Disable automatic scroll',
+ type: 'checkbox'
+ },
+ {
+ key: 'alwaysShowSidebarOnDesktop',
+ label: 'Always show sidebar on desktop',
+ type: 'checkbox'
+ },
+ {
+ key: 'autoShowSidebarOnNewChat',
+ label: 'Auto-show sidebar on new chat',
+ type: 'checkbox'
+ }
+ ]
+ },
+ {
+ title: 'Sampling',
+ icon: Funnel,
+ fields: [
+ {
+ key: 'temperature',
+ label: 'Temperature',
+ type: 'input'
+ },
+ {
+ key: 'dynatemp_range',
+ label: 'Dynamic temperature range',
+ type: 'input'
+ },
+ {
+ key: 'dynatemp_exponent',
+ label: 'Dynamic temperature exponent',
+ type: 'input'
+ },
+ {
+ key: 'top_k',
+ label: 'Top K',
+ type: 'input'
+ },
+ {
+ key: 'top_p',
+ label: 'Top P',
+ type: 'input'
+ },
+ {
+ key: 'min_p',
+ label: 'Min P',
+ type: 'input'
+ },
+ {
+ key: 'xtc_probability',
+ label: 'XTC probability',
+ type: 'input'
+ },
+ {
+ key: 'xtc_threshold',
+ label: 'XTC threshold',
+ type: 'input'
+ },
+ {
+ key: 'typ_p',
+ label: 'Typical P',
+ type: 'input'
+ },
+ {
+ key: 'max_tokens',
+ label: 'Max tokens',
+ type: 'input'
+ },
+ {
+ key: 'samplers',
+ label: 'Samplers',
+ type: 'input'
+ }
+ ]
+ },
+ {
+ title: 'Penalties',
+ icon: AlertTriangle,
+ fields: [
+ {
+ key: 'repeat_last_n',
+ label: 'Repeat last N',
+ type: 'input'
+ },
+ {
+ key: 'repeat_penalty',
+ label: 'Repeat penalty',
+ type: 'input'
+ },
+ {
+ key: 'presence_penalty',
+ label: 'Presence penalty',
+ type: 'input'
+ },
+ {
+ key: 'frequency_penalty',
+ label: 'Frequency penalty',
+ type: 'input'
+ },
+ {
+ key: 'dry_multiplier',
+ label: 'DRY multiplier',
+ type: 'input'
+ },
+ {
+ key: 'dry_base',
+ label: 'DRY base',
+ type: 'input'
+ },
+ {
+ key: 'dry_allowed_length',
+ label: 'DRY allowed length',
+ type: 'input'
+ },
+ {
+ key: 'dry_penalty_last_n',
+ label: 'DRY penalty last N',
+ type: 'input'
+ }
+ ]
+ },
+ {
+ title: 'Import/Export',
+ icon: Database,
+ fields: []
+ },
+ {
+ title: 'Developer',
+ icon: Code,
+ fields: [
+ {
+ key: 'showToolCalls',
+ label: 'Show tool call labels',
+ type: 'checkbox'
+ },
+ {
+ key: 'disableReasoningFormat',
+ label: 'Show raw LLM output',
+ type: 'checkbox'
+ },
+ {
+ key: 'custom',
+ label: 'Custom JSON',
+ type: 'textarea'
+ }
+ ]
+ },
+ {
+ title: 'Tools',
+ icon: Wrench,
+ fields: getToolFields(localConfig)
+ }
+ // TODO: Experimental features section will be implemented after initial release
+ // This includes Python interpreter (Pyodide integration) and other experimental features
+ // {
+ // title: 'Experimental',
+ // icon: Beaker,
+ // fields: [
+ // {
+ // key: 'pyInterpreterEnabled',
+ // label: 'Enable Python interpreter',
+ // type: 'checkbox'
+ // }
+ // ]
+ // }
+ ]
+ );
let activeSection = $state('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);
@@ -333,7 +363,8 @@
'dry_multiplier',
'dry_base',
'dry_allowed_length',
- 'dry_penalty_last_n'
+ 'dry_penalty_last_n',
+ 'codeInterpreterTimeoutSeconds'
];
for (const field of numericFields) {
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 a6f51f47d6..53fc1e951e 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
@@ -69,6 +69,7 @@
{
// Update local config immediately for real-time badge feedback
onConfigChange(field.key, e.currentTarget.value);
@@ -110,6 +111,7 @@