ui: ESLint config updates (#27700)
* chore: Spacing between sibling elements in html markup * chore: Formatting and linting rules
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user