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