* feat: Add shimmer text animation for processing state indicators * feat: Redesign CollapsibleContentBlock component with improved UX * feat: Add conditional setting display support with dependsOn field * feat: Add showAgenticTurnStats setting for per-turn statistics * feat: Update ChatMessageAgenticContent with improved UI and new features * feat: Enhance file read tool UI/UX * feat: Refine styling of collapsible content and code preview blocks * feat: add terminal variant to CollapsibleContentBlock * feat: add built-in tools UI registry * feat: extract ChatMessageReasoningBlock and ChatMessageToolCallBlock * refactor: simplify ChatMessageAgenticContent to use extracted blocks * fix: correct markdown content block margin spacing * fix: reorganize SettingsChatFields layout and reset button positioning * fix: use direct map access in agentic store session methods * refactor: remove reasoning preview/throttle system from CollapsibleContentBlock * feat: add auto-scroll to reasoning block and remove showThoughtInProgress * feat: add ChatMessageToolCallDateTime component and support for new tool types * feat: improve auto-scroll reliability in reasoning block with RAF coalescing and MutationObserver * feat: show MCP server favicon for tools without a built-in icon * feat: add search-results parsing utilities and tests * feat: add ChatMessageToolCallSearchResults component * feat: integrate search results rendering into ChatMessageAgenticContent * feat: display tool call input alongside output in ChatMessageToolCallBlock * style: use muted foreground color in reasoning block content * chore: Format * feat: Refine reasoning block layout and make pending thoughts display configurable * feat: Stream tool call code blocks with auto-scroll and handle partial JSON * feat: add streaming permission gate infrastructure * feat: wire permission gate into the agentic loop * fix: bail out on abort and skip already-approved tool calls * fix: clear partial tool calls on abort and savePartialResponse * test: cover partial tool call cleanup end-to-end * refactor: Remove streaming permission gate logic * fix: Correct autoscroll and streaming gates for tool calls and reasoning blocks * refactor: Chat Message Assistant componentization * fix: Show health metadata for disabled MCP servers and promote connections on enable * fix: Inherit global enabled state for missing MCP per-chat overrides * refactor: Cleanup * refactor: Split ChatMessageToolCallBlock into dedicated components * feat: Add live streaming and auto-scroll for tool execution output * feat: Add line numbers and change markers to file edit diffs * chore: Formatting * feat: Add type definitions and utilities for recommended MCP servers * feat: Add recommended MCP servers configuration and storage key * feat: Add McpServerCardCompact component for recommended servers * feat: Add recommended servers section to Add New Server dialog * feat: Update McpServerForm to support authorization requirements * feat: Add select-none classes for text selection prevention * feat: Add recommended MCP server icon assets * refactor: Store dismissed MCP recommendations as a boolean flag * feat: Render tool results as JSON or Markdown based on detected content type * feat: UI improvement * feat: Render search block early and update heading to show execution state * fix: Prevent non-web-search tools from triggering the search UI block * refactor: Cleanup * refactor: Extract hardcoded icon size classes into shared constants * refactor: Extract hardcoded tool result separator into a shared constant * refactor: Tool Calls UI/logic * refactor: Cleanup * refactor: Cleanup * refactor: Cleanup
198 lines
5.1 KiB
Svelte
198 lines
5.1 KiB
Svelte
<script lang="ts">
|
|
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
|
import { goto } from '$app/navigation';
|
|
import { page } from '$app/state';
|
|
import { Search } from '@lucide/svelte';
|
|
import { ActionIcon, KeyboardShortcutInfo, SearchInput } from '$lib/components/app';
|
|
import { Button } from '$lib/components/ui/button';
|
|
import {
|
|
ICON_STRIP_TRANSITION_DURATION,
|
|
ICON_STRIP_TRANSITION_DELAY_MULTIPLIER,
|
|
ROUTES,
|
|
SIDEBAR_ACTIONS_ITEMS
|
|
} from '$lib/constants';
|
|
import { isMobile } from '$lib/stores/viewport.svelte';
|
|
import { TooltipSide } from '$lib/enums';
|
|
import { fade } from 'svelte/transition';
|
|
import { circIn } from 'svelte/easing';
|
|
import { onMount } from 'svelte';
|
|
import type { Component } from 'svelte';
|
|
|
|
interface Props {
|
|
class: string;
|
|
isExpandedMode: boolean;
|
|
isSearchModeActive: boolean;
|
|
searchQuery: string;
|
|
onSearchDeactivated?: () => void;
|
|
onSearchClick?: () => void;
|
|
onNewChat?: () => void;
|
|
}
|
|
|
|
let {
|
|
class: className,
|
|
isExpandedMode = false,
|
|
isSearchModeActive = $bindable(false),
|
|
searchQuery = $bindable(''),
|
|
onSearchDeactivated,
|
|
onSearchClick,
|
|
onNewChat
|
|
}: Props = $props();
|
|
|
|
let initialized = $state(false);
|
|
let showIcons = $state(false);
|
|
let searchInputRef = $state<HTMLInputElement | null>(null);
|
|
|
|
const isOnMobile = $derived(isMobile.current);
|
|
|
|
$effect(() => {
|
|
if (isSearchModeActive && searchInputRef) {
|
|
searchInputRef.focus();
|
|
}
|
|
});
|
|
|
|
onMount(() => {
|
|
showIcons = true;
|
|
|
|
setTimeout(() => {
|
|
initialized = true;
|
|
}, ICON_STRIP_TRANSITION_DELAY_MULTIPLIER * SIDEBAR_ACTIONS_ITEMS.length);
|
|
});
|
|
|
|
function handleSearchModeDeactivate() {
|
|
isSearchModeActive = false;
|
|
searchQuery = '';
|
|
onSearchDeactivated?.();
|
|
}
|
|
|
|
function isItemActive(item: {
|
|
activeRouteId?: string;
|
|
activeRoutePrefix?: string;
|
|
activeUrlIncludes?: string;
|
|
}): boolean {
|
|
if (item.activeRouteId) {
|
|
return page.route.id === item.activeRouteId;
|
|
}
|
|
|
|
if (item.activeRoutePrefix) {
|
|
return !!page.route.id?.startsWith(item.activeRoutePrefix);
|
|
}
|
|
|
|
if (item.activeUrlIncludes) {
|
|
return page.url?.hash?.includes(item.activeUrlIncludes) ?? false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
</script>
|
|
|
|
{#snippet itemIcon(IconComponent: Component)}
|
|
<IconComponent class={ICON_CLASS_DEFAULT} />
|
|
{/snippet}
|
|
|
|
{#if isSearchModeActive}
|
|
<div class="px-4 my-2">
|
|
<SearchInput
|
|
bind:value={searchQuery}
|
|
bind:ref={searchInputRef}
|
|
onClose={handleSearchModeDeactivate}
|
|
onKeyDown={(e) => e.key === 'Escape' && handleSearchModeDeactivate()}
|
|
placeholder="Search conversations..."
|
|
/>
|
|
</div>
|
|
{:else if isExpandedMode || isOnMobile}
|
|
<div
|
|
class="{className} flex flex-col gap-5 md:gap-1 mt-2 md:mt-0 {!isExpandedMode && isOnMobile
|
|
? 'hidden pointer-events-none'
|
|
: ''}"
|
|
>
|
|
{#each SIDEBAR_ACTIONS_ITEMS as item, i (item.tooltip)}
|
|
{@const isActive = isItemActive(item)}
|
|
{@const isSearchOnMobile = item.icon === Search && isMobile.current}
|
|
{@const itemHref = isSearchOnMobile ? ROUTES.SEARCH : item.route}
|
|
{@const itemOnClick = item.route
|
|
? () => {
|
|
onNewChat?.();
|
|
goto(item.route!);
|
|
}
|
|
: isSearchOnMobile
|
|
? undefined
|
|
: onSearchClick}
|
|
{@const itemTransition = {
|
|
duration: ICON_STRIP_TRANSITION_DURATION,
|
|
delay: !initialized
|
|
? ICON_STRIP_TRANSITION_DELAY_MULTIPLIER + i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER
|
|
: 0,
|
|
easing: circIn
|
|
}}
|
|
|
|
{#if showIcons}
|
|
<div transition:fade={itemTransition}>
|
|
<Button
|
|
class="w-full min-w-9 justify-between px-2 backdrop-blur-none! hover:[&>kbd]:opacity-100 {isActive
|
|
? 'bg-accent text-accent-foreground'
|
|
: ''}"
|
|
href={itemHref}
|
|
onclick={itemOnClick}
|
|
variant="ghost"
|
|
size="default"
|
|
>
|
|
<span class="flex min-w-0 items-center px-0.5 gap-2">
|
|
{@render itemIcon(item.icon)}
|
|
|
|
{#if showIcons}
|
|
<span
|
|
in:fade={{ duration: 150, easing: circIn, delay: 50 }}
|
|
out:fade={{ duration: 100 }}
|
|
class="min-w-0 truncate">{item.tooltip}</span
|
|
>
|
|
{/if}
|
|
</span>
|
|
|
|
{#if item.keys}
|
|
<KeyboardShortcutInfo keys={item.keys} />
|
|
{/if}
|
|
</Button>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{:else}
|
|
<div class="{className} flex-col gap-1 hidden md:flex">
|
|
{#each SIDEBAR_ACTIONS_ITEMS as item, i (item.tooltip)}
|
|
{@const isActive = isItemActive(item)}
|
|
{@const isSearchOnMobile = item.icon === Search && isMobile.current}
|
|
{@const itemOnClick = item.route
|
|
? () => {
|
|
onNewChat?.();
|
|
goto(item.route!);
|
|
}
|
|
: isSearchOnMobile
|
|
? undefined
|
|
: onSearchClick}
|
|
{@const itemTransition = {
|
|
duration: ICON_STRIP_TRANSITION_DURATION,
|
|
delay: !initialized
|
|
? ICON_STRIP_TRANSITION_DELAY_MULTIPLIER + i * ICON_STRIP_TRANSITION_DELAY_MULTIPLIER
|
|
: 0,
|
|
easing: circIn
|
|
}}
|
|
|
|
{#if showIcons}
|
|
<div transition:fade={itemTransition}>
|
|
<ActionIcon
|
|
icon={item.icon}
|
|
tooltip={item.tooltip}
|
|
tooltipSide={TooltipSide.RIGHT}
|
|
size="lg"
|
|
iconSize={ICON_CLASS_DEFAULT}
|
|
class="h-9 w-9 rounded-full hover:bg-accent! {isActive
|
|
? 'bg-accent text-accent-foreground'
|
|
: ''}"
|
|
onclick={itemOnClick}
|
|
/>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
{/if}
|