Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified tools/server/public/index.html.gz
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,6 @@
}
}
function handleScroll() {
if (isOpen) {
updateMenuPosition();
}
}
async function handleSelect(value: string | undefined) {
if (!value) return;
Expand Down Expand Up @@ -259,7 +253,7 @@
}
</script>

<svelte:window onresize={handleResize} onscroll={handleScroll} />
<svelte:window onresize={handleResize} />

<svelte:document onpointerdown={handlePointerDown} onkeydown={handleKeydown} />

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import type { ApiChatCompletionToolCall } from '$lib/types/api';
import { getDeletionInfo } from '$lib/stores/chat.svelte';
import { copyToClipboard } from '$lib/utils/copy';
import { isIMEComposing } from '$lib/utils/is-ime-composing';
Expand Down Expand Up @@ -54,6 +55,29 @@
return null;
});

let toolCallContent = $derived.by((): ApiChatCompletionToolCall[] | string | null => {
if (message.role === 'assistant') {
const trimmedToolCalls = message.toolCalls?.trim();

if (!trimmedToolCalls) {
return null;
}

try {
const parsed = JSON.parse(trimmedToolCalls);

if (Array.isArray(parsed)) {
return parsed as ApiChatCompletionToolCall[];
}
} catch {
// Harmony-only path: fall back to the raw string so issues surface visibly.
}

return trimmedToolCalls;
}
return null;
});

function handleCancelEdit() {
isEditing = false;
editedContent = message.content;
Expand Down Expand Up @@ -171,5 +195,6 @@
{showDeleteDialog}
{siblingInfo}
{thinkingContent}
{toolCallContent}
/>
{/if}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<script lang="ts">
import { ChatMessageThinkingBlock, MarkdownContent } from '$lib/components/app';
import {
ChatMessageThinkingBlock,
ChatMessageToolCallBlock,
MarkdownContent
} from '$lib/components/app';
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
import { isLoading } from '$lib/stores/chat.svelte';
import { fade } from 'svelte/transition';
Expand All @@ -21,6 +25,7 @@
import { config } from '$lib/stores/settings.svelte';
import { modelName as serverModelName } from '$lib/stores/server.svelte';
import { copyToClipboard } from '$lib/utils/copy';
import type { ApiChatCompletionToolCall } from '$lib/types/api';

interface Props {
class?: string;
Expand Down Expand Up @@ -51,6 +56,7 @@
siblingInfo?: ChatMessageSiblingInfo | null;
textareaElement?: HTMLTextAreaElement;
thinkingContent: string | null;
toolCallContent: ApiChatCompletionToolCall[] | string | null;
}

let {
Expand All @@ -76,7 +82,8 @@
shouldBranchAfterEdit = false,
siblingInfo = null,
textareaElement = $bindable(),
thinkingContent
thinkingContent,
toolCallContent
}: Props = $props();

const processingState = useProcessingState();
Expand Down Expand Up @@ -112,6 +119,10 @@
/>
{/if}

{#if toolCallContent && config().showToolCalls}
<ChatMessageToolCallBlock {toolCallContent} />
{/if}

{#if message?.role === 'assistant' && isLoading() && !message?.content?.trim()}
<div class="mt-6 w-full max-w-[48rem]" in:fade>
<div class="processing-container">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script lang="ts">
import { Wrench } from '@lucide/svelte';
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { Card } from '$lib/components/ui/card';
import ChatMessageToolCallItem from './ChatMessageToolCallItem.svelte';
import type { ApiChatCompletionToolCall } from '$lib/types/api';

interface Props {
class?: string;
toolCallContent: ApiChatCompletionToolCall[] | string | null;
}

let { class: className = '', toolCallContent }: Props = $props();
let fallbackExpanded = $state(false);

const toolCalls = $derived.by(() => (Array.isArray(toolCallContent) ? toolCallContent : null));
const fallbackContent = $derived.by(() =>
typeof toolCallContent === 'string' ? toolCallContent : null
);
</script>

{#if toolCalls && toolCalls.length > 0}
<div class="mb-6 flex flex-col gap-3 {className}">
{#each toolCalls as toolCall, index (toolCall.id ?? `${index}`)}
<ChatMessageToolCallItem {toolCall} {index} />
{/each}
</div>
{:else if fallbackContent}
<Collapsible.Root bind:open={fallbackExpanded} class="mb-6 {className}">
<Card class="gap-0 border-muted bg-muted/30 py-0">
<Collapsible.Trigger class="flex cursor-pointer items-center justify-between p-3">
<div class="flex items-center gap-2 text-muted-foreground">
<Wrench class="h-4 w-4" />

<span class="text-sm font-medium">Tool calls</span>
</div>

<div
class={buttonVariants({
variant: 'ghost',
size: 'sm',
class: 'h-6 w-6 p-0 text-muted-foreground hover:text-foreground'
})}
>
<ChevronsUpDownIcon class="h-4 w-4" />

<span class="sr-only">Toggle tool call content</span>
</div>
</Collapsible.Trigger>

<Collapsible.Content>
<div class="border-t border-muted px-3 pb-3">
<div class="pt-3">
<pre class="tool-call-content">{fallbackContent}</pre>
</div>
</div>
</Collapsible.Content>
</Card>
</Collapsible.Root>
{/if}

<style>
.tool-call-content {
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.25rem;
white-space: pre-wrap;
word-break: break-word;
margin: 0;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<script lang="ts">
import { Wrench } from '@lucide/svelte';
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { Card } from '$lib/components/ui/card';
import type { ApiChatCompletionToolCall } from '$lib/types/api';

interface Props {
class?: string;
index: number;
toolCall: ApiChatCompletionToolCall;
}

let { class: className = '', index, toolCall }: Props = $props();

let isExpanded = $state(false);

const headerLabel = $derived.by(() => {
const callNumber = index + 1;
const functionName = toolCall.function?.name?.trim();

return functionName ? `Tool call #${callNumber} · ${functionName}` : `Tool call #${callNumber}`;
});

const formattedPayload = $derived.by(() => {
const payload: Record<string, unknown> = {};

if (toolCall.id) {
payload.id = toolCall.id;
}

if (toolCall.type) {
payload.type = toolCall.type;
}

if (toolCall.function) {
const fnPayload: Record<string, unknown> = {};
const { name, arguments: args } = toolCall.function;

if (name) {
fnPayload.name = name;
}

const trimmedArguments = args?.trim();
if (trimmedArguments) {
try {
fnPayload.arguments = JSON.parse(trimmedArguments);
} catch {
fnPayload.arguments = trimmedArguments;
}
}

if (Object.keys(fnPayload).length > 0) {
payload.function = fnPayload;
}
}

return JSON.stringify(payload, null, 2);
});
</script>

<Collapsible.Root bind:open={isExpanded} class="mb-3 last:mb-0 {className}">
<Card class="gap-0 border-muted bg-muted/30 py-0">
<Collapsible.Trigger class="flex cursor-pointer items-center justify-between p-3">
<div class="flex items-center gap-2 text-muted-foreground">
<Wrench class="h-4 w-4" />

<span class="text-sm font-medium">{headerLabel}</span>
</div>

<div
class={buttonVariants({
variant: 'ghost',
size: 'sm',
class: 'h-6 w-6 p-0 text-muted-foreground hover:text-foreground'
})}
>
<ChevronsUpDownIcon class="h-4 w-4" />

<span class="sr-only">Toggle tool call payload</span>
</div>
</Collapsible.Trigger>

<Collapsible.Content>
<div class="border-t border-muted px-3 pb-3">
<div class="pt-3">
<pre class="tool-call-content">{formattedPayload}</pre>
</div>
</div>
</Collapsible.Content>
</Card>
</Collapsible.Root>

<style>
.tool-call-content {
font-family: var(--font-mono);
font-size: 0.75rem;
line-height: 1.25rem;
white-space: pre-wrap;
word-break: break-word;
margin: 0;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@
label: 'Show raw LLM output',
type: 'checkbox'
},
{
key: 'showToolCalls',
label: 'Show tool call chunks',
type: 'checkbox'
},
{
key: 'custom',
label: 'Custom JSON',
Expand Down
1 change: 1 addition & 0 deletions tools/server/webui/src/lib/components/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { default as ChatFormFileInputInvisible } from './chat/ChatForm/ChatFormF
export { default as ChatMessage } from './chat/ChatMessages/ChatMessage.svelte';
export { default as ChatMessages } from './chat/ChatMessages/ChatMessages.svelte';
export { default as ChatMessageThinkingBlock } from './chat/ChatMessages/ChatMessageThinkingBlock.svelte';
export { default as ChatMessageToolCallBlock } from './chat/ChatMessages/ChatMessageToolCallBlock.svelte';
export { default as MessageBranchingControls } from './chat/ChatMessages/ChatMessageBranchingControls.svelte';

export { default as ChatProcessingInfo } from './chat/ChatProcessingInfo.svelte';
Expand Down
3 changes: 3 additions & 0 deletions tools/server/webui/src/lib/constants/settings-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const SETTING_CONFIG_DEFAULT: Record<string, string | number | boolean> =
theme: 'system',
showTokensPerSecond: false,
showThoughtInProgress: false,
showToolCalls: false,
disableReasoningFormat: false,
keepStatsVisible: false,
showMessageStats: true,
Expand Down Expand Up @@ -80,6 +81,8 @@ export const SETTING_CONFIG_INFO: Record<string, string> = {
custom: 'Custom JSON parameters to send to the API. Must be valid JSON format.',
showTokensPerSecond: 'Display generation speed in tokens per second during streaming.',
showThoughtInProgress: 'Expand thought process by default when generating messages.',
showToolCalls:
'Display streamed tool call payloads from Harmony-compatible delta.tool_calls data inside assistant messages.',
disableReasoningFormat:
'Show raw LLM output without backend parsing and frontend Markdown rendering to inspect streaming across different models.',
keepStatsVisible: 'Keep processing statistics visible after generation finishes.',
Expand Down
Loading