ui: ESLint config updates (#27700)

* chore: Spacing between sibling elements in html markup

* chore: Formatting and linting rules
This commit is contained in:
Aleksander Grygier
2026-08-25 14:34:34 +02:00
committed by GitHub
parent 3737e41370
commit f1357e4998
267 changed files with 1706 additions and 1344 deletions
+151 -3
View File
@@ -12,6 +12,107 @@ import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint'; import ts from 'typescript-eslint';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
// Require a blank line between sibling element-like nodes in a Svelte template
// (elements, components, and the {#if} / {#each} / {#await} / {#snippet} /
// {@render} blocks) that sit on separate lines at the same nesting level.
// Whitespace between siblings is a whitespace-only SvelteText node; when it
// holds a single newline (no blank line) the fix adds one, keeping the
// indentation of the second sibling. Real text content (e.g. `foo\n\nbar`)
// is left alone.
const ELEMENT_LIKE_TYPES = new Set([
'SvelteAwaitBlock',
'SvelteComponent',
'SvelteEachBlock',
'SvelteElement',
'SvelteIfBlock',
'SvelteKeyBlock',
'SvelteRenderTag',
'SvelteSelf',
'SvelteSnippetBlock'
]);
const paddingLineBetweenElements = {
create(context) {
// Check one list of template children. Each children array holds the
// element-like nodes plus the whitespace/comment text between them.
function checkChildren(children) {
if (!Array.isArray(children)) return;
let lastElement = null;
let lastWhitespace = null;
for (const child of children) {
if (child.type === 'SvelteText' && /^\s*$/.test(child.value)) {
lastWhitespace = child;
continue;
}
if (!ELEMENT_LIKE_TYPES.has(child.type)) continue;
if (
lastElement &&
lastWhitespace &&
child.loc.start.line - lastElement.loc.end.line === 1
) {
const textNode = lastWhitespace;
context.report({
fix(fixer) {
// Add a second newline so the two siblings are separated by a
// blank line, keeping the trailing indentation.
return fixer.replaceText(textNode, textNode.value.replace(/\n/, '\n\n'));
},
message: 'Expected a blank line between sibling elements.',
node: child
});
}
lastElement = child;
lastWhitespace = null;
}
}
return {
SvelteAwaitBlock(node) {
checkChildren(node.children);
checkChildren(node.then?.children);
checkChildren(node.else?.children);
},
SvelteComponent(node) {
checkChildren(node.children);
},
SvelteEachBlock(node) {
checkChildren(node.children);
checkChildren(node.else?.children);
},
SvelteElement(node) {
checkChildren(node.children);
},
SvelteFragment(node) {
checkChildren(node.children);
},
SvelteIfBlock(node) {
checkChildren(node.children);
checkChildren(node.else?.children);
},
SvelteKeyBlock(node) {
checkChildren(node.children);
},
SvelteProgram(node) {
checkChildren(node.children);
},
SvelteSnippetBlock(node) {
checkChildren(node.children);
}
};
},
meta: {
docs: { description: 'Require a blank line between sibling elements in a Svelte template.' },
fixable: 'whitespace',
schema: [],
type: 'layout'
}
};
// Require a blank line between consecutive class accessors (get/set). The core // Require a blank line between consecutive class accessors (get/set). The core
// `padding-line-between-statements` rule only handles statements, not class // `padding-line-between-statements` rule only handles statements, not class
// members, so this is enforced with a small custom rule. // members, so this is enforced with a small custom rule.
@@ -66,7 +167,12 @@ export default ts.config(
{ {
languageOptions: { globals: { ...globals.browser, ...globals.node } }, languageOptions: { globals: { ...globals.browser, ...globals.node } },
plugins: { plugins: {
local: { rules: { 'blank-line-between-accessors': blankLineBetweenAccessors } }, local: {
rules: {
'blank-line-between-accessors': blankLineBetweenAccessors,
'padding-line-between-elements': paddingLineBetweenElements
}
},
perfectionist, perfectionist,
'simple-import-sort': simpleImportSort 'simple-import-sort': simpleImportSort
}, },
@@ -82,6 +188,8 @@ export default ts.config(
'eol-last': 'error', 'eol-last': 'error',
// Enforce a blank line between consecutive get/set accessors // Enforce a blank line between consecutive get/set accessors
'local/blank-line-between-accessors': 'error', 'local/blank-line-between-accessors': 'error',
// Require a blank line between sibling elements in a Svelte template
'local/padding-line-between-elements': 'error',
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off', 'no-undef': 'off',
@@ -156,9 +264,49 @@ export default ts.config(
// grouping); Prettier normalizes comma spacing afterwards. // grouping); Prettier normalizes comma spacing afterwards.
'simple-import-sort/imports': ['error', { groups: [['.*']] }], 'simple-import-sort/imports': ['error', { groups: [['.*']] }],
'svelte/no-at-html-tags': 'off', 'svelte/no-at-html-tags': 'off',
// This app uses hash-based routing (#/) where resolve() from $app/paths does not apply // This app uses hash-based routing (#/) where resolve() from $app/paths does not apply
'svelte/no-navigation-without-resolve': 'off' 'svelte/no-navigation-without-resolve': 'off',
// Sort HTML attributes alphabetically in the markup. The Svelte directives
// (bind:/use:/animate:/style:/in:/out:/transition:/class:) sort first,
// alphabetically among themselves, then all remaining attributes sort
// alphabetically. The rule keeps spread attributes in place and does not cross
// them. `this` stays first on <svelte:element> because Prettier forces it there
// - reordering it alphabetically would fight the formatter.
'svelte/sort-attributes': [
'error',
{
order: [
'this',
{
match: [
'/^bind:/u',
'/^use:/u',
'/^animate:/u',
'/^style:/u',
'/^in:/u',
'/^out:/u',
'/^transition:/u',
'/^class:/u'
],
sort: 'alphabetical'
},
{
match: [
'!/^bind:/u',
'!/^use:/u',
'!/^animate:/u',
'!/^style:/u',
'!/^in:/u',
'!/^out:/u',
'!/^transition:/u',
'!/^class:/u'
],
sort: 'alphabetical'
}
]
}
]
} }
}, },
{ {
@@ -41,17 +41,17 @@
{#snippet button(props = {})} {#snippet button(props = {})}
<Button <Button
{...props} {...props}
{href} aria-label={ariaLabel || tooltip}
{variant} class="h-6 w-6 p-0 {className} flex hover:bg-transparent data-[state=open]:bg-transparent!"
{size}
{disabled} {disabled}
{href}
onclick={(e: MouseEvent) => { onclick={(e: MouseEvent) => {
if (stopPropagationOnClick) e.stopPropagation(); if (stopPropagationOnClick) e.stopPropagation();
onclick?.(e); onclick?.(e);
}} }}
class="h-6 w-6 p-0 {className} flex hover:bg-transparent data-[state=open]:bg-transparent!" {size}
aria-label={ariaLabel || tooltip} {variant}
> >
{#if icon} {#if icon}
{@const IconComponent = icon} {@const IconComponent = icon}
@@ -10,9 +10,9 @@
</script> </script>
<ActionIcon <ActionIcon
icon={Copy}
tooltip={ariaLabel}
iconSize={ICON_CLASS_DEFAULT}
disabled={!canCopy} disabled={!canCopy}
icon={Copy}
iconSize={ICON_CLASS_DEFAULT}
onclick={() => canCopy && copyToClipboard(text)} onclick={() => canCopy && copyToClipboard(text)}
tooltip={ariaLabel}
/> />
@@ -108,13 +108,13 @@
{/if} {/if}
<DialogChatAttachmentsPreview <DialogChatAttachmentsPreview
bind:open={viewAllDialogOpen}
{activeModelId} {activeModelId}
{attachments} {attachments}
bind:open={viewAllDialogOpen}
{previewFocusIndex} {previewFocusIndex}
{uploadedFiles} {uploadedFiles}
/> />
{#if mcpResourcePreviewExtra} {#if mcpResourcePreviewExtra}
<DialogMcpResourcePreview extra={mcpResourcePreviewExtra} bind:open={mcpResourcePreviewOpen} /> <DialogMcpResourcePreview bind:open={mcpResourcePreviewOpen} extra={mcpResourcePreviewExtra} />
{/if} {/if}
@@ -75,58 +75,58 @@
{#if mcpPrompt} {#if mcpPrompt}
<ChatAttachmentsListItemMcpPrompt <ChatAttachmentsListItemMcpPrompt
class="max-w-[300px] min-w-[200px] flex-shrink-0 {className} {scrollClasses}" class="max-w-[300px] min-w-[200px] flex-shrink-0 {className} {scrollClasses}"
prompt={mcpPrompt}
{readonly}
isLoading={item.isLoading} isLoading={item.isLoading}
loadError={item.loadError} loadError={item.loadError}
onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined} onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined}
prompt={mcpPrompt}
{readonly}
/> />
{/if} {/if}
{:else if isMcpResource(item)} {:else if isMcpResource(item)}
{@const mcpResource = item.attachment as DatabaseMessageExtraMcpResource} {@const mcpResource = item.attachment as DatabaseMessageExtraMcpResource}
<ChatAttachmentsListItemMcpResource <ChatAttachmentsListItemMcpResource
class="flex-shrink-0 {className} {scrollClasses}"
attachment={toMcpResourceAttachment(mcpResource, item.id)} attachment={toMcpResourceAttachment(mcpResource, item.id)}
class="flex-shrink-0 {className} {scrollClasses}"
onclick={() => onMcpResourcePreview?.(mcpResource)} onclick={() => onMcpResourcePreview?.(mcpResource)}
/> />
{:else if item.isImage && item.preview} {:else if item.isImage && item.preview}
<ChatAttachmentsListItemThumbnailImage <ChatAttachmentsListItemThumbnailImage
class="flex-shrink-0 cursor-pointer {className} {scrollClasses}" class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
height={imageHeight}
id={item.id} id={item.id}
{imageClass}
name={item.name} name={item.name}
onRemove={onFileRemove}
onclick={() => onPreview?.(item)}
preview={item.preview} preview={item.preview}
{readonly} {readonly}
onRemove={onFileRemove}
height={imageHeight}
width={imageWidth} width={imageWidth}
{imageClass}
onclick={() => onPreview?.(item)}
/> />
{:else if isPdfFile(item.attachment, item.uploadedFile)} {:else if isPdfFile(item.attachment, item.uploadedFile)}
<ChatAttachmentsListItemThumbnailFile <ChatAttachmentsListItemThumbnailFile
attachment={item.attachment}
class="flex-shrink-0 cursor-pointer {className} {scrollClasses}" class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
id={item.id} id={item.id}
name={item.name} name={item.name}
size={item.size}
{readonly}
onRemove={onFileRemove} onRemove={onFileRemove}
textContent={item.textContent}
attachment={item.attachment}
uploadedFile={item.uploadedFile}
onclick={() => onPreview?.(item)} onclick={() => onPreview?.(item)}
{readonly}
size={item.size}
textContent={item.textContent}
uploadedFile={item.uploadedFile}
/> />
{:else} {:else}
<ChatAttachmentsListItemThumbnailFile <ChatAttachmentsListItemThumbnailFile
attachment={item.attachment}
class="flex-shrink-0 cursor-pointer {className} {scrollClasses}" class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
id={item.id} id={item.id}
name={item.name} name={item.name}
size={item.size}
{readonly}
onRemove={onFileRemove} onRemove={onFileRemove}
textContent={item.textContent}
attachment={item.attachment}
uploadedFile={item.uploadedFile}
onclick={() => onPreview?.(item)} onclick={() => onPreview?.(item)}
{readonly}
size={item.size}
textContent={item.textContent}
uploadedFile={item.uploadedFile}
/> />
{/if} {/if}
@@ -35,7 +35,7 @@
<div <div
class="absolute top-10 right-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100" class="absolute top-10 right-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
> >
<ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.()} /> <ActionIcon icon={X} onclick={() => onRemove?.()} stopPropagationOnClick tooltip="Remove" />
</div> </div>
{/if} {/if}
</div> </div>
@@ -101,7 +101,7 @@
<div <div
class="absolute top-2 right-2 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100" class="absolute top-2 right-2 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
> >
<ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.(id)} /> <ActionIcon icon={X} onclick={() => onRemove?.(id)} stopPropagationOnClick tooltip="Remove" />
</div> </div>
{/snippet} {/snippet}
@@ -30,7 +30,7 @@
</script> </script>
{#snippet image()} {#snippet image()}
<img src={preview} alt={name} class="{height} {width} cursor-pointer object-cover {imageClass}" /> <img alt={name} class="{height} {width} cursor-pointer object-cover {imageClass}" src={preview} />
{/snippet} {/snippet}
<div <div
@@ -185,30 +185,30 @@
<div class="{className} flex flex-col text-white"> <div class="{className} flex flex-col text-white">
<div class="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden"> <div class="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden">
<ChatAttachmentsPreviewNavButtons onPrev={prev} onNext={next} show={allItems.length > 1} /> <ChatAttachmentsPreviewNavButtons onNext={next} onPrev={prev} show={allItems.length > 1} />
<div class="flex h-full w-full flex-col items-center justify-start overflow-auto py-4"> <div class="flex h-full w-full flex-col items-center justify-start overflow-auto py-4">
{#if currentItem} {#if currentItem}
<ChatAttachmentsPreviewFileInfo {displayName} {fileSize} /> <ChatAttachmentsPreviewFileInfo {displayName} {fileSize} />
<ChatAttachmentsPreviewCurrentItem <ChatAttachmentsPreviewCurrentItem
{activeModelId}
{audioSrc}
{currentItem} {currentItem}
{isImage}
{isAudio}
{isVideo}
{isPdf}
{isText}
{displayPreview} {displayPreview}
{displayTextContent} {displayTextContent}
{audioSrc}
{videoSrc}
{language}
{hasVisionModality} {hasVisionModality}
{activeModelId} {isAudio}
{isImage}
{isPdf}
{isText}
{isVideo}
{language}
{videoSrc}
/> />
{/if} {/if}
<ChatAttachmentsPreviewThumbnailStrip items={allItems} {currentIndex} {onNavigate} /> <ChatAttachmentsPreviewThumbnailStrip {currentIndex} items={allItems} {onNavigate} />
</div> </div>
</div> </div>
</div> </div>
@@ -53,18 +53,18 @@
{#key currentItem.id} {#key currentItem.id}
{#if isPdf} {#if isPdf}
<ChatAttachmentsPreviewCurrentItemPdf <ChatAttachmentsPreviewCurrentItemPdf
{activeModelId}
{currentItem} {currentItem}
displayName={currentItem.name} displayName={currentItem.name}
{displayTextContent} {displayTextContent}
{hasVisionModality} {hasVisionModality}
{activeModelId}
/> />
{:else if isImage} {:else if isImage}
<ChatAttachmentsPreviewCurrentItemImage {currentItem} {displayPreview} /> <ChatAttachmentsPreviewCurrentItemImage {currentItem} {displayPreview} />
{:else if isText && displayTextContent} {:else if isText && displayTextContent}
<ChatAttachmentsPreviewCurrentItemText {displayTextContent} {language} /> <ChatAttachmentsPreviewCurrentItemText {displayTextContent} {language} />
{:else if isAudio} {:else if isAudio}
<ChatAttachmentsPreviewCurrentItemAudio {currentItem} {audioSrc} /> <ChatAttachmentsPreviewCurrentItemAudio {audioSrc} {currentItem} />
{:else if isVideo} {:else if isVideo}
<ChatAttachmentsPreviewCurrentItemVideo {currentItem} {videoSrc} /> <ChatAttachmentsPreviewCurrentItemVideo {currentItem} {videoSrc} />
{:else if isUnavailable} {:else if isUnavailable}
@@ -14,7 +14,7 @@
<Music class="mx-auto mb-4 h-16 w-16 text-white/50" /> <Music class="mx-auto mb-4 h-16 w-16 text-white/50" />
{#if audioSrc} {#if audioSrc}
<audio controls class="mb-4 w-full" src={audioSrc}> <audio class="mb-4 w-full" controls src={audioSrc}>
Your browser does not support the audio element. Your browser does not support the audio element.
</audio> </audio>
{:else} {:else}
@@ -10,9 +10,9 @@
{#if displayPreview} {#if displayPreview}
<div class="flex flex-1 items-center justify-center"> <div class="flex flex-1 items-center justify-center">
<img <img
src={displayPreview}
alt={currentItem?.name || 'preview'} alt={currentItem?.name || 'preview'}
class="max-h-[80vh] max-w-[80vw] rounded-lg object-contain shadow-lg" class="max-h-[80vh] max-w-[80vw] rounded-lg object-contain shadow-lg"
src={displayPreview}
/> />
</div> </div>
{/if} {/if}
@@ -87,20 +87,20 @@
<div class="mb-4 flex items-center justify-end gap-2"> <div class="mb-4 flex items-center justify-end gap-2">
<Button <Button
variant={pdfViewMode === PdfViewMode.TEXT ? 'default' : 'outline'}
size="sm"
onclick={() => (pdfViewMode = PdfViewMode.TEXT)}
disabled={pdfImagesLoading} disabled={pdfImagesLoading}
onclick={() => (pdfViewMode = PdfViewMode.TEXT)}
size="sm"
variant={pdfViewMode === PdfViewMode.TEXT ? 'default' : 'outline'}
> >
<FileText class="mr-1 {ICON_CLASS_DEFAULT}" /> <FileText class="mr-1 {ICON_CLASS_DEFAULT}" />
Text Text
</Button> </Button>
<Button <Button
variant={pdfViewMode === PdfViewMode.PAGES ? 'default' : 'outline'}
size="sm"
onclick={() => (pdfViewMode = PdfViewMode.PAGES)}
disabled={pdfImagesLoading} disabled={pdfImagesLoading}
onclick={() => (pdfViewMode = PdfViewMode.PAGES)}
size="sm"
variant={pdfViewMode === PdfViewMode.PAGES ? 'default' : 'outline'}
> >
{#if pdfImagesLoading} {#if pdfImagesLoading}
<div <div
@@ -116,7 +116,9 @@
{#if !hasVisionModality && activeModelId && currentItem} {#if !hasVisionModality && activeModelId && currentItem}
<Alert.Root class="mb-4 max-w-4xl"> <Alert.Root class="mb-4 max-w-4xl">
<Info class={ICON_CLASS_DEFAULT} /> <Info class={ICON_CLASS_DEFAULT} />
<Alert.Title>Preview only</Alert.Title> <Alert.Title>Preview only</Alert.Title>
<Alert.Description> <Alert.Description>
<span class="inline-flex"> <span class="inline-flex">
The selected model does not support vision. Only the extracted The selected model does not support vision. Only the extracted
@@ -140,6 +142,7 @@
<div <div
class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-white border-t-transparent" class="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-4 border-white border-t-transparent"
></div> ></div>
<p class="text-white/70">Converting PDF to images...</p> <p class="text-white/70">Converting PDF to images...</p>
</div> </div>
</div> </div>
@@ -147,20 +150,25 @@
<div class="flex flex-1 items-center justify-center p-8"> <div class="flex flex-1 items-center justify-center p-8">
<div class="text-center"> <div class="text-center">
<FileText class="mx-auto mb-4 h-16 w-16 text-white/50" /> <FileText class="mx-auto mb-4 h-16 w-16 text-white/50" />
<p class="mb-4 text-white/70">Failed to load PDF images</p> <p class="mb-4 text-white/70">Failed to load PDF images</p>
<p class="text-sm text-white/50">{pdfImagesError}</p> <p class="text-sm text-white/50">{pdfImagesError}</p>
</div> </div>
</div> </div>
{:else if pdfImages.length > 0} {:else if pdfImages.length > 0}
{#each pdfImages as image, index (image)} {#each pdfImages as image, index (image)}
<p class="mb-2 text-sm text-white/50">Page {index + 1}</p> <p class="mb-2 text-sm text-white/50">Page {index + 1}</p>
<img src={image} alt="PDF Page {index + 1}" class="mx-auto max-w-[85vw] rounded-lg shadow-lg" />
<img alt="PDF Page {index + 1}" class="mx-auto max-w-[85vw] rounded-lg shadow-lg" src={image} />
<div class="h-4"></div> <div class="h-4"></div>
{/each} {/each}
{:else} {:else}
<div class="flex flex-1 items-center justify-center p-8"> <div class="flex flex-1 items-center justify-center p-8">
<div class="text-center"> <div class="text-center">
<FileText class="mx-auto mb-4 h-16 w-16 text-white/50" /> <FileText class="mx-auto mb-4 h-16 w-16 text-white/50" />
<p class="text-white/70">No PDF pages available</p> <p class="text-white/70">No PDF pages available</p>
</div> </div>
</div> </div>
@@ -14,7 +14,7 @@
<Video class="mx-auto mb-4 h-16 w-16 text-white/50" /> <Video class="mx-auto mb-4 h-16 w-16 text-white/50" />
{#if videoSrc} {#if videoSrc}
<video controls class="mb-4 w-full" src={videoSrc}> <video class="mb-4 w-full" controls src={videoSrc}>
<track kind="captions" src="" /> <track kind="captions" src="" />
Your browser does not support the video element. Your browser does not support the video element.
</video> </video>
@@ -13,21 +13,21 @@
{#if show} {#if show}
<Button <Button
variant="secondary" aria-label="Previous"
size="icon"
class="absolute top-1/2 left-4 z-10 h-8 w-8 -translate-y-1/2 rounded-full bg-background/5 p-0 text-white!" class="absolute top-1/2 left-4 z-10 h-8 w-8 -translate-y-1/2 rounded-full bg-background/5 p-0 text-white!"
onclick={onPrev} onclick={onPrev}
aria-label="Previous" size="icon"
variant="secondary"
> >
<ChevronLeft class="size-4" /> <ChevronLeft class="size-4" />
</Button> </Button>
<Button <Button
variant="secondary" aria-label="Next"
size="icon"
class="absolute top-1/2 right-4 z-10 h-8 w-8 -translate-y-1/2 rounded-full bg-background/5 p-0 text-white!" class="absolute top-1/2 right-4 z-10 h-8 w-8 -translate-y-1/2 rounded-full bg-background/5 p-0 text-white!"
onclick={onNext} onclick={onNext}
aria-label="Next" size="icon"
variant="secondary"
> >
<ChevronRight class="size-4" /> <ChevronRight class="size-4" />
</Button> </Button>
@@ -38,16 +38,16 @@
{#each items as item, index (item.id)} {#each items as item, index (item.id)}
<button <button
{...{ [UI_DATA_ATTRS.THUMBNAIL_INDEX]: index }} {...{ [UI_DATA_ATTRS.THUMBNAIL_INDEX]: index }}
aria-label={`Go to ${item.name}`}
class={[ class={[
'relative flex-shrink-0 cursor-pointer overflow-hidden rounded border-2 bg-black/80 backdrop-blur-sm transition-all hover:opacity-90', 'relative flex-shrink-0 cursor-pointer overflow-hidden rounded border-2 bg-black/80 backdrop-blur-sm transition-all hover:opacity-90',
index === currentIndex ? 'border-white' : 'border-transparent opacity-60', index === currentIndex ? 'border-white' : 'border-transparent opacity-60',
'[&:not(:first-child)]:last:mr-4 [&:not(:last-child)]:first:ml-4' '[&:not(:first-child)]:last:mr-4 [&:not(:last-child)]:first:ml-4'
]} ]}
onclick={() => onNavigate(index)} onclick={() => onNavigate(index)}
aria-label={`Go to ${item.name}`}
> >
{#if item.isImage && item.preview} {#if item.isImage && item.preview}
<img src={item.preview} alt={item.name} class="h-12 w-12 object-cover" /> <img alt={item.name} class="h-12 w-12 object-cover" src={item.preview} />
{:else} {:else}
<div <div
class="bg-foreground-muted/50 flex h-12 w-12 flex-col items-center justify-center gap-0.5 py-1" class="bg-foreground-muted/50 flex h-12 w-12 flex-col items-center justify-center gap-0.5 py-1"
@@ -537,30 +537,30 @@
> >
<ChatFormPickers <ChatFormPickers
bind:this={pickersRef} bind:this={pickersRef}
isCommandPickerOpen={pickers.isCommandPickerOpen}
commandQuery={pickers.commandQuery} commandQuery={pickers.commandQuery}
commands={pickers.availableCommands} commands={pickers.availableCommands}
isCommandPickerOpen={pickers.isCommandPickerOpen}
isMentionPickerOpen={pickers.isMentionPickerOpen}
isPromptPickerOpen={pickers.isPromptPickerOpen}
{mentionAnchor}
mentionQuery={pickers.mentionQuery}
onCommandPickerClose={pickers.handleCommandPickerClose} onCommandPickerClose={pickers.handleCommandPickerClose}
onCommandSelect={pickers.handleCommandSelect} onCommandSelect={pickers.handleCommandSelect}
isPromptPickerOpen={pickers.isPromptPickerOpen}
promptSearchQuery={pickers.promptSearchQuery}
isMentionPickerOpen={pickers.isMentionPickerOpen}
mentionQuery={pickers.mentionQuery}
{mentionAnchor}
scopePath={pickers.mentionScopePath}
onPromptPickerClose={pickers.handlePromptPickerClose}
onMentionPickerClose={pickers.handleMentionPickerClose}
onMentionOpened={() => inputRef?.focus()} onMentionOpened={() => inputRef?.focus()}
onMentionPickerClose={pickers.handleMentionPickerClose}
onMentionSelect={handleMentionSelect} onMentionSelect={handleMentionSelect}
onPromptLoadStart={handlePromptLoadStart}
onPromptLoadComplete={handlePromptLoadComplete} onPromptLoadComplete={handlePromptLoadComplete}
onPromptLoadError={handlePromptLoadError} onPromptLoadError={handlePromptLoadError}
onPromptLoadStart={handlePromptLoadStart}
onPromptPickerClose={pickers.handlePromptPickerClose}
promptSearchQuery={pickers.promptSearchQuery}
scopePath={pickers.mentionScopePath}
/> />
<div <div
bind:this={mentionAnchor} bind:this={mentionAnchor}
class="pointer-events-none absolute top-0 right-0 left-0 h-px"
aria-hidden="true" aria-hidden="true"
class="pointer-events-none absolute top-0 right-0 left-0 h-px"
></div> ></div>
<div <div
@@ -570,29 +570,29 @@
data-slot="input-area" data-slot="input-area"
> >
<ChatAttachmentsList <ChatAttachmentsList
{attachments}
bind:uploadedFiles bind:uploadedFiles
onFileRemove={handleFileRemove}
limitToSingleRow
class="py-5"
style="scroll-padding: 1rem;"
activeModelId={activeModelId ?? undefined} activeModelId={activeModelId ?? undefined}
{attachments}
class="py-5"
limitToSingleRow
onFileRemove={handleFileRemove}
style="scroll-padding: 1rem;"
/> />
<div <div
class="flex-column relative min-h-12 items-center rounded-4xl md:rounded-3xl py-2 pb-2.25 shadow-sm transition-all focus-within:shadow-md md:py-3!" class="flex-column relative min-h-12 items-center rounded-4xl md:rounded-3xl py-2 pb-2.25 shadow-sm transition-all focus-within:shadow-md md:py-3!"
> >
<ChatFormInput <ChatFormInput
class="px-5 py-1.5 md:pt-0"
bind:this={inputRef} bind:this={inputRef}
bind:value bind:value
onKeydown={handleKeydown} class="px-5 py-1.5 md:pt-0"
{disabled}
onInput={() => { onInput={() => {
pickers.handleInput(); pickers.handleInput();
onValueChange?.(value); onValueChange?.(value);
}} }}
onKeydown={handleKeydown}
onPaste={handlePaste} onPaste={handlePaste}
{disabled}
{placeholder} {placeholder}
{useRichInput} {useRichInput}
/> />
@@ -608,22 +608,22 @@
{/if} {/if}
<ChatFormActions <ChatFormActions
class="px-3"
bind:this={chatFormActionsRef} bind:this={chatFormActionsRef}
canSend={canSubmit} canSend={canSubmit}
class="px-3"
{disabled} {disabled}
{isLoading} {isLoading}
isReasoning={chatStore.isReasoning} isReasoning={chatStore.isReasoning}
{isRecording} {isRecording}
{showAddButton}
{showModelSelector}
{uploadedFiles}
onFileUpload={handleFileUpload} onFileUpload={handleFileUpload}
onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined}
onMcpResourcesClick={() => (isResourceDialogOpen = true)}
onMicClick={handleMicClick} onMicClick={handleMicClick}
{onStop} {onStop}
onSystemPromptClick={() => onSystemPromptClick?.({ files: uploadedFiles, message: value })} onSystemPromptClick={() => onSystemPromptClick?.({ files: uploadedFiles, message: value })}
onMcpPromptClick={showMcpPromptButton ? () => pickers.openPromptPicker() : undefined} {showAddButton}
onMcpResourcesClick={() => (isResourceDialogOpen = true)} {showModelSelector}
{uploadedFiles}
/> />
</div> </div>
</div> </div>
@@ -632,21 +632,20 @@
{#if toolsStore.hasEnabledCwdTools} {#if toolsStore.hasEnabledCwdTools}
<ChatFormCurrentWorkingDirectory <ChatFormCurrentWorkingDirectory
directory={cwd}
isOpen={pickers.isWorkingDirectoryPickerOpen}
bind:query={pickers.workingDirectoryQuery} bind:query={pickers.workingDirectoryQuery}
customAnchor={mentionAnchor} customAnchor={mentionAnchor}
directory={cwd}
{disabled}
isOpen={pickers.isWorkingDirectoryPickerOpen}
onChange={handleWorkingDirectoryChange} onChange={handleWorkingDirectoryChange}
onClose={pickers.handleWorkingDirectoryClose} onClose={pickers.handleWorkingDirectoryClose}
onOpen={pickers.handleWorkingDirectoryOpen} onOpen={pickers.handleWorkingDirectoryOpen}
{disabled}
/> />
{/if} {/if}
</form> </form>
<DialogMcpResourcesBrowser <DialogMcpResourcesBrowser
bind:open={isResourceDialogOpen} bind:open={isResourceDialogOpen}
preSelectedUri={preSelectedResourceUri}
onAttach={(resource: MCPResourceInfo) => { onAttach={(resource: MCPResourceInfo) => {
mcpStore.attachResource(resource.uri); mcpStore.attachResource(resource.uri);
}} }}
@@ -655,4 +654,5 @@
preSelectedResourceUri = undefined; preSelectedResourceUri = undefined;
} }
}} }}
preSelectedUri={preSelectedResourceUri}
/> />
@@ -18,8 +18,8 @@
class="file-upload-button md:h-8 md:w-8 h-9 w-9 rounded-full p-0" class="file-upload-button md:h-8 md:w-8 h-9 w-9 rounded-full p-0"
{disabled} {disabled}
{onclick} {onclick}
variant="secondary"
type="button" type="button"
variant="secondary"
> >
<span class="sr-only">{ATTACHMENT_TOOLTIP_TEXT}</span> <span class="sr-only">{ATTACHMENT_TOOLTIP_TEXT}</span>
@@ -70,10 +70,10 @@
<DropdownMenu.SubContent class="w-72 pt-0"> <DropdownMenu.SubContent class="w-72 pt-0">
{#if hasMcpServers} {#if hasMcpServers}
<DropdownMenuSearchable <DropdownMenuSearchable
placeholder="Search servers..."
bind:searchValue={mcpSearchQuery} bind:searchValue={mcpSearchQuery}
emptyMessage="No servers found" emptyMessage="No servers found"
isEmpty={filteredMcpServers.length === 0} isEmpty={filteredMcpServers.length === 0}
placeholder="Search servers..."
> >
<div class="max-h-64 overflow-y-auto"> <div class="max-h-64 overflow-y-auto">
{#each filteredMcpServers as server (server.id)} {#each filteredMcpServers as server (server.id)}
@@ -84,10 +84,10 @@
{@const faviconUrl = mcpStore.getServerFavicon(server.id)} {@const faviconUrl = mcpStore.getServerFavicon(server.id)}
<button <button
type="button"
class="flex w-full items-center justify-between gap-2 rounded-sm px-2 py-2 text-left transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50" class="flex w-full items-center justify-between gap-2 rounded-sm px-2 py-2 text-left transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50"
onclick={() => !hasError && toggleServerForChat(server.id)}
disabled={hasError} disabled={hasError}
onclick={() => !hasError && toggleServerForChat(server.id)}
type="button"
> >
<div class="flex min-w-0 flex-1 items-center gap-2"> <div class="flex min-w-0 flex-1 items-center gap-2">
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
@@ -96,8 +96,8 @@
{faviconUrl} {faviconUrl}
iconClass={ICON_CLASS_DEFAULT} iconClass={ICON_CLASS_DEFAULT}
iconRounded="rounded-sm" iconRounded="rounded-sm"
showVersion={false}
nameClass="text-sm" nameClass="text-sm"
showVersion={false}
/> />
</div> </div>
@@ -113,8 +113,8 @@
<Switch <Switch
checked={isEnabledForChat} checked={isEnabledForChat}
disabled={hasError} disabled={hasError}
onclick={(e) => e.stopPropagation()}
onCheckedChange={() => toggleServerForChat(server.id)} onCheckedChange={() => toggleServerForChat(server.id)}
onclick={(e) => e.stopPropagation()}
/> />
</button> </button>
{/each} {/each}
@@ -64,6 +64,7 @@
<Tooltip.Trigger> <Tooltip.Trigger>
<Info class="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> <Info class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content side="left"> <Tooltip.Content side="left">
<p>Maximum reasoning effort with extended context usage</p> <p>Maximum reasoning effort with extended context usage</p>
</Tooltip.Content> </Tooltip.Content>
@@ -78,7 +78,7 @@
<Sheet.Root bind:open={sheetOpen}> <Sheet.Root bind:open={sheetOpen}>
{@render trigger({ disabled: chatFormActions.disabled, onclick: () => (sheetOpen = true) })} {@render trigger({ disabled: chatFormActions.disabled, onclick: () => (sheetOpen = true) })}
<Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto"> <Sheet.Content class="max-h-[85vh] gap-0 overflow-y-auto" side="bottom">
<Sheet.Header> <Sheet.Header>
<Sheet.Title>Add to chat</Sheet.Title> <Sheet.Title>Add to chat</Sheet.Title>
@@ -90,8 +90,8 @@
<div class="flex flex-col gap-1 px-1.5 pb-2"> <div class="flex flex-col gap-1 px-1.5 pb-2">
{#if reasoning.modelSupportsThinking} {#if reasoning.modelSupportsThinking}
<Collapsible.Root <Collapsible.Root
open={reasoningExpanded}
onOpenChange={(open) => (reasoningExpanded = open)} onOpenChange={(open) => (reasoningExpanded = open)}
open={reasoningExpanded}
> >
<Collapsible.Trigger class={sheetItemClass}> <Collapsible.Trigger class={sheetItemClass}>
{#if reasoningExpanded} {#if reasoningExpanded}
@@ -120,10 +120,10 @@
{#each reasoning.levels as level (level.value)} {#each reasoning.levels as level (level.value)}
{@const tokenLabel = reasoning.tokenLabel(level)} {@const tokenLabel = reasoning.tokenLabel(level)}
<button <button
type="button"
class={sheetItemRowClass}
class:bg-accent={reasoning.isSelected(level)} class:bg-accent={reasoning.isSelected(level)}
class={sheetItemRowClass}
onclick={() => reasoning.select(level)} onclick={() => reasoning.select(level)}
type="button"
> >
<div class="flex min-w-0 items-center gap-3"> <div class="flex min-w-0 items-center gap-3">
{#if reasoning.isSelected(level)} {#if reasoning.isSelected(level)}
@@ -147,7 +147,7 @@
</Collapsible.Root> </Collapsible.Root>
{/if} {/if}
<Collapsible.Root open={filesExpanded} onOpenChange={(open) => (filesExpanded = open)}> <Collapsible.Root onOpenChange={(open) => (filesExpanded = open)} open={filesExpanded}>
<Collapsible.Trigger class={sheetItemClass}> <Collapsible.Trigger class={sheetItemClass}>
{#if filesExpanded} {#if filesExpanded}
<ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" /> <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
@@ -166,9 +166,9 @@
{@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)} {@const enabled = attachmentMenu.isItemEnabled(item.enabledWhen)}
{#if enabled} {#if enabled}
<button <button
type="button"
class={sheetItemClass} class={sheetItemClass}
onclick={() => attachmentMenu.callbacks[item.action]()} onclick={() => attachmentMenu.callbacks[item.action]()}
type="button"
> >
<item.icon class="{ICON_CLASS_DEFAULT} shrink-0" /> <item.icon class="{ICON_CLASS_DEFAULT} shrink-0" />
@@ -177,7 +177,7 @@
{:else if item.disabledTooltip} {:else if item.disabledTooltip}
<Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}> <Tooltip.Root delayDuration={TOOLTIP_DELAY_DURATION}>
<Tooltip.Trigger> <Tooltip.Trigger>
<button type="button" class={sheetItemClass} disabled> <button class={sheetItemClass} disabled type="button">
<item.icon class="{ICON_CLASS_DEFAULT} shrink-0" /> <item.icon class="{ICON_CLASS_DEFAULT} shrink-0" />
<span>{item.label}</span> <span>{item.label}</span>
@@ -194,7 +194,7 @@
</Collapsible.Content> </Collapsible.Content>
</Collapsible.Root> </Collapsible.Root>
<Collapsible.Root open={mcpExpanded} onOpenChange={(open) => (mcpExpanded = open)}> <Collapsible.Root onOpenChange={(open) => (mcpExpanded = open)} open={mcpExpanded}>
<Collapsible.Trigger class={sheetItemClass}> <Collapsible.Trigger class={sheetItemClass}>
{#if mcpExpanded} {#if mcpExpanded}
<ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" /> <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
@@ -223,21 +223,21 @@
)} )}
<button <button
type="button"
class={sheetItemRowClass} class={sheetItemRowClass}
disabled={hasError}
onclick={() => onclick={() =>
!hasError && conversationsStore.preferences.toggleMcpServerForChat(server.id)} !hasError && conversationsStore.preferences.toggleMcpServerForChat(server.id)}
disabled={hasError} type="button"
> >
<div class="flex min-w-0 flex-1 items-center gap-2"> <div class="flex min-w-0 flex-1 items-center gap-2">
{#if faviconUrl} {#if faviconUrl}
<img <img
src={faviconUrl}
alt="" alt=""
class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm" class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm"
onerror={(e) => { onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none'; (e.currentTarget as HTMLImageElement).style.display = 'none';
}} }}
src={faviconUrl}
/> />
{/if} {/if}
@@ -270,7 +270,7 @@
</Collapsible.Root> </Collapsible.Root>
{#if toolsPanel.totalToolCount > 0} {#if toolsPanel.totalToolCount > 0}
<Collapsible.Root open={toolsExpanded} onOpenChange={(open) => (toolsExpanded = open)}> <Collapsible.Root onOpenChange={(open) => (toolsExpanded = open)} open={toolsExpanded}>
<Collapsible.Trigger class={sheetItemClass}> <Collapsible.Trigger class={sheetItemClass}>
{#if toolsExpanded} {#if toolsExpanded}
<ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" /> <ChevronDown class="{ICON_CLASS_DEFAULT} shrink-0" />
@@ -295,18 +295,18 @@
{@const favicon = toolsPanel.getFavicon(group)} {@const favicon = toolsPanel.getFavicon(group)}
<button <button
type="button"
class={sheetItemRowClass} class={sheetItemRowClass}
onclick={() => toolsPanel.toggleGroupByKey(group.key)} onclick={() => toolsPanel.toggleGroupByKey(group.key)}
type="button"
> >
{#if favicon} {#if favicon}
<img <img
src={favicon}
alt="" alt=""
class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm" class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm"
onerror={(e) => { onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none'; (e.currentTarget as HTMLImageElement).style.display = 'none';
}} }}
src={favicon}
/> />
{/if} {/if}
@@ -319,8 +319,8 @@
<Checkbox <Checkbox
{checked} {checked}
class="{ICON_CLASS_DEFAULT} shrink-0" class="{ICON_CLASS_DEFAULT} shrink-0"
onclick={(e) => e.stopPropagation()}
onCheckedChange={() => toolsPanel.toggleGroupByKey(group.key)} onCheckedChange={() => toolsPanel.toggleGroupByKey(group.key)}
onclick={(e) => e.stopPropagation()}
/> />
</button> </button>
{/each} {/each}
@@ -330,9 +330,9 @@
{/if} {/if}
<button <button
type="button"
class={sheetItemClass} class={sheetItemClass}
onclick={() => attachmentMenu.callbacks[AttachmentAction.SYSTEM_PROMPT_CLICK]()} onclick={() => attachmentMenu.callbacks[AttachmentAction.SYSTEM_PROMPT_CLICK]()}
type="button"
> >
<MessageSquare class="{ICON_CLASS_DEFAULT} shrink-0" /> <MessageSquare class="{ICON_CLASS_DEFAULT} shrink-0" />
@@ -341,9 +341,9 @@
{#if chatFormActions.hasMcpPromptsSupport} {#if chatFormActions.hasMcpPromptsSupport}
<button <button
type="button"
class={sheetItemClass} class={sheetItemClass}
onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_PROMPT_CLICK]()} onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_PROMPT_CLICK]()}
type="button"
> >
<Zap class="{ICON_CLASS_DEFAULT} shrink-0" /> <Zap class="{ICON_CLASS_DEFAULT} shrink-0" />
@@ -353,9 +353,9 @@
{#if chatFormActions.hasMcpResourcesSupport} {#if chatFormActions.hasMcpResourcesSupport}
<button <button
type="button"
class={sheetItemClass} class={sheetItemClass}
onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_RESOURCES_CLICK]()} onclick={() => attachmentMenu.callbacks[AttachmentAction.MCP_RESOURCES_CLICK]()}
type="button"
> >
<FolderOpen class="{ICON_CLASS_DEFAULT} shrink-0" /> <FolderOpen class="{ICON_CLASS_DEFAULT} shrink-0" />
@@ -68,8 +68,8 @@
{@const favicon = toolsPanel.getFavicon(group)} {@const favicon = toolsPanel.getFavicon(group)}
<Collapsible.Root <Collapsible.Root
open={isExpanded}
onOpenChange={() => toolsPanel.toggleGroupExpanded(group.key)} onOpenChange={() => toolsPanel.toggleGroupExpanded(group.key)}
open={isExpanded}
> >
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<Collapsible.Trigger <Collapsible.Trigger
@@ -84,12 +84,12 @@
<span class="inline-flex min-w-0 items-center gap-1.5 font-medium"> <span class="inline-flex min-w-0 items-center gap-1.5 font-medium">
{#if favicon} {#if favicon}
<img <img
src={favicon}
alt="" alt=""
class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm" class="{ICON_CLASS_DEFAULT} shrink-0 rounded-sm"
onerror={(e) => { onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none'; (e.currentTarget as HTMLImageElement).style.display = 'none';
}} }}
src={favicon}
/> />
{/if} {/if}
@@ -107,8 +107,8 @@
<Checkbox <Checkbox
{...props} {...props}
{checked} {checked}
onCheckedChange={() => toolsPanel.toggleGroupByKey(group.key)}
class="mr-2 {ICON_CLASS_DEFAULT} shrink-0" class="mr-2 {ICON_CLASS_DEFAULT} shrink-0"
onCheckedChange={() => toolsPanel.toggleGroupByKey(group.key)}
/> />
{/snippet} {/snippet}
</Tooltip.Trigger> </Tooltip.Trigger>
@@ -127,14 +127,14 @@
{#each group.tools as entry (entry.key)} {#each group.tools as entry (entry.key)}
{@const enabled = toolsStore.isToolEnabled(entry.key)} {@const enabled = toolsStore.isToolEnabled(entry.key)}
<button <button
type="button"
class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted/50" class="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted/50"
onclick={() => toolsStore.toggleTool(entry.key)} onclick={() => toolsStore.toggleTool(entry.key)}
type="button"
> >
<span <span
class="flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
data-slot="checkbox" data-slot="checkbox"
data-state={enabled ? 'checked' : 'unchecked'} data-state={enabled ? 'checked' : 'unchecked'}
class="flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground"
> >
{#if enabled} {#if enabled}
<Check class="size-3.5" /> <Check class="size-3.5" />
@@ -153,17 +153,17 @@
{#if deviceStore.isMobile} {#if deviceStore.isMobile}
<ModelsSelectorSheet <ModelsSelectorSheet
disabled={disabled || isOffline}
bind:this={selectorModelRef} bind:this={selectorModelRef}
currentModel={selectorModel} currentModel={selectorModel}
disabled={disabled || isOffline}
{forceForegroundText} {forceForegroundText}
{useGlobalSelection} {useGlobalSelection}
/> />
{:else} {:else}
<ModelsSelectorDropdown <ModelsSelectorDropdown
disabled={disabled || isOffline}
bind:this={selectorModelRef} bind:this={selectorModelRef}
currentModel={selectorModel} currentModel={selectorModel}
disabled={disabled || isOffline}
{forceForegroundText} {forceForegroundText}
{useGlobalSelection} {useGlobalSelection}
/> />
@@ -17,16 +17,17 @@
{#snippet submitButton(props = {})} {#snippet submitButton(props = {})}
<Button <Button
type="submit"
disabled={isDisabled}
class={[ class={[
'md:h-8 md:w-8 h-9 w-9 rounded-full p-0', 'md:h-8 md:w-8 h-9 w-9 rounded-full p-0',
showErrorState && showErrorState &&
'bg-red-400/10 text-red-400 hover:bg-red-400/20 hover:text-red-400 disabled:opacity-100' 'bg-red-400/10 text-red-400 hover:bg-red-400/20 hover:text-red-400 disabled:opacity-100'
]} ]}
disabled={isDisabled}
type="submit"
{...props} {...props}
> >
<span class="sr-only">Send</span> <span class="sr-only">Send</span>
<ArrowUp class="h-12 w-12" /> <ArrowUp class="h-12 w-12" />
</Button> </Button>
{/snippet} {/snippet}
@@ -188,14 +188,14 @@
{#if showModelSelector} {#if showModelSelector}
<ChatFormActionModels <ChatFormActionModels
{disabled}
bind:this={selectorModelRef}
bind:hasAudioModality bind:hasAudioModality
bind:hasModelSelected
bind:hasVideoModality bind:hasVideoModality
bind:hasVisionModality bind:hasVisionModality
bind:hasModelSelected
bind:isSelectedModelInCache bind:isSelectedModelInCache
bind:submitTooltip bind:submitTooltip
bind:this={selectorModelRef}
{disabled}
forceForegroundText forceForegroundText
useGlobalSelection useGlobalSelection
/> />
@@ -204,12 +204,12 @@
{#if isReasoning} {#if isReasoning}
<Button <Button
type="button" class="group h-8 w-8 rounded-full p-0"
variant="secondary"
onclick={() => onclick={() =>
ChatService.stopReasoning(activeMessage?.completionId ?? '', activeMessage?.model)} ChatService.stopReasoning(activeMessage?.completionId ?? '', activeMessage?.model)}
class="group h-8 w-8 rounded-full p-0"
title="Skip reasoning" title="Skip reasoning"
type="button"
variant="secondary"
> >
<span class="sr-only">Skip reasoning</span> <span class="sr-only">Skip reasoning</span>
@@ -221,10 +221,10 @@
{#if isLoading && !canSubmit} {#if isLoading && !canSubmit}
<Button <Button
class="group h-8 w-8 rounded-full p-0 hover:bg-destructive/10!"
onclick={onStop}
type="button" type="button"
variant="secondary" variant="secondary"
onclick={onStop}
class="group h-8 w-8 rounded-full p-0 hover:bg-destructive/10!"
> >
<span class="sr-only">Stop</span> <span class="sr-only">Stop</span>
@@ -238,8 +238,8 @@
<ChatFormActionSubmit <ChatFormActionSubmit
canSend={canSend && (showModelSelector ? hasModelSelected && isSelectedModelInCache : true)} canSend={canSend && (showModelSelector ? hasModelSelected && isSelectedModelInCache : true)}
{disabled} {disabled}
tooltipLabel={submitTooltip}
showErrorState={showModelSelector && hasModelSelected && !isSelectedModelInCache} showErrorState={showModelSelector && hasModelSelected && !isSelectedModelInCache}
tooltipLabel={submitTooltip}
/> />
{/if} {/if}
</div> </div>
@@ -42,16 +42,16 @@
</script> </script>
<div <div
role="button"
tabindex="0"
aria-label="Context usage" aria-label="Context usage"
data-context-gauge-trigger
class="flex h-5 w-5 cursor-default items-center justify-center" class="flex h-5 w-5 cursor-default items-center justify-center"
data-context-gauge-trigger
onclick={gaugeTriggerClick} onclick={gaugeTriggerClick}
onkeydown={gaugeTriggerKeydown} onkeydown={gaugeTriggerKeydown}
onpointerdown={gaugeTriggerPointerDown} onpointerdown={gaugeTriggerPointerDown}
onpointerenter={gaugeTriggerEnter} onpointerenter={gaugeTriggerEnter}
onpointerleave={gaugeTriggerLeave} onpointerleave={gaugeTriggerLeave}
role="button"
tabindex="0"
> >
<ContextGaugeDial percent={gauge.contextPercent} level={gauge.colorLevel} /> <ContextGaugeDial level={gauge.colorLevel} percent={gauge.contextPercent} />
</div> </div>
@@ -11,6 +11,7 @@
<div class="grid gap-1.5"> <div class="grid gap-1.5">
<div class="flex items-baseline justify-between"> <div class="flex items-baseline justify-between">
<span class="text-muted-foreground">{label}</span> <span class="text-muted-foreground">{label}</span>
<span class="font-mono text-muted-foreground">{value}</span> <span class="font-mono text-muted-foreground">{value}</span>
</div> </div>
@@ -57,12 +57,13 @@
{#if cumulativeRead > 0} {#if cumulativeRead > 0}
<ContextGaugeDetailRow <ContextGaugeDetailRow
label="Prompt tokens evaluated" label="Prompt tokens evaluated"
value={`${cumulativeRead.toLocaleString()} tok`}
subtitle={cumulativeCacheTotal > 0 subtitle={cumulativeCacheTotal > 0
? `${cumulativeCacheTotal.toLocaleString()} reused from KV cache` ? `${cumulativeCacheTotal.toLocaleString()} reused from KV cache`
: undefined} : undefined}
value={`${cumulativeRead.toLocaleString()} tok`}
/> />
{/if} {/if}
{#if cumulativeOutput > 0} {#if cumulativeOutput > 0}
<ContextGaugeDetailRow <ContextGaugeDetailRow
label="Tokens generated" label="Tokens generated"
@@ -83,10 +84,10 @@
{#if currentRead > 0} {#if currentRead > 0}
<ContextGaugeDetailRow <ContextGaugeDetailRow
label="Prompt" label="Prompt"
value={`${currentRead.toLocaleString()} tok`}
subtitle={currentCache > 0 subtitle={currentCache > 0
? `${currentFresh.toLocaleString()} fresh + ${currentCache.toLocaleString()} cached` ? `${currentFresh.toLocaleString()} fresh + ${currentCache.toLocaleString()} cached`
: undefined} : undefined}
value={`${currentRead.toLocaleString()} tok`}
/> />
{/if} {/if}
@@ -100,6 +101,7 @@
<div class="pt-1 mt-0.5 border-t border-border/30"> <div class="pt-1 mt-0.5 border-t border-border/30">
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-muted-foreground">KV cache total</span> <span class="text-muted-foreground">KV cache total</span>
<span class="font-mono font-medium">{kvTotal.toLocaleString()} tok</span> <span class="font-mono font-medium">{kvTotal.toLocaleString()} tok</span>
</div> </div>
</div> </div>
@@ -18,7 +18,7 @@
const strokeWidth = $derived(size === 'md' ? 4 : 3); const strokeWidth = $derived(size === 'md' ? 4 : 3);
</script> </script>
<svg viewBox="0 0 32 32" fill="none" class={dimensions}> <svg class={dimensions} fill="none" viewBox="0 0 32 32">
<circle <circle
cx="16" cx="16"
cy="16" cy="16"
@@ -29,15 +29,15 @@
/> />
<circle <circle
class="transition-colors duration-300 {strokeLevelClass}"
cx="16" cx="16"
cy="16" cy="16"
r={RADIUS} r={RADIUS}
class="transition-colors duration-300 {strokeLevelClass}"
stroke="currentColor" stroke="currentColor"
stroke-width={strokeWidth}
stroke-linecap="round"
stroke-dasharray={CIRCUMFERENCE} stroke-dasharray={CIRCUMFERENCE}
stroke-dashoffset={percent !== null ? CIRCUMFERENCE * (1 - percent / 100) : CIRCUMFERENCE} stroke-dashoffset={percent !== null ? CIRCUMFERENCE * (1 - percent / 100) : CIRCUMFERENCE}
stroke-linecap="round"
stroke-width={strokeWidth}
transform="rotate(-90 16 16)" transform="rotate(-90 16 16)"
/> />
</svg> </svg>
@@ -14,11 +14,13 @@
{#if modelId !== null && !isLoading} {#if modelId !== null && !isLoading}
<div class="flex flex-col gap-2 border-t border-border/50 pt-2 text-xs text-muted-foreground"> <div class="flex flex-col gap-2 border-t border-border/50 pt-2 text-xs text-muted-foreground">
<span>Available context size is only visible once the model is loaded.</span> <span>Available context size is only visible once the model is loaded.</span>
<Button size="sm" variant="secondary" class="self-start" onclick={onLoad}>Load model</Button>
<Button class="self-start" onclick={onLoad} size="sm" variant="secondary">Load model</Button>
</div> </div>
{:else if isLoading} {:else if isLoading}
<div class="flex items-center gap-2 border-t border-border/50 pt-2 text-xs text-muted-foreground"> <div class="flex items-center gap-2 border-t border-border/50 pt-2 text-xs text-muted-foreground">
<Loader2 class="h-3.5 w-3.5 animate-spin" /> <Loader2 class="h-3.5 w-3.5 animate-spin" />
<span>Loading model...</span> <span>Loading model...</span>
</div> </div>
{/if} {/if}
@@ -54,17 +54,19 @@
{#if gaugePopup.open} {#if gaugePopup.open}
<div <div
role="status"
bind:this={cardEl} bind:this={cardEl}
class="absolute z-50 w-64 -translate-x-1/2 rounded-lg border border-border/50 bg-popover p-3 text-sm text-popover-foreground shadow-lg ring-1 ring-foreground/10" class="absolute z-50 w-64 -translate-x-1/2 rounded-lg border border-border/50 bg-popover p-3 text-sm text-popover-foreground shadow-lg ring-1 ring-foreground/10"
style="left: {gaugePopup.centerX}px; bottom: {gaugePopup.bottom}px"
onpointerenter={gaugeCardEnter} onpointerenter={gaugeCardEnter}
onpointerleave={gaugeCardLeave} onpointerleave={gaugeCardLeave}
role="status"
style="left: {gaugePopup.centerX}px; bottom: {gaugePopup.bottom}px"
> >
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="font-medium">Context</span> <span class="font-medium">Context</span>
<span class="text-muted-foreground">·</span> <span class="text-muted-foreground">·</span>
<span class="font-mono text-muted-foreground"> <span class="font-mono text-muted-foreground">
{formatParameters(gauge.contextUsed)} {formatParameters(gauge.contextUsed)}
/ {gauge.contextTotal !== null ? formatParameters(gauge.contextTotal) : '-'} / {gauge.contextTotal !== null ? formatParameters(gauge.contextTotal) : '-'}
@@ -73,8 +75,8 @@
{#if gauge.activeModelId !== null && !gauge.isActiveModelLoaded} {#if gauge.activeModelId !== null && !gauge.isActiveModelLoaded}
<ContextGaugeLoadModel <ContextGaugeLoadModel
modelId={gauge.activeModelId}
isLoading={gauge.isActiveModelLoading} isLoading={gauge.isActiveModelLoading}
modelId={gauge.activeModelId}
onLoad={gauge.loadModel} onLoad={gauge.loadModel}
/> />
{:else if showProgressBar} {:else if showProgressBar}
@@ -91,6 +93,7 @@
<span> <span>
<span class={colorLevelTextClass(gauge.colorLevel)}>{gauge.contextPercent}%</span> used <span class={colorLevelTextClass(gauge.colorLevel)}>{gauge.contextPercent}%</span> used
</span> </span>
<span> <span>
{formatParameters(gauge.contextAvailable ?? 0)} remaining {formatParameters(gauge.contextAvailable ?? 0)} remaining
</span> </span>
@@ -101,15 +104,15 @@
{#if gauge.hasAnyUsage} {#if gauge.hasAnyUsage}
<ContextGaugeDetails <ContextGaugeDetails
currentRead={gauge.currentRead}
currentFresh={gauge.currentFresh}
currentCache={gauge.currentCache}
currentOutput={gauge.currentOutput}
kvTotal={gauge.kvTotal}
cumulativeRead={gauge.cumulativeRead}
cumulativeOutput={gauge.cumulativeOutput}
cumulativeCacheTotal={gauge.cumulativeCacheTotal}
averageTokensPerSecond={gauge.averageTokensPerSecond} averageTokensPerSecond={gauge.averageTokensPerSecond}
cumulativeCacheTotal={gauge.cumulativeCacheTotal}
cumulativeOutput={gauge.cumulativeOutput}
cumulativeRead={gauge.cumulativeRead}
currentCache={gauge.currentCache}
currentFresh={gauge.currentFresh}
currentOutput={gauge.currentOutput}
currentRead={gauge.currentRead}
kvTotal={gauge.kvTotal}
transientDetails={gauge.transientDetails} transientDetails={gauge.transientDetails}
/> />
{/if} {/if}
@@ -323,80 +323,81 @@
</script> </script>
<button <button
type="button"
class={[ class={[
'justify-self-start flex min-w-0 w-auto items-center gap-1 mt-1.5 py-1 px-2 backdrop-blur-2xl rounded-md', 'justify-self-start flex min-w-0 w-auto items-center gap-1 mt-1.5 py-1 px-2 backdrop-blur-2xl rounded-md',
className className
]} ]}
onclick={onOpen}
{disabled} {disabled}
onclick={onOpen}
type="button"
> >
<ChatFormCurrentWorkingDirectoryChip <ChatFormCurrentWorkingDirectoryChip
{directory} {directory}
{homeBase}
{disabled} {disabled}
{showTooltip} {homeBase}
onClear={handleDismiss} onClear={handleDismiss}
{showTooltip}
/> />
</button> </button>
<Popover.Root open={isOpen} onOpenChange={handleOpenChange}> <Popover.Root onOpenChange={handleOpenChange} open={isOpen}>
<Popover.Trigger <Popover.Trigger
aria-hidden="true"
class="pointer-events-none absolute inset-0 opacity-0" class="pointer-events-none absolute inset-0 opacity-0"
tabindex={-1} tabindex={-1}
aria-hidden="true"
> >
<span class="sr-only">Open working directory picker</span> <span class="sr-only">Open working directory picker</span>
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
side="top"
align="start" align="start"
sideOffset={12}
{customAnchor}
preventScroll={false}
onkeydown={handleKeydown}
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
class="w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl" class="w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl"
{customAnchor}
onCloseAutoFocus={(event) => event.preventDefault()}
onOpenAutoFocus={(event) => event.preventDefault()}
onkeydown={handleKeydown}
preventScroll={false}
side="top"
sideOffset={12}
> >
<div class="p-2 min-h-22 flex flex-col justify-between"> <div class="p-2 min-h-22 flex flex-col justify-between">
<SearchInput <SearchInput
bind:ref={searchInputRef} bind:ref={searchInputRef}
bind:value={query} bind:value={query}
placeholder="Choose working directory"
onClose={closePicker}
class="w-full" class="w-full"
onClose={closePicker}
placeholder="Choose working directory"
/> />
{#if !fileSearchEnabled} {#if !fileSearchEnabled}
<div class="px-2 py-1.5 text-sm text-muted-foreground">{searchUnavailableMessage}</div> <div class="px-2 py-1.5 text-sm text-muted-foreground">{searchUnavailableMessage}</div>
{:else if query.trim() && (search.isSearching || queryResults.length > 0 || searchError)} {:else if query.trim() && (search.isSearching || queryResults.length > 0 || searchError)}
<ChatFormCurrentWorkingDirectoryResultsList <ChatFormCurrentWorkingDirectoryResultsList
results={queryResults} bind:container={listContainer}
error={searchError}
hoveredIndex={nav.hoveredIndex} hoveredIndex={nav.hoveredIndex}
isSearching={search.isSearching} isSearching={search.isSearching}
error={searchError}
rawQuery={query}
bind:container={listContainer}
onCommit={commit} onCommit={commit}
onHover={(index) => nav.setHover(index)} onHover={(index) => nav.setHover(index)}
rawQuery={query}
results={queryResults}
/> />
{/if} {/if}
{#if pickerSupported && fileSearchEnabled} {#if pickerSupported && fileSearchEnabled}
<button <button
type="button"
class="-mt-1 flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground" class="-mt-1 flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none hover:bg-accent hover:text-accent-foreground"
onclick={browseNative} onclick={browseNative}
type="button"
> >
<FolderOpen class="size-4 shrink-0 text-muted-foreground" /> <FolderOpen class="size-4 shrink-0 text-muted-foreground" />
<span>Browse</span> <span>Browse</span>
</button> </button>
{/if} {/if}
{#if homeBase && fileSearchEnabled} {#if homeBase && fileSearchEnabled}
<div class="-mx-2 my-2 h-px bg-border/20" aria-hidden="true"></div> <div aria-hidden="true" class="-mx-2 my-2 h-px bg-border/20"></div>
<span class="px-2 py-1.5 font-mono text-[10px]"> <span class="px-2 py-1.5 font-mono text-[10px]">
Searching in: Searching in:
@@ -29,8 +29,8 @@
</script> </script>
<span <span
class="text-muted-foreground inline-flex items-center gap-1 text-xs group"
class:text-foreground={directory} class:text-foreground={directory}
class="text-muted-foreground inline-flex items-center gap-1 text-xs group"
> >
<div class="flex min-w-0 items-center gap-1 cursor-pointer"> <div class="flex min-w-0 items-center gap-1 cursor-pointer">
<Folder class="w-3.5 h-3.5" /> <Folder class="w-3.5 h-3.5" />
@@ -42,6 +42,7 @@
<span {...props} class="max-w-64 truncate">{displayLabel}</span> <span {...props} class="max-w-64 truncate">{displayLabel}</span>
{/snippet} {/snippet}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content> <Tooltip.Content>
<p>{displayLabelTitle}</p> <p>{displayLabelTitle}</p>
</Tooltip.Content> </Tooltip.Content>
@@ -56,14 +57,14 @@
class="w-0 overflow-hidden opacity-0 transition-[width,opacity] duration-200 ease-out group-hover:w-auto group-hover:opacity-100" class="w-0 overflow-hidden opacity-0 transition-[width,opacity] duration-200 ease-out group-hover:w-auto group-hover:opacity-100"
> >
<ActionIcon <ActionIcon
icon={X}
tooltip="Reset working directory"
ariaLabel="Reset working directory" ariaLabel="Reset working directory"
{disabled}
onclick={onClear}
iconSize="h-3 w-3"
stopPropagationOnClick
class="!h-4 !w-4 shrink-0 text-muted-foreground hover:text-foreground" class="!h-4 !w-4 shrink-0 text-muted-foreground hover:text-foreground"
{disabled}
icon={X}
iconSize="h-3 w-3"
onclick={onClear}
stopPropagationOnClick
tooltip="Reset working directory"
/> />
</div> </div>
{/if} {/if}
@@ -34,8 +34,8 @@
<div <div
bind:this={container} bind:this={container}
class="max-h-48 overflow-y-auto py-2"
transition:fly={{ duration: FLY_DURATION_MS, y: FLY_Y_PX }} transition:fly={{ duration: FLY_DURATION_MS, y: FLY_Y_PX }}
class="max-h-48 overflow-y-auto py-2"
> >
{#if isSearching && results.length === 0} {#if isSearching && results.length === 0}
<div class="px-2 py-1.5 text-sm text-muted-foreground">Searching...</div> <div class="px-2 py-1.5 text-sm text-muted-foreground">Searching...</div>
@@ -48,14 +48,15 @@
<button <button
type="button" type="button"
{...{ [UI_DATA_ATTRS.RESULT_INDEX]: index }} {...{ [UI_DATA_ATTRS.RESULT_INDEX]: index }}
data-highlighted={index === hoveredIndex ? '' : undefined}
class={cn( class={cn(
'relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground' 'relative flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground'
)} )}
data-highlighted={index === hoveredIndex ? '' : undefined}
onclick={() => onCommit?.(path)} onclick={() => onCommit?.(path)}
onmouseenter={() => onHover?.(index)} onmouseenter={() => onHover?.(index)}
> >
<Folder class="size-4 shrink-0 text-muted-foreground" /> <Folder class="size-4 shrink-0 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate font-mono text-left"> <span class="min-w-0 flex-1 truncate font-mono text-left">
{#each highlightMatch(path, rawQuery.trim()) as seg, segIndex (segIndex)} {#each highlightMatch(path, rawQuery.trim()) as seg, segIndex (segIndex)}
{#if seg.match} {#if seg.match}
@@ -56,23 +56,23 @@
{#if useRichInput} {#if useRichInput}
<ChatFormInputRich <ChatFormInputRich
bind:this={richRef} bind:this={richRef}
bind:value
class={className} class={className}
{disabled} {disabled}
{onInput} {onInput}
{onKeydown} {onKeydown}
{onPaste} {onPaste}
{placeholder} {placeholder}
bind:value
/> />
{:else} {:else}
<ChatFormInputBasic <ChatFormInputBasic
bind:this={basicRef} bind:this={basicRef}
bind:value
class={className} class={className}
{disabled} {disabled}
{onInput} {onInput}
{onKeydown} {onKeydown}
{onPaste} {onPaste}
{placeholder} {placeholder}
bind:value
/> />
{/if} {/if}
@@ -69,14 +69,14 @@
'text-md min-h-12 w-full resize-none border-0 bg-transparent p-0 leading-6 outline-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0', 'text-md min-h-12 w-full resize-none border-0 bg-transparent p-0 leading-6 outline-none placeholder:text-muted-foreground focus-visible:ring-0 focus-visible:ring-offset-0',
disabled && 'cursor-not-allowed' disabled && 'cursor-not-allowed'
]} ]}
style="max-height: var(--max-message-height);"
{disabled} {disabled}
onkeydown={onKeydown}
oninput={(event) => { oninput={(event) => {
autoResizeTextarea(event.currentTarget); autoResizeTextarea(event.currentTarget);
onInput?.(); onInput?.();
}} }}
onkeydown={onKeydown}
onpaste={onPaste} onpaste={onPaste}
{placeholder} {placeholder}
style="max-height: var(--max-message-height);"
></textarea> ></textarea>
</div> </div>
@@ -24,8 +24,8 @@
<input <input
bind:this={fileInputElement} bind:this={fileInputElement}
type="file" class="hidden {className}"
{multiple} {multiple}
onchange={handleFileSelect} onchange={handleFileSelect}
class="hidden {className}" type="file"
/> />
@@ -808,25 +808,25 @@
<div class="flex-1 {className} mb-0.5"> <div class="flex-1 {className} mb-0.5">
<div <div
bind:this={rootElement} bind:this={rootElement}
contenteditable={!disabled}
role="textbox"
aria-multiline="true"
aria-disabled={disabled} aria-disabled={disabled}
aria-multiline="true"
aria-placeholder={placeholder} aria-placeholder={placeholder}
data-placeholder={placeholder}
tabindex={disabled ? -1 : 0}
class={[ class={[
'chat-form-input-rich text-md min-h-12 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0', 'chat-form-input-rich text-md min-h-12 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
disabled && 'cursor-not-allowed' disabled && 'cursor-not-allowed'
]} ]}
style="max-height: var(--max-message-height);" contenteditable={!disabled}
oncompositionstart={handleCompositionStart} data-placeholder={placeholder}
oncompositionend={handleCompositionEnd} oncompositionend={handleCompositionEnd}
oncompositionstart={handleCompositionStart}
oncopy={handleCopy}
oncut={handleCut}
oninput={handleInput} oninput={handleInput}
onkeydown={handleKeydown} onkeydown={handleKeydown}
onpaste={handlePaste} onpaste={handlePaste}
oncopy={handleCopy} role="textbox"
oncut={handleCut} style="max-height: var(--max-message-height);"
tabindex={disabled ? -1 : 0}
></div> ></div>
</div> </div>
@@ -27,8 +27,8 @@
<ScrollCarousel gapSize="2" variant={ScrollCarouselVariant.CENTER}> <ScrollCarousel gapSize="2" variant={ScrollCarouselVariant.CENTER}>
{#each attachments as attachment, i (attachment.id)} {#each attachments as attachment, i (attachment.id)}
<ChatAttachmentsListItemMcpResource <ChatAttachmentsListItemMcpResource
class={i === 0 ? 'ml-3' : ''}
{attachment} {attachment}
class={i === 0 ? 'ml-3' : ''}
onRemove={handleRemove} onRemove={handleRemove}
onclick={() => handleResourceClick(attachment.resource.uri)} onclick={() => handleResourceClick(attachment.resource.uri)}
/> />
@@ -21,12 +21,12 @@
<div class="mb-0.5 flex items-center gap-1.5 text-xs text-muted-foreground"> <div class="mb-0.5 flex items-center gap-1.5 text-xs text-muted-foreground">
{#if faviconUrl} {#if faviconUrl}
<img <img
src={faviconUrl}
alt="" alt=""
class="h-3 w-3 shrink-0 rounded-sm" class="h-3 w-3 shrink-0 rounded-sm"
onerror={(e) => { onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none'; (e.currentTarget as HTMLImageElement).style.display = 'none';
}} }}
src={faviconUrl}
/> />
{/if} {/if}
@@ -1,4 +1,4 @@
<script lang="ts" generics="T"> <script generics="T" lang="ts">
import { SearchInput } from '$lib/components/app'; import { SearchInput } from '$lib/components/app';
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte'; import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
import { CHAT_FORM_POPOVER_MAX_HEIGHT, UI_DATA_ATTRS } from '$lib/constants'; import { CHAT_FORM_POPOVER_MAX_HEIGHT, UI_DATA_ATTRS } from '$lib/constants';
@@ -67,11 +67,11 @@
{#if showSearchInput} {#if showSearchInput}
<div class="absolute top-0 right-0 left-0 z-10 p-2 pb-0"> <div class="absolute top-0 right-0 left-0 z-10 p-2 pb-0">
<SearchInput <SearchInput
{autofocus}
placeholder={searchPlaceholder}
bind:value={searchQuery}
bind:ref={inputRef} bind:ref={inputRef}
bind:value={searchQuery}
{autofocus}
onClose={onSearchClose} onClose={onSearchClose}
placeholder={searchPlaceholder}
/> />
</div> </div>
{/if} {/if}
@@ -85,8 +85,10 @@
{#each { length: skeletonCount } as _, rowIndex (rowIndex)} {#each { length: skeletonCount } as _, rowIndex (rowIndex)}
<div class="flex items-start gap-3 rounded-lg px-3 py-2"> <div class="flex items-start gap-3 rounded-lg px-3 py-2">
<div class="mt-0.5 size-4 shrink-0 animate-pulse rounded-md bg-muted/60"></div> <div class="mt-0.5 size-4 shrink-0 animate-pulse rounded-md bg-muted/60"></div>
<div class="flex min-w-0 flex-1 flex-col"> <div class="flex min-w-0 flex-1 flex-col">
<div class="h-5 w-2/5 animate-pulse rounded-sm bg-muted/60"></div> <div class="h-5 w-2/5 animate-pulse rounded-sm bg-muted/60"></div>
<div class="h-4 w-1/3 animate-pulse rounded-sm bg-muted/40"></div> <div class="h-4 w-1/3 animate-pulse rounded-sm bg-muted/40"></div>
</div> </div>
</div> </div>
@@ -24,11 +24,11 @@
</script> </script>
<button <button
type="button"
{...{ [UI_DATA_ATTRS.PICKER_INDEX]: dataIndex }}
{disabled} {disabled}
{onclick} {onclick}
{onmouseenter} {onmouseenter}
type="button"
{...{ [UI_DATA_ATTRS.PICKER_INDEX]: dataIndex }}
class="flex w-full cursor-pointer items-start gap-3 rounded-lg px-3 py-2 text-left hover:bg-accent/50 {isSelected class="flex w-full cursor-pointer items-start gap-3 rounded-lg px-3 py-2 text-left hover:bg-accent/50 {isSelected
? 'bg-accent/50' ? 'bg-accent/50'
: ''} {disabled ? 'cursor-not-allowed opacity-50' : ''} {className}" : ''} {disabled ? 'cursor-not-allowed opacity-50' : ''} {className}"
@@ -12,6 +12,7 @@
<!-- Server label skeleton --> <!-- Server label skeleton -->
<div class="mb-2 flex items-center gap-1.5"> <div class="mb-2 flex items-center gap-1.5">
<div class="h-3 w-3 shrink-0 animate-pulse rounded-sm bg-muted"></div> <div class="h-3 w-3 shrink-0 animate-pulse rounded-sm bg-muted"></div>
<div class="h-3 w-24 animate-pulse rounded bg-muted"></div> <div class="h-3 w-24 animate-pulse rounded bg-muted"></div>
</div> </div>
@@ -30,21 +30,21 @@
}} }}
> >
<Popover.Trigger <Popover.Trigger
aria-hidden="true"
class="pointer-events-none absolute inset-0 opacity-0" class="pointer-events-none absolute inset-0 opacity-0"
tabindex={-1} tabindex={-1}
aria-hidden="true"
> >
<span class="sr-only">{srLabel}</span> <span class="sr-only">{srLabel}</span>
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
side="top"
align="start" align="start"
sideOffset={12}
class="w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl {className}" class="w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl {className}"
preventScroll={false}
onkeydown={onKeydown}
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onkeydown={onKeydown}
preventScroll={false}
side="top"
sideOffset={12}
> >
{@render children()} {@render children()}
</Popover.Content> </Popover.Content>
@@ -104,34 +104,36 @@
<ChatFormPickerPopover <ChatFormPickerPopover
bind:isOpen bind:isOpen
class={className} class={className}
srLabel="Open command picker"
{onClose} {onClose}
onKeydown={handleKeydown} onKeydown={handleKeydown}
srLabel="Open command picker"
> >
<ChatFormPickerList <ChatFormPickerList
items={filteredCommands} emptyMessage="No matching command"
isLoading={false} isLoading={false}
itemKey={(command) => command.name}
items={filteredCommands}
scrollTrigger={nav.scrollTrigger}
searchQuery={query ?? ''}
selectedIndex={nav.hoveredIndex} selectedIndex={nav.hoveredIndex}
showSearchInput={false} showSearchInput={false}
searchQuery={query ?? ''}
emptyMessage="No matching command"
itemKey={(command) => command.name}
scrollTrigger={nav.scrollTrigger}
> >
{#snippet item(command, index, isSelected)} {#snippet item(command, index, isSelected)}
{@const Icon = commandIcon[command.action]} {@const Icon = commandIcon[command.action]}
<ChatFormPickerListItem <ChatFormPickerListItem
dataIndex={index} dataIndex={index}
{isSelected}
disabled={command.disabled} disabled={command.disabled}
{isSelected}
onclick={() => handleSelect(command)} onclick={() => handleSelect(command)}
onmouseenter={() => { onmouseenter={() => {
if (!command.disabled) nav.setHover(index); if (!command.disabled) nav.setHover(index);
}} }}
> >
<Icon class="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" /> <Icon class="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<div class="flex min-w-0 flex-1 flex-col"> <div class="flex min-w-0 flex-1 flex-col">
<span class="font-mono text-sm font-medium">/{command.name}</span> <span class="font-mono text-sm font-medium">/{command.name}</span>
<span class="min-w-0 flex-1 truncate text-left text-xs text-muted-foreground"> <span class="min-w-0 flex-1 truncate text-left text-xs text-muted-foreground">
{command.description} {command.description}
</span> </span>
@@ -359,9 +359,9 @@
<ChatFormPickerPopover <ChatFormPickerPopover
bind:isOpen bind:isOpen
class={className} class={className}
srLabel="Open prompt picker"
{onClose} {onClose}
onKeydown={handleKeydown} onKeydown={handleKeydown}
srLabel="Open prompt picker"
> >
{#if selectedPrompt} {#if selectedPrompt}
{@const prompt = selectedPrompt} {@const prompt = selectedPrompt}
@@ -370,10 +370,10 @@
<div class="p-4"> <div class="p-4">
<ChatFormPickerItemHeader <ChatFormPickerItemHeader
description={prompt.description}
{server} {server}
{serverLabel} {serverLabel}
title={prompt.title || prompt.name} title={prompt.title || prompt.name}
description={prompt.description}
> >
{#snippet titleExtra()} {#snippet titleExtra()}
{#if prompt.arguments?.length} {#if prompt.arguments?.length}
@@ -385,33 +385,33 @@
</ChatFormPickerItemHeader> </ChatFormPickerItemHeader>
<ChatFormPromptPickerArgumentForm <ChatFormPromptPickerArgumentForm
prompt={selectedPrompt}
{promptArgs}
{suggestions}
{loadingSuggestions}
{activeAutocomplete} {activeAutocomplete}
{autocompleteIndex} {autocompleteIndex}
{promptError} {loadingSuggestions}
onArgInput={handleArgInput}
onArgKeydown={handleArgKeydown}
onArgBlur={handleArgBlur} onArgBlur={handleArgBlur}
onArgFocus={handleArgFocus} onArgFocus={handleArgFocus}
onArgInput={handleArgInput}
onArgKeydown={handleArgKeydown}
onCancel={handleCancelArgumentForm}
onSelectSuggestion={selectSuggestion} onSelectSuggestion={selectSuggestion}
onSubmit={handleArgumentSubmit} onSubmit={handleArgumentSubmit}
onCancel={handleCancelArgumentForm} prompt={selectedPrompt}
{promptArgs}
{promptError}
{suggestions}
/> />
</div> </div>
{:else} {:else}
<ChatFormPickerList <ChatFormPickerList
items={filteredPrompts}
{isLoading}
{selectedIndex}
bind:searchQuery={internalSearchQuery} bind:searchQuery={internalSearchQuery}
{showSearchInput}
searchPlaceholder="Search prompts..."
emptyMessage="No MCP prompts available" emptyMessage="No MCP prompts available"
{isLoading}
itemKey={(prompt) => prompt.serverName + ':' + prompt.name} itemKey={(prompt) => prompt.serverName + ':' + prompt.name}
items={filteredPrompts}
{scrollTrigger} {scrollTrigger}
searchPlaceholder="Search prompts..."
{selectedIndex}
{showSearchInput}
> >
{#snippet item(prompt, index, isSelected)} {#snippet item(prompt, index, isSelected)}
{@const server = serverSettingsMap.get(prompt.serverName)} {@const server = serverSettingsMap.get(prompt.serverName)}
@@ -423,10 +423,10 @@
onclick={() => handlePromptClick(prompt)} onclick={() => handlePromptClick(prompt)}
> >
<ChatFormPickerItemHeader <ChatFormPickerItemHeader
description={prompt.description}
{server} {server}
{serverLabel} {serverLabel}
title={prompt.title || prompt.name} title={prompt.title || prompt.name}
description={prompt.description}
> >
{#snippet titleExtra()} {#snippet titleExtra()}
{#if prompt.arguments?.length} {#if prompt.arguments?.length}
@@ -440,7 +440,7 @@
{/snippet} {/snippet}
{#snippet skeleton()} {#snippet skeleton()}
<ChatFormPickerListItemSkeleton titleWidth="w-32" showBadge /> <ChatFormPickerListItemSkeleton showBadge titleWidth="w-32" />
{/snippet} {/snippet}
</ChatFormPickerList> </ChatFormPickerList>
{/if} {/if}
@@ -38,20 +38,20 @@
}: Props = $props(); }: Props = $props();
</script> </script>
<form onsubmit={onSubmit} class="space-y-3 pt-4"> <form class="space-y-3 pt-4" onsubmit={onSubmit}>
{#each prompt.arguments ?? [] as arg (arg.name)} {#each prompt.arguments ?? [] as arg (arg.name)}
<ChatFormPromptPickerArgumentInput <ChatFormPromptPickerArgumentInput
argument={arg} argument={arg}
value={promptArgs[arg.name] ?? ''}
suggestions={suggestions[arg.name] ?? []}
isLoadingSuggestions={loadingSuggestions[arg.name] ?? false}
isAutocompleteActive={activeAutocomplete === arg.name}
autocompleteIndex={activeAutocomplete === arg.name ? autocompleteIndex : 0} autocompleteIndex={activeAutocomplete === arg.name ? autocompleteIndex : 0}
onInput={(value) => onArgInput(arg.name, value)} isAutocompleteActive={activeAutocomplete === arg.name}
onKeydown={(e) => onArgKeydown(e, arg.name)} isLoadingSuggestions={loadingSuggestions[arg.name] ?? false}
onBlur={() => onArgBlur(arg.name)} onBlur={() => onArgBlur(arg.name)}
onFocus={() => onArgFocus(arg.name)} onFocus={() => onArgFocus(arg.name)}
onInput={(value) => onArgInput(arg.name, value)}
onKeydown={(e) => onArgKeydown(e, arg.name)}
onSelectSuggestion={(value) => onSelectSuggestion(arg.name, value)} onSelectSuggestion={(value) => onSelectSuggestion(arg.name, value)}
suggestions={suggestions[arg.name] ?? []}
value={promptArgs[arg.name] ?? ''}
/> />
{/each} {/each}
@@ -67,7 +67,7 @@
{/if} {/if}
<div class="mt-8 flex justify-end gap-2"> <div class="mt-8 flex justify-end gap-2">
<Button type="button" size="sm" onclick={onCancel} variant="secondary">Cancel</Button> <Button onclick={onCancel} size="sm" type="button" variant="secondary">Cancel</Button>
<Button size="sm" type="submit">Use Prompt</Button> <Button size="sm" type="submit">Use Prompt</Button>
</div> </div>
@@ -36,7 +36,7 @@
</script> </script>
<div class="relative grid gap-1"> <div class="relative grid gap-1">
<Label for="arg-{argument.name}" class="mb-1 text-muted-foreground"> <Label class="mb-1 text-muted-foreground" for="arg-{argument.name}">
<span> <span>
{argument.name} {argument.name}
@@ -51,30 +51,30 @@
</Label> </Label>
<Input <Input
autocomplete="off"
id="arg-{argument.name}" id="arg-{argument.name}"
type="text"
{value}
oninput={(e) => onInput(e.currentTarget.value)}
onkeydown={onKeydown}
onblur={onBlur} onblur={onBlur}
onfocus={onFocus} onfocus={onFocus}
oninput={(e) => onInput(e.currentTarget.value)}
onkeydown={onKeydown}
placeholder={argument.description || argument.name} placeholder={argument.description || argument.name}
required={argument.required} required={argument.required}
autocomplete="off" type="text"
{value}
/> />
{#if isAutocompleteActive && suggestions.length > 0} {#if isAutocompleteActive && suggestions.length > 0}
<div <div
class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
transition:fly={{ duration: 100, y: -5 }} transition:fly={{ duration: 100, y: -5 }}
class="absolute top-full right-0 left-0 z-10 mt-1 max-h-32 overflow-y-auto rounded-lg border border-border/50 bg-background shadow-lg"
> >
{#each suggestions as suggestion, i (suggestion)} {#each suggestions as suggestion, i (suggestion)}
<button <button
type="button"
onmousedown={() => onSelectSuggestion(suggestion)}
class="w-full px-3 py-1.5 text-left text-sm hover:bg-accent {i === autocompleteIndex class="w-full px-3 py-1.5 text-left text-sm hover:bg-accent {i === autocompleteIndex
? 'bg-accent' ? 'bg-accent'
: ''}" : ''}"
onmousedown={() => onSelectSuggestion(suggestion)}
type="button"
> >
{suggestion} {suggestion}
</button> </button>
@@ -187,10 +187,10 @@
</script> </script>
<Popover.Root <Popover.Root
open={isOpen}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) onClose(); if (!open) onClose();
}} }}
open={isOpen}
> >
<!-- Invisible form-wide trigger: stops bits-ui's outside-click detector <!-- Invisible form-wide trigger: stops bits-ui's outside-click detector
from closing the picker when the user clicks inside the textarea. from closing the picker when the user clicks inside the textarea.
@@ -198,36 +198,36 @@
(tabindex=-1 + pointer-events-none + opacity-0 + aria-hidden). (tabindex=-1 + pointer-events-none + opacity-0 + aria-hidden).
Positioning comes from `customAnchor` at the form's top edge. --> Positioning comes from `customAnchor` at the form's top edge. -->
<Popover.Trigger <Popover.Trigger
aria-hidden="true"
class="pointer-events-none absolute inset-0 opacity-0" class="pointer-events-none absolute inset-0 opacity-0"
tabindex={-1} tabindex={-1}
aria-hidden="true"
> >
<span class="sr-only">Open file mention picker</span> <span class="sr-only">Open file mention picker</span>
</Popover.Trigger> </Popover.Trigger>
<Popover.Content <Popover.Content
align="start" align="start"
side="top"
sideOffset={12}
{customAnchor}
preventScroll={false}
onkeydown={handleKeydown}
onOpenAutoFocus={(event) => event.preventDefault()}
onCloseAutoFocus={(event) => event.preventDefault()}
class={[ class={[
'w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl', 'w-[var(--bits-popover-anchor-width)] max-w-none rounded-xl border-border/50 p-0 shadow-xl',
className className
]} ]}
{customAnchor}
onCloseAutoFocus={(event) => event.preventDefault()}
onOpenAutoFocus={(event) => event.preventDefault()}
onkeydown={handleKeydown}
preventScroll={false}
side="top"
sideOffset={12}
> >
<ChatFormPickerList <ChatFormPickerList
items={displayedItems} {emptyMessage}
isLoading={search.isSearching} isLoading={search.isSearching}
itemKey={(entry) => entry.type + ':' + entry.path}
items={displayedItems}
scrollTrigger={nav.scrollTrigger}
searchQuery={query ?? ''}
selectedIndex={nav.hoveredIndex} selectedIndex={nav.hoveredIndex}
showSearchInput={false} showSearchInput={false}
searchQuery={query ?? ''}
{emptyMessage}
itemKey={(entry) => entry.type + ':' + entry.path}
scrollTrigger={nav.scrollTrigger}
> >
{#snippet item(entry, index, isSelected)} {#snippet item(entry, index, isSelected)}
<ChatFormPickerListItem <ChatFormPickerListItem
@@ -245,6 +245,7 @@
: 'text-muted-foreground' : 'text-muted-foreground'
]} ]}
/> />
<div class="flex min-w-0 flex-1 flex-col"> <div class="flex min-w-0 flex-1 flex-col">
<div class="flex min-w-0 items-center gap-2"> <div class="flex min-w-0 items-center gap-2">
{#if showTooltip} {#if showTooltip}
@@ -254,6 +255,7 @@
<span {...props} class="truncate text-sm font-medium">{entry.name}</span> <span {...props} class="truncate text-sm font-medium">{entry.name}</span>
{/snippet} {/snippet}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content> <Tooltip.Content>
<p>{entry.path}</p> <p>{entry.path}</p>
</Tooltip.Content> </Tooltip.Content>
@@ -261,14 +263,16 @@
{:else} {:else}
<span class="truncate text-sm font-medium">{entry.name}</span> <span class="truncate text-sm font-medium">{entry.name}</span>
{/if} {/if}
<span <span
class="shrink-0 rounded-full bg-muted px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wide text-muted-foreground" class="shrink-0 rounded-full bg-muted px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-wide text-muted-foreground"
> >
{entry.type} {entry.type}
</span> </span>
</div> </div>
<span class="min-w-0 flex-1 truncate font-mono text-left text-xs"> <span class="min-w-0 flex-1 truncate font-mono text-left text-xs">
<HighlightedMatch text={abbreviateHome(entry.path, home)} query={trimmedQuery} /> <HighlightedMatch query={trimmedQuery} text={abbreviateHome(entry.path, home)} />
</span> </span>
</div> </div>
</ChatFormPickerListItem> </ChatFormPickerListItem>
@@ -79,30 +79,30 @@
<ChatFormPickerCommand <ChatFormPickerCommand
bind:this={commandPickerRef} bind:this={commandPickerRef}
isOpen={isCommandPickerOpen ?? false}
query={commandQuery ?? ''}
{commands} {commands}
isOpen={isCommandPickerOpen ?? false}
onClose={onCommandPickerClose ?? (() => {})} onClose={onCommandPickerClose ?? (() => {})}
onSelect={onCommandSelect ?? (() => {})} onSelect={onCommandSelect ?? (() => {})}
query={commandQuery ?? ''}
/> />
<ChatFormPickerMcpPrompts <ChatFormPickerMcpPrompts
bind:this={promptPickerRef} bind:this={promptPickerRef}
isOpen={isPromptPickerOpen} isOpen={isPromptPickerOpen}
searchQuery={promptSearchQuery}
onClose={onPromptPickerClose} onClose={onPromptPickerClose}
{onPromptLoadStart}
{onPromptLoadComplete} {onPromptLoadComplete}
{onPromptLoadError} {onPromptLoadError}
{onPromptLoadStart}
searchQuery={promptSearchQuery}
/> />
<ChatFormPickerMention <ChatFormPickerMention
bind:this={mentionPickerRef} bind:this={mentionPickerRef}
isOpen={isMentionPickerOpen ?? false}
query={mentionQuery ?? ''}
customAnchor={mentionAnchor} customAnchor={mentionAnchor}
scopePath={scopePath ?? null} isOpen={isMentionPickerOpen ?? false}
onClose={onMentionPickerClose ?? (() => {})} onClose={onMentionPickerClose ?? (() => {})}
onOpened={onMentionOpened} onOpened={onMentionOpened}
onSelect={onMentionSelect ?? (() => {})} onSelect={onMentionSelect ?? (() => {})}
query={mentionQuery ?? ''}
scopePath={scopePath ?? null}
/> />
@@ -381,13 +381,13 @@
} }
</script> </script>
<div class="chat-message" class:chat-message--synthetic={isSynthetic}> <div class:chat-message--synthetic={isSynthetic} class="chat-message">
{#if message.role === MessageRole.SYSTEM} {#if message.role === MessageRole.SYSTEM}
<ChatMessageSystem bind:textareaElement class={className} {message} /> <ChatMessageSystem bind:textareaElement class={className} {message} />
{:else if mcpPromptExtra} {:else if mcpPromptExtra}
<ChatMessageMcpPrompt class={className} {message} mcpPrompt={mcpPromptExtra} /> <ChatMessageMcpPrompt class={className} mcpPrompt={mcpPromptExtra} {message} />
{:else if isSynthetic} {:else if isSynthetic}
<ChatMessageSynthetic {message} class={className} /> <ChatMessageSynthetic class={className} {message} />
{:else if message.role === MessageRole.USER} {:else if message.role === MessageRole.USER}
<ChatMessageUser class={className} {isLastUserMessage} {message} {nextAssistantMessage} /> <ChatMessageUser class={className} {isLastUserMessage} {message} {nextAssistantMessage} />
{:else} {:else}
@@ -396,9 +396,9 @@
class={className} class={className}
{isLastAssistantMessage} {isLastAssistantMessage}
{message} {message}
{toolMessages}
onContinue={handleContinue} onContinue={handleContinue}
onRegenerate={handleRegenerate} onRegenerate={handleRegenerate}
{toolMessages}
/> />
{/if} {/if}
</div> </div>
@@ -126,16 +126,16 @@
<div <div
bind:this={assistantEl} bind:this={assistantEl}
class="chat-message-assistant text-md group w-full leading-7.5 {className}" style:--assistant-margin-top={assistantMarginTop > 0 ? `${assistantMarginTop}px` : undefined}
style:--last-user-message-height={lastUserMessageHeight > 0 style:--last-user-message-height={lastUserMessageHeight > 0
? `${lastUserMessageHeight}px` ? `${lastUserMessageHeight}px`
: undefined} : undefined}
style:--assistant-margin-top={assistantMarginTop > 0 ? `${assistantMarginTop}px` : undefined}
role="group"
aria-label="Assistant message with actions" aria-label="Assistant message with actions"
class="chat-message-assistant text-md group w-full leading-7.5 {className}"
role="group"
> >
{#if showProcessingInfoTop} {#if showProcessingInfoTop}
<ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="top" /> <ChatMessageAssistantProcessingInfo {modelLoadingText} position="top" {processingState} />
{/if} {/if}
{#if editCtx.isEditing} {#if editCtx.isEditing}
@@ -145,16 +145,16 @@
<ChatMessageAssistantRawOutput {message} {toolMessages} /> <ChatMessageAssistantRawOutput {message} {toolMessages} />
{:else} {:else}
<ChatMessageAgenticContent <ChatMessageAgenticContent
{isLastAssistantMessage}
isStreaming={chatStore.isStreaming()}
{message} {message}
{toolMessages} {toolMessages}
isStreaming={chatStore.isStreaming()}
{isLastAssistantMessage}
/> />
{/if} {/if}
{/if} {/if}
{#if showProcessingInfoBottom} {#if showProcessingInfoBottom}
<ChatMessageAssistantProcessingInfo {modelLoadingText} {processingState} position="bottom" /> <ChatMessageAssistantProcessingInfo {modelLoadingText} position="bottom" {processingState} />
{/if} {/if}
{#if displayedModel} {#if displayedModel}
@@ -168,8 +168,8 @@
/> />
<ChatMessageAssistantStatistics <ChatMessageAssistantStatistics
{message}
isLoading={chatStore.isLoading} isLoading={chatStore.isLoading}
{message}
{processingState} {processingState}
showMessageStats={currentConfig.showMessageStats} showMessageStats={currentConfig.showMessageStats}
/> />
@@ -179,14 +179,14 @@
{#if message.timestamp && !editCtx.isEditing} {#if message.timestamp && !editCtx.isEditing}
<ChatMessageActionIcons <ChatMessageActionIcons
role={MessageRole.ASSISTANT}
justify="start"
actionsPosition="left" actionsPosition="left"
{onRegenerate} justify="start"
onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined} onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
showRawOutputSwitch={currentConfig.showRawOutputSwitch}
rawOutputEnabled={showRawOutput}
onRawOutputToggle={(enabled) => (showRawOutput = enabled)} onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
{onRegenerate}
rawOutputEnabled={showRawOutput}
role={MessageRole.ASSISTANT}
showRawOutputSwitch={currentConfig.showRawOutputSwitch}
/> />
{/if} {/if}
</div> </div>
@@ -13,7 +13,7 @@
const marginClass = $derived(position === 'top' ? 'mt-6' : 'mt-4'); const marginClass = $derived(position === 'top' ? 'mt-6' : 'mt-4');
</script> </script>
<div class="{marginClass} w-full max-w-3xl" in:fade> <div in:fade class="{marginClass} w-full max-w-3xl">
<div class="flex flex-col items-start gap-2"> <div class="flex flex-col items-start gap-2">
<span class="shimmer-text text-sm"> <span class="shimmer-text text-sm">
{modelLoadingText ?? {modelLoadingText ??
@@ -24,22 +24,22 @@
{#if showMessageStats && isLiveFlowRoot && liveLlm} {#if showMessageStats && isLiveFlowRoot && liveLlm}
<ChatMessageStatistics <ChatMessageStatistics
mode={ChatMessageStatisticsMode.GENERATION}
isLive isLive
promptTokens={liveLlm.prompt_n} mode={ChatMessageStatisticsMode.GENERATION}
promptMs={liveLlm.prompt_ms}
predictedTokens={liveLlm.predicted_n}
predictedMs={liveLlm.predicted_ms} predictedMs={liveLlm.predicted_ms}
predictedTokens={liveLlm.predicted_n}
promptMs={liveLlm.prompt_ms}
promptTokens={liveLlm.prompt_n}
/> />
{:else if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms} {:else if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
{@const agentic = message.timings.agentic} {@const agentic = message.timings.agentic}
<ChatMessageStatistics <ChatMessageStatistics
mode={ChatMessageStatisticsMode.GENERATION}
promptTokens={agentic ? agentic.llm.prompt_n : message.timings.prompt_n}
promptMs={agentic ? agentic.llm.prompt_ms : message.timings.prompt_ms}
predictedTokens={agentic ? agentic.llm.predicted_n : message.timings.predicted_n}
predictedMs={agentic ? agentic.llm.predicted_ms : message.timings.predicted_ms}
agenticTimings={agentic} agenticTimings={agentic}
mode={ChatMessageStatisticsMode.GENERATION}
predictedMs={agentic ? agentic.llm.predicted_ms : message.timings.predicted_ms}
predictedTokens={agentic ? agentic.llm.predicted_n : message.timings.predicted_n}
promptMs={agentic ? agentic.llm.prompt_ms : message.timings.prompt_ms}
promptTokens={agentic ? agentic.llm.prompt_n : message.timings.prompt_n}
/> />
{:else if isLoading && showMessageStats} {:else if isLoading && showMessageStats}
{@const liveStats = processingState.getLiveProcessingStats()} {@const liveStats = processingState.getLiveProcessingStats()}
@@ -47,12 +47,12 @@
{#if genStats} {#if genStats}
<ChatMessageStatistics <ChatMessageStatistics
mode={ChatMessageStatisticsMode.GENERATION}
isLive isLive
promptTokens={liveStats?.tokensProcessed} mode={ChatMessageStatisticsMode.GENERATION}
promptMs={liveStats?.timeMs}
predictedTokens={genStats.tokensGenerated}
predictedMs={genStats.timeMs} predictedMs={genStats.timeMs}
predictedTokens={genStats.tokensGenerated}
promptMs={liveStats?.timeMs}
promptTokens={liveStats?.tokensProcessed}
/> />
{/if} {/if}
{/if} {/if}
@@ -19,10 +19,13 @@
<div class="text-muted-foreground flex items-center gap-2 py-1.5 {className}"> <div class="text-muted-foreground flex items-center gap-2 py-1.5 {className}">
{#if info.path === null} {#if info.path === null}
<FolderX class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" /> <FolderX class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
<span class="text-foreground/80 text-sm font-medium">Working directory cleared</span> <span class="text-foreground/80 text-sm font-medium">Working directory cleared</span>
{:else} {:else}
<Folder class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" /> <Folder class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
<span class="text-foreground/80 text-sm font-medium">Set working directory to&nbsp;</span> <span class="text-foreground/80 text-sm font-medium">Set working directory to&nbsp;</span>
<span class="font-mono text-foreground/90 text-sm break-all" title={info.path}> <span class="font-mono text-foreground/90 text-sm break-all" title={info.path}>
{info.display} {info.display}
</span> </span>
@@ -29,9 +29,9 @@
<ChatMessageEditForm /> <ChatMessageEditForm />
{:else} {:else}
<ChatMessageMcpPromptContent <ChatMessageMcpPromptContent
class="w-full max-w-[80%]"
prompt={mcpPrompt} prompt={mcpPrompt}
variant={McpPromptVariant.MESSAGE} variant={McpPromptVariant.MESSAGE}
class="w-full max-w-[80%]"
/> />
{#if message.timestamp} {#if message.timestamp}
@@ -99,12 +99,12 @@
<Tooltip.Trigger> <Tooltip.Trigger>
{#if serverFavicon} {#if serverFavicon}
<img <img
src={serverFavicon}
alt="" alt=""
class="h-3.5 w-3.5 shrink-0 rounded-sm" class="h-3.5 w-3.5 shrink-0 rounded-sm"
onerror={(e) => { onerror={(e) => {
(e.currentTarget as HTMLImageElement).style.display = 'none'; (e.currentTarget as HTMLImageElement).style.display = 'none';
}} }}
src={serverFavicon}
/> />
{/if} {/if}
</Tooltip.Trigger> </Tooltip.Trigger>
@@ -17,7 +17,7 @@
</script> </script>
{#if isCwdChange} {#if isCwdChange}
<ChatMessageCwdChange {message} class={className} /> <ChatMessageCwdChange class={className} {message} />
{:else} {:else}
<span class="text-muted-foreground block text-sm {className}">{message.content}</span> <span class="text-muted-foreground block text-sm {className}">{message.content}</span>
{/if} {/if}
@@ -83,16 +83,16 @@
{#if editCtx.isEditing} {#if editCtx.isEditing}
<div class="w-full max-w-[80%]"> <div class="w-full max-w-[80%]">
<textarea <textarea
style="max-height: var(--max-message-height);"
bind:this={textareaElement} bind:this={textareaElement}
value={editCtx.editedContent}
class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}" class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
onkeydown={handleEditKeydown}
oninput={(e) => { oninput={(e) => {
autoResizeTextarea(e.currentTarget); autoResizeTextarea(e.currentTarget);
editCtx.setContent(e.currentTarget.value); editCtx.setContent(e.currentTarget.value);
}} }}
onkeydown={handleEditKeydown}
placeholder="Edit system message..." placeholder="Edit system message..."
style="max-height: var(--max-message-height);"
value={editCtx.editedContent}
></textarea> ></textarea>
<div class="mt-2 flex justify-end gap-2"> <div class="mt-2 flex justify-end gap-2">
@@ -104,8 +104,8 @@
<Button <Button
class="h-8 px-3" class="h-8 px-3"
onclick={editCtx.save}
disabled={!editCtx.editedContent.trim()} disabled={!editCtx.editedContent.trim()}
onclick={editCtx.save}
size="sm" size="sm"
> >
<Check class="mr-1 h-3 w-3" /> <Check class="mr-1 h-3 w-3" />
@@ -34,34 +34,34 @@
</script> </script>
{#if isSearchCall} {#if isSearchCall}
<ChatMessageToolCallBlockSearchResults {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockSearchResults {isStreaming} {onToggle} {open} {section} />
{:else if section.toolName === BuiltInTool.BROWSER_GET_DATETIME} {:else if section.toolName === BuiltInTool.BROWSER_GET_DATETIME}
<ChatMessageToolCallBlockGetDatetime {section} {isStreaming} /> <ChatMessageToolCallBlockGetDatetime {isStreaming} {section} />
{:else if section.toolName === BuiltInTool.SERVER_GET_INFO} {:else if section.toolName === BuiltInTool.SERVER_GET_INFO}
<ChatMessageToolCallBlockGetInfo {section} {isStreaming} /> <ChatMessageToolCallBlockGetInfo {isStreaming} {section} />
{:else if section.toolName === BuiltInTool.SERVER_READ_FILE} {:else if section.toolName === BuiltInTool.SERVER_READ_FILE}
<ChatMessageToolCallBlockReadFile {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockReadFile {isStreaming} {onToggle} {open} {section} />
{:else if section.toolName === BuiltInTool.BROWSER_READ_MEDIA} {:else if section.toolName === BuiltInTool.BROWSER_READ_MEDIA}
<ChatMessageToolCallBlockReadMedia {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockReadMedia {isStreaming} {onToggle} {open} {section} />
{:else if section.toolName === BuiltInTool.SERVER_EDIT_FILE} {:else if section.toolName === BuiltInTool.SERVER_EDIT_FILE}
<ChatMessageToolCallBlockEditFile {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockEditFile {isStreaming} {onToggle} {open} {section} />
{:else if section.toolName === BuiltInTool.SERVER_WRITE_FILE} {:else if section.toolName === BuiltInTool.SERVER_WRITE_FILE}
<ChatMessageToolCallBlockWriteFile {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockWriteFile {isStreaming} {onToggle} {open} {section} />
{:else if section.toolName === BuiltInTool.SERVER_EXEC_SHELL_COMMAND} {:else if section.toolName === BuiltInTool.SERVER_EXEC_SHELL_COMMAND}
<ChatMessageToolCallBlockExecShellCommand <ChatMessageToolCallBlockExecShellCommand
{section}
{open}
{isStreaming}
{isExecuting}
{attachments} {attachments}
{isExecuting}
{isStreaming}
{onToggle} {onToggle}
{open}
{section}
/> />
{:else if section.toolName === BuiltInTool.SERVER_FILE_GLOB_SEARCH} {:else if section.toolName === BuiltInTool.SERVER_FILE_GLOB_SEARCH}
<ChatMessageToolCallBlockFileGlobSearch {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockFileGlobSearch {isStreaming} {onToggle} {open} {section} />
{:else if section.toolName === BuiltInTool.SERVER_GREP_SEARCH} {:else if section.toolName === BuiltInTool.SERVER_GREP_SEARCH}
<ChatMessageToolCallBlockGrepSearch {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockGrepSearch {isStreaming} {onToggle} {open} {section} />
{:else if section.toolName === BuiltInTool.BROWSER_RUN_JAVASCRIPT} {:else if section.toolName === BuiltInTool.BROWSER_RUN_JAVASCRIPT}
<ChatMessageToolCallBlockRunJavascript {section} {open} {isStreaming} {onToggle} /> <ChatMessageToolCallBlockRunJavascript {isStreaming} {onToggle} {open} {section} />
{:else} {:else}
<ChatMessageToolCallBlockDefault {section} {open} {isStreaming} {attachments} {onToggle} /> <ChatMessageToolCallBlockDefault {attachments} {isStreaming} {onToggle} {open} {section} />
{/if} {/if}
@@ -34,15 +34,17 @@
); );
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={null} {title} {onToggle}> <ToolCallBlock {isStreaming} meta={null} {onToggle} {open} {section} {title}>
{#snippet children(_meta, ctx)} {#snippet children(_meta, ctx)}
{#if ctx.isStreamingCall} {#if ctx.isStreamingCall}
<div class="mb-2 flex items-center gap-2 text-xs text-muted-foreground/70"> <div class="mb-2 flex items-center gap-2 text-xs text-muted-foreground/70">
<span>Input</span> <span>Input</span>
{#if ctx.isStreaming} {#if ctx.isStreaming}
<Loader2 class="h-3 w-3 animate-spin" /> <Loader2 class="h-3 w-3 animate-spin" />
{/if} {/if}
</div> </div>
{#if section.toolArgs} {#if section.toolArgs}
<SyntaxHighlightedCode <SyntaxHighlightedCode
code={formatJsonPretty(section.toolArgs)} code={formatJsonPretty(section.toolArgs)}
@@ -67,6 +69,7 @@
<div class="mb-1.5 flex items-center gap-2 text-xs text-muted-foreground/70"> <div class="mb-1.5 flex items-center gap-2 text-xs text-muted-foreground/70">
<span>Input</span> <span>Input</span>
</div> </div>
<SyntaxHighlightedCode <SyntaxHighlightedCode
code={formatJsonPretty(section.toolArgs ?? '')} code={formatJsonPretty(section.toolArgs ?? '')}
language={FileTypeText.JSON} language={FileTypeText.JSON}
@@ -74,16 +77,19 @@
streaming={ctx.isCodeStreaming} streaming={ctx.isCodeStreaming}
/> />
{/if} {/if}
<div <div
class={showInput class={showInput
? 'mt-4 mb-1.5 flex items-center gap-2 text-xs text-muted-foreground/70' ? 'mt-4 mb-1.5 flex items-center gap-2 text-xs text-muted-foreground/70'
: 'mb-1.5 flex items-center gap-2 text-xs text-muted-foreground/70'} : 'mb-1.5 flex items-center gap-2 text-xs text-muted-foreground/70'}
> >
<span>Output</span> <span>Output</span>
{#if ctx.isPending} {#if ctx.isPending}
<Loader2 class="h-3 w-3 animate-spin" /> <Loader2 class="h-3 w-3 animate-spin" />
{/if} {/if}
</div> </div>
{#if ctx.isPending} {#if ctx.isPending}
<div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic"> <div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic">
Waiting for result... Waiting for result...
@@ -96,18 +102,19 @@
maxHeight={MAX_HEIGHT_CODE_BLOCK} maxHeight={MAX_HEIGHT_CODE_BLOCK}
/> />
{:else if outputKind === ToolResultKind.MARKDOWN} {:else if outputKind === ToolResultKind.MARKDOWN}
<MarkdownContent content={section.toolResult} {attachments} /> <MarkdownContent {attachments} content={section.toolResult} />
{:else} {:else}
<div class="overflow-auto"> <div class="overflow-auto">
{#each parsedLines as line, i (i)} {#each parsedLines as line, i (i)}
<div class="font-mono text-[11px] leading-relaxed whitespace-pre-wrap"> <div class="font-mono text-[11px] leading-relaxed whitespace-pre-wrap">
{line.text} {line.text}
</div> </div>
{#if line.media} {#if line.media}
{#if line.media.type === AttachmentType.AUDIO} {#if line.media.type === AttachmentType.AUDIO}
{@const audioMimeType = line.media.mimeType ?? MimeTypeAudio.MP3_MPEG} {@const audioMimeType = line.media.mimeType ?? MimeTypeAudio.MP3_MPEG}
<div class="mt-2 mb-2"> <div class="mt-2 mb-2">
<audio controls class="w-full rounded-lg"> <audio class="w-full rounded-lg" controls>
<source <source
src={createBase64DataUrl(audioMimeType, line.media.base64Data)} src={createBase64DataUrl(audioMimeType, line.media.base64Data)}
type={audioMimeType} type={audioMimeType}
@@ -117,10 +124,10 @@
</div> </div>
{:else} {:else}
<img <img
src={line.media.base64Url}
alt={line.media.name} alt={line.media.name}
class="mt-2 mb-2 h-auto max-w-full rounded-lg" class="mt-2 mb-2 h-auto max-w-full rounded-lg"
loading="lazy" loading="lazy"
src={line.media.base64Url}
/> />
{/if} {/if}
{/if} {/if}
@@ -23,12 +23,14 @@
); );
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={editFileMeta} {onToggle}> <ToolCallBlock {isStreaming} meta={editFileMeta} {onToggle} {open} {section}>
{#snippet titleSnippet()} {#snippet titleSnippet()}
<span class="text-muted-foreground">Edit file </span> <span class="text-muted-foreground">Edit file </span>
<span class="font-mono" title={editFileMeta?.filePath} <span class="font-mono" title={editFileMeta?.filePath}
>{abbreviateHome(editFileMeta?.filePath ?? '', home)}</span >{abbreviateHome(editFileMeta?.filePath ?? '', home)}</span
> >
{#if editFileMeta?.errorMessage} {#if editFileMeta?.errorMessage}
<span class="ml-1 text-xs italic text-muted-foreground/70">(failed)</span> <span class="ml-1 text-xs italic text-muted-foreground/70">(failed)</span>
{/if} {/if}
@@ -40,6 +42,7 @@
class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400" class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400"
> >
<XCircle class="mt-0.5 h-3 w-3 shrink-0" /> <XCircle class="mt-0.5 h-3 w-3 shrink-0" />
<span>{meta.errorMessage}</span> <span>{meta.errorMessage}</span>
</div> </div>
{:else if meta && meta.edits.length > 0} {:else if meta && meta.edits.length > 0}
@@ -48,13 +51,17 @@
<div class="mb-1.5 text-xs text-muted-foreground/70 italic"> <div class="mb-1.5 text-xs text-muted-foreground/70 italic">
Edit {ei + 1}&nbsp;of&nbsp;{meta.edits.length} Edit {ei + 1}&nbsp;of&nbsp;{meta.edits.length}
</div> </div>
<div class="diff-block" style:max-height={MAX_HEIGHT_CODE_BLOCK}>
<div style:max-height={MAX_HEIGHT_CODE_BLOCK} class="diff-block">
<div class="diff-pre"> <div class="diff-pre">
{#each diffLines as line, li (li)} {#each diffLines as line, li (li)}
<div class="diff-line diff-{line.kind}"> <div class="diff-line diff-{line.kind}">
<span class="diff-old-num">{line.oldLine ?? ''}</span> <span class="diff-old-num">{line.oldLine ?? ''}</span>
<span class="diff-marker">{prefixFor(line.kind)}</span> <span class="diff-marker">{prefixFor(line.kind)}</span>
<span class="diff-new-num">{line.newLine ?? ''}</span> <span class="diff-new-num">{line.newLine ?? ''}</span>
<span class="diff-text">{line.text || ' '}</span> <span class="diff-text">{line.text || ' '}</span>
</div> </div>
{/each} {/each}
@@ -62,9 +69,11 @@
</div> </div>
</div> </div>
{/each} {/each}
<div class="mt-1.5 text-xs text-muted-foreground/70 italic"> <div class="mt-1.5 text-xs text-muted-foreground/70 italic">
{#if meta.resultMessage} {#if meta.resultMessage}
{meta.resultMessage}{meta.editsApplied != null ? RESULT_STAT_SEPARATOR : ''}{/if} {meta.resultMessage}{meta.editsApplied != null ? RESULT_STAT_SEPARATOR : ''}{/if}
{#if meta.editsApplied != null} {#if meta.editsApplied != null}
<span class="font-mono">{meta.editsApplied}</span> <span class="font-mono">{meta.editsApplied}</span>
{meta.editsApplied === 1 ? 'edit' : 'edits'}&nbsp;applied {meta.editsApplied === 1 ? 'edit' : 'edits'}&nbsp;applied
@@ -176,6 +176,7 @@
{#snippet execShellTitle()} {#snippet execShellTitle()}
{#if cwd} {#if cwd}
<span class="exec-wd" title={cwd}>{wdDisplay}</span> <span class="exec-wd" title={cwd}>{wdDisplay}</span>
<span class="exec-prompt">$</span> <span class="exec-prompt">$</span>
{/if} {/if}
@@ -187,14 +188,14 @@
{/snippet} {/snippet}
<ToolCallBlock <ToolCallBlock
{section} extraLiveStreaming={isLive}
{open}
{isStreaming} {isStreaming}
meta={execShellMeta ? { errorMessage: execShellError } : null} meta={execShellMeta ? { errorMessage: execShellError } : null}
wrapper={CollapsibleTerminalBlock}
extraLiveStreaming={isLive}
spinIconWhenActive={true}
{onToggle} {onToggle}
{open}
{section}
spinIconWhenActive={true}
wrapper={CollapsibleTerminalBlock}
> >
{#snippet titleSnippet()} {#snippet titleSnippet()}
{@render execShellTitle()} {@render execShellTitle()}
@@ -209,23 +210,25 @@
{:else if execShellError} {:else if execShellError}
<div class="flex items-start gap-2 text-xs text-red-600 italic dark:text-red-400"> <div class="flex items-start gap-2 text-xs text-red-600 italic dark:text-red-400">
<XCircle class="mt-0.5 h-3 w-3 shrink-0" /> <XCircle class="mt-0.5 h-3 w-3 shrink-0" />
<span>{execShellError}</span> <span>{execShellError}</span>
</div> </div>
{:else if section.toolResult} {:else if section.toolResult}
<div <div
bind:this={scrollEl} bind:this={scrollEl}
class="terminal-output"
class:is-clamped={!useFullHeightCodeBlocks} class:is-clamped={!useFullHeightCodeBlocks}
class="terminal-output"
onscroll={handleScrollEvent} onscroll={handleScrollEvent}
> >
{#each outputLines as line, i (i)} {#each outputLines as line, i (i)}
<div class="font-mono text-[11px] leading-relaxed whitespace-pre-wrap">{line.text}</div> <div class="font-mono text-[11px] leading-relaxed whitespace-pre-wrap">{line.text}</div>
{#if line.media?.type === AttachmentType.IMAGE} {#if line.media?.type === AttachmentType.IMAGE}
<img <img
src={line.media.base64Url}
alt={line.media.name} alt={line.media.name}
class="mt-2 mb-2 h-auto max-w-full rounded-lg" class="mt-2 mb-2 h-auto max-w-full rounded-lg"
loading="lazy" loading="lazy"
src={line.media.base64Url}
/> />
{/if} {/if}
{/each} {/each}
@@ -234,14 +237,19 @@
<div class={exitBadgeClass}> <div class={exitBadgeClass}>
{#if execShellExitStatus.timedOut} {#if execShellExitStatus.timedOut}
<AlertTriangle class="h-3 w-3" /> <AlertTriangle class="h-3 w-3" />
<span>timed out</span> <span>timed out</span>
<span class="exit-sep">&middot;</span> <span class="exit-sep">&middot;</span>
<span>exit {execShellExitStatus.code}</span> <span>exit {execShellExitStatus.code}</span>
{:else if execShellExitStatus.code === 0} {:else if execShellExitStatus.code === 0}
<Check class="h-3 w-3" /> <Check class="h-3 w-3" />
<span>exit 0</span> <span>exit 0</span>
{:else} {:else}
<XCircle class="h-3 w-3" /> <XCircle class="h-3 w-3" />
<span>exit {execShellExitStatus.code}</span> <span>exit {execShellExitStatus.code}</span>
{/if} {/if}
</div> </div>
@@ -19,16 +19,19 @@
const home = $derived(toolsStore.serverHome); const home = $derived(toolsStore.serverHome);
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={fileGlobMeta} {onToggle}> <ToolCallBlock {isStreaming} meta={fileGlobMeta} {onToggle} {open} {section}>
{#snippet titleSnippet()} {#snippet titleSnippet()}
{#if fileGlobMeta} {#if fileGlobMeta}
<span class="text-muted-foreground" <span class="text-muted-foreground"
>{fileGlobMeta.include === '**' ? 'List files' : 'Search files'}&nbsp;</span >{fileGlobMeta.include === '**' ? 'List files' : 'Search files'}&nbsp;</span
> >
{#if fileGlobMeta.include !== '**'} {#if fileGlobMeta.include !== '**'}
<span class="font-mono">{fileGlobMeta.include}</span> <span class="font-mono">{fileGlobMeta.include}</span>
{/if} {/if}
<span class="text-muted-foreground">&nbsp;in&nbsp;</span> <span class="text-muted-foreground">&nbsp;in&nbsp;</span>
<span class="font-mono" title={fileGlobMeta.path} <span class="font-mono" title={fileGlobMeta.path}
>{abbreviateHome(fileGlobMeta.path, home)}</span >{abbreviateHome(fileGlobMeta.path, home)}</span
> >
@@ -45,6 +48,7 @@
class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400" class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400"
> >
<XCircle class="mt-0.5 h-3 w-3 shrink-0" /> <XCircle class="mt-0.5 h-3 w-3 shrink-0" />
<span>{meta.errorMessage}</span> <span>{meta.errorMessage}</span>
</div> </div>
{:else if meta && meta.matches.length > 0} {:else if meta && meta.matches.length > 0}
@@ -53,11 +57,13 @@
<div class="font-mono text-[11px] leading-relaxed whitespace-pre-wrap">{match}</div> <div class="font-mono text-[11px] leading-relaxed whitespace-pre-wrap">{match}</div>
{/each} {/each}
</div> </div>
<div class="mt-1.5 text-xs text-muted-foreground/70 italic"> <div class="mt-1.5 text-xs text-muted-foreground/70 italic">
Total matches: <span class="font-mono">{meta.totalMatches ?? meta.matches.length}</span> Total matches: <span class="font-mono">{meta.totalMatches ?? meta.matches.length}</span>
</div> </div>
{:else} {:else}
<div class="text-xs text-muted-foreground/70 italic">No matches</div> <div class="text-xs text-muted-foreground/70 italic">No matches</div>
<div class="mt-1.5 text-xs text-muted-foreground/70 italic"> <div class="mt-1.5 text-xs text-muted-foreground/70 italic">
Total matches: <span class="font-mono">{meta?.totalMatches ?? 0}</span> Total matches: <span class="font-mono">{meta?.totalMatches ?? 0}</span>
</div> </div>
@@ -44,15 +44,19 @@
<div class="text-muted-foreground flex items-center gap-2 py-1.5"> <div class="text-muted-foreground flex items-center gap-2 py-1.5">
<Clock class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" /> <Clock class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
{#if showSpinner} {#if showSpinner}
<span class="text-foreground/80 text-sm font-medium">Current time</span> <span class="text-foreground/80 text-sm font-medium">Current time</span>
<Loader2 class="text-muted-foreground/70 h-3 w-3 animate-spin" /> <Loader2 class="text-muted-foreground/70 h-3 w-3 animate-spin" />
{:else if dateMeta.errorMessage} {:else if dateMeta.errorMessage}
<span class="text-foreground/80 text-sm font-medium">Current time&nbsp;</span> <span class="text-foreground/80 text-sm font-medium">Current time&nbsp;</span>
<span class="text-red-600 text-xs italic dark:text-red-400">-&nbsp;{dateMeta.errorMessage}</span <span class="text-red-600 text-xs italic dark:text-red-400">-&nbsp;{dateMeta.errorMessage}</span
> >
{:else if dateMeta.dateString} {:else if dateMeta.dateString}
<span class="text-foreground/80 text-sm font-medium">Current time is&nbsp;</span> <span class="text-foreground/80 text-sm font-medium">Current time is&nbsp;</span>
<span class="font-mono text-foreground/90 text-sm">{dateMeta.dateString}</span> <span class="font-mono text-foreground/90 text-sm">{dateMeta.dateString}</span>
{:else} {:else}
<span class="text-foreground/80 text-sm font-medium">Current time</span> <span class="text-foreground/80 text-sm font-medium">Current time</span>
@@ -52,18 +52,23 @@
<div class="text-muted-foreground flex items-center gap-2 py-1.5"> <div class="text-muted-foreground flex items-center gap-2 py-1.5">
<Info class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" /> <Info class="text-muted-foreground/60 h-3.5 w-3.5 shrink-0" />
{#if showSpinner} {#if showSpinner}
<span class="text-foreground/80 text-sm font-medium">Runtime info</span> <span class="text-foreground/80 text-sm font-medium">Runtime info</span>
<Loader2 class="text-muted-foreground/70 h-3 w-3 animate-spin" /> <Loader2 class="text-muted-foreground/70 h-3 w-3 animate-spin" />
{:else if infoMeta.errorMessage} {:else if infoMeta.errorMessage}
<span class="text-foreground/80 text-sm font-medium">Runtime info&nbsp;</span> <span class="text-foreground/80 text-sm font-medium">Runtime info&nbsp;</span>
<span class="text-red-600 text-xs italic dark:text-red-400">-&nbsp;{infoMeta.errorMessage}</span <span class="text-red-600 text-xs italic dark:text-red-400">-&nbsp;{infoMeta.errorMessage}</span
> >
{:else if infoMeta.os || infoMeta.cwd} {:else if infoMeta.os || infoMeta.cwd}
<span class="text-foreground/80 text-sm font-medium">Runtime info&nbsp;</span> <span class="text-foreground/80 text-sm font-medium">Runtime info&nbsp;</span>
{#if infoMeta.os} {#if infoMeta.os}
<span class="font-mono text-foreground/90 text-sm">{infoMeta.os}</span> <span class="font-mono text-foreground/90 text-sm">{infoMeta.os}</span>
{/if} {/if}
{#if infoMeta.cwd} {#if infoMeta.cwd}
<span class="font-mono text-foreground/90 text-sm" title={infoMeta.cwd}>{cwdDisplay}</span> <span class="font-mono text-foreground/90 text-sm" title={infoMeta.cwd}>{cwdDisplay}</span>
{/if} {/if}
@@ -19,12 +19,15 @@
const home = $derived(toolsStore.serverHome); const home = $derived(toolsStore.serverHome);
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={grepMeta} {onToggle}> <ToolCallBlock {isStreaming} meta={grepMeta} {onToggle} {open} {section}>
{#snippet titleSnippet()} {#snippet titleSnippet()}
{#if grepMeta} {#if grepMeta}
<span class="text-muted-foreground">Search for&nbsp;</span> <span class="text-muted-foreground">Search for&nbsp;</span>
<span class="font-mono">{grepMeta.pattern}</span> <span class="font-mono">{grepMeta.pattern}</span>
<span class="text-muted-foreground">&nbsp;in&nbsp;</span> <span class="text-muted-foreground">&nbsp;in&nbsp;</span>
<span class="font-mono" title={grepMeta.path}>{abbreviateHome(grepMeta.path, home)}</span> <span class="font-mono" title={grepMeta.path}>{abbreviateHome(grepMeta.path, home)}</span>
{/if} {/if}
{/snippet} {/snippet}
@@ -39,6 +42,7 @@
class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400" class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400"
> >
<XCircle class="mt-0.5 h-3 w-3 shrink-0" /> <XCircle class="mt-0.5 h-3 w-3 shrink-0" />
<span>{meta.errorMessage}</span> <span>{meta.errorMessage}</span>
</div> </div>
{:else if meta && meta.matches.length > 0} {:else if meta && meta.matches.length > 0}
@@ -46,22 +50,28 @@
{#each meta.matches as match, mi (mi)} {#each meta.matches as match, mi (mi)}
<div class="font-mono text-[11px] leading-relaxed"> <div class="font-mono text-[11px] leading-relaxed">
<span class="text-muted-foreground/70">{match.file}</span> <span class="text-muted-foreground/70">{match.file}</span>
{#if meta.showLineNumbers && match.line != null} {#if meta.showLineNumbers && match.line != null}
<span class="text-muted-foreground/70">:{match.line}</span> <span class="text-muted-foreground/70">:{match.line}</span>
{/if} {/if}
<span class="text-muted-foreground/70">:</span> <span class="text-muted-foreground/70">:</span>
<span>{match.content}</span> <span>{match.content}</span>
</div> </div>
{/each} {/each}
</div> </div>
<div class="mt-1.5 text-xs text-muted-foreground/70 italic"> <div class="mt-1.5 text-xs text-muted-foreground/70 italic">
Total matches: <span class="font-mono">{meta.totalMatches ?? meta.matches.length}</span> Total matches: <span class="font-mono">{meta.totalMatches ?? meta.matches.length}</span>
{#if meta.showLineNumbers} {#if meta.showLineNumbers}
&nbsp;<span class="italic">(with line numbers)</span> &nbsp;<span class="italic">(with line numbers)</span>
{/if} {/if}
</div> </div>
{:else} {:else}
<div class="text-xs text-muted-foreground/70 italic">No matches</div> <div class="text-xs text-muted-foreground/70 italic">No matches</div>
<div class="mt-1.5 text-xs text-muted-foreground/70 italic"> <div class="mt-1.5 text-xs text-muted-foreground/70 italic">
Total matches: <span class="font-mono">{meta?.totalMatches ?? 0}</span> Total matches: <span class="font-mono">{meta?.totalMatches ?? 0}</span>
</div> </div>
@@ -17,10 +17,12 @@
const readFileMeta = $derived(parseReadFileMeta(section)); const readFileMeta = $derived(parseReadFileMeta(section));
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={readFileMeta} {onToggle}> <ToolCallBlock {isStreaming} meta={readFileMeta} {onToggle} {open} {section}>
{#snippet titleSnippet()} {#snippet titleSnippet()}
<span class="text-muted-foreground">Read file </span> <span class="text-muted-foreground">Read file </span>
<span class="font-mono">{readFileMeta?.fileName}</span> <span class="font-mono">{readFileMeta?.fileName}</span>
{#if readFileMeta?.lineRange} {#if readFileMeta?.lineRange}
<span class="text-muted-foreground" <span class="text-muted-foreground"
>&nbsp;(lines {readFileMeta.lineRange.start}-{readFileMeta.lineRange.end})</span >&nbsp;(lines {readFileMeta.lineRange.start}-{readFileMeta.lineRange.end})</span
@@ -43,9 +43,10 @@
const audioMimeType = $derived(readMediaMeta?.mimeType ?? MimeTypeAudio.MP3_MPEG); const audioMimeType = $derived(readMediaMeta?.mimeType ?? MimeTypeAudio.MP3_MPEG);
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={readMediaMeta} {onToggle}> <ToolCallBlock {isStreaming} meta={readMediaMeta} {onToggle} {open} {section}>
{#snippet titleSnippet()} {#snippet titleSnippet()}
<span class="text-muted-foreground">Read media </span> <span class="text-muted-foreground">Read media </span>
<span class="font-mono">{readMediaMeta?.fileName}</span> <span class="font-mono">{readMediaMeta?.fileName}</span>
{/snippet} {/snippet}
@@ -57,7 +58,7 @@
</div> </div>
{:else if mediaAttachment.type === AttachmentType.AUDIO} {:else if mediaAttachment.type === AttachmentType.AUDIO}
<div class="mt-2"> <div class="mt-2">
<audio controls class="w-full rounded-lg"> <audio class="w-full rounded-lg" controls>
<source <source
src={createBase64DataUrl(audioMimeType, mediaAttachment.base64Data)} src={createBase64DataUrl(audioMimeType, mediaAttachment.base64Data)}
type={audioMimeType} type={audioMimeType}
@@ -68,10 +69,10 @@
{:else} {:else}
<div class="mt-2"> <div class="mt-2">
<img <img
src={mediaAttachment.base64Url}
alt={readMediaMeta?.fileName ?? 'media'} alt={readMediaMeta?.fileName ?? 'media'}
class="max-h-[60vh] max-w-full rounded-lg object-contain shadow-lg" class="max-h-[60vh] max-w-full rounded-lg object-contain shadow-lg"
loading="lazy" loading="lazy"
src={mediaAttachment.base64Url}
/> />
</div> </div>
{/if} {/if}
@@ -81,6 +82,7 @@
{#if readMediaMeta?.sizeBytes} {#if readMediaMeta?.sizeBytes}
<span>Size: {readMediaMeta.sizeBytes} bytes</span> <span>Size: {readMediaMeta.sizeBytes} bytes</span>
{/if} {/if}
{#if readMediaMeta?.mimeType} {#if readMediaMeta?.mimeType}
<span>MIME: {readMediaMeta.mimeType}</span> <span>MIME: {readMediaMeta.mimeType}</span>
{/if} {/if}
@@ -21,7 +21,7 @@
const title = $derived(getToolUi(section.toolName)?.label ?? section.toolName ?? ''); const title = $derived(getToolUi(section.toolName)?.label ?? section.toolName ?? '');
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={runJsMeta} {title} {onToggle}> <ToolCallBlock {isStreaming} meta={runJsMeta} {onToggle} {open} {section} {title}>
{#snippet children(meta, ctx)} {#snippet children(meta, ctx)}
{#if ctx.isPending} {#if ctx.isPending}
<div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic">Running...</div> <div class="rounded bg-muted/20 p-2 text-xs text-muted-foreground/70 italic">Running...</div>
@@ -30,8 +30,10 @@
class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400" class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400"
> >
<XCircle class="mt-0.5 h-3 w-3 shrink-0" /> <XCircle class="mt-0.5 h-3 w-3 shrink-0" />
<span>{meta.errorMessage}</span> <span>{meta.errorMessage}</span>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<SyntaxHighlightedCode <SyntaxHighlightedCode
code={meta.code} code={meta.code}
@@ -47,13 +49,17 @@
maxHeight={MAX_HEIGHT_CODE_BLOCK} maxHeight={MAX_HEIGHT_CODE_BLOCK}
streaming={ctx.isCodeStreaming} streaming={ctx.isCodeStreaming}
/> />
<div class="mb-2 mt-3 flex items-center gap-2 text-xs text-muted-foreground/70"> <div class="mb-2 mt-3 flex items-center gap-2 text-xs text-muted-foreground/70">
<Terminal class="h-3 w-3" /> <Terminal class="h-3 w-3" />
<span>Console</span> <span>Console</span>
{#if meta.timeoutMs != null} {#if meta.timeoutMs != null}
<span class="font-mono">&middot;&nbsp;timeout&nbsp;{meta.timeoutMs}&nbsp;ms</span> <span class="font-mono">&middot;&nbsp;timeout&nbsp;{meta.timeoutMs}&nbsp;ms</span>
{/if} {/if}
</div> </div>
{#if section.toolResult} {#if section.toolResult}
<div class="mt-1"> <div class="mt-1">
<SyntaxHighlightedCode <SyntaxHighlightedCode
@@ -86,55 +86,60 @@
{@const safeUrl = sanitizeExternalUrl(result.url)} {@const safeUrl = sanitizeExternalUrl(result.url)}
{@const showHoverCard = safeUrl !== null && hasDetails(result)} {@const showHoverCard = safeUrl !== null && hasDetails(result)}
{#if safeUrl} {#if safeUrl}
<HoverCard.Root openDelay={150} closeDelay={100}> <HoverCard.Root closeDelay={100} openDelay={150}>
<HoverCard.Trigger <HoverCard.Trigger
href={safeUrl}
target="_blank"
rel="noopener noreferrer"
class="hover:bg-muted/80 focus-visible:ring-ring inline-flex max-w-full items-center gap-1.5 rounded-full border bg-muted px-2.5 py-1 text-xs transition-colors outline-none focus-visible:ring-2" class="hover:bg-muted/80 focus-visible:ring-ring inline-flex max-w-full items-center gap-1.5 rounded-full border bg-muted px-2.5 py-1 text-xs transition-colors outline-none focus-visible:ring-2"
href={safeUrl}
rel="noopener noreferrer"
target="_blank"
> >
{#if faviconUrl} {#if faviconUrl}
<img <img
src={faviconUrl}
alt="" alt=""
class="h-3 w-3 shrink-0 rounded-sm" class="h-3 w-3 shrink-0 rounded-sm"
onerror={hideBrokenIcon} onerror={hideBrokenIcon}
src={faviconUrl}
/> />
{:else} {:else}
<Globe class="text-muted-foreground/70 h-3 w-3 shrink-0" /> <Globe class="text-muted-foreground/70 h-3 w-3 shrink-0" />
{/if} {/if}
<span class="truncate font-medium text-foreground/80">{result.title}</span> <span class="truncate font-medium text-foreground/80">{result.title}</span>
</HoverCard.Trigger> </HoverCard.Trigger>
{#if showHoverCard} {#if showHoverCard}
{@const publishDate = formatPublishDate(result.published)} {@const publishDate = formatPublishDate(result.published)}
{@const host = hostFor(safeUrl)} {@const host = hostFor(safeUrl)}
<HoverCard.Content <HoverCard.Content
side="top"
align="start" align="start"
sideOffset={6}
class="bg-popover text-popover-foreground z-50 w-80 max-w-[90vw] rounded-lg border p-0 shadow-lg" class="bg-popover text-popover-foreground z-50 w-80 max-w-[90vw] rounded-lg border p-0 shadow-lg"
side="top"
sideOffset={6}
> >
<div class="flex flex-col gap-2 p-3"> <div class="flex flex-col gap-2 p-3">
<a <a
href={safeUrl}
target="_blank"
rel="noopener noreferrer"
class="line-clamp-3 text-sm font-medium leading-snug hover:underline" class="line-clamp-3 text-sm font-medium leading-snug hover:underline"
>{result.title}</a href={safeUrl}
rel="noopener noreferrer"
target="_blank">{result.title}</a
> >
{#if publishDate || result.author} {#if publishDate || result.author}
<div class="text-muted-foreground flex items-center gap-1.5 text-[11px]"> <div class="text-muted-foreground flex items-center gap-1.5 text-[11px]">
{#if publishDate} {#if publishDate}
<span>{publishDate}</span> <span>{publishDate}</span>
{/if} {/if}
{#if publishDate && result.author} {#if publishDate && result.author}
<span class="opacity-50">&middot;</span> <span class="opacity-50">&middot;</span>
{/if} {/if}
{#if result.author} {#if result.author}
<span class="truncate">{result.author}</span> <span class="truncate">{result.author}</span>
{/if} {/if}
</div> </div>
{/if} {/if}
{#if result.highlights} {#if result.highlights}
<p <p
class="text-popover-foreground/85 line-clamp-5 text-xs leading-relaxed whitespace-pre-line" class="text-popover-foreground/85 line-clamp-5 text-xs leading-relaxed whitespace-pre-line"
@@ -142,6 +147,7 @@
{result.highlights} {result.highlights}
</p> </p>
{/if} {/if}
{#if host} {#if host}
<div class="text-muted-foreground/80 truncate text-[11px]">{host}</div> <div class="text-muted-foreground/80 truncate text-[11px]">{host}</div>
{/if} {/if}
@@ -152,7 +158,7 @@
{/if} {/if}
{/snippet} {/snippet}
<CollapsibleContentBlock {open} class="my-2" {icon} {iconClass} {iconUrl} {title} {onToggle}> <CollapsibleContentBlock class="my-2" {icon} {iconClass} {iconUrl} {onToggle} {open} {title}>
{#if results.length > 0} {#if results.length > 0}
<div class="flex flex-wrap items-center gap-2 pb-1"> <div class="flex flex-wrap items-center gap-2 pb-1">
{#each results as result (result.url)} {#each results as result (result.url)}
@@ -162,6 +168,7 @@
{:else if showSpinner} {:else if showSpinner}
<div class="text-muted-foreground/70 flex items-center gap-2 py-1 text-xs italic"> <div class="text-muted-foreground/70 flex items-center gap-2 py-1 text-xs italic">
<Loader2 class="h-3 w-3 animate-spin" /> <Loader2 class="h-3 w-3 animate-spin" />
<span>Searching...</span> <span>Searching...</span>
</div> </div>
{:else} {:else}
@@ -21,12 +21,14 @@
const home = $derived(toolsStore.serverHome); const home = $derived(toolsStore.serverHome);
</script> </script>
<ToolCallBlock {section} {open} {isStreaming} meta={writeFileMeta} {onToggle}> <ToolCallBlock {isStreaming} meta={writeFileMeta} {onToggle} {open} {section}>
{#snippet titleSnippet()} {#snippet titleSnippet()}
<span class="text-muted-foreground">Write file </span> <span class="text-muted-foreground">Write file </span>
<span class="font-mono" title={writeFileMeta?.filePath} <span class="font-mono" title={writeFileMeta?.filePath}
>{abbreviateHome(writeFileMeta?.filePath ?? '', home)}</span >{abbreviateHome(writeFileMeta?.filePath ?? '', home)}</span
> >
{#if writeFileMeta?.errorMessage} {#if writeFileMeta?.errorMessage}
<span class="ml-1 text-xs italic text-muted-foreground/70">(failed)</span> <span class="ml-1 text-xs italic text-muted-foreground/70">(failed)</span>
{/if} {/if}
@@ -38,6 +40,7 @@
class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400" class="flex items-start gap-2 rounded bg-red-500/10 p-2 text-xs text-red-600 italic dark:text-red-400"
> >
<XCircle class="mt-0.5 h-3 w-3 shrink-0" /> <XCircle class="mt-0.5 h-3 w-3 shrink-0" />
<span>{meta.errorMessage}</span> <span>{meta.errorMessage}</span>
</div> </div>
{:else if meta} {:else if meta}
@@ -47,9 +50,11 @@
maxHeight={MAX_HEIGHT_CODE_BLOCK} maxHeight={MAX_HEIGHT_CODE_BLOCK}
streaming={ctx.isCodeStreaming} streaming={ctx.isCodeStreaming}
/> />
<div class="mt-1.5 text-xs text-muted-foreground/70 italic"> <div class="mt-1.5 text-xs text-muted-foreground/70 italic">
{#if meta.resultMessage} {#if meta.resultMessage}
{meta.resultMessage}{meta.bytesWritten != null ? RESULT_STAT_SEPARATOR : ''}{/if} {meta.resultMessage}{meta.bytesWritten != null ? RESULT_STAT_SEPARATOR : ''}{/if}
{#if meta.bytesWritten != null} {#if meta.bytesWritten != null}
<span class="font-mono">{meta.bytesWritten}</span> <span class="font-mono">{meta.bytesWritten}</span>
bytes bytes
@@ -1,4 +1,4 @@
<script lang="ts" generics="TMeta"> <script generics="TMeta" lang="ts">
// Generic chrome shell shared by every per-tool block under // Generic chrome shell shared by every per-tool block under
// `ChatMessageToolCall/`. Owns: // `ChatMessageToolCall/`. Owns:
// - the collapsible wrapper (defaults to CollapsibleContentBlock; // - the collapsible wrapper (defaults to CollapsibleContentBlock;
@@ -114,15 +114,15 @@
</script> </script>
<Wrapper <Wrapper
{open}
class="my-2" class="my-2"
icon={toolIcon} icon={toolIcon}
iconClass={toolIconClass} iconClass={toolIconClass}
{iconUrl} {iconUrl}
{onToggle}
{open}
{subtitle}
{title} {title}
{titleSnippet} {titleSnippet}
{subtitle}
{onToggle}
> >
{@render children(meta, { {@render children(meta, {
isCodeStreaming, isCodeStreaming,
@@ -69,8 +69,8 @@
<ChatMessageEditForm /> <ChatMessageEditForm />
{:else} {:else}
<ChatMessageUserBubble <ChatMessageUserBubble
content={message.content}
attachments={message.extra} attachments={message.extra}
content={message.content}
renderMarkdown={true} renderMarkdown={true}
/> />
@@ -82,8 +82,8 @@
> >
<ChatMessageStatistics <ChatMessageStatistics
mode={ChatMessageStatisticsMode.READING} mode={ChatMessageStatisticsMode.READING}
promptTokens={storedReadingStats!.promptTokens}
promptMs={storedReadingStats!.promptMs} promptMs={storedReadingStats!.promptMs}
promptTokens={storedReadingStats!.promptTokens}
/> />
</div> </div>
</div> </div>
@@ -95,10 +95,10 @@
class="inline-flex flex-wrap items-start justify-end gap-2 text-xs text-muted-foreground" class="inline-flex flex-wrap items-start justify-end gap-2 text-xs text-muted-foreground"
> >
<ChatMessageStatistics <ChatMessageStatistics
mode={ChatMessageStatisticsMode.READING}
isLive isLive
promptTokens={liveStats.tokensProcessed} mode={ChatMessageStatisticsMode.READING}
promptMs={liveStats.timeMs} promptMs={liveStats.timeMs}
promptTokens={liveStats.tokensProcessed}
/> />
</div> </div>
</div> </div>
@@ -54,7 +54,7 @@
{#if attachments && attachments.length > 0} {#if attachments && attachments.length > 0}
<div class="mb-2 max-w-[80%]"> <div class="mb-2 max-w-[80%]">
<ChatAttachmentsList {attachments} readonly imageHeight="h-40" /> <ChatAttachmentsList {attachments} imageHeight="h-40" readonly />
</div> </div>
{/if} {/if}
@@ -37,11 +37,11 @@
<ChatMessageEditForm /> <ChatMessageEditForm />
{:else} {:else}
<ChatMessageUserBubble <ChatMessageUserBubble
{content}
attachments={extras} attachments={extras}
textColorClass="text-muted-foreground"
cardBgClass="dark:bg-primary/8" cardBgClass="dark:bg-primary/8"
{content}
maxHeightStyle="overflow-wrap: anywhere; word-break: break-word;" maxHeightStyle="overflow-wrap: anywhere; word-break: break-word;"
textColorClass="text-muted-foreground"
/> />
<div class="max-w-[80%]"> <div class="max-w-[80%]">
@@ -50,9 +50,11 @@
<div <div
class="pointer-events-auto inset-0 flex items-center gap-1 opacity-0 transition-all duration-150 group-hover:opacity-100" class="pointer-events-auto inset-0 flex items-center gap-1 opacity-0 transition-all duration-150 group-hover:opacity-100"
> >
<ActionIcon icon={Edit} tooltip="Edit" onclick={editCtx.handleEdit} /> <ActionIcon icon={Edit} onclick={editCtx.handleEdit} tooltip="Edit" />
<ActionIcon icon={Trash2} tooltip="Delete" onclick={onDelete} />
<ActionIcon icon={ArrowUp} tooltip="Send immediately" onclick={onSendImmediately} /> <ActionIcon icon={Trash2} onclick={onDelete} tooltip="Delete" />
<ActionIcon icon={ArrowUp} onclick={onSendImmediately} tooltip="Send immediately" />
</div> </div>
</div> </div>
</div> </div>
@@ -14,10 +14,12 @@
<div class="my-2 rounded-lg border border-border bg-card p-3"> <div class="my-2 rounded-lg border border-border bg-card p-3">
<div class="mb-3 flex items-center gap-2 text-sm"> <div class="mb-3 flex items-center gap-2 text-sm">
<IconComponent class="{ICON_CLASS_DEFAULT} shrink-0 text-muted-foreground" /> <IconComponent class="{ICON_CLASS_DEFAULT} shrink-0 text-muted-foreground" />
<span> <span>
{@render message()} {@render message()}
</span> </span>
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
{@render actions()} {@render actions()}
</div> </div>
@@ -16,13 +16,13 @@
{/snippet} {/snippet}
{#snippet actions()} {#snippet actions()}
<Button size="sm" onclick={() => onDecision(true)}>Continue</Button> <Button onclick={() => onDecision(true)} size="sm">Continue</Button>
<Button <Button
variant="destructive"
size="sm"
class="text-destructive hover:text-destructive" class="text-destructive hover:text-destructive"
onclick={() => onDecision(false)} onclick={() => onDecision(false)}
size="sm"
variant="destructive"
> >
Stop Stop
</Button> </Button>
@@ -28,10 +28,10 @@
<DropdownMenu.Root> <DropdownMenu.Root>
<ButtonGroup.Root class="overflow-hidden rounded-md shadow-sm"> <ButtonGroup.Root class="overflow-hidden rounded-md shadow-sm">
<Button <Button
variant="secondary"
size="sm"
class="!rounded-r-none !shadow-none" class="!rounded-r-none !shadow-none"
onclick={() => onDecision(ToolPermissionDecision.ONCE)} onclick={() => onDecision(ToolPermissionDecision.ONCE)}
size="sm"
variant="secondary"
> >
Allow once Allow once
</Button> </Button>
@@ -39,11 +39,11 @@
<ButtonGroup.Separator /> <ButtonGroup.Separator />
<DropdownMenu.Trigger <DropdownMenu.Trigger
aria-label="More allow options"
class={cn( class={cn(
buttonVariants({ size: 'sm', variant: 'secondary' }), buttonVariants({ size: 'sm', variant: 'secondary' }),
'inline-flex cursor-pointer items-center !rounded-l-none !shadow-none !px-2' 'inline-flex cursor-pointer items-center !rounded-l-none !shadow-none !px-2'
)} )}
aria-label="More allow options"
> >
<ChevronDown class="h-3.5 w-3.5" /> <ChevronDown class="h-3.5 w-3.5" />
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
@@ -54,6 +54,7 @@
Always allow <pre>{toolName}</pre> Always allow <pre>{toolName}</pre>
tool tool
</DropdownMenu.Item> </DropdownMenu.Item>
{#if serverLabel} {#if serverLabel}
<DropdownMenu.Item onclick={() => onDecision(ToolPermissionDecision.ALWAYS_SERVER)}> <DropdownMenu.Item onclick={() => onDecision(ToolPermissionDecision.ALWAYS_SERVER)}>
Always allow all tools from {serverLabel} Always allow all tools from {serverLabel}
@@ -73,7 +74,7 @@
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
<Button variant="destructive" size="sm" onclick={() => onDecision(ToolPermissionDecision.DENY)}> <Button onclick={() => onDecision(ToolPermissionDecision.DENY)} size="sm" variant="destructive">
Deny Deny
</Button> </Button>
{/snippet} {/snippet}
@@ -77,29 +77,30 @@
<div <div
class="pointer-events-auto inset-0 flex items-center gap-1 opacity-100 transition-all duration-150" class="pointer-events-auto inset-0 flex items-center gap-1 opacity-100 transition-all duration-150"
> >
<ActionIcon icon={Copy} tooltip="Copy" onclick={messageActions.copy} /> <ActionIcon icon={Copy} onclick={messageActions.copy} tooltip="Copy" />
<ActionIcon icon={Edit} tooltip="Edit" onclick={editCtx.startEdit} /> <ActionIcon icon={Edit} onclick={editCtx.startEdit} tooltip="Edit" />
{#if role === MessageRole.ASSISTANT && onRegenerate} {#if role === MessageRole.ASSISTANT && onRegenerate}
<ActionIcon icon={RefreshCw} tooltip="Regenerate" onclick={() => onRegenerate()} /> <ActionIcon icon={RefreshCw} onclick={() => onRegenerate()} tooltip="Regenerate" />
{/if} {/if}
{#if role === MessageRole.ASSISTANT && onContinue} {#if role === MessageRole.ASSISTANT && onContinue}
<ActionIcon icon={ArrowRight} tooltip="Continue" onclick={onContinue} /> <ActionIcon icon={ArrowRight} onclick={onContinue} tooltip="Continue" />
{/if} {/if}
{#if messageActions.forkConversation} {#if messageActions.forkConversation}
<ActionIcon icon={GitBranch} tooltip="Fork conversation" onclick={handleOpenForkDialog} /> <ActionIcon icon={GitBranch} onclick={handleOpenForkDialog} tooltip="Fork conversation" />
{/if} {/if}
<ActionIcon icon={Trash2} tooltip="Delete" onclick={messageActions.requestDelete} /> <ActionIcon icon={Trash2} onclick={messageActions.requestDelete} tooltip="Delete" />
</div> </div>
</div> </div>
{#if showRawOutputSwitch} {#if showRawOutputSwitch}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="text-xs text-muted-foreground">Show raw output</span> <span class="text-xs text-muted-foreground">Show raw output</span>
<Switch <Switch
checked={rawOutputEnabled} checked={rawOutputEnabled}
onCheckedChange={(checked) => onRawOutputToggle?.(checked)} onCheckedChange={(checked) => onRawOutputToggle?.(checked)}
@@ -109,54 +110,54 @@
</div> </div>
<DialogConfirmation <DialogConfirmation
open={messageActions.showDeleteDialog} cancelText="Cancel"
title="Delete Message"
description={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1
? `This will delete ${messageActions.deletionInfo.totalCount} messages including: ${messageActions.deletionInfo.userMessages} user message${messageActions.deletionInfo.userMessages > 1 ? 's' : ''} and ${messageActions.deletionInfo.assistantMessages} assistant response${messageActions.deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.`
: 'Are you sure you want to delete this message? This action cannot be undone.'}
confirmText={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1 confirmText={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1
? `Delete ${messageActions.deletionInfo.totalCount} Messages` ? `Delete ${messageActions.deletionInfo.totalCount} Messages`
: 'Delete'} : 'Delete'}
cancelText="Cancel" description={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1
variant="destructive" ? `This will delete ${messageActions.deletionInfo.totalCount} messages including: ${messageActions.deletionInfo.userMessages} user message${messageActions.deletionInfo.userMessages > 1 ? 's' : ''} and ${messageActions.deletionInfo.assistantMessages} assistant response${messageActions.deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.`
: 'Are you sure you want to delete this message? This action cannot be undone.'}
icon={Trash2} icon={Trash2}
onConfirm={handleConfirmDelete}
onCancel={() => messageActions.setShowDeleteDialog(false)} onCancel={() => messageActions.setShowDeleteDialog(false)}
onConfirm={handleConfirmDelete}
open={messageActions.showDeleteDialog}
title="Delete Message"
variant="destructive"
/> />
<DialogConfirmation <DialogConfirmation
bind:open={showForkDialog} bind:open={showForkDialog}
title="Fork Conversation"
description="Create a new conversation branching from this message."
confirmText="Fork"
cancelText="Cancel" cancelText="Cancel"
confirmText="Fork"
description="Create a new conversation branching from this message."
icon={GitBranch} icon={GitBranch}
onConfirm={handleConfirmFork}
onCancel={() => (showForkDialog = false)} onCancel={() => (showForkDialog = false)}
onConfirm={handleConfirmFork}
title="Fork Conversation"
> >
<div class="flex flex-col gap-4 py-2"> <div class="flex flex-col gap-4 py-2">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<Label for="fork-name">Title</Label> <Label for="fork-name">Title</Label>
<Input <Input
id="fork-name" bind:value={forkName}
class="text-foreground" class="text-foreground"
id="fork-name"
placeholder="Enter fork name" placeholder="Enter fork name"
type="text" type="text"
bind:value={forkName}
/> />
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Checkbox <Checkbox
id="fork-attachments"
checked={forkIncludeAttachments} checked={forkIncludeAttachments}
id="fork-attachments"
onCheckedChange={(checked) => { onCheckedChange={(checked) => {
forkIncludeAttachments = checked === true; forkIncludeAttachments = checked === true;
}} }}
/> />
<Label for="fork-attachments" class="cursor-pointer text-sm font-normal"> <Label class="cursor-pointer text-sm font-normal" for="fork-attachments">
Include all attachments Include all attachments
</Label> </Label>
</div> </div>
@@ -30,11 +30,11 @@
role="navigation" role="navigation"
> >
<ActionIcon <ActionIcon
icon={ChevronLeft}
tooltip="Previous version"
disabled={!hasPrevious}
class="h-5 w-5 p-0 {!hasPrevious ? '!cursor-not-allowed opacity-30' : ''}" class="h-5 w-5 p-0 {!hasPrevious ? '!cursor-not-allowed opacity-30' : ''}"
disabled={!hasPrevious}
icon={ChevronLeft}
onclick={() => messageActions.navigateToSibling(previousSiblingId!)} onclick={() => messageActions.navigateToSibling(previousSiblingId!)}
tooltip="Previous version"
/> />
<span class="px-1 font-mono text-xs"> <span class="px-1 font-mono text-xs">
@@ -42,11 +42,11 @@
</span> </span>
<ActionIcon <ActionIcon
icon={ChevronRight}
tooltip="Next version"
disabled={!hasNext}
class="h-5 w-5 p-0 {!hasNext ? 'opacity-30' : ''}" class="h-5 w-5 p-0 {!hasNext ? 'opacity-30' : ''}"
disabled={!hasNext}
icon={ChevronRight}
onclick={() => messageActions.navigateToSibling(nextSiblingId!)} onclick={() => messageActions.navigateToSibling(nextSiblingId!)}
tooltip="Next version"
/> />
</div> </div>
{/if} {/if}
@@ -181,26 +181,26 @@
{#snippet renderSection(section: AgenticSection, index: number)} {#snippet renderSection(section: AgenticSection, index: number)}
{#if section.type === AgenticSectionType.TEXT} {#if section.type === AgenticSectionType.TEXT}
<div class="agentic-text"> <div class="agentic-text">
<MarkdownContent content={section.content} attachments={message?.extra} /> <MarkdownContent attachments={message?.extra} content={section.content} />
</div> </div>
{:else if section.type === AgenticSectionType.REASONING || section.type === AgenticSectionType.REASONING_PENDING} {:else if section.type === AgenticSectionType.REASONING || section.type === AgenticSectionType.REASONING_PENDING}
<ChatMessageReasoningBlock <ChatMessageReasoningBlock
{section}
open={isExpanded(index, section)}
{isStreaming}
{hasReasoningError}
attachments={message?.extra} attachments={message?.extra}
{hasReasoningError}
{isStreaming}
onToggle={() => toggleExpanded(index, section)} onToggle={() => toggleExpanded(index, section)}
open={isExpanded(index, section)}
{section}
/> />
{:else if section.type === AgenticSectionType.TOOL_CALL || section.type === AgenticSectionType.TOOL_CALL_PENDING || section.type === AgenticSectionType.TOOL_CALL_STREAMING} {:else if section.type === AgenticSectionType.TOOL_CALL || section.type === AgenticSectionType.TOOL_CALL_PENDING || section.type === AgenticSectionType.TOOL_CALL_STREAMING}
<ChatMessageToolCallBlock <ChatMessageToolCallBlock
{section} attachments={message?.extra}
open={isExpanded(index, section)}
{isStreaming}
isExecuting={section.toolCallId !== undefined && isExecuting={section.toolCallId !== undefined &&
section.toolCallId === currentlyExecutingToolCallId} section.toolCallId === currentlyExecutingToolCallId}
attachments={message?.extra} {isStreaming}
onToggle={() => toggleExpanded(index, section)} onToggle={() => toggleExpanded(index, section)}
open={isExpanded(index, section)}
{section}
/> />
{/if} {/if}
{/snippet} {/snippet}
@@ -218,15 +218,15 @@
{#if turnStats && showAgenticTurnStats} {#if turnStats && showAgenticTurnStats}
<div class="turn-stats transition-opacity duration-150 mt-1 mb-4"> <div class="turn-stats transition-opacity duration-150 mt-1 mb-4">
<ChatMessageStatistics <ChatMessageStatistics
promptTokens={turnStats.llm.prompt_n}
promptMs={turnStats.llm.prompt_ms}
predictedTokens={turnStats.llm.predicted_n}
predictedMs={turnStats.llm.predicted_ms}
agenticTimings={turnStats.toolCalls.length > 0 agenticTimings={turnStats.toolCalls.length > 0
? buildTurnAgenticTimings(turnStats) ? buildTurnAgenticTimings(turnStats)
: undefined} : undefined}
initialView={ChatMessageStatsView.GENERATION}
hideSummary hideSummary
initialView={ChatMessageStatsView.GENERATION}
predictedMs={turnStats.llm.predicted_ms}
predictedTokens={turnStats.llm.predicted_n}
promptMs={turnStats.llm.prompt_ms}
promptTokens={turnStats.llm.prompt_n}
/> />
</div> </div>
{/if} {/if}
@@ -240,9 +240,9 @@
{#if pendingPermission && !permissionDismissed} {#if pendingPermission && !permissionDismissed}
<ChatMessageActionCardPermissionRequest <ChatMessageActionCardPermissionRequest
toolName={pendingPermission.toolName}
serverLabel={pendingPermission.serverLabel}
onDecision={handlePermission} onDecision={handlePermission}
serverLabel={pendingPermission.serverLabel}
toolName={pendingPermission.toolName}
/> />
{/if} {/if}
@@ -102,35 +102,35 @@
<div class="relative w-full max-w-[80%]"> <div class="relative w-full max-w-[80%]">
<ChatForm <ChatForm
value={editCtx.editedContent}
attachments={editCtx.editedExtras}
bind:uploadedFiles={editCtx.editedUploadedFiles} bind:uploadedFiles={editCtx.editedUploadedFiles}
placeholder="Edit your message..." attachments={editCtx.editedExtras}
showMcpPromptButton
showAddButton={editCtx.messageRole === MessageRole.USER}
showModelSelector={editCtx.messageRole === MessageRole.USER}
onValueChange={editCtx.setContent}
onAttachmentRemove={handleAttachmentRemove} onAttachmentRemove={handleAttachmentRemove}
onUploadedFileRemove={handleUploadedFileRemove}
onFilesAdd={handleFilesAdd} onFilesAdd={handleFilesAdd}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onUploadedFileRemove={handleUploadedFileRemove}
onValueChange={editCtx.setContent}
placeholder="Edit your message..."
showAddButton={editCtx.messageRole === MessageRole.USER}
showMcpPromptButton
showModelSelector={editCtx.messageRole === MessageRole.USER}
value={editCtx.editedContent}
/> />
</div> </div>
<div class="mt-2 flex w-full max-w-[80%] items-center justify-between"> <div class="mt-2 flex w-full max-w-[80%] items-center justify-between">
{#if isUserMessage && editCtx.showSaveOnlyOption} {#if isUserMessage && editCtx.showSaveOnlyOption}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Switch id="save-only-switch" bind:checked={saveWithoutRegenerate} class="scale-75" /> <Switch bind:checked={saveWithoutRegenerate} class="scale-75" id="save-only-switch" />
<label for="save-only-switch" class="cursor-pointer text-xs text-muted-foreground"> <label class="cursor-pointer text-xs text-muted-foreground" for="save-only-switch">
Update without re-sending Update without re-sending
</label> </label>
</div> </div>
{:else if isAssistantMessage} {:else if isAssistantMessage}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<Switch id="branch-after-edit" bind:checked={branchAfterEdit} class="scale-75" /> <Switch bind:checked={branchAfterEdit} class="scale-75" id="branch-after-edit" />
<label for="branch-after-edit" class="cursor-pointer text-xs text-muted-foreground"> <label class="cursor-pointer text-xs text-muted-foreground" for="branch-after-edit">
Branch conversation after edit Branch conversation after edit
</label> </label>
</div> </div>
@@ -147,12 +147,12 @@
<DialogConfirmation <DialogConfirmation
bind:open={showDiscardDialog} bind:open={showDiscardDialog}
title="Discard changes?"
description="You have unsaved changes. Are you sure you want to discard them?"
confirmText="Discard"
cancelText="Keep editing" cancelText="Keep editing"
variant="destructive" confirmText="Discard"
description="You have unsaved changes. Are you sure you want to discard them?"
icon={AlertTriangle} icon={AlertTriangle}
onConfirm={editCtx.cancel}
onCancel={() => (showDiscardDialog = false)} onCancel={() => (showDiscardDialog = false)}
onConfirm={editCtx.cancel}
title="Discard changes?"
variant="destructive"
/> />
@@ -124,23 +124,23 @@
</script> </script>
<CollapsibleContentBlock <CollapsibleContentBlock
{open}
class="my-2" class="my-2"
icon={Lightbulb} icon={Lightbulb}
iconClass="h-3.5 w-3.5" iconClass="h-3.5 w-3.5"
{title}
{subtitle}
{shimmerTitle}
{onToggle} {onToggle}
{open}
{shimmerTitle}
{subtitle}
{title}
> >
<div <div
bind:this={scrollEl} bind:this={scrollEl}
class="reasoning-content"
class:is-streaming={isPending} class:is-streaming={isPending}
class="reasoning-content"
onscroll={handleScrollEvent} onscroll={handleScrollEvent}
> >
{#if currentConfig.renderThinkingAsMarkdown} {#if currentConfig.renderThinkingAsMarkdown}
<MarkdownContent content={section.content} class="text-muted-foreground" {attachments} /> <MarkdownContent {attachments} class="text-muted-foreground" content={section.content} />
{:else} {:else}
<div <div
class="text-[13px] leading-relaxed wrap-break-word whitespace-pre-wrap text-muted-foreground" class="text-[13px] leading-relaxed wrap-break-word whitespace-pre-wrap text-muted-foreground"
@@ -140,15 +140,15 @@
{#snippet child({ props })} {#snippet child({ props })}
<button <button
{...props} {...props}
type="button"
class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView === class="inline-flex h-5 w-5 items-center justify-center rounded-sm transition-colors {activeView ===
opts.view opts.view
? 'bg-background text-foreground shadow-sm' ? 'bg-background text-foreground shadow-sm'
: opts.disabled : opts.disabled
? 'cursor-not-allowed opacity-40' ? 'cursor-not-allowed opacity-40'
: 'hover:text-foreground'}" : 'hover:text-foreground'}"
onclick={() => !opts.disabled && (activeView = opts.view)}
disabled={opts.disabled} disabled={opts.disabled}
onclick={() => !opts.disabled && (activeView = opts.view)}
type="button"
> >
<IconComponent class="h-3 w-3" /> <IconComponent class="h-3 w-3" />
@@ -208,85 +208,85 @@
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={WholeWord} icon={WholeWord}
value="{predictedTokens?.toLocaleString()} tokens"
tooltipLabel="Generated tokens" tooltipLabel="Generated tokens"
value="{predictedTokens?.toLocaleString()} tokens"
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Clock} icon={Clock}
value={formattedTime}
tooltipLabel="Generation time" tooltipLabel="Generation time"
value={formattedTime}
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Gauge} icon={Gauge}
value="{tokensPerSecond.toFixed(2)} t/s"
tooltipLabel="Generation speed" tooltipLabel="Generation speed"
value="{tokensPerSecond.toFixed(2)} t/s"
/> />
{:else if activeView === ChatMessageStatsView.TOOLS && hasAgenticStats} {:else if activeView === ChatMessageStatsView.TOOLS && hasAgenticStats}
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Wrench} icon={Wrench}
value="{agenticTimings!.toolCallsCount} calls"
tooltipLabel="Tool calls executed" tooltipLabel="Tool calls executed"
value="{agenticTimings!.toolCallsCount} calls"
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Clock} icon={Clock}
value={formattedAgenticToolsTime}
tooltipLabel="Tool execution time" tooltipLabel="Tool execution time"
value={formattedAgenticToolsTime}
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Gauge} icon={Gauge}
value="{agenticToolsPerSecond.toFixed(2)} calls/s"
tooltipLabel="Tool execution rate" tooltipLabel="Tool execution rate"
value="{agenticToolsPerSecond.toFixed(2)} calls/s"
/> />
{:else if activeView === ChatMessageStatsView.SUMMARY && hasAgenticStats} {:else if activeView === ChatMessageStatsView.SUMMARY && hasAgenticStats}
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Layers} icon={Layers}
value="{agenticTimings!.turns} turns"
tooltipLabel="Agentic turns (LLM calls)" tooltipLabel="Agentic turns (LLM calls)"
value="{agenticTimings!.turns} turns"
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={WholeWord} icon={WholeWord}
value="{agenticTimings!.llm.predicted_n.toLocaleString()} tokens"
tooltipLabel="Total tokens generated" tooltipLabel="Total tokens generated"
value="{agenticTimings!.llm.predicted_n.toLocaleString()} tokens"
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Clock} icon={Clock}
value={formattedAgenticTotalTime}
tooltipLabel="Total time (LLM + tools)" tooltipLabel="Total time (LLM + tools)"
value={formattedAgenticTotalTime}
/> />
{:else if hasPromptStats && (mode === ChatMessageStatisticsMode.READING || isSwitchable)} {:else if hasPromptStats && (mode === ChatMessageStatisticsMode.READING || isSwitchable)}
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={WholeWord} icon={WholeWord}
value="{promptTokens} tokens"
tooltipLabel="Prompt tokens" tooltipLabel="Prompt tokens"
value="{promptTokens} tokens"
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Clock} icon={Clock}
value={formattedPromptTime ?? '0s'}
tooltipLabel="Prompt processing time" tooltipLabel="Prompt processing time"
value={formattedPromptTime ?? '0s'}
/> />
<ChatMessageStatisticsBadge <ChatMessageStatisticsBadge
class="bg-transparent" class="bg-transparent"
icon={Gauge} icon={Gauge}
value="{promptTokensPerSecond!.toFixed(2)} tokens/s"
tooltipLabel="Prompt processing speed" tooltipLabel="Prompt processing speed"
value="{promptTokensPerSecond!.toFixed(2)} tokens/s"
/> />
{/if} {/if}
</div> </div>
@@ -32,6 +32,7 @@
</BadgeInfo> </BadgeInfo>
{/snippet} {/snippet}
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Content> <Tooltip.Content>
<p>{tooltipLabel}</p> <p>{tooltipLabel}</p>
</Tooltip.Content> </Tooltip.Content>
@@ -227,14 +227,14 @@
<div> <div>
{#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)} {#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
<ChatMessage <ChatMessage
class="mx-auto mt-12 w-full max-w-3xl"
{chatActions} {chatActions}
{message} class="mx-auto mt-12 w-full max-w-3xl"
{toolMessages}
{isLastAssistantMessage} {isLastAssistantMessage}
{isLastUserMessage} {isLastUserMessage}
{message}
{nextAssistantMessage} {nextAssistantMessage}
{siblingInfo} {siblingInfo}
{toolMessages}
/> />
{/each} {/each}
@@ -247,10 +247,10 @@
class="mx-auto mt-12 w-full max-w-[48rem]" class="mx-auto mt-12 w-full max-w-[48rem]"
content={pendingContent} content={pendingContent}
extras={agenticStore.getPendingSteeringMessageExtras(convId)} extras={agenticStore.getPendingSteeringMessageExtras(convId)}
onSendImmediately={() => chatStore.abortCurrentFlow(convId)} onDelete={() => agenticStore.clearSteeringMessage(convId)}
onEdit={(newContent, extras) => onEdit={(newContent, extras) =>
agenticStore.injectSteeringMessage(convId, newContent, extras)} agenticStore.injectSteeringMessage(convId, newContent, extras)}
onDelete={() => agenticStore.clearSteeringMessage(convId)} onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
/> />
{/if} {/if}
{:else if conversationsStore.activeConversation && chatStore.getPendingMessageContent(conversationsStore.activeConversation!.id)} {:else if conversationsStore.activeConversation && chatStore.getPendingMessageContent(conversationsStore.activeConversation!.id)}
@@ -262,9 +262,9 @@
class="mx-auto mt-12 w-full max-w-[48rem]" class="mx-auto mt-12 w-full max-w-[48rem]"
content={pendingContent} content={pendingContent}
extras={chatStore.getPendingMessageExtras(convId)} extras={chatStore.getPendingMessageExtras(convId)}
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
onEdit={(newContent, extras) => chatStore.injectPendingMessage(convId, newContent, extras)}
onDelete={() => chatStore.clearPendingMessage(convId)} onDelete={() => chatStore.clearPendingMessage(convId)}
onEdit={(newContent, extras) => chatStore.injectPendingMessage(convId, newContent, extras)}
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
/> />
{/if} {/if}
{/if} {/if}
@@ -294,8 +294,8 @@
<ServerLoadingSplash /> <ServerLoadingSplash />
{:else} {:else}
<div <div
class="chat-screen flex grow flex-col min-h-[calc(100dvh-1rem)] md:min-h-[calc(100dvh-1rem-var(--chat-tabs-offset,0px))] px-4 md:py-0 pt-12 pb-48 md:pb-4"
style:--chat-form-bottom-position={chatFormBottomPosition} style:--chat-form-bottom-position={chatFormBottomPosition}
class="chat-screen flex grow flex-col min-h-[calc(100dvh-1rem)] md:min-h-[calc(100dvh-1rem-var(--chat-tabs-offset,0px))] px-4 md:py-0 pt-12 pb-48 md:pb-4"
ondragenter={dragAndDrop.dragHandlers.dragenter} ondragenter={dragAndDrop.dragHandlers.dragenter}
ondragleave={dragAndDrop.dragHandlers.dragleave} ondragleave={dragAndDrop.dragHandlers.dragleave}
ondragover={dragAndDrop.dragHandlers.dragover} ondragover={dragAndDrop.dragHandlers.dragover}
@@ -313,6 +313,7 @@
{/if} {/if}
<div <div
style:padding-top={!isEmpty ? 'var(--chat-form-padding-top)' : undefined}
class={[ class={[
'pointer-events-none md:sticky fixed mt-auto transition-all duration-200', 'pointer-events-none md:sticky fixed mt-auto transition-all duration-200',
deviceStore.isStandalone deviceStore.isStandalone
@@ -322,7 +323,6 @@
: 'bottom-2 right-2 left-2', : 'bottom-2 right-2 left-2',
isEmpty ? 'md:bottom-[calc(50dvh-7rem)] 2xl:bottom-[calc(50dvh-4rem)]' : 'md:bottom-4' isEmpty ? 'md:bottom-[calc(50dvh-7rem)] 2xl:bottom-[calc(50dvh-4rem)]' : 'md:bottom-4'
]} ]}
style:padding-top={!isEmpty ? 'var(--chat-form-padding-top)' : undefined}
> >
<ChatScreenGreeting {isEmpty} /> <ChatScreenGreeting {isEmpty} />
@@ -347,6 +347,7 @@
</div> </div>
<ChatScreenForm <ChatScreenForm
bind:uploadedFiles={fileUpload.uploadedFiles}
class="pointer-events-auto conversation-chat-form" class="pointer-events-auto conversation-chat-form"
disabled={hasPropsError || chatStore.isEditing()} disabled={hasPropsError || chatStore.isEditing()}
{initialMessage} {initialMessage}
@@ -356,18 +357,17 @@
onSend={handleSendMessage} onSend={handleSendMessage}
onStop={() => chatStore.stopGeneration()} onStop={() => chatStore.stopGeneration()}
onSystemPromptAdd={handleSystemPromptAdd} onSystemPromptAdd={handleSystemPromptAdd}
bind:uploadedFiles={fileUpload.uploadedFiles}
/> />
</div> </div>
</div> </div>
{/if} {/if}
<ChatScreenDialogsAndAlerts <ChatScreenDialogsAndAlerts
{showDeleteDialog}
{handleDeleteConfirm}
{showEmptyFileDialog}
{emptyFileNames}
{activeErrorDialog} {activeErrorDialog}
{handleErrorDialogOpenChange} {emptyFileNames}
{fileUpload} {fileUpload}
{handleDeleteConfirm}
{handleErrorDialogOpenChange}
{showDeleteDialog}
{showEmptyFileDialog}
/> />
@@ -8,12 +8,12 @@
<div class="pointer-events-auto flex justify-center relative h-0"> <div class="pointer-events-auto flex justify-center relative h-0">
<ActionIcon <ActionIcon
icon={ArrowDown}
{onclick}
ariaLabel="Scroll to bottom" ariaLabel="Scroll to bottom"
tooltip="Scroll to bottom"
size="lg"
iconSize={ICON_CLASS_DEFAULT}
class="h-9 w-9 rounded-full bg-muted/60 border border-border/20 shadow-sm text-accent-foreground absolute bottom-4" class="h-9 w-9 rounded-full bg-muted/60 border border-border/20 shadow-sm text-accent-foreground absolute bottom-4"
icon={ArrowDown}
iconSize={ICON_CLASS_DEFAULT}
{onclick}
size="lg"
tooltip="Scroll to bottom"
/> />
</div> </div>
@@ -26,14 +26,14 @@
<DialogConfirmation <DialogConfirmation
bind:open={showDeleteDialog} bind:open={showDeleteDialog}
title="Delete Conversation"
description="Are you sure you want to delete this conversation? This action cannot be undone and will permanently remove all messages in this conversation."
confirmText="Delete"
cancelText="Cancel" cancelText="Cancel"
variant="destructive" confirmText="Delete"
description="Are you sure you want to delete this conversation? This action cannot be undone and will permanently remove all messages in this conversation."
icon={Trash2} icon={Trash2}
onConfirm={handleDeleteConfirm}
onCancel={() => (showDeleteDialog = false)} onCancel={() => (showDeleteDialog = false)}
onConfirm={handleDeleteConfirm}
title="Delete Conversation"
variant="destructive"
/> />
<DialogEmptyFileAlert <DialogEmptyFileAlert
@@ -47,8 +47,8 @@
/> />
<DialogChatError <DialogChatError
message={activeErrorDialog?.message ?? ''}
contextInfo={activeErrorDialog?.contextInfo} contextInfo={activeErrorDialog?.contextInfo}
message={activeErrorDialog?.message ?? ''}
onOpenChange={handleErrorDialogOpenChange} onOpenChange={handleErrorDialogOpenChange}
open={Boolean(activeErrorDialog)} open={Boolean(activeErrorDialog)}
type={activeErrorDialog?.type ?? ErrorDialogType.SERVER} type={activeErrorDialog?.type ?? ErrorDialogType.SERVER}
@@ -147,19 +147,19 @@
}); });
</script> </script>
<div class="chat-screen-form-wrapper" bind:this={formWrapperEl}> <div bind:this={formWrapperEl} class="chat-screen-form-wrapper">
<ChatForm <ChatForm
class="mx-auto max-w-3xl {className}"
bind:this={chatFormRef} bind:this={chatFormRef}
bind:value={message}
bind:uploadedFiles bind:uploadedFiles
bind:value={message}
class="mx-auto max-w-3xl {className}"
{disabled} {disabled}
{isLoading} {isLoading}
showMcpPromptButton
onFilesAdd={handleFilesAdd} onFilesAdd={handleFilesAdd}
{onStop} {onStop}
onSubmit={handleSubmit} onSubmit={handleSubmit}
onSystemPromptClick={handleSystemPromptClick} onSystemPromptClick={handleSystemPromptClick}
onUploadedFileRemove={handleUploadedFileRemove} onUploadedFileRemove={handleUploadedFileRemove}
showMcpPromptButton
/> />
</div> </div>
@@ -22,9 +22,9 @@
{#if !isLoadingModel} {#if !isLoadingModel}
<button <button
onclick={() => serverStore.fetch()}
disabled={serverStore.loading}
class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-2 py-1 text-xs font-medium hover:bg-destructive/30 disabled:opacity-50" class="flex items-center gap-1.5 rounded-lg bg-destructive/20 px-2 py-1 text-xs font-medium hover:bg-destructive/30 disabled:opacity-50"
disabled={serverStore.loading}
onclick={() => serverStore.fetch()}
> >
<RefreshCw class="h-3 w-3 {serverStore.loading ? 'animate-spin' : ''}" /> <RefreshCw class="h-3 w-3 {serverStore.loading ? 'animate-spin' : ''}" />
{serverStore.loading ? 'Retrying...' : 'Retry'} {serverStore.loading ? 'Retrying...' : 'Retry'}
@@ -8,11 +8,12 @@
{#if state === StreamConnectionState.RESUMING} {#if state === StreamConnectionState.RESUMING}
<div <div
aria-live="polite"
class="pointer-events-auto mx-auto mt-2 mb-2 flex max-w-[48rem] items-center gap-2 rounded-md border border-blue-400/40 bg-blue-50/60 px-3 py-1.5 text-sm text-blue-700 dark:bg-blue-950/40 dark:text-blue-200" class="pointer-events-auto mx-auto mt-2 mb-2 flex max-w-[48rem] items-center gap-2 rounded-md border border-blue-400/40 bg-blue-50/60 px-3 py-1.5 text-sm text-blue-700 dark:bg-blue-950/40 dark:text-blue-200"
role="status" role="status"
aria-live="polite"
> >
<Loader2 class="h-3.5 w-3.5 animate-spin" /> <Loader2 class="h-3.5 w-3.5 animate-spin" />
<span>Reconnecting to the stream...</span> <span>Reconnecting to the stream...</span>
</div> </div>
{/if} {/if}
@@ -81,27 +81,27 @@
</script> </script>
<nav <nav
aria-label="Open conversations"
class="group sticky pl-1 top-0 z-10 hidden md:block chat-tabs-fade transition-[padding] duration-200 ease-in-out pt-3.25 {uiStore.isSidebarExpanded class="group sticky pl-1 top-0 z-10 hidden md:block chat-tabs-fade transition-[padding] duration-200 ease-in-out pt-3.25 {uiStore.isSidebarExpanded
? CHAT_TABS_MAX_WIDTH.EXPANDED_SIDEBAR ? CHAT_TABS_MAX_WIDTH.EXPANDED_SIDEBAR
: CHAT_TABS_MAX_WIDTH.COLLAPSED_SIDEBAR}" : CHAT_TABS_MAX_WIDTH.COLLAPSED_SIDEBAR}"
aria-label="Open conversations"
> >
<div class="relative"> <div class="relative">
<ScrollCarousel <ScrollCarousel
{carousel}
class="h-10" class="h-10"
containerClass="flex h-10 min-w-0 items-center" containerClass="flex h-10 min-w-0 items-center"
innerClass="items-center gap-1.25" innerClass="items-center gap-1.25"
{carousel}
> >
{#each tabs as tab (tab.id)} {#each tabs as tab (tab.id)}
<ChatTabsItem <ChatTabsItem
{tab}
isActive={tab.id === activeId} isActive={tab.id === activeId}
isLoading={loadingIds.has(tab.id)} isLoading={loadingIds.has(tab.id)}
onActivate={(id) => tabsStore.activate(id)} onActivate={(id) => tabsStore.activate(id)}
onAuxClick={handleAuxClick}
onClose={handleClose} onClose={handleClose}
onStop={handleStop} onStop={handleStop}
onAuxClick={handleAuxClick} {tab}
/> />
{/each} {/each}
@@ -115,6 +115,7 @@
? 'opacity-100' ? 'opacity-100'
: 'opacity-0'}" : 'opacity-0'}"
></div> ></div>
<div <div
class="pointer-events-none absolute inset-y-0 right-0 z-[5] w-8 bg-gradient-to-l from-background to-transparent transition-opacity {carousel.canScrollRight class="pointer-events-none absolute inset-y-0 right-0 z-[5] w-8 bg-gradient-to-l from-background to-transparent transition-opacity {carousel.canScrollRight
? 'opacity-100' ? 'opacity-100'
@@ -67,12 +67,12 @@
)} )}
> >
<a <a
{href}
class="absolute inset-0 z-0 rounded-lg"
onclick={handleActivate}
onauxclick={(e) => onAuxClick?.(tab.id, e)}
aria-current={isActive ? 'page' : undefined} aria-current={isActive ? 'page' : undefined}
aria-label={tab.name} aria-label={tab.name}
class="absolute inset-0 z-0 rounded-lg"
{href}
onauxclick={(e) => onAuxClick?.(tab.id, e)}
onclick={handleActivate}
></a> ></a>
{#if isLoading} {#if isLoading}
@@ -81,9 +81,9 @@
{#snippet child({ props })} {#snippet child({ props })}
<button <button
{...props} {...props}
aria-label="Stop generation"
class="stop-button relative z-10 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground" class="stop-button relative z-10 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-colors hover:text-foreground"
onclick={(e) => handleActionClick(e, () => onStop?.(tab.id, e))} onclick={(e) => handleActionClick(e, () => onStop?.(tab.id, e))}
aria-label="Stop generation"
> >
<Loader2 <Loader2
class="loading-icon {ICON_CLASS_SM} animate-spin transition-opacity duration-300 {contentOpacity}" class="loading-icon {ICON_CLASS_SM} animate-spin transition-opacity duration-300 {contentOpacity}"
@@ -115,12 +115,12 @@
{#snippet child({ props })} {#snippet child({ props })}
<button <button
{...props} {...props}
aria-label="Close tab"
class={cn( class={cn(
'relative z-10 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:bg-foreground/10 hover:text-foreground', 'relative z-10 flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground transition-opacity hover:bg-foreground/10 hover:text-foreground',
contentOpacity contentOpacity
)} )}
onclick={(e) => handleActionClick(e, () => onClose?.(tab.id))} onclick={(e) => handleActionClick(e, () => onClose?.(tab.id))}
aria-label="Close tab"
> >
<X class={ICON_CLASS_SM} /> <X class={ICON_CLASS_SM} />
</button> </button>
@@ -15,9 +15,9 @@
{#snippet child({ props })} {#snippet child({ props })}
<button <button
{...props} {...props}
aria-label="New chat"
class="backdrop-blur-lg flex h-8 w-8 mr-4 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-foreground/5" class="backdrop-blur-lg flex h-8 w-8 mr-4 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors hover:bg-foreground/5"
{onclick} {onclick}
aria-label="New chat"
> >
<Plus class="{ICON_CLASS_DEFAULT} opacity-40 transition-opacity group-hover:opacity-100" /> <Plus class="{ICON_CLASS_DEFAULT} opacity-40 transition-opacity group-hover:opacity-100" />
</button> </button>
@@ -40,12 +40,12 @@
</script> </script>
<Collapsible.Root <Collapsible.Root
{open} class={cn('group/collapsible', 'my-0!', className)}
onOpenChange={(value) => { onOpenChange={(value) => {
open = value; open = value;
onToggle?.(); onToggle?.();
}} }}
class={cn('group/collapsible', 'my-0!', className)} {open}
> >
<Collapsible.Trigger <Collapsible.Trigger
class={cn( class={cn(
@@ -56,10 +56,10 @@
<div class="flex min-w-0 items-start gap-2 text-muted-foreground"> <div class="flex min-w-0 items-start gap-2 text-muted-foreground">
{#if iconUrl} {#if iconUrl}
<img <img
src={iconUrl}
alt="" alt=""
class={cn('shrink-0 rounded-sm mt-0.75', iconClass)} class={cn('shrink-0 rounded-sm mt-0.75', iconClass)}
onerror={hideBrokenIcon} onerror={hideBrokenIcon}
src={iconUrl}
/> />
{:else if IconComponent} {:else if IconComponent}
<IconComponent class={cn('shrink-0 text-muted-foreground/60 mt-0.75', iconClass)} /> <IconComponent class={cn('shrink-0 text-muted-foreground/60 mt-0.75', iconClass)} />
@@ -40,12 +40,12 @@
</script> </script>
<Collapsible.Root <Collapsible.Root
{open} class={cn('group/collapsible', 'overflow-hidden rounded-md', className)}
onOpenChange={(value) => { onOpenChange={(value) => {
open = value; open = value;
onToggle?.(); onToggle?.();
}} }}
class={cn('group/collapsible', 'overflow-hidden rounded-md', className)} {open}
style="background: var(--code-background); border: 1px solid color-mix(in oklch, var(--border) 30%, transparent);" style="background: var(--code-background); border: 1px solid color-mix(in oklch, var(--border) 30%, transparent);"
> >
<Collapsible.Trigger <Collapsible.Trigger
@@ -57,10 +57,10 @@
<div class="flex min-w-0 items-start gap-2 text-muted-foreground"> <div class="flex min-w-0 items-start gap-2 text-muted-foreground">
{#if iconUrl} {#if iconUrl}
<img <img
src={iconUrl}
alt="" alt=""
class={cn('shrink-0 rounded-sm mt-0.5', iconClass)} class={cn('shrink-0 rounded-sm mt-0.5', iconClass)}
onerror={hideBrokenIcon} onerror={hideBrokenIcon}
src={iconUrl}
/> />
{:else if IconComponent} {:else if IconComponent}
<IconComponent class={cn('shrink-0 text-muted-foreground/60 mt-0.5', iconClass)} /> <IconComponent class={cn('shrink-0 text-muted-foreground/60 mt-0.5', iconClass)} />
@@ -867,10 +867,10 @@
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
bind:this={containerRef} bind:this={containerRef}
onclick={handleMermaidClick}
class="markdown-content {className}{settingsStore.config[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS] class="markdown-content {className}{settingsStore.config[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS]
? ' full-height-code-blocks' ? ' full-height-code-blocks'
: ''}" : ''}"
onclick={handleMermaidClick}
> >
{#each renderedBlocks as block (block.id)} {#each renderedBlocks as block (block.id)}
<div class="markdown-block" {...{ [MARKDOWN_DATA_ATTRS.BLOCK_ID]: block.id }}> <div class="markdown-block" {...{ [MARKDOWN_DATA_ATTRS.BLOCK_ID]: block.id }}>
@@ -893,14 +893,16 @@
<div class="mermaid-block-wrapper streaming-mermaid-block"> <div class="mermaid-block-wrapper streaming-mermaid-block">
<div class="code-block-header"> <div class="code-block-header">
<span class="code-language">mermaid</span> <span class="code-language">mermaid</span>
<div class="code-block-actions"> <div class="code-block-actions">
<ActionIconCopyToClipboard <ActionIconCopyToClipboard
text={incompleteCodeBlock.code}
canCopy={false}
ariaLabel="Diagram incomplete" ariaLabel="Diagram incomplete"
canCopy={false}
text={incompleteCodeBlock.code}
/> />
</div> </div>
</div> </div>
<div class="mermaid-loading-placeholder"> <div class="mermaid-loading-placeholder">
<span class="mermaid-loading-text">Generating diagram...</span> <span class="mermaid-loading-text">Generating diagram...</span>
</div> </div>
@@ -909,14 +911,16 @@
<div class="svg-block-wrapper streaming-svg-block"> <div class="svg-block-wrapper streaming-svg-block">
<div class="code-block-header"> <div class="code-block-header">
<span class="code-language">svg</span> <span class="code-language">svg</span>
<div class="code-block-actions"> <div class="code-block-actions">
<ActionIconCopyToClipboard <ActionIconCopyToClipboard
text={incompleteCodeBlock.code}
canCopy={false}
ariaLabel="Diagram incomplete" ariaLabel="Diagram incomplete"
canCopy={false}
text={incompleteCodeBlock.code}
/> />
</div> </div>
</div> </div>
{#if liveSvgHtml} {#if liveSvgHtml}
<div class="svg-scroll-container"> <div class="svg-scroll-container">
<div class={SVG.BLOCK_CLASS}> <div class={SVG.BLOCK_CLASS}>
@@ -933,10 +937,11 @@
<div class="code-block-wrapper streaming-code-block relative"> <div class="code-block-wrapper streaming-code-block relative">
<div class="code-block-header"> <div class="code-block-header">
<span class="code-language">{incompleteCodeBlock.language || 'text'}</span> <span class="code-language">{incompleteCodeBlock.language || 'text'}</span>
<CodeBlockActions <CodeBlockActions
code={incompleteCodeBlock.code} code={incompleteCodeBlock.code}
language={incompleteCodeBlock.language || 'text'}
disabled disabled
language={incompleteCodeBlock.language || 'text'}
onPreview={(code, lang) => { onPreview={(code, lang) => {
previewCode = code; previewCode = code;
previewLanguage = lang; previewLanguage = lang;
@@ -961,16 +966,16 @@
</div> </div>
<DialogCodePreview <DialogCodePreview
open={previewDialogOpen}
code={previewCode} code={previewCode}
language={previewLanguage} language={previewLanguage}
onOpenChange={handlePreviewDialogOpenChange} onOpenChange={handlePreviewDialogOpenChange}
open={previewDialogOpen}
/> />
<DialogMermaidPreview <DialogMermaidPreview
onOpenChange={handleMermaidPreviewOpenChange}
open={mermaidPreviewOpen} open={mermaidPreviewOpen}
svgHtml={mermaidPreviewSvgHtml} svgHtml={mermaidPreviewSvgHtml}
onOpenChange={handleMermaidPreviewOpenChange}
/> />
<style> <style>
@@ -103,22 +103,22 @@
<div <div
class="mermaid-preview-diagram transform-origin-center inline-block min-h-fit min-w-fit will-change-transform {isDragging && class="mermaid-preview-diagram transform-origin-center inline-block min-h-fit min-w-fit will-change-transform {isDragging &&
'select-none'}" 'select-none'}"
onpointerdown={handlePointerDown}
onpointerleave={handlePointerUp}
onpointermove={handlePointerMove}
onpointerup={handlePointerUp}
style="transform: translate({translateX}px, {translateY}px) scale({scale}); cursor: {isDragging style="transform: translate({translateX}px, {translateY}px) scale({scale}); cursor: {isDragging
? 'grabbing' ? 'grabbing'
: 'grab'};" : 'grab'};"
onpointerdown={handlePointerDown}
onpointermove={handlePointerMove}
onpointerup={handlePointerUp}
onpointerleave={handlePointerUp}
> >
<div bind:this={svgHost}></div> <div bind:this={svgHost}></div>
</div> </div>
<MermaidPreviewControls <MermaidPreviewControls
{scale} onResetView={resetView}
{svgHtml}
onZoomIn={zoomIn} onZoomIn={zoomIn}
onZoomOut={zoomOut} onZoomOut={zoomOut}
onResetView={resetView} {scale}
{svgHtml}
/> />
</div> </div>

Some files were not shown because too many files have changed in this diff Show More