Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
agenticPendingPermissionRequest,
agenticResolvePermission,
agenticPendingContinueRequest,
agenticResolveContinue
agenticResolveContinue,
agenticLastError
} from '$lib/stores/agentic.svelte';
import { config } from '$lib/stores/settings.svelte';

Expand All @@ -56,6 +57,10 @@
const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean);
const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean);

const hasReasoningError = $derived(
isLastAssistantMessage ? !!agenticLastError(message.convId) : false
);

let permissionDismissed = $state(false);

const pendingPermission = $derived(
Expand Down Expand Up @@ -293,11 +298,21 @@
</div>
</CollapsibleContentBlock>
{:else if section.type === AgenticSectionType.REASONING}
{@const reasoningSubtitle = section.wasInterrupted
? hasReasoningError
? 'Error'
: 'Cancelled'
: isStreaming
? ''
: undefined}

<CollapsibleContentBlock
open={isExpanded(index, section)}
class="my-2"
icon={Brain}
title="Reasoning"
subtitle={reasoningSubtitle}
rawContent={section.content}
onToggle={() => toggleExpanded(index, section)}
>
<div class="pt-3">
Expand All @@ -308,14 +323,15 @@
</CollapsibleContentBlock>
{:else if section.type === AgenticSectionType.REASONING_PENDING}
{@const reasoningTitle = isStreaming ? 'Reasoning...' : 'Reasoning'}
{@const reasoningSubtitle = isStreaming ? '' : 'incomplete'}
{@const reasoningSubtitle = isStreaming ? '' : hasReasoningError ? 'Error' : 'Cancelled'}

<CollapsibleContentBlock
open={isExpanded(index, section)}
class="my-2"
icon={Brain}
title={reasoningTitle}
subtitle={reasoningSubtitle}
rawContent={section.content}
{isStreaming}
onToggle={() => toggleExpanded(index, section)}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import { buttonVariants } from '$lib/components/ui/button/index.js';
import { Card } from '$lib/components/ui/card';
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
import { useThrottle } from '$lib/hooks/use-throttle.svelte';
import { formatReasoningPreview } from '$lib/utils';
import { config } from '$lib/stores/settings.svelte';
import type { Snippet } from 'svelte';
import type { Component } from 'svelte';

Expand All @@ -14,6 +17,8 @@
iconClass?: string;
title: string;
subtitle?: string;
preview?: string;
rawContent?: string;
isStreaming?: boolean;
onToggle?: () => void;
children: Snippet;
Expand All @@ -26,13 +31,29 @@
iconClass = 'h-4 w-4',
title,
subtitle,
preview,
rawContent,
isStreaming = false,
onToggle,
children
}: Props = $props();

let contentContainer: HTMLDivElement | undefined = $state();

const showThoughtInProgress = $derived(config().showThoughtInProgress as boolean);

let previewKey = useThrottle(() => rawContent ?? preview ?? '', 500);
let displayedPreview = $state('');
let displayedOverflow = $state(0);

$effect(() => {
void previewKey.key;
const content = rawContent ?? preview ?? '';
const result = formatReasoningPreview(content);
displayedPreview = result.preview;
displayedOverflow = result.overflow;
});

const autoScroll = createAutoScrollController();

$effect(() => {
Expand All @@ -58,16 +79,31 @@
class={className}
>
<Card class="gap-0 border-muted bg-muted/30 py-0">
<Collapsible.Trigger class="flex w-full cursor-pointer items-center justify-between p-3">
<div class="flex items-center gap-2 text-muted-foreground">
{#if IconComponent}
<IconComponent class={iconClass} />
{/if}
<Collapsible.Trigger class="flex w-full cursor-pointer items-start justify-between gap-2 p-3">
<div class="flex min-w-0 items-center gap-2">
<div class="flex items-center gap-2 text-muted-foreground">
{#if IconComponent}
<IconComponent class={iconClass} />
{/if}

<span class="font-mono text-sm font-medium">{title}</span>

<span class="font-mono text-sm font-medium">{title}</span>
{#if subtitle}
<span class="text-xs italic">{subtitle}</span>
{/if}
</div>

{#if subtitle}
<span class="text-xs italic">{subtitle}</span>
{#if displayedPreview && !showThoughtInProgress}
<div class="flex min-w-0 items-baseline justify-between gap-2">
<div class="w-3/4 truncate text-xs text-muted-foreground/80">
{displayedPreview}
</div>
{#if displayedOverflow > 0}
<span class="shrink-0 text-xs text-muted-foreground/60"
>{displayedOverflow}+ chars</span
>
{/if}
</div>
{/if}
</div>

Expand Down
27 changes: 27 additions & 0 deletions tools/ui/src/lib/constants/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,30 @@ export const MEDIUM_DURATION_THRESHOLD = 10;

/** Default display value when no performance time is available */
export const DEFAULT_PERFORMANCE_TIME = '0s';

/** Max length before reasoning preview is truncated */
export const MAX_PREVIEW_LENGTH = 120;

export const STRIP_MARKDOWN_CAPTURE_PATTERNS: [RegExp, string][] = [
[/^```(.*)/gm, '$1'],
[/(.*)```$/gm, '$1'],
[/`([^`]*)`/g, '$1'],
[/\*\*(.*?)\*\*/g, '$1'],
[/__(.*?)__/g, '$1'],
[/\*(.*?)\*/g, '$1'],
[/_(.*?)_/g, '$1']
];

/* eslint-disable no-misleading-character-class */
export const STRIP_MARKDOWN_INLINE_REGEX = new RegExp(
[
'<[^>]*>',
'^>\\s*',
'^#{1,6}\\s+',
'^[\\s]*[-*+]\\s+',
'^[\\s]*\\d+[.)]\\s+',
'[\\u{1F600}-\\u{1F64F}\\u{1F300}-\\u{1F5FF}\\u{1F680}-\\u{1F6FF}\\u{1F1E0}-\\u{1F1FF}\\u{2600}-\\u{26FF}\\u{2700}-\\u{27BF}\\u{FE00}-\\u{FE0F}\\u{1F900}-\\u{1F9FF}\\u{1FA00}-\\u{1FA6F}\\u{1FA70}-\\u{1FAFF}\\u{200D}\\u{20E3}\\u{231A}-\\u{231B}\\u{23E9}-\\u{23F3}\\u{23F8}-\\u{23FA}\\u{25AA}-\\u{25AB}\\u{25B6}\\u{25C0}\\u{25FB}-\\u{25FE}\\u{2934}-\\u{2935}\\u{2B05}-\\u{2B07}\\u{2B1B}-\\u{2B1C}\\u{2B50}\\u{2B55}\\u{3030}\\u{303D}\\u{3297}\\u{3299}]'
].join('|'),
'gmu'
);
/* eslint-enable no-misleading-character-class */
32 changes: 32 additions & 0 deletions tools/ui/src/lib/hooks/use-throttle.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Creates a reactive throttle key that increments when `getValue()` changes
* and the throttle window has elapsed since the last increment.
*
* Useful for throttling animations that should not fire on every rapid update.
*
* @param getValue - A reactive getter for the value to watch
* @param ms - Throttle window in milliseconds
* @returns A reactive number that increments when the throttled value changes
*/
export function useThrottle(getValue: () => string | undefined, ms: number) {
let key = $state(0);
let throttleEnd = $state(0);
let lastValue: string | undefined = getValue();

$effect(() => {
const value = getValue();
if (value === lastValue) return;
const now = Date.now();
if (now >= throttleEnd) {
lastValue = value;
key++;
throttleEnd = now + ms;
}
});

return {
get key() {
return key;
}
};
}
4 changes: 3 additions & 1 deletion tools/ui/src/lib/utils/agentic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface AgenticSection {
toolArgs?: string;
toolResult?: string;
toolResultExtras?: DatabaseMessageExtra[];
wasInterrupted?: boolean;
}

/**
Expand Down Expand Up @@ -51,7 +52,8 @@ function deriveSingleTurnSections(
const isPending = isStreaming && !hasContentAfterReasoning;
sections.push({
type: isPending ? AgenticSectionType.REASONING_PENDING : AgenticSectionType.REASONING,
content: message.reasoningContent
content: message.reasoningContent,
wasInterrupted: !isStreaming && !hasContentAfterReasoning
});
}

Expand Down
36 changes: 35 additions & 1 deletion tools/ui/src/lib/utils/formatters.ts
Comment thread
allozaur marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import {
SECONDS_PER_MINUTE,
SECONDS_PER_HOUR,
SHORT_DURATION_THRESHOLD,
MEDIUM_DURATION_THRESHOLD
MEDIUM_DURATION_THRESHOLD,
MAX_PREVIEW_LENGTH,
STRIP_MARKDOWN_INLINE_REGEX,
STRIP_MARKDOWN_CAPTURE_PATTERNS,
NEWLINE_SEPARATOR
} from '$lib/constants';

/**
Expand Down Expand Up @@ -151,3 +155,33 @@ export function formatAttachmentText(
const header = extra ? `${name} (${extra})` : name;
return `\n\n--- ${label}: ${header} ---\n${content}`;
}

export function formatReasoningPreview(content: string): { preview: string; overflow: number } {
if (!content) return { preview: '', overflow: 0 };

const lines = content.split(NEWLINE_SEPARATOR);
let lastLine = '';

for (let i = lines.length - 1; i >= 0; i--) {
let cleaned = lines[i].trim();
if (!cleaned) continue;

cleaned = cleaned.replace(STRIP_MARKDOWN_INLINE_REGEX, '');
for (const [pattern, replacement] of STRIP_MARKDOWN_CAPTURE_PATTERNS) {
cleaned = cleaned.replace(pattern, replacement);
}

if (cleaned.length > 0) {
lastLine = cleaned;
break;
}
}

const fullLength = lastLine.length;
const overflow = Math.max(0, fullLength - MAX_PREVIEW_LENGTH);
if (fullLength > MAX_PREVIEW_LENGTH) {
lastLine = lastLine.slice(0, MAX_PREVIEW_LENGTH) + '...';
}

return { preview: lastLine, overflow };
}
3 changes: 2 additions & 1 deletion tools/ui/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ export {
formatJsonPretty,
formatTime,
formatPerformanceTime,
formatAttachmentText
formatAttachmentText,
formatReasoningPreview
} from './formatters';

// IME utilities
Expand Down