This commit is contained in:
Timothy Jaeryang Baek 2026-10-25 15:29:58 +09:00
parent 06c382e5d2
commit f465a4be11
7 changed files with 88 additions and 7 deletions

View file

@ -0,0 +1,34 @@
<script lang="ts">
import { getContext } from 'svelte';
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
export let summary = '';
</script>
{#if summary.trim()}
<!-- Only stop the response card's click handler; summary provides keyboard interaction. -->
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
<details class="my-1" on:click|stopPropagation>
<summary
class="flex w-full cursor-pointer items-center gap-2 text-xs leading-5 text-gray-500 outline-offset-4 transition-colors before:flex-1 before:border-t before:border-dashed before:border-current before:opacity-25 after:flex-1 after:border-t after:border-dashed after:border-current after:opacity-25 hover:text-gray-700 focus-visible:outline-2 focus-visible:outline-gray-400 dark:text-gray-400 dark:hover:text-gray-200 [&::-webkit-details-marker]:hidden"
>
<span class="min-w-0 px-2 text-center">
{$i18n.t('Context compacted')}
</span>
</summary>
<div
role="region"
aria-label={$i18n.t('Compaction summary')}
class="my-1.5 max-h-80 overflow-y-auto whitespace-pre-wrap break-words text-xs leading-5 text-gray-600 dark:text-gray-300"
>
{summary}
</div>
</details>
{:else}
<div
class="my-1 flex w-full items-center gap-2 text-xs leading-5 text-gray-500 before:flex-1 before:border-t before:border-dashed before:border-current before:opacity-25 after:flex-1 after:border-t after:border-dashed after:border-current after:opacity-25 dark:text-gray-400"
>
<span class="min-w-0 px-2 text-center">{$i18n.t('Context compacted')}</span>
</div>
{/if}

View file

@ -61,6 +61,7 @@
import Citations from './Citations.svelte';
import CodeExecutions from './CodeExecutions.svelte';
import ContentRenderer from './ContentRenderer.svelte';
import ContextCompactionLabel from './ContextCompactionLabel.svelte';
import { KokoroWorker } from '$lib/workers/KokoroWorker';
import FileItem from '$lib/components/common/FileItem.svelte';
import FollowUps from './ResponseMessage/FollowUps.svelte';
@ -78,6 +79,7 @@
model: string;
content: string;
output?: OutputItem[];
contextSummary?: string;
files?: { type: string; url: string }[];
timestamp: number;
role: string;
@ -712,6 +714,10 @@
</Name>
{/if}
{#if message.contextSummary}
<ContextCompactionLabel summary={message.contextSummary} />
{/if}
<div>
<div class="chat-{message.role} w-full min-w-full">
<div>

View file

@ -8,6 +8,7 @@
import Markdown from './Markdown.svelte';
import ConsecutiveDetailsGroup from './Markdown/ConsecutiveDetailsGroup.svelte';
import ContextCompactionLabel from './ContextCompactionLabel.svelte';
import {
buildOutputDisplayItems,
type OutputDetailToken,
@ -170,6 +171,8 @@
{#if displayItem.item?.displayed || $settings?.terminalFileDisplay === 'inline'}
<TerminalOutputFile item={displayItem.item} {chatId} />
{/if}
{:else if displayItem.type === 'context_compaction'}
<ContextCompactionLabel summary={displayItem.summary} />
{:else}
{@const detailToken = displayItem.token}
{#if detailToken.attributes?.type === 'tool_calls'}

View file

@ -18,6 +18,7 @@
import equal from 'fast-deep-equal';
import Name from './Name.svelte';
import ContextCompactionLabel from './ContextCompactionLabel.svelte';
import ProfileImage from './ProfileImage.svelte';
import ProfilePreview from '$lib/components/channel/Messages/Message/ProfilePreview.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
@ -195,6 +196,10 @@
</div>
{/if}
{#if message.contextSummary}
<ContextCompactionLabel summary={message.contextSummary} />
{/if}
<div class="chat-{message.role} w-full min-w-full">
{#if edit !== true}
{#if message.files}

View file

@ -14,7 +14,7 @@ export type OutputItem = {
status?: string;
arguments?: unknown;
content?: OutputContentPart[];
summary?: OutputContentPart[];
summary?: OutputContentPart[] | string;
output?: OutputContentPart[];
files?: unknown;
embeds?: unknown;
@ -64,6 +64,11 @@ export type OutputDisplayItem =
type: 'file';
id: string;
item: Record<string, unknown>;
}
| {
type: 'context_compaction';
id: string;
summary: string;
};
type ResponseStreamEvent = {
@ -409,6 +414,16 @@ export function buildOutputDisplayItems(output: OutputItem[] = []): OutputDispla
return;
}
if (item.type === 'open_webui:context_compaction') {
flushDetails();
displayItems.push({
type: 'context_compaction',
id: item.id ?? `context-compaction-${index}`,
summary: typeof item.summary === 'string' ? item.summary : ''
});
return;
}
if (item.type === 'message') {
const text = getMessageText(item);
if (text.trim()) {
@ -618,7 +633,7 @@ export function applyResponseStreamEvent(
if (!event.part) {
return nextOutput;
}
item.summary = [...(item.summary ?? [])];
item.summary = Array.isArray(item.summary) ? [...item.summary] : [];
const summaryIndex = event.summary_index ?? item.summary.length;
setPart(item.summary, summaryIndex, { ...event.part }, { type: 'summary_text', text: '' });
return nextOutput;
@ -633,7 +648,7 @@ export function applyResponseStreamEvent(
if (deltaType === 'reasoning_summary_text') {
const summaryIndex = event.summary_index ?? 0;
item.summary = [...(item.summary ?? [])];
item.summary = Array.isArray(item.summary) ? [...item.summary] : [];
const part = ensurePart(item.summary, summaryIndex, { type: 'summary_text', text: '' });
part.text = appendDelta(part.text ?? '', event.delta);
return nextOutput;

View file

@ -18,6 +18,10 @@
getOutputText(nodeData?.message?.output) || nodeData?.message?.content || '';
$: messageContent = getMessageContent(data);
$: isCompacted =
data?.message?.contextSummary ||
data?.message?.output?.some((item) => item?.type === 'open_webui:context_compaction');
$: previewClass = `text-xs mt-0.5 ${isCompacted ? 'line-clamp-1' : 'line-clamp-2'}`;
$: modelName = data?.model
? resolveLocalizedModelName(data.model, $i18n.language)
: (data?.message?.model ?? 'Assistant');
@ -45,9 +49,13 @@
</div>
{#if data?.message?.error}
<div class="text-red-500 line-clamp-2 text-xs mt-0.5">{data.message.error.content}</div>
<div class="text-red-500 {previewClass}">
{data.message.error.content}
</div>
{:else}
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{messageContent}</div>
<div class="text-gray-500 {previewClass}">
{messageContent}
</div>
{/if}
</div>
</div>
@ -83,16 +91,25 @@
</div>
{#if data?.message?.error}
<div class="text-red-500 line-clamp-2 text-xs mt-0.5">
<div class="text-red-500 {previewClass}">
{data.message.error.content}
</div>
{:else}
<div class="text-gray-500 line-clamp-2 text-xs mt-0.5">{messageContent}</div>
<div class="text-gray-500 {previewClass}">
{messageContent}
</div>
{/if}
</div>
</div>
{/if}
</Tooltip>
{#if isCompacted}
<div
class="ms-7 mt-0.5 w-fit max-w-[calc(100%-1.75rem)] truncate rounded bg-gray-100 px-1.5 text-[10px] leading-4 text-gray-500 dark:bg-gray-900 dark:text-gray-400"
>
{$i18n.t('Context compacted')}
</div>
{/if}
<Handle
type="target"
position={data?.direction === 'horizontal' ? Position.Left : Position.Top}

View file

@ -554,6 +554,7 @@
"Compact: shorten older messages so this chat can keep going.": "",
"Compacting context": "",
"Compacting context...": "",
"Compaction summary": "",
"Compare": "",
"Compare files": "",
"Compare revision": "",