mirror of
https://github.com/LostRuins/koboldcpp.git
synced 2026-09-20 01:31:42 +02:00
Merge branch 'upstream' into concedo_experimental
# Conflicts: # .github/workflows/build-cache.yml # .github/workflows/build-cmake-pkg.yml # .github/workflows/build-cpu.yml # .github/workflows/build-cuda-windows.yml # .github/workflows/build-sanitize.yml # .github/workflows/release.yml # .github/workflows/winget.yml # CMakeLists.txt # README.md # app/llama.cpp # cmake/llama-config.cmake.in # cmake/llama.pc.in # common/CMakeLists.txt # common/build-info.h # docs/backend/OPENVINO.md # docs/backend/SYCL.md # docs/preset.md # ggml/CMakeLists.txt # ggml/cmake/ggml-config.cmake.in # ggml/src/ggml-cpu/kleidiai/kleidiai.cpp # ggml/src/ggml-hip/CMakeLists.txt # ggml/src/ggml-openvino/ggml-decoder.cpp # ggml/src/ggml-openvino/ggml-decoder.h # ggml/src/ggml-openvino/ggml-openvino-extra.cpp # ggml/src/ggml-openvino/ggml-openvino-extra.h # ggml/src/ggml-openvino/ggml-openvino.cpp # ggml/src/ggml-openvino/ggml-quants.cpp # ggml/src/ggml-openvino/ggml-quants.h # ggml/src/ggml-openvino/openvino/decoder.h # ggml/src/ggml-openvino/openvino/node_context.h # ggml/src/ggml-openvino/openvino/op/cpy.cpp # ggml/src/ggml-openvino/openvino/op/gated_delta_net.cpp # ggml/src/ggml-openvino/openvino/op/get_rows.cpp # ggml/src/ggml-openvino/openvino/op/l2_norm.cpp # ggml/src/ggml-openvino/openvino/op/mul_mat_id.cpp # ggml/src/ggml-openvino/openvino/op/repeat.cpp # ggml/src/ggml-openvino/openvino/op/reshape.cpp # ggml/src/ggml-openvino/openvino/op/rms_norm.cpp # ggml/src/ggml-openvino/openvino/op/rope.cpp # ggml/src/ggml-openvino/openvino/op/scale.cpp # ggml/src/ggml-openvino/openvino/op/set_rows.cpp # ggml/src/ggml-openvino/openvino/op/ssm_conv.cpp # ggml/src/ggml-openvino/openvino/op/view.cpp # ggml/src/ggml-openvino/openvino/op_table.cpp # ggml/src/ggml-openvino/openvino/op_table.h # ggml/src/ggml-openvino/openvino/translate_session.cpp # ggml/src/ggml-openvino/openvino/utils.cpp # ggml/src/ggml-openvino/openvino/utils.h # ggml/src/ggml-openvino/utils.cpp # ggml/src/ggml-openvino/utils.h # ggml/src/ggml-sycl/common.hpp # ggml/src/ggml-sycl/concat.cpp # ggml/src/ggml-sycl/dmmv.cpp # ggml/src/ggml-sycl/element_wise.cpp # ggml/src/ggml-sycl/element_wise.hpp # ggml/src/ggml-sycl/fusion.cpp # ggml/src/ggml-sycl/fusion.hpp # ggml/src/ggml-sycl/gated_delta_net.cpp # ggml/src/ggml-sycl/gated_delta_net.hpp # ggml/src/ggml-sycl/ggml-sycl.cpp # ggml/src/ggml-sycl/mmvq.cpp # ggml/src/ggml-sycl/mmvq.hpp # scripts/sync-ggml.last # src/CMakeLists.txt # tests/test-backend-ops.cpp # tests/test-chat.cpp # tests/test-gguf.cpp # tests/test-quantize-stats.cpp # tools/cvector-generator/cvector-generator.cpp # tools/mtmd/CMakeLists.txt # tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte
This commit is contained in:
Vendored
+1
-1
@@ -32,8 +32,8 @@ import type {
|
||||
ApiRouterModelsStatusResponse,
|
||||
ApiRouterModelsUnloadRequest,
|
||||
ApiRouterModelsUnloadResponse,
|
||||
// Chat types
|
||||
ChatAttachmentDisplayItem,
|
||||
// Chat types
|
||||
ChatMessagePromptProgress,
|
||||
ChatMessageSiblingInfo,
|
||||
ChatMessageTimings,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link rel="icon" href="favicon.ico" sizes="48x48" />
|
||||
<link rel="icon" href="favicon.svg" sizes="any" type="image/svg+xml" />
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ActionIcon from './ActionIcon.svelte';
|
||||
import { Copy } from '@lucide/svelte';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
export let ariaLabel: string = 'Copy to clipboard';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
import { X } from '@lucide/svelte';
|
||||
import { ActionIcon } from '$lib/components/app';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { mcpStore } from '$lib/stores';
|
||||
import type { MCPResourceAttachment } from '$lib/types';
|
||||
import { getResourceDisplayName, getResourceIcon } from '$lib/utils';
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Music, Video, X } from '@lucide/svelte';
|
||||
import { ActionIcon } from '$lib/components/app';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { AttachmentType } from '$lib/enums';
|
||||
import {
|
||||
formatFileSize,
|
||||
|
||||
+3
-2
@@ -5,7 +5,8 @@
|
||||
ChatAttachmentsPreviewNavButtons,
|
||||
ChatAttachmentsPreviewThumbnailStrip
|
||||
} from '$lib/components/app';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { UI_DATA_ATTRS } from '$lib/constants';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import {
|
||||
createBase64DataUrl,
|
||||
formatFileSize,
|
||||
@@ -90,7 +91,7 @@
|
||||
const index = currentIndex;
|
||||
|
||||
setTimeout(() => {
|
||||
const thumbnail = document.querySelector(`[data-thumbnail-index="${index}"]`);
|
||||
const thumbnail = document.querySelector(`[${UI_DATA_ATTRS.THUMBNAIL_INDEX}="${index}"]`);
|
||||
|
||||
thumbnail?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
||||
}, 0);
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { PdfViewMode } from '$lib/enums';
|
||||
import type { ChatAttachmentDisplayItem } from '$lib/types';
|
||||
import { getLanguageFromFilename } from '$lib/utils';
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { FileText, Music, Video } from '@lucide/svelte';
|
||||
import { HorizontalScrollCarousel } from '$lib/components/app/misc';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT, UI_DATA_ATTRS } from '$lib/constants';
|
||||
|
||||
interface PreviewItem {
|
||||
id: string;
|
||||
@@ -36,7 +36,7 @@
|
||||
<HorizontalScrollCarousel class="max-w-full">
|
||||
{#each items as item, index (item.id)}
|
||||
<button
|
||||
data-thumbnail-index={index}
|
||||
{...{ [UI_DATA_ATTRS.THUMBNAIL_INDEX]: index }}
|
||||
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',
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
import {
|
||||
ChatAttachmentsList,
|
||||
ChatFormActions,
|
||||
ChatFormContenteditable,
|
||||
ChatFormFileInputInvisible,
|
||||
ChatFormCurrentWorkingDirectory,
|
||||
ChatFormInput,
|
||||
ChatFormInputFileInputInvisible,
|
||||
ChatFormMcpResourcesList,
|
||||
ChatFormPickers,
|
||||
ChatFormTextarea,
|
||||
ChatFormWorkingDirectory,
|
||||
DialogMcpResourcesBrowser
|
||||
} from '$lib/components/app';
|
||||
import {
|
||||
@@ -26,19 +25,16 @@
|
||||
SpecialFileType
|
||||
} from '$lib/enums';
|
||||
import { useChatFormPickers } from '$lib/hooks/use-chat-form-pickers.svelte';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
activeConversation,
|
||||
activeMessages,
|
||||
chatStore,
|
||||
conversationsStore,
|
||||
pendingCwd
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { mcpHasResourceAttachments } from '$lib/stores/mcp-resources.svelte';
|
||||
import { modelOptions, selectedModelId } from '$lib/stores/models.svelte';
|
||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
mcpResourceStore,
|
||||
mcpStore,
|
||||
modelsStore,
|
||||
serverStore,
|
||||
settingsStore,
|
||||
toolsStore
|
||||
} from '$lib/stores';
|
||||
import type {
|
||||
FileMentionEntry,
|
||||
GetPromptResult,
|
||||
@@ -113,7 +109,7 @@
|
||||
}: Props = $props();
|
||||
|
||||
// Component References
|
||||
// Shared handle of the two input renderers (textarea + contenteditable).
|
||||
// Shared handle of the two input renderers (plain textarea + rich chat form input).
|
||||
type ChatInputHandle = {
|
||||
focus(): void;
|
||||
resetHeight(): void;
|
||||
@@ -124,16 +120,16 @@
|
||||
|
||||
let audioRecorder: AudioRecorder | undefined;
|
||||
let chatFormActionsRef: ChatFormActions | undefined = $state(undefined);
|
||||
let fileInputRef: ChatFormFileInputInvisible | undefined = $state(undefined);
|
||||
let fileInputRef: ChatFormInputFileInputInvisible | undefined = $state(undefined);
|
||||
let pickersRef: { handleKeydown: (event: KeyboardEvent) => boolean } | undefined =
|
||||
$state(undefined);
|
||||
let inputRef: ChatInputHandle | undefined = $state(undefined);
|
||||
|
||||
// Render-mode gate: the plain textarea by default, the contenteditable
|
||||
// Render-mode gate: the plain textarea by default, the rich chat form input
|
||||
// while the buffer carries a `file://` mention link or a complete code
|
||||
// span (badges and code chips need a DOM the textarea cannot provide).
|
||||
// Demotes back once neither remains.
|
||||
let useContenteditable = $state(false);
|
||||
let useRichInput = $state(false);
|
||||
|
||||
// Audio Recording State
|
||||
let isRecording = $state(false);
|
||||
@@ -143,7 +139,7 @@
|
||||
// float above the box.
|
||||
let mentionAnchor: HTMLDivElement | null = $state(null);
|
||||
|
||||
let cwd = $derived(activeConversation()?.cwd ?? pendingCwd());
|
||||
let cwd = $derived(conversationsStore.activeConversation?.cwd ?? conversationsStore.pendingCwd);
|
||||
|
||||
const pickers = useChatFormPickers({
|
||||
focusInput: refocusInput,
|
||||
@@ -184,7 +180,7 @@
|
||||
let isResourceDialogOpen = $state(false);
|
||||
let preSelectedResourceUri = $state<string | undefined>(undefined);
|
||||
|
||||
let currentConfig = $derived(config());
|
||||
let currentConfig = $derived(settingsStore.config);
|
||||
|
||||
let pasteLongTextToFileLength = $derived.by(() => {
|
||||
const n = Number(currentConfig.pasteLongTextToFileLen);
|
||||
@@ -192,18 +188,18 @@
|
||||
return Number.isNaN(n) ? Number(SETTING_CONFIG_DEFAULT.pasteLongTextToFileLen) : n;
|
||||
});
|
||||
|
||||
let isRouter = $derived(isRouterMode());
|
||||
let isRouter = $derived(serverStore.isRouterMode);
|
||||
let conversationModel = $derived(
|
||||
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
|
||||
chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
|
||||
);
|
||||
let activeModelId = $derived.by(() => {
|
||||
const options = modelOptions();
|
||||
const options = modelsStore.models;
|
||||
|
||||
if (!isRouter) {
|
||||
return options.length > 0 ? options[0].model : null;
|
||||
}
|
||||
|
||||
const selectedId = selectedModelId();
|
||||
const selectedId = modelsStore.selectedModelId;
|
||||
|
||||
if (selectedId) {
|
||||
const model = options.find((m) => m.id === selectedId);
|
||||
@@ -220,7 +216,9 @@
|
||||
return null;
|
||||
});
|
||||
|
||||
let hasModelSelected = $derived(!isRouter || !!conversationModel || !!selectedModelId());
|
||||
let hasModelSelected = $derived(
|
||||
!isRouter || !!conversationModel || !!modelsStore.selectedModelId
|
||||
);
|
||||
let hasLoadingAttachments = $derived(uploadedFiles.some((f) => f.isLoading));
|
||||
let hasAttachments = $derived(
|
||||
(attachments && attachments.length > 0) || (uploadedFiles && uploadedFiles.length > 0)
|
||||
@@ -243,16 +241,15 @@
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
const wantContenteditable =
|
||||
containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? '');
|
||||
const wantRichInput = containsFileMentionLink(value ?? '') || containsCodeSpan(value ?? '');
|
||||
|
||||
if (useContenteditable === wantContenteditable) return;
|
||||
if (useRichInput === wantRichInput) return;
|
||||
|
||||
if (!caretOffsetPinned) {
|
||||
pendingCaretOffset = inputRef?.getCaretOffset() ?? (value ?? '').length;
|
||||
}
|
||||
|
||||
useContenteditable = wantContenteditable;
|
||||
useRichInput = wantRichInput;
|
||||
queueCaretRestore();
|
||||
});
|
||||
|
||||
@@ -316,7 +313,7 @@
|
||||
|
||||
// Caret inside a fenced code block (closed, or still open
|
||||
// while being typed): Enter adds a line, never submits. The
|
||||
// contenteditable consumes this case locally; this gate
|
||||
// rich chat form input consumes this case locally; this gate
|
||||
// covers the plain textarea, where skipping submit lets the
|
||||
// native newline through.
|
||||
if (!isModifier && isOffsetInCodeBlock(value ?? '', inputRef?.getCaretOffset() ?? 0)) {
|
||||
@@ -509,9 +506,9 @@
|
||||
value = built.newValue;
|
||||
onValueChange?.(built.newValue);
|
||||
|
||||
// Already in contenteditable mode: no renderer flip, so the swap
|
||||
// Already in rich chat form input mode: no renderer flip, so the swap
|
||||
// effect's caret restore never runs.
|
||||
if (useContenteditable) {
|
||||
if (useRichInput) {
|
||||
queueCaretRestore();
|
||||
}
|
||||
}
|
||||
@@ -545,7 +542,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ChatFormFileInputInvisible bind:this={fileInputRef} onFileSelect={handleFileSelect} />
|
||||
<ChatFormInputFileInputInvisible bind:this={fileInputRef} onFileSelect={handleFileSelect} />
|
||||
|
||||
<form
|
||||
class="relative grid {className}"
|
||||
@@ -604,37 +601,22 @@
|
||||
<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!"
|
||||
>
|
||||
{#if useContenteditable}
|
||||
<ChatFormContenteditable
|
||||
class="px-5 py-1.5 md:pt-0 mb-0.5"
|
||||
bind:this={inputRef}
|
||||
bind:value
|
||||
onKeydown={handleKeydown}
|
||||
onInput={() => {
|
||||
pickers.handleInput();
|
||||
onValueChange?.(value);
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
{disabled}
|
||||
{placeholder}
|
||||
/>
|
||||
{:else}
|
||||
<ChatFormTextarea
|
||||
class="px-5 py-1.5 md:pt-0"
|
||||
bind:this={inputRef}
|
||||
bind:value
|
||||
onKeydown={handleKeydown}
|
||||
onInput={() => {
|
||||
pickers.handleInput();
|
||||
onValueChange?.(value);
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
{disabled}
|
||||
{placeholder}
|
||||
/>
|
||||
{/if}
|
||||
<ChatFormInput
|
||||
class="px-5 py-1.5 md:pt-0"
|
||||
bind:this={inputRef}
|
||||
bind:value
|
||||
onKeydown={handleKeydown}
|
||||
onInput={() => {
|
||||
pickers.handleInput();
|
||||
onValueChange?.(value);
|
||||
}}
|
||||
onPaste={handlePaste}
|
||||
{disabled}
|
||||
{placeholder}
|
||||
{useRichInput}
|
||||
/>
|
||||
|
||||
{#if mcpHasResourceAttachments()}
|
||||
{#if mcpResourceStore.hasAttachments}
|
||||
<ChatFormMcpResourcesList
|
||||
class="mb-3"
|
||||
onResourceClick={(uri) => {
|
||||
@@ -668,7 +650,7 @@
|
||||
<ContextGaugePopup />
|
||||
|
||||
{#if toolsStore.hasEnabledCwdTools}
|
||||
<ChatFormWorkingDirectory
|
||||
<ChatFormCurrentWorkingDirectory
|
||||
directory={cwd}
|
||||
isOpen={pickers.isWorkingDirectoryPickerOpen}
|
||||
bind:query={pickers.workingDirectoryQuery}
|
||||
|
||||
+1
-2
@@ -2,8 +2,7 @@
|
||||
import { Plus } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { ATTACHMENT_TOOLTIP_TEXT } from '$lib/constants';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ATTACHMENT_TOOLTIP_TEXT, ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
|
||||
+23
-39
@@ -12,40 +12,19 @@
|
||||
import {
|
||||
ATTACHMENT_FILE_ITEMS,
|
||||
ATTACHMENT_TOOLTIP_TEXT,
|
||||
ICON_CLASS_DEFAULT,
|
||||
TOOLTIP_DELAY_DURATION
|
||||
} from '$lib/constants';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { getChatFormActionsContext } from '$lib/contexts';
|
||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
hasAudioModality?: boolean;
|
||||
hasVideoModality?: boolean;
|
||||
hasVisionModality?: boolean;
|
||||
hasMcpPromptsSupport?: boolean;
|
||||
hasMcpResourcesSupport?: boolean;
|
||||
onFileUpload?: () => void;
|
||||
onSystemPromptClick?: () => void;
|
||||
onMcpPromptClick?: () => void;
|
||||
onMcpSettingsClick?: () => void;
|
||||
onMcpResourcesClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
disabled = false,
|
||||
hasAudioModality = false,
|
||||
hasMcpPromptsSupport = false,
|
||||
hasMcpResourcesSupport = false,
|
||||
hasVideoModality = false,
|
||||
hasVisionModality = false,
|
||||
onFileUpload,
|
||||
onMcpPromptClick,
|
||||
onMcpResourcesClick,
|
||||
onMcpSettingsClick,
|
||||
onSystemPromptClick
|
||||
}: Props = $props();
|
||||
let { class: className = '' }: Props = $props();
|
||||
|
||||
const chatFormActions = getChatFormActionsContext();
|
||||
|
||||
let dropdownOpen = $state(false);
|
||||
// The system message action moves focus to the message editor, so the menu
|
||||
@@ -54,18 +33,23 @@
|
||||
|
||||
function handleMcpSettingsClick() {
|
||||
dropdownOpen = false;
|
||||
onMcpSettingsClick?.();
|
||||
chatFormActions.onMcpSettingsClick?.();
|
||||
}
|
||||
|
||||
const attachmentMenu = useAttachmentMenu(
|
||||
() => ({
|
||||
hasAudioModality,
|
||||
hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport,
|
||||
hasVideoModality,
|
||||
hasVisionModality
|
||||
hasAudioModality: chatFormActions.hasAudioModality,
|
||||
hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport,
|
||||
hasVideoModality: chatFormActions.hasVideoModality,
|
||||
hasVisionModality: chatFormActions.hasVisionModality
|
||||
}),
|
||||
() => ({
|
||||
onFileUpload: chatFormActions.onFileUpload,
|
||||
onMcpPromptClick: chatFormActions.onMcpPromptClick,
|
||||
onMcpResourcesClick: chatFormActions.onMcpResourcesClick,
|
||||
onSystemPromptClick: chatFormActions.onSystemPromptClick
|
||||
}),
|
||||
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
||||
() => {
|
||||
dropdownOpen = false;
|
||||
}
|
||||
@@ -85,7 +69,7 @@
|
||||
buttonVariants({ variant: 'secondary' }),
|
||||
'file-upload-button h-8 w-8 cursor-pointer rounded-full p-0'
|
||||
)}
|
||||
{disabled}
|
||||
disabled={chatFormActions.disabled}
|
||||
>
|
||||
<span class="sr-only">{ATTACHMENT_TOOLTIP_TEXT}</span>
|
||||
|
||||
@@ -162,7 +146,7 @@
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={() => {
|
||||
suppressCloseAutoFocus = true;
|
||||
onSystemPromptClick?.();
|
||||
chatFormActions.onSystemPromptClick?.();
|
||||
}}
|
||||
>
|
||||
<MessageSquare class={ICON_CLASS_DEFAULT} />
|
||||
@@ -174,12 +158,12 @@
|
||||
|
||||
<ChatFormActionAddMcpServersSubmenu onMcpSettingsClick={handleMcpSettingsClick} />
|
||||
|
||||
{#if hasMcpPromptsSupport}
|
||||
{#if chatFormActions.hasMcpPromptsSupport}
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={onMcpPromptClick}
|
||||
onclick={chatFormActions.onMcpPromptClick}
|
||||
>
|
||||
<Zap class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
@@ -187,10 +171,10 @@
|
||||
</DropdownMenu.Item>
|
||||
{/if}
|
||||
|
||||
{#if hasMcpResourcesSupport}
|
||||
{#if chatFormActions.hasMcpResourcesSupport}
|
||||
<DropdownMenu.Item
|
||||
class="flex cursor-pointer items-center gap-2"
|
||||
onclick={onMcpResourcesClick}
|
||||
onclick={chatFormActions.onMcpResourcesClick}
|
||||
>
|
||||
<FolderOpen class={ICON_CLASS_DEFAULT} />
|
||||
|
||||
|
||||
+2
-4
@@ -4,11 +4,9 @@
|
||||
import { DropdownMenuSearchable, McpLogo, McpServerIdentity } from '$lib/components/app';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ROUTES } from '$lib/constants/routes';
|
||||
import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { conversationsStore, mcpStore } from '$lib/stores';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import { Check, Info, Lightbulb, LightbulbOff } from '@lucide/svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
|
||||
|
||||
const reasoning = useReasoningMenu();
|
||||
|
||||
+24
-38
@@ -14,47 +14,28 @@
|
||||
import * as Sheet from '$lib/components/ui/sheet';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { TOOLTIP_DELAY_DURATION } from '$lib/constants';
|
||||
import { ATTACHMENT_FILE_ITEMS } from '$lib/constants/attachment-menu';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import {
|
||||
ATTACHMENT_FILE_ITEMS,
|
||||
ICON_CLASS_DEFAULT,
|
||||
TOOLTIP_DELAY_DURATION
|
||||
} from '$lib/constants';
|
||||
import { getChatFormActionsContext } from '$lib/contexts';
|
||||
import { HealthCheckStatus } from '$lib/enums';
|
||||
import { AttachmentAction } from '$lib/enums/attachment.enums';
|
||||
import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
|
||||
import { useReasoningMenu } from '$lib/hooks/use-reasoning-menu.svelte';
|
||||
import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { conversationsStore, mcpStore } from '$lib/stores';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
hasAudioModality?: boolean;
|
||||
hasVideoModality?: boolean;
|
||||
hasVisionModality?: boolean;
|
||||
hasMcpPromptsSupport?: boolean;
|
||||
hasMcpResourcesSupport?: boolean;
|
||||
onFileUpload?: () => void;
|
||||
onSystemPromptClick?: () => void;
|
||||
onMcpPromptClick?: () => void;
|
||||
onMcpResourcesClick?: () => void;
|
||||
trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
disabled = false,
|
||||
hasAudioModality = false,
|
||||
hasMcpPromptsSupport = false,
|
||||
hasMcpResourcesSupport = false,
|
||||
hasVideoModality = false,
|
||||
hasVisionModality = false,
|
||||
onFileUpload,
|
||||
onMcpPromptClick,
|
||||
onMcpResourcesClick,
|
||||
onSystemPromptClick,
|
||||
trigger
|
||||
}: Props = $props();
|
||||
let { class: className = '', trigger }: Props = $props();
|
||||
|
||||
const chatFormActions = getChatFormActionsContext();
|
||||
|
||||
let sheetOpen = $state(false);
|
||||
let reasoningExpanded = $state(false);
|
||||
@@ -64,13 +45,18 @@
|
||||
|
||||
const attachmentMenu = useAttachmentMenu(
|
||||
() => ({
|
||||
hasAudioModality,
|
||||
hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport,
|
||||
hasVideoModality,
|
||||
hasVisionModality
|
||||
hasAudioModality: chatFormActions.hasAudioModality,
|
||||
hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport,
|
||||
hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport,
|
||||
hasVideoModality: chatFormActions.hasVideoModality,
|
||||
hasVisionModality: chatFormActions.hasVisionModality
|
||||
}),
|
||||
() => ({
|
||||
onFileUpload: chatFormActions.onFileUpload,
|
||||
onMcpPromptClick: chatFormActions.onMcpPromptClick,
|
||||
onMcpResourcesClick: chatFormActions.onMcpResourcesClick,
|
||||
onSystemPromptClick: chatFormActions.onSystemPromptClick
|
||||
}),
|
||||
() => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }),
|
||||
() => {
|
||||
sheetOpen = false;
|
||||
}
|
||||
@@ -90,7 +76,7 @@
|
||||
|
||||
<div class="flex items-center gap-1 {className}">
|
||||
<Sheet.Root bind:open={sheetOpen}>
|
||||
{@render trigger({ disabled, onclick: () => (sheetOpen = true) })}
|
||||
{@render trigger({ disabled: chatFormActions.disabled, onclick: () => (sheetOpen = true) })}
|
||||
|
||||
<Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
|
||||
<Sheet.Header>
|
||||
@@ -349,7 +335,7 @@
|
||||
<span>System Message</span>
|
||||
</button>
|
||||
|
||||
{#if hasMcpPromptsSupport}
|
||||
{#if chatFormActions.hasMcpPromptsSupport}
|
||||
<button
|
||||
type="button"
|
||||
class={sheetItemClass}
|
||||
@@ -361,7 +347,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if hasMcpResourcesSupport}
|
||||
{#if chatFormActions.hasMcpResourcesSupport}
|
||||
<button
|
||||
type="button"
|
||||
class={sheetItemClass}
|
||||
|
||||
+2
-4
@@ -4,11 +4,9 @@
|
||||
import * as Collapsible from '$lib/components/ui/collapsible';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { CLI_FLAGS } from '$lib/constants';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { CLI_FLAGS, ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { useToolsPanel } from '$lib/hooks/use-tools-panel.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { mcpStore, toolsStore } from '$lib/stores';
|
||||
|
||||
const toolsPanel = useToolsPanel();
|
||||
const hasMcpServersAvailable = $derived(mcpStore.getServers().length > 0);
|
||||
|
||||
+3
-54
@@ -2,66 +2,15 @@
|
||||
import ChatFormActionAddButton from './ChatFormActionAddButton.svelte';
|
||||
import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
|
||||
import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
hasAudioModality?: boolean;
|
||||
hasVideoModality?: boolean;
|
||||
hasMcpPromptsSupport?: boolean;
|
||||
hasMcpResourcesSupport?: boolean;
|
||||
hasVisionModality?: boolean;
|
||||
onFileUpload?: () => void;
|
||||
onMcpPromptClick?: () => void;
|
||||
onMcpResourcesClick?: () => void;
|
||||
onMcpSettingsClick?: () => void;
|
||||
onSystemPromptClick?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
disabled = false,
|
||||
hasAudioModality = false,
|
||||
hasMcpPromptsSupport = false,
|
||||
hasMcpResourcesSupport = false,
|
||||
hasVideoModality = false,
|
||||
hasVisionModality = false,
|
||||
onFileUpload,
|
||||
onMcpPromptClick,
|
||||
onMcpResourcesClick,
|
||||
onMcpSettingsClick,
|
||||
onSystemPromptClick
|
||||
}: Props = $props();
|
||||
import { isMobile } from '$lib/stores';
|
||||
</script>
|
||||
|
||||
{#if isMobile.current}
|
||||
<ChatFormActionAddSheet
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVideoModality}
|
||||
{hasVisionModality}
|
||||
{hasMcpPromptsSupport}
|
||||
{hasMcpResourcesSupport}
|
||||
{onFileUpload}
|
||||
{onSystemPromptClick}
|
||||
{onMcpPromptClick}
|
||||
{onMcpResourcesClick}
|
||||
>
|
||||
<ChatFormActionAddSheet>
|
||||
{#snippet trigger({ disabled, onclick })}
|
||||
<ChatFormActionAddButton {disabled} {onclick} />
|
||||
{/snippet}
|
||||
</ChatFormActionAddSheet>
|
||||
{:else}
|
||||
<ChatFormActionAddDropdown
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVideoModality}
|
||||
{hasVisionModality}
|
||||
{hasMcpPromptsSupport}
|
||||
{hasMcpResourcesSupport}
|
||||
{onFileUpload}
|
||||
{onMcpPromptClick}
|
||||
{onMcpResourcesClick}
|
||||
{onMcpSettingsClick}
|
||||
{onSystemPromptClick}
|
||||
/>
|
||||
<ChatFormActionAddDropdown />
|
||||
{/if}
|
||||
|
||||
+11
-20
@@ -1,15 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ModelsSelectorDropdown, ModelsSelectorSheet } from '$lib/components/app';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { activeMessages } from '$lib/stores/conversations.svelte';
|
||||
import {
|
||||
modelOptions,
|
||||
modelsStore,
|
||||
selectedModelId,
|
||||
selectedModelName
|
||||
} from '$lib/stores/models.svelte';
|
||||
import { isRouterMode, serverError } from '$lib/stores/server.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import { chatStore, conversationsStore, isMobile, modelsStore, serverStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
@@ -35,17 +26,17 @@
|
||||
useGlobalSelection = false
|
||||
}: Props = $props();
|
||||
|
||||
let isRouter = $derived(isRouterMode());
|
||||
let isOffline = $derived(!!serverError());
|
||||
let isRouter = $derived(serverStore.isRouterMode);
|
||||
let isOffline = $derived(!!serverStore.error);
|
||||
|
||||
let conversationModel = $derived(
|
||||
chatStore.getConversationModel(activeMessages() as DatabaseMessage[])
|
||||
chatStore.getConversationModel(conversationsStore.activeMessages as DatabaseMessage[])
|
||||
);
|
||||
|
||||
let lastSyncedConversationModel: string | null = null;
|
||||
|
||||
let selectorModel = $derived.by(() => {
|
||||
const storeModel = selectedModelName();
|
||||
const storeModel = modelsStore.selectedModelName;
|
||||
|
||||
if (storeModel && storeModel !== conversationModel) {
|
||||
return storeModel;
|
||||
@@ -60,7 +51,7 @@
|
||||
|
||||
$effect(() => {
|
||||
if (conversationModel && conversationModel !== lastSyncedConversationModel) {
|
||||
if (modelOptions().some((m) => m.model === conversationModel)) {
|
||||
if (modelsStore.models.some((m) => m.model === conversationModel)) {
|
||||
modelsStore.selectedModelName = conversationModel;
|
||||
modelsStore.selectModelByName(conversationModel);
|
||||
} else {
|
||||
@@ -73,24 +64,24 @@
|
||||
isRouter &&
|
||||
!modelsStore.selectedModelId &&
|
||||
modelsStore.loadedModelIds.length > 0 &&
|
||||
activeMessages().length > 0 &&
|
||||
conversationsStore.activeMessages.length > 0 &&
|
||||
!conversationModel
|
||||
) {
|
||||
lastSyncedConversationModel = null;
|
||||
const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
|
||||
const first = modelsStore.models.find((m) => modelsStore.loadedModelIds.includes(m.model));
|
||||
|
||||
if (first) modelsStore.selectModelById(first.id);
|
||||
}
|
||||
});
|
||||
|
||||
let activeModelId = $derived.by(() => {
|
||||
const options = modelOptions();
|
||||
const options = modelsStore.models;
|
||||
|
||||
if (!isRouter) {
|
||||
return options.length > 0 ? options[0].model : null;
|
||||
}
|
||||
|
||||
const selectedId = selectedModelId();
|
||||
const selectedId = modelsStore.selectedModelId;
|
||||
|
||||
if (selectedId) {
|
||||
const model = options.find((m) => m.id === selectedId);
|
||||
@@ -140,7 +131,7 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
hasModelSelected = !isRouter || !!conversationModel || !!selectedModelId();
|
||||
hasModelSelected = !isRouter || !!conversationModel || !!modelsStore.selectedModelId;
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
import { Mic, Square } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
|
||||
+44
-27
@@ -10,18 +10,11 @@
|
||||
ChatFormContextGauge
|
||||
} from '$lib/components/app';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ROUTES } from '$lib/constants/routes';
|
||||
import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
|
||||
import { setChatFormActionsContext } from '$lib/contexts';
|
||||
import { FileTypeCategory, MessageRole } from '$lib/enums';
|
||||
import { ChatService } from '$lib/services';
|
||||
import {
|
||||
activeProcessingState,
|
||||
isChatStreaming,
|
||||
isLoading as chatIsLoading
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import { activeMessages, conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { chatStore, conversationsStore, mcpStore, settingsStore } from '$lib/stores';
|
||||
import { getFileTypeCategory } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
@@ -62,7 +55,7 @@
|
||||
uploadedFiles = []
|
||||
}: Props = $props();
|
||||
|
||||
let currentConfig = $derived(config());
|
||||
let currentConfig = $derived(settingsStore.config);
|
||||
|
||||
let hasMcpPromptsSupport = $derived.by(() => {
|
||||
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
|
||||
@@ -104,7 +97,7 @@
|
||||
let hasProcessedTokens = $derived.by(() => {
|
||||
if (!page.params.id) return false;
|
||||
|
||||
const messages = activeMessages() as DatabaseMessage[];
|
||||
const messages = conversationsStore.activeMessages as DatabaseMessage[];
|
||||
|
||||
let totalHistoricalTokens = 0;
|
||||
|
||||
@@ -126,9 +119,9 @@
|
||||
|
||||
if (totalHistoricalTokens > 0) return true;
|
||||
|
||||
if (!chatIsLoading() && !isChatStreaming()) return false;
|
||||
if (!chatStore.isLoading && !chatStore.isStreaming()) return false;
|
||||
|
||||
const processingState = activeProcessingState();
|
||||
const processingState = chatStore.activeProcessingState;
|
||||
|
||||
if (!processingState) return false;
|
||||
|
||||
@@ -140,6 +133,42 @@
|
||||
|
||||
return livePromptTokens > 0 || liveOutputTokens > 0;
|
||||
});
|
||||
|
||||
setChatFormActionsContext({
|
||||
get disabled() {
|
||||
return disabled;
|
||||
},
|
||||
get hasAudioModality() {
|
||||
return hasAudioModality;
|
||||
},
|
||||
get hasMcpPromptsSupport() {
|
||||
return hasMcpPromptsSupport;
|
||||
},
|
||||
get hasMcpResourcesSupport() {
|
||||
return hasMcpResourcesSupport;
|
||||
},
|
||||
get hasVideoModality() {
|
||||
return hasVideoModality;
|
||||
},
|
||||
get hasVisionModality() {
|
||||
return hasVisionModality;
|
||||
},
|
||||
get onFileUpload() {
|
||||
return onFileUpload;
|
||||
},
|
||||
get onMcpPromptClick() {
|
||||
return onMcpPromptClick;
|
||||
},
|
||||
get onMcpResourcesClick() {
|
||||
return onMcpResourcesClick;
|
||||
},
|
||||
get onMcpSettingsClick() {
|
||||
return () => goto(ROUTES.MCP_SERVERS);
|
||||
},
|
||||
get onSystemPromptClick() {
|
||||
return onSystemPromptClick;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -148,19 +177,7 @@
|
||||
>
|
||||
{#if showAddButton}
|
||||
<div class="mr-auto flex items-center gap-2">
|
||||
<ChatFormActionsAdd
|
||||
{disabled}
|
||||
{hasAudioModality}
|
||||
{hasVideoModality}
|
||||
{hasVisionModality}
|
||||
{hasMcpPromptsSupport}
|
||||
{hasMcpResourcesSupport}
|
||||
{onFileUpload}
|
||||
{onSystemPromptClick}
|
||||
{onMcpPromptClick}
|
||||
{onMcpResourcesClick}
|
||||
onMcpSettingsClick={() => goto(ROUTES.MCP_SERVERS)}
|
||||
/>
|
||||
<ChatFormActionsAdd />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
+7
-7
@@ -1,32 +1,32 @@
|
||||
<script lang="ts">
|
||||
import ContextGaugeDial from './ContextGaugeDial.svelte';
|
||||
import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
|
||||
import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
chatStore,
|
||||
conversationsStore,
|
||||
gaugeTriggerClick,
|
||||
gaugeTriggerEnter,
|
||||
gaugeTriggerKeydown,
|
||||
gaugeTriggerLeave,
|
||||
gaugeTriggerPointerDown
|
||||
} from '$lib/stores/context-gauge-popup.svelte';
|
||||
import { activeConversation, activeMessages } from '$lib/stores/conversations.svelte';
|
||||
} from '$lib/stores';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
const gauge = useContextGauge();
|
||||
|
||||
$effect(() => {
|
||||
const conv = activeConversation();
|
||||
const conv = conversationsStore.activeConversation;
|
||||
|
||||
untrack(() => chatStore.setActiveProcessingConversation(conv?.id ?? null));
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const conv = activeConversation();
|
||||
const messages = activeMessages() as DatabaseMessage[];
|
||||
const conv = conversationsStore.activeConversation;
|
||||
const messages = conversationsStore.activeMessages as DatabaseMessage[];
|
||||
|
||||
if (!conv) return;
|
||||
|
||||
if (isLoading() || isChatStreaming()) return;
|
||||
if (chatStore.isLoading || chatStore.isStreaming()) return;
|
||||
|
||||
if (messages.length === 0) {
|
||||
untrack(() => chatStore.clearProcessingState(conv.id));
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ColorLevel } from './context-gauge';
|
||||
import { colorLevelTextClass } from './context-gauge';
|
||||
import type { ColorLevel } from '$lib/enums';
|
||||
|
||||
interface Props {
|
||||
percent: number | null;
|
||||
|
||||
+2
-7
@@ -3,12 +3,7 @@
|
||||
import ContextGaugeDetails from './ContextGaugeDetails.svelte';
|
||||
import ContextGaugeLoadModel from './ContextGaugeLoadModel.svelte';
|
||||
import { useContextGauge } from '$lib/hooks/use-context-gauge.svelte';
|
||||
import {
|
||||
gaugeCardEnter,
|
||||
gaugeCardLeave,
|
||||
gaugePopup,
|
||||
gaugePopupClose
|
||||
} from '$lib/stores/context-gauge-popup.svelte';
|
||||
import { gaugeCardEnter, gaugeCardLeave, gaugePopup, gaugePopupClose } from '$lib/stores';
|
||||
import { formatParameters } from '$lib/utils/formatters';
|
||||
|
||||
const gauge = useContextGauge();
|
||||
@@ -92,7 +87,7 @@
|
||||
<span class={colorLevelTextClass(gauge.colorLevel)}>{gauge.contextPercent}%</span> used
|
||||
</span>
|
||||
<span>
|
||||
{formatParameters((gauge.contextTotal ?? 0) - gauge.contextUsed)} remaining
|
||||
{formatParameters(gauge.contextAvailable ?? 0)} remaining
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
+11
-11
@@ -1,25 +1,25 @@
|
||||
export type ColorLevel = 'ok' | 'warning' | 'critical' | 'neutral';
|
||||
import { ColorLevel } from '$lib/enums';
|
||||
|
||||
const WARNING_THRESHOLD = 80;
|
||||
const CRITICAL_THRESHOLD = 95;
|
||||
|
||||
export function colorLevelFromPercent(percent: number | null): ColorLevel {
|
||||
if (percent === null) return 'neutral';
|
||||
if (percent === null) return ColorLevel.NEUTRAL;
|
||||
|
||||
if (percent >= CRITICAL_THRESHOLD) return 'critical';
|
||||
if (percent >= CRITICAL_THRESHOLD) return ColorLevel.CRITICAL;
|
||||
|
||||
if (percent >= WARNING_THRESHOLD) return 'warning';
|
||||
if (percent >= WARNING_THRESHOLD) return ColorLevel.WARNING;
|
||||
|
||||
return 'ok';
|
||||
return ColorLevel.OK;
|
||||
}
|
||||
|
||||
export function colorLevelTextClass(level: ColorLevel): string {
|
||||
switch (level) {
|
||||
case 'critical':
|
||||
case ColorLevel.CRITICAL:
|
||||
return 'text-red-400';
|
||||
case 'warning':
|
||||
case ColorLevel.WARNING:
|
||||
return 'text-amber-400';
|
||||
case 'ok':
|
||||
case ColorLevel.OK:
|
||||
return 'text-muted-foreground';
|
||||
default:
|
||||
return 'text-muted-foreground';
|
||||
@@ -28,11 +28,11 @@ export function colorLevelTextClass(level: ColorLevel): string {
|
||||
|
||||
export function colorLevelBgClass(level: ColorLevel): string {
|
||||
switch (level) {
|
||||
case 'critical':
|
||||
case ColorLevel.CRITICAL:
|
||||
return 'bg-red-500';
|
||||
case 'warning':
|
||||
case ColorLevel.WARNING:
|
||||
return 'bg-amber-500';
|
||||
case 'ok':
|
||||
case ColorLevel.OK:
|
||||
return 'bg-green-500';
|
||||
default:
|
||||
return 'bg-muted';
|
||||
|
||||
+15
-24
@@ -1,29 +1,20 @@
|
||||
<script lang="ts">
|
||||
import ChatFormWorkingDirectoryChip from './ChatFormWorkingDirectoryChip.svelte';
|
||||
import ChatFormWorkingDirectoryResultsList from './ChatFormWorkingDirectoryResultsList.svelte';
|
||||
import ChatFormCurrentWorkingDirectoryChip from './ChatFormCurrentWorkingDirectoryChip.svelte';
|
||||
import ChatFormCurrentWorkingDirectoryResultsList from './ChatFormCurrentWorkingDirectoryResultsList.svelte';
|
||||
import { FolderOpen } from '@lucide/svelte';
|
||||
import SearchInput from '$lib/components/app/forms/SearchInput.svelte';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import {
|
||||
DEFAULT_MOBILE_BREAKPOINT,
|
||||
HOME_TILDE,
|
||||
MAX_RESULTS_SHOWN,
|
||||
NATIVE_LIMIT,
|
||||
NATIVE_MAX_DEPTH,
|
||||
SEARCH_DEBOUNCE_MS,
|
||||
SEARCH_LIMIT,
|
||||
SEARCH_MAX_DEPTH
|
||||
} from '$lib/constants';
|
||||
import { DEFAULT_MOBILE_BREAKPOINT, HOME_TILDE, SEARCH, UI_DATA_ATTRS } from '$lib/constants';
|
||||
import { BuiltInTool, GlobSearchType, KeyboardKey } from '$lib/enums';
|
||||
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
|
||||
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
||||
import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
|
||||
import { ToolsService } from '$lib/services/tools.service';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { GlobEntry } from '$lib/types';
|
||||
import {
|
||||
abbreviateHome,
|
||||
buildCaseInsensitiveGlob,
|
||||
type GlobEntry,
|
||||
joinPath,
|
||||
lastPathSegment,
|
||||
runGlobSearchWithChildren
|
||||
@@ -129,7 +120,7 @@
|
||||
});
|
||||
|
||||
useScrollActiveRow({
|
||||
dataIndex: 'result',
|
||||
dataAttr: UI_DATA_ATTRS.RESULT_INDEX,
|
||||
getContainer: () => listContainer,
|
||||
getCount: () => queryResults.length,
|
||||
getIndex: () => nav.hoveredIndex,
|
||||
@@ -142,7 +133,7 @@
|
||||
// children too, so path navigation does not require a trailing slash.
|
||||
const search = useDebouncedSearch({
|
||||
canRun: () => isOpen && fileSearchEnabled,
|
||||
debounceMs: SEARCH_DEBOUNCE_MS,
|
||||
debounceMs: SEARCH.DEBOUNCE_MS,
|
||||
getQuery: () => query.trim(),
|
||||
run: async (q, signal, isCurrent) => {
|
||||
const trimmed = q.trim();
|
||||
@@ -162,8 +153,8 @@
|
||||
const res = await runGlobSearchWithChildren(
|
||||
trimmed,
|
||||
homeBase ?? HOME_TILDE,
|
||||
SEARCH_MAX_DEPTH,
|
||||
SEARCH_LIMIT,
|
||||
SEARCH.MAX_DEPTH,
|
||||
SEARCH.LIMIT,
|
||||
signal,
|
||||
{ type: GlobSearchType.DIR }
|
||||
);
|
||||
@@ -179,7 +170,7 @@
|
||||
}
|
||||
|
||||
searchScope = res.exactDir ?? res.args.path;
|
||||
queryResults = res.entries.map((e) => e.path).slice(0, MAX_RESULTS_SHOWN);
|
||||
queryResults = res.entries.map((e) => e.path).slice(0, SEARCH.MAX_RESULTS_SHOWN);
|
||||
|
||||
if (queryResults.length > 0) {
|
||||
nav.reset(0);
|
||||
@@ -223,8 +214,8 @@
|
||||
try {
|
||||
const res = await ToolsService.executeToolRaw(BuiltInTool.FILE_GLOB_SEARCH, {
|
||||
include: buildCaseInsensitiveGlob(name),
|
||||
limit: NATIVE_LIMIT,
|
||||
max_depth: NATIVE_MAX_DEPTH,
|
||||
limit: SEARCH.NATIVE_LIMIT,
|
||||
max_depth: SEARCH.NATIVE_MAX_DEPTH,
|
||||
path: homeBase ?? HOME_TILDE,
|
||||
type: GlobSearchType.DIR
|
||||
});
|
||||
@@ -259,7 +250,7 @@
|
||||
// user cancelled - silently ignore; other errors are logged
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return;
|
||||
|
||||
console.error('[ChatFormWorkingDirectory] showDirectoryPicker failed:', err);
|
||||
console.error('[ChatFormCurrentWorkingDirectory] showDirectoryPicker failed:', err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +331,7 @@
|
||||
onclick={onOpen}
|
||||
{disabled}
|
||||
>
|
||||
<ChatFormWorkingDirectoryChip
|
||||
<ChatFormCurrentWorkingDirectoryChip
|
||||
{directory}
|
||||
{homeBase}
|
||||
{disabled}
|
||||
@@ -381,7 +372,7 @@
|
||||
{#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)}
|
||||
<ChatFormWorkingDirectoryResultsList
|
||||
<ChatFormCurrentWorkingDirectoryResultsList
|
||||
results={queryResults}
|
||||
hoveredIndex={nav.hoveredIndex}
|
||||
isSearching={search.isSearching}
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Folder } from '@lucide/svelte';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { UI_DATA_ATTRS } from '$lib/constants';
|
||||
import { highlightMatch } from '$lib/utils';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
@@ -46,7 +47,7 @@
|
||||
{#each results as path, index (path)}
|
||||
<button
|
||||
type="button"
|
||||
data-result-index={index}
|
||||
{...{ [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'
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import ChatFormInputBasic from './ChatFormInputBasic.svelte';
|
||||
import ChatFormInputRich from './ChatFormInputRich.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
onInput?: () => void;
|
||||
onKeydown?: (event: KeyboardEvent) => void;
|
||||
onPaste?: (event: ClipboardEvent) => void;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
useRichInput?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
disabled = false,
|
||||
onInput,
|
||||
onKeydown,
|
||||
onPaste,
|
||||
placeholder = 'Ask anything...',
|
||||
useRichInput = false,
|
||||
value = $bindable('')
|
||||
}: Props = $props();
|
||||
|
||||
let basicRef: ChatFormInputBasic | undefined = $state();
|
||||
let richRef: ChatFormInputRich | undefined = $state();
|
||||
|
||||
// The two renderers share one imperative handle (focus/caret/height), so
|
||||
// the parent can drive whichever variant is mounted through this one.
|
||||
export function getElement() {
|
||||
return useRichInput ? richRef?.getElement() : basicRef?.getElement();
|
||||
}
|
||||
|
||||
export function focus() {
|
||||
if (useRichInput) richRef?.focus();
|
||||
else basicRef?.focus();
|
||||
}
|
||||
|
||||
export function resetHeight() {
|
||||
if (useRichInput) richRef?.resetHeight();
|
||||
else basicRef?.resetHeight();
|
||||
}
|
||||
|
||||
export function getCaretOffset(): number {
|
||||
return useRichInput ? (richRef?.getCaretOffset() ?? 0) : (basicRef?.getCaretOffset() ?? 0);
|
||||
}
|
||||
|
||||
export function setCaretOffset(offset: number) {
|
||||
if (useRichInput) richRef?.setCaretOffset(offset);
|
||||
else basicRef?.setCaretOffset(offset);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if useRichInput}
|
||||
<ChatFormInputRich
|
||||
bind:this={richRef}
|
||||
class={className}
|
||||
{disabled}
|
||||
{onInput}
|
||||
{onKeydown}
|
||||
{onPaste}
|
||||
{placeholder}
|
||||
bind:value
|
||||
/>
|
||||
{:else}
|
||||
<ChatFormInputBasic
|
||||
bind:this={basicRef}
|
||||
class={className}
|
||||
{disabled}
|
||||
{onInput}
|
||||
{onKeydown}
|
||||
{onPaste}
|
||||
{placeholder}
|
||||
bind:value
|
||||
/>
|
||||
{/if}
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import { isMobile } from '$lib/stores';
|
||||
import { autoResizeTextarea } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Plain-text caret offsets, shared with the contenteditable variant so
|
||||
// Plain-text caret offsets, shared with the rich chat form input variant so
|
||||
// the picker/paste flows can address either renderer through one handle.
|
||||
export function getCaretOffset(): number {
|
||||
if (!textareaElement) return 0;
|
||||
+44
-25
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { TRIM_LEADING_PADDING_REGEX, TRIM_TRAILING_PADDING_REGEX } from '$lib/constants';
|
||||
import { ColorMode } from '$lib/enums';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
|
||||
import { CODE_BLOCK, CODE_TOKEN_ATTR, UI_DATA_ATTRS } from '$lib/constants';
|
||||
import { BooleanString, ChatFormInputRichTokenKind, ColorMode } from '$lib/enums';
|
||||
import { isMobile } from '$lib/stores';
|
||||
import type { ChatFormInputRichToken } from '$lib/types';
|
||||
import type { SourceHistoryEntry } from '$lib/utils';
|
||||
import {
|
||||
badgeAwareWordJump,
|
||||
buildFragment,
|
||||
@@ -52,7 +53,7 @@
|
||||
// browser's native undo stack.
|
||||
const history = new SourceHistory();
|
||||
|
||||
// Browsers disagree on what an empty contenteditable contains (`<br>`,
|
||||
// Browsers disagree on what an empty rich chat form input contains (`<br>`,
|
||||
// `<div><br></div>`, or nothing), so emptiness is decided by the
|
||||
// serialized source, not the DOM shape.
|
||||
function syncEmptyState(serialized?: string) {
|
||||
@@ -60,15 +61,15 @@
|
||||
|
||||
const source = serialized ?? serializeContent(rootElement);
|
||||
|
||||
rootElement.dataset.empty = source.length === 0 ? 'true' : 'false';
|
||||
rootElement.dataset.empty = source.length === 0 ? BooleanString.TRUE : BooleanString.FALSE;
|
||||
}
|
||||
|
||||
function renderTokens(tokens: ContentToken[]) {
|
||||
function renderTokens(tokens: ChatFormInputRichToken[]) {
|
||||
if (!rootElement) return;
|
||||
|
||||
const caret = rangeToTextOffset(rootElement, safeRange());
|
||||
|
||||
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
|
||||
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the rich chat form input host, never its children
|
||||
rootElement.replaceChildren(buildFragment(tokens));
|
||||
|
||||
syncCodeBlockHatches(rootElement);
|
||||
@@ -105,8 +106,8 @@
|
||||
const prefix = open[0];
|
||||
const language = open[1].trim().split(/\s+/)[0] ?? '';
|
||||
const content = segment.slice(prefix.length, -3);
|
||||
const leading = content.match(TRIM_LEADING_PADDING_REGEX)?.[0] ?? '';
|
||||
const trailing = content.match(TRIM_TRAILING_PADDING_REGEX)?.[0] ?? '';
|
||||
const leading = content.match(CODE_BLOCK.TRIM_LEADING_PADDING_REGEX)?.[0] ?? '';
|
||||
const trailing = content.match(CODE_BLOCK.TRIM_TRAILING_PADDING_REGEX)?.[0] ?? '';
|
||||
const core = content.slice(leading.length, content.length - trailing.length);
|
||||
// autoDetect off: re-guessing the language on every keystroke
|
||||
// costs ~38ms a call and flickers while typing
|
||||
@@ -126,7 +127,9 @@
|
||||
}
|
||||
|
||||
function highlightCodeBlocks(root: HTMLElement) {
|
||||
for (const el of root.querySelectorAll<HTMLElement>('code[data-code-token="block"]')) {
|
||||
for (const el of root.querySelectorAll<HTMLElement>(
|
||||
`code[${CODE_TOKEN_ATTR}="${ChatFormInputRichTokenKind.CODE_BLOCK}"]`
|
||||
)) {
|
||||
highlightCodeBlockElement(el);
|
||||
}
|
||||
}
|
||||
@@ -150,7 +153,10 @@
|
||||
}
|
||||
|
||||
while (node && node !== rootElement) {
|
||||
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
|
||||
if (
|
||||
node instanceof HTMLElement &&
|
||||
node.getAttribute(CODE_TOKEN_ATTR) === ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
) {
|
||||
const caret = rangeToTextOffset(rootElement, range);
|
||||
|
||||
if (highlightCodeBlockElement(node)) {
|
||||
@@ -188,11 +194,13 @@
|
||||
* (deduped via the data attribute) swapped on mode change.
|
||||
*/
|
||||
function loadHighlightTheme(isDark: boolean) {
|
||||
document.querySelectorAll('style[data-highlight-theme-preview]').forEach((s) => s.remove());
|
||||
document
|
||||
.querySelectorAll(`style[${UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW}]`)
|
||||
.forEach((s) => s.remove());
|
||||
|
||||
const style = document.createElement('style');
|
||||
|
||||
style.setAttribute('data-highlight-theme-preview', 'true');
|
||||
style.setAttribute(UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW, BooleanString.TRUE);
|
||||
style.textContent = isDark ? githubDarkCss : githubLightCss;
|
||||
|
||||
document.head.appendChild(style);
|
||||
@@ -310,7 +318,7 @@
|
||||
source[source.length - 2] !== '\n' &&
|
||||
last?.nodeType === Node.TEXT_NODE
|
||||
) {
|
||||
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
|
||||
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the rich chat form input host, never its children
|
||||
rootElement.appendChild(document.createTextNode('\n'));
|
||||
restoreCaret(source.length);
|
||||
resizeHeight();
|
||||
@@ -403,7 +411,10 @@
|
||||
let node: Node | null = container.parentNode;
|
||||
|
||||
while (node && node !== rootElement) {
|
||||
if (node instanceof HTMLElement && node.dataset.codeToken === 'block') {
|
||||
if (
|
||||
node instanceof HTMLElement &&
|
||||
node.getAttribute(CODE_TOKEN_ATTR) === ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
) {
|
||||
const tail = document.createRange();
|
||||
|
||||
tail.setStart(container, offset);
|
||||
@@ -461,7 +472,11 @@
|
||||
|
||||
const first = rootElement.firstChild;
|
||||
|
||||
if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'block') return false;
|
||||
if (
|
||||
!(first instanceof HTMLElement) ||
|
||||
first.getAttribute(CODE_TOKEN_ATTR) !== ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
)
|
||||
return false;
|
||||
|
||||
const range = safeRange();
|
||||
|
||||
@@ -482,7 +497,7 @@
|
||||
if (firstLineEnd !== -1 && caret > firstLineEnd) return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the contenteditable host, never its children
|
||||
// eslint-disable-next-line svelte/no-dom-manipulating -- the token layer is owned imperatively; Svelte renders only the rich chat form input host, never its children
|
||||
rootElement.prepend(document.createElement('br'));
|
||||
restoreCaret(0, extend);
|
||||
|
||||
@@ -506,7 +521,11 @@
|
||||
|
||||
const second = first.nextSibling;
|
||||
|
||||
if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'block') return;
|
||||
if (
|
||||
!(second instanceof HTMLElement) ||
|
||||
second.getAttribute(CODE_TOKEN_ATTR) !== ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
)
|
||||
return;
|
||||
|
||||
const range = safeRange();
|
||||
const onHatch =
|
||||
@@ -786,7 +805,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-1 {className}">
|
||||
<div class="flex-1 {className} mb-0.5">
|
||||
<div
|
||||
bind:this={rootElement}
|
||||
contenteditable={!disabled}
|
||||
@@ -797,7 +816,7 @@
|
||||
data-placeholder={placeholder}
|
||||
tabindex={disabled ? -1 : 0}
|
||||
class={[
|
||||
'chat-form-contenteditable text-md min-h-12 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
'chat-form-input-rich text-md min-h-12 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word border-0 bg-transparent p-0 leading-6 outline-none focus-visible:ring-0 focus-visible:ring-offset-0',
|
||||
disabled && 'cursor-not-allowed'
|
||||
]}
|
||||
style="max-height: var(--max-message-height);"
|
||||
@@ -814,18 +833,18 @@
|
||||
<style>
|
||||
/* pre-wrap is load-bearing: without it Chromium collapses \n in
|
||||
text nodes and converts them to spaces while typing */
|
||||
.chat-form-contenteditable {
|
||||
.chat-form-input-rich {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.chat-form-contenteditable:global([data-empty='true'])::before {
|
||||
.chat-form-input-rich:global([data-empty='true'])::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--muted-foreground);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Inline code - mirrors markdown-content.css */
|
||||
.chat-form-contenteditable :global(code[data-code-token='inline']) {
|
||||
.chat-form-input-rich :global(code[data-code-token='code_inline']) {
|
||||
background: var(--muted);
|
||||
color: var(--muted-foreground);
|
||||
padding: 0.125rem 0.375rem;
|
||||
@@ -834,7 +853,7 @@
|
||||
}
|
||||
|
||||
/* Fenced code block - mirrors .code-block-wrapper in markdown-content.css */
|
||||
.chat-form-contenteditable :global(code[data-code-token='block']) {
|
||||
.chat-form-input-rich :global(code[data-code-token='code_block']) {
|
||||
display: block;
|
||||
margin: 0.25rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
@@ -3,11 +3,7 @@
|
||||
ChatAttachmentsListItemMcpResource,
|
||||
HorizontalScrollCarousel
|
||||
} from '$lib/components/app';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import {
|
||||
mcpHasResourceAttachments,
|
||||
mcpResourceAttachments
|
||||
} from '$lib/stores/mcp-resources.svelte';
|
||||
import { mcpResourceStore, mcpStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -16,8 +12,8 @@
|
||||
|
||||
let { class: className, onResourceClick }: Props = $props();
|
||||
|
||||
const attachments = $derived(mcpResourceAttachments());
|
||||
const hasAttachments = $derived(mcpHasResourceAttachments());
|
||||
const attachments = $derived(mcpResourceStore.attachments);
|
||||
const hasAttachments = $derived(mcpResourceStore.hasAttachments);
|
||||
|
||||
function handleRemove(attachmentId: string) {
|
||||
mcpStore.removeResourceAttachment(attachmentId);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { mcpStore } from '$lib/stores';
|
||||
import type { MCPServerSettingsEntry } from '$lib/types';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" generics="T">
|
||||
import { SearchInput } from '$lib/components/app';
|
||||
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
|
||||
import { CHAT_FORM_POPOVER_MAX_HEIGHT } from '$lib/constants';
|
||||
import { CHAT_FORM_POPOVER_MAX_HEIGHT, UI_DATA_ATTRS } from '$lib/constants';
|
||||
import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
// selectedIndex/items.length are untracked so hover and result replacement
|
||||
// never re-fire the scroll; keyboard nav is the only path that bumps the trigger.
|
||||
useScrollActiveRow({
|
||||
dataIndex: 'picker',
|
||||
dataAttr: UI_DATA_ATTRS.PICKER_INDEX,
|
||||
getContainer: () => listContainer,
|
||||
getCount: () => items.length,
|
||||
getIndex: () => selectedIndex,
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { UI_DATA_ATTRS } from '$lib/constants';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -24,7 +25,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-picker-index={dataIndex}
|
||||
{...{ [UI_DATA_ATTRS.PICKER_INDEX]: dataIndex }}
|
||||
{disabled}
|
||||
{onclick}
|
||||
{onmouseenter}
|
||||
|
||||
+1
-2
@@ -9,8 +9,7 @@
|
||||
} from '$lib/components/app/chat';
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
import { KeyboardKey } from '$lib/enums';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { conversationsStore, mcpStore } from '$lib/stores';
|
||||
import type { GetPromptResult, MCPPromptInfo, MCPServerSettingsEntry } from '$lib/types';
|
||||
import { debounce, uuid } from '$lib/utils';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
+7
-13
@@ -4,19 +4,13 @@
|
||||
import HighlightedMatch from '$lib/components/app/forms/HighlightedMatch.svelte';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import {
|
||||
FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH,
|
||||
HOME_TILDE,
|
||||
SEARCH_DEBOUNCE_MS
|
||||
} from '$lib/constants';
|
||||
import { FILE_GLOB_SEARCH_PICKERS, HOME_TILDE, SEARCH } from '$lib/constants';
|
||||
import { BuiltInTool, FileMentionEntryType, GlobSearchType, KeyboardKey } from '$lib/enums';
|
||||
import { useDebouncedSearch } from '$lib/hooks/use-debounced-search.svelte';
|
||||
import { usePickerNavigation } from '$lib/hooks/use-picker-navigation.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import type { FileMentionEntry } from '$lib/types';
|
||||
import { abbreviateHome, type GlobEntryResult, runGlobSearchWithChildren } from '$lib/utils';
|
||||
import { isMobile, settingsStore, toolsStore } from '$lib/stores';
|
||||
import type { FileMentionEntry, GlobEntryResult } from '$lib/types';
|
||||
import { abbreviateHome, runGlobSearchWithChildren } from '$lib/utils';
|
||||
|
||||
/**
|
||||
* Floating file/folder mention picker. The chat input is the search
|
||||
@@ -68,9 +62,9 @@
|
||||
// Coerce the depth setting to a positive integer; an invalid value
|
||||
// would otherwise reach the server as max_depth 0 = unlimited.
|
||||
const searchDepth = $derived.by(() => {
|
||||
const n = Number(config().mentionSearchMaxDepth);
|
||||
const n = Number(settingsStore.config.mentionSearchMaxDepth);
|
||||
|
||||
return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS_DEFAULT_SEARCH_DEPTH;
|
||||
return Number.isInteger(n) && n > 0 ? n : FILE_GLOB_SEARCH_PICKERS.DEFAULT_SEARCH_DEPTH;
|
||||
});
|
||||
|
||||
const home = $derived(toolsStore.serverHome);
|
||||
@@ -80,7 +74,7 @@
|
||||
|
||||
const search = useDebouncedSearch({
|
||||
canRun: () => isOpen && fileSearchEnabled,
|
||||
debounceMs: SEARCH_DEBOUNCE_MS,
|
||||
debounceMs: SEARCH.DEBOUNCE_MS,
|
||||
getQuery: () => trimmedQuery,
|
||||
run: async (query, signal, isCurrent) => {
|
||||
try {
|
||||
+6
-6
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ChatFormCommandPicker from './ChatFormCommandPicker.svelte';
|
||||
import ChatFormMentionPicker from './ChatFormMentionPicker.svelte';
|
||||
import ChatFormPickerCommand from './ChatFormPickerCommand.svelte';
|
||||
import ChatFormPickerMcpPrompts from './ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte';
|
||||
import ChatFormPickerMention from './ChatFormPickerMention.svelte';
|
||||
import type {
|
||||
ChatFormCommand,
|
||||
FileMentionEntry,
|
||||
@@ -55,9 +55,9 @@
|
||||
scopePath
|
||||
}: Props = $props();
|
||||
|
||||
let commandPickerRef: ChatFormCommandPicker | undefined = $state(undefined);
|
||||
let commandPickerRef: ChatFormPickerCommand | undefined = $state(undefined);
|
||||
let promptPickerRef: ChatFormPickerMcpPrompts | undefined = $state(undefined);
|
||||
let mentionPickerRef: ChatFormMentionPicker | undefined = $state(undefined);
|
||||
let mentionPickerRef: ChatFormPickerMention | undefined = $state(undefined);
|
||||
|
||||
/** Delegate keyboard events to the active picker child; true if handled. */
|
||||
export function handleKeydown(event: KeyboardEvent): boolean {
|
||||
@@ -77,7 +77,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ChatFormCommandPicker
|
||||
<ChatFormPickerCommand
|
||||
bind:this={commandPickerRef}
|
||||
isOpen={isCommandPickerOpen ?? false}
|
||||
query={commandQuery ?? ''}
|
||||
@@ -96,7 +96,7 @@
|
||||
{onPromptLoadError}
|
||||
/>
|
||||
|
||||
<ChatFormMentionPicker
|
||||
<ChatFormPickerMention
|
||||
bind:this={mentionPickerRef}
|
||||
isOpen={isMentionPickerOpen ?? false}
|
||||
query={mentionQuery ?? ''}
|
||||
|
||||
@@ -7,21 +7,22 @@
|
||||
ChatMessageSystem,
|
||||
ChatMessageUser
|
||||
} from '$lib/components/app/chat';
|
||||
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
|
||||
import { REASONING_TAGS } from '$lib/constants/agentic';
|
||||
import { ROUTES } from '$lib/constants/routes';
|
||||
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
|
||||
import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
|
||||
import { setChatMessageActionsContext, setChatMessageEditContext } from '$lib/contexts';
|
||||
import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
|
||||
import { DatabaseService } from '$lib/services/database.service';
|
||||
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
|
||||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||
import { chatStore, conversationsStore, isMobile } from '$lib/stores';
|
||||
import type {
|
||||
ChatMessageActions,
|
||||
ChatMessageDeletionInfo,
|
||||
DatabaseMessageExtraMcpPrompt
|
||||
} from '$lib/types';
|
||||
import { deriveAgenticSections } from '$lib/utils';
|
||||
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
chatActions: ChatMessageActions;
|
||||
message: DatabaseMessage;
|
||||
toolMessages?: DatabaseMessage[];
|
||||
isLastAssistantMessage?: boolean;
|
||||
@@ -31,6 +32,7 @@
|
||||
}
|
||||
|
||||
let {
|
||||
chatActions,
|
||||
class: className = '',
|
||||
isLastAssistantMessage = false,
|
||||
isLastUserMessage = false,
|
||||
@@ -40,14 +42,7 @@
|
||||
toolMessages = []
|
||||
}: Props = $props();
|
||||
|
||||
const chatActions = getChatActionsContext();
|
||||
|
||||
let deletionInfo = $state<{
|
||||
totalCount: number;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
messageTypes: string[];
|
||||
} | null>(null);
|
||||
let deletionInfo = $state<ChatMessageDeletionInfo | null>(null);
|
||||
// The system message placeholder must never surface as editable content; keeping
|
||||
// it in the derived (not just in handleEdit) guards against prop invalidation
|
||||
// reverting the override while editing
|
||||
@@ -116,7 +111,7 @@
|
||||
let showSaveOnlyOption = $derived(message.role === MessageRole.USER);
|
||||
let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
|
||||
|
||||
setMessageEditContext({
|
||||
setChatMessageEditContext({
|
||||
cancel: handleCancelEdit,
|
||||
get editedContent() {
|
||||
return editedContent;
|
||||
@@ -170,6 +165,30 @@
|
||||
startEdit: handleEdit
|
||||
});
|
||||
|
||||
setChatMessageActionsContext({
|
||||
confirmDelete: handleConfirmDelete,
|
||||
copy: handleCopy,
|
||||
get deletionInfo() {
|
||||
return deletionInfo;
|
||||
},
|
||||
get forkConversation() {
|
||||
const isForkableUser = message.role === MessageRole.USER && !mcpPromptExtra;
|
||||
|
||||
return isForkableUser || message.role === MessageRole.ASSISTANT
|
||||
? handleForkConversation
|
||||
: undefined;
|
||||
},
|
||||
navigateToSibling: handleNavigateToSibling,
|
||||
requestDelete: handleDelete,
|
||||
setShowDeleteDialog: handleShowDeleteDialogChange,
|
||||
get showDeleteDialog() {
|
||||
return showDeleteDialog;
|
||||
},
|
||||
get siblingInfo() {
|
||||
return siblingInfo;
|
||||
}
|
||||
});
|
||||
|
||||
let mcpPromptExtra = $derived.by(() => {
|
||||
if (message.role !== MessageRole.USER) return null;
|
||||
|
||||
@@ -187,7 +206,7 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const pendingId = pendingEditMessageId();
|
||||
const pendingId = chatStore.pendingEditMessageId;
|
||||
|
||||
if (pendingId && pendingId === message.id && !isEditing) {
|
||||
handleEdit();
|
||||
@@ -364,73 +383,22 @@
|
||||
|
||||
<div class="chat-message" class:chat-message--synthetic={isSynthetic}>
|
||||
{#if message.role === MessageRole.SYSTEM}
|
||||
<ChatMessageSystem
|
||||
bind:textareaElement
|
||||
class={className}
|
||||
{deletionInfo}
|
||||
{message}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onCopy={handleCopy}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
onNavigateToSibling={handleNavigateToSibling}
|
||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||
{showDeleteDialog}
|
||||
{siblingInfo}
|
||||
/>
|
||||
<ChatMessageSystem bind:textareaElement class={className} {message} />
|
||||
{:else if mcpPromptExtra}
|
||||
<ChatMessageMcpPrompt
|
||||
class={className}
|
||||
{deletionInfo}
|
||||
{message}
|
||||
mcpPrompt={mcpPromptExtra}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onCopy={handleCopy}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
onNavigateToSibling={handleNavigateToSibling}
|
||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||
{showDeleteDialog}
|
||||
{siblingInfo}
|
||||
/>
|
||||
<ChatMessageMcpPrompt class={className} {message} mcpPrompt={mcpPromptExtra} />
|
||||
{:else if isSynthetic}
|
||||
<ChatMessageSynthetic {message} class={className} />
|
||||
{:else if message.role === MessageRole.USER}
|
||||
<ChatMessageUser
|
||||
class={className}
|
||||
{deletionInfo}
|
||||
{isLastUserMessage}
|
||||
{message}
|
||||
{nextAssistantMessage}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onCopy={handleCopy}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
onForkConversation={handleForkConversation}
|
||||
onNavigateToSibling={handleNavigateToSibling}
|
||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||
{showDeleteDialog}
|
||||
{siblingInfo}
|
||||
/>
|
||||
<ChatMessageUser class={className} {isLastUserMessage} {message} {nextAssistantMessage} />
|
||||
{:else}
|
||||
<ChatMessageAssistant
|
||||
bind:textareaElement
|
||||
class={className}
|
||||
{deletionInfo}
|
||||
{isLastAssistantMessage}
|
||||
{message}
|
||||
{toolMessages}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onContinue={handleContinue}
|
||||
onCopy={handleCopy}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
onForkConversation={handleForkConversation}
|
||||
onNavigateToSibling={handleNavigateToSibling}
|
||||
onRegenerate={handleRegenerate}
|
||||
onShowDeleteDialogChange={handleShowDeleteDialogChange}
|
||||
{showDeleteDialog}
|
||||
{siblingInfo}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+10
-48
@@ -8,76 +8,48 @@
|
||||
ChatMessageAssistantStatistics,
|
||||
ChatMessageEditForm
|
||||
} from '$lib/components/app';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||
import { chatStore, isChatStreaming, isLoading } from '$lib/stores/chat.svelte';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { isRouterMode } from '$lib/stores/server.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { chatStore, modelsStore, serverStore, settingsStore } from '$lib/stores';
|
||||
import { modelLoadProgressText } from '$lib/utils';
|
||||
import { hasAgenticContent } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
deletionInfo: {
|
||||
totalCount: number;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
messageTypes: string[];
|
||||
} | null;
|
||||
isLastAssistantMessage?: boolean;
|
||||
message: DatabaseMessage;
|
||||
toolMessages?: DatabaseMessage[];
|
||||
onCopy: () => void;
|
||||
onConfirmDelete: () => void;
|
||||
onContinue?: () => void;
|
||||
onDelete: () => void;
|
||||
onEdit?: () => void;
|
||||
onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
|
||||
onNavigateToSibling?: (siblingId: string) => void;
|
||||
onRegenerate: (modelOverride?: string) => void;
|
||||
onShowDeleteDialogChange: (show: boolean) => void;
|
||||
showDeleteDialog: boolean;
|
||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||
textareaElement?: HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
deletionInfo,
|
||||
isLastAssistantMessage = false,
|
||||
message,
|
||||
onConfirmDelete,
|
||||
onContinue,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onForkConversation,
|
||||
onNavigateToSibling,
|
||||
onRegenerate,
|
||||
onShowDeleteDialogChange,
|
||||
showDeleteDialog,
|
||||
siblingInfo = null,
|
||||
textareaElement = $bindable(),
|
||||
toolMessages = []
|
||||
}: Props = $props();
|
||||
|
||||
// Get edit context
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
const isAgentic = $derived(hasAgenticContent(message, toolMessages));
|
||||
const processingState = useProcessingState();
|
||||
|
||||
let currentConfig = $derived(config());
|
||||
let isRouter = $derived(isRouterMode());
|
||||
let currentConfig = $derived(settingsStore.config);
|
||||
let isRouter = $derived(serverStore.isRouterMode);
|
||||
|
||||
let showRawOutput = $state(false);
|
||||
|
||||
let displayedModel = $derived(message.model ?? null);
|
||||
|
||||
let isCurrentlyLoading = $derived(isLoading());
|
||||
let isStreaming = $derived(isChatStreaming());
|
||||
let isCurrentlyLoading = $derived(chatStore.isLoading);
|
||||
let isStreaming = $derived(chatStore.isStreaming());
|
||||
let hasNoContent = $derived(!message?.content?.trim());
|
||||
let isActivelyProcessing = $derived(isCurrentlyLoading || isStreaming);
|
||||
|
||||
@@ -175,7 +147,7 @@
|
||||
<ChatMessageAgenticContent
|
||||
{message}
|
||||
{toolMessages}
|
||||
isStreaming={isChatStreaming()}
|
||||
isStreaming={chatStore.isStreaming()}
|
||||
{isLastAssistantMessage}
|
||||
/>
|
||||
{/if}
|
||||
@@ -190,14 +162,14 @@
|
||||
<div class="inline-flex flex-wrap items-start gap-2 text-xs text-muted-foreground">
|
||||
<ChatMessageAssistantModel
|
||||
{displayedModel}
|
||||
isLoading={isLoading()}
|
||||
isLoading={chatStore.isLoading}
|
||||
{isRouter}
|
||||
{onRegenerate}
|
||||
/>
|
||||
|
||||
<ChatMessageAssistantStatistics
|
||||
{message}
|
||||
isLoading={isLoading()}
|
||||
isLoading={chatStore.isLoading}
|
||||
{processingState}
|
||||
showMessageStats={currentConfig.showMessageStats}
|
||||
/>
|
||||
@@ -210,18 +182,8 @@
|
||||
role={MessageRole.ASSISTANT}
|
||||
justify="start"
|
||||
actionsPosition="left"
|
||||
{siblingInfo}
|
||||
{showDeleteDialog}
|
||||
{deletionInfo}
|
||||
{onCopy}
|
||||
{onEdit}
|
||||
{onRegenerate}
|
||||
onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
|
||||
{onForkConversation}
|
||||
{onDelete}
|
||||
{onConfirmDelete}
|
||||
{onNavigateToSibling}
|
||||
{onShowDeleteDialogChange}
|
||||
showRawOutputSwitch={currentConfig.showRawOutputSwitch}
|
||||
rawOutputEnabled={showRawOutput}
|
||||
onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ModelBadge, ModelsSelectorDropdown } from '$lib/components/app';
|
||||
import { ServerModelStatus } from '$lib/enums';
|
||||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { modelsStore } from '$lib/stores';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
let { modelLoadingText, position, processingState }: Props = $props();
|
||||
|
||||
const marginClass = position === 'top' ? 'mt-6' : 'mt-4';
|
||||
const marginClass = $derived(position === 'top' ? 'mt-6' : 'mt-4');
|
||||
</script>
|
||||
|
||||
<div class="{marginClass} w-full max-w-3xl" in:fade>
|
||||
|
||||
+19
-1
@@ -2,6 +2,7 @@
|
||||
import { ChatMessageStatistics } from '$lib/components/app';
|
||||
import { ChatMessageStatisticsMode } from '$lib/enums';
|
||||
import type { UseProcessingStateReturn } from '$lib/hooks/use-processing-state.svelte';
|
||||
import { agenticStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
message: DatabaseMessage;
|
||||
@@ -11,9 +12,26 @@
|
||||
}
|
||||
|
||||
let { isLoading, message, processingState, showMessageStats }: Props = $props();
|
||||
|
||||
// A running agentic flow stamps per-turn timings on its root message at each
|
||||
// turn boundary and the cumulative agentic totals only on exit; while it runs,
|
||||
// show the session's live totals on the root message instead.
|
||||
const liveLlm = $derived(agenticStore.getLiveLlmTotals(message.convId));
|
||||
const isLiveFlowRoot = $derived(
|
||||
liveLlm !== null && agenticStore.getFlowRootMessageId(message.convId) === message.id
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
|
||||
{#if showMessageStats && isLiveFlowRoot && liveLlm}
|
||||
<ChatMessageStatistics
|
||||
mode={ChatMessageStatisticsMode.GENERATION}
|
||||
isLive
|
||||
promptTokens={liveLlm.prompt_n}
|
||||
promptMs={liveLlm.prompt_ms}
|
||||
predictedTokens={liveLlm.predicted_n}
|
||||
predictedMs={liveLlm.predicted_ms}
|
||||
/>
|
||||
{:else if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
|
||||
{@const agentic = message.timings.agentic}
|
||||
<ChatMessageStatistics
|
||||
mode={ChatMessageStatisticsMode.GENERATION}
|
||||
|
||||
+4
-44
@@ -4,7 +4,7 @@
|
||||
ChatMessageEditForm,
|
||||
ChatMessageMcpPromptContent
|
||||
} from '$lib/components/app';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { McpPromptVariant, MessageRole } from '$lib/enums';
|
||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||
|
||||
@@ -12,39 +12,12 @@
|
||||
class?: string;
|
||||
message: DatabaseMessage;
|
||||
mcpPrompt: DatabaseMessageExtraMcpPrompt;
|
||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||
showDeleteDialog: boolean;
|
||||
deletionInfo: {
|
||||
totalCount: number;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
messageTypes: string[];
|
||||
} | null;
|
||||
onCopy: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onConfirmDelete: () => void;
|
||||
onNavigateToSibling?: (siblingId: string) => void;
|
||||
onShowDeleteDialogChange: (show: boolean) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
deletionInfo,
|
||||
mcpPrompt,
|
||||
message,
|
||||
onConfirmDelete,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onNavigateToSibling,
|
||||
onShowDeleteDialogChange,
|
||||
showDeleteDialog,
|
||||
siblingInfo = null
|
||||
}: Props = $props();
|
||||
let { class: className = '', mcpPrompt, message }: Props = $props();
|
||||
|
||||
// Get edit context
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -63,20 +36,7 @@
|
||||
|
||||
{#if message.timestamp}
|
||||
<div class="max-w-[80%]">
|
||||
<ChatMessageActionIcons
|
||||
actionsPosition="right"
|
||||
{deletionInfo}
|
||||
justify="end"
|
||||
{onConfirmDelete}
|
||||
{onCopy}
|
||||
{onDelete}
|
||||
{onEdit}
|
||||
{onNavigateToSibling}
|
||||
{onShowDeleteDialogChange}
|
||||
{siblingInfo}
|
||||
{showDeleteDialog}
|
||||
role={MessageRole.USER}
|
||||
/>
|
||||
<ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { McpPromptVariant } from '$lib/enums';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { mcpStore } from '$lib/stores';
|
||||
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
|
||||
|
||||
+6
-46
@@ -4,47 +4,20 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { INPUT_CLASSES } from '$lib/constants';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { KeyboardKey, MessageRole } from '$lib/enums';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
message: DatabaseMessage;
|
||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||
showDeleteDialog: boolean;
|
||||
deletionInfo: {
|
||||
totalCount: number;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
messageTypes: string[];
|
||||
} | null;
|
||||
onCopy: () => void;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onConfirmDelete: () => void;
|
||||
onNavigateToSibling?: (siblingId: string) => void;
|
||||
onShowDeleteDialogChange: (show: boolean) => void;
|
||||
textareaElement?: HTMLTextAreaElement;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
deletionInfo,
|
||||
message,
|
||||
onConfirmDelete,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onNavigateToSibling,
|
||||
onShowDeleteDialogChange,
|
||||
showDeleteDialog,
|
||||
siblingInfo = null,
|
||||
textareaElement = $bindable()
|
||||
}: Props = $props();
|
||||
let { class: className = '', message, textareaElement = $bindable() }: Props = $props();
|
||||
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
function handleEditKeydown(event: KeyboardEvent) {
|
||||
if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
|
||||
@@ -64,7 +37,7 @@
|
||||
let contentHeight = $state(0);
|
||||
|
||||
const MAX_HEIGHT = 200; // pixels
|
||||
const currentConfig = config();
|
||||
const currentConfig = settingsStore.config;
|
||||
|
||||
let showExpandButton = $derived(contentHeight > MAX_HEIGHT);
|
||||
|
||||
@@ -218,20 +191,7 @@
|
||||
|
||||
{#if message.timestamp}
|
||||
<div class="max-w-[80%]">
|
||||
<ChatMessageActionIcons
|
||||
actionsPosition="right"
|
||||
{deletionInfo}
|
||||
justify="end"
|
||||
{onConfirmDelete}
|
||||
{onCopy}
|
||||
{onDelete}
|
||||
{onEdit}
|
||||
{onNavigateToSibling}
|
||||
{onShowDeleteDialogChange}
|
||||
{siblingInfo}
|
||||
{showDeleteDialog}
|
||||
role={MessageRole.USER}
|
||||
/>
|
||||
<ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
+2
-7
@@ -12,13 +12,8 @@
|
||||
import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
|
||||
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import {
|
||||
type AgenticSection,
|
||||
extractSearchQuery,
|
||||
extractSearchResults,
|
||||
isWebSearchToolName
|
||||
} from '$lib/utils';
|
||||
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
|
||||
import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+3
-5
@@ -7,15 +7,13 @@
|
||||
import { Loader2 } from '@lucide/svelte';
|
||||
import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
|
||||
import { AttachmentType, FileTypeText, MimeTypeAudio, ToolResultKind } from '$lib/enums';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import type { AgenticSection, DatabaseMessageExtra, ToolResultLine } from '$lib/types';
|
||||
import {
|
||||
type AgenticSection,
|
||||
classifyToolResult,
|
||||
formatJsonPretty,
|
||||
parseToolResultWithMedia,
|
||||
type ToolResultLine
|
||||
getBuiltinToolUi,
|
||||
parseToolResultWithMedia
|
||||
} from '$lib/utils';
|
||||
import { createBase64DataUrl } from '$lib/utils/data-url';
|
||||
|
||||
|
||||
+3
-2
@@ -3,8 +3,9 @@
|
||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||
import { XCircle } from '@lucide/svelte';
|
||||
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { abbreviateHome, type AgenticSection, computeLineDiff, prefixFor } from '$lib/utils';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { abbreviateHome, computeLineDiff, prefixFor } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+7
-10
@@ -10,21 +10,18 @@
|
||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||
import { AlertTriangle, Check, Loader2, XCircle } from '@lucide/svelte';
|
||||
import { CollapsibleTerminalBlock } from '$lib/components/app';
|
||||
import { SETTINGS_KEYS } from '$lib/constants';
|
||||
import { TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
||||
import { AttachmentType } from '$lib/enums';
|
||||
import { settingsStore, toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection, DatabaseMessageExtra, ToolResultLine } from '$lib/types';
|
||||
import {
|
||||
abbreviateHome,
|
||||
type AgenticSection,
|
||||
type ExecShellExitStatus,
|
||||
highlightCode,
|
||||
isExitCodeSummaryLine,
|
||||
parseExecShellCommandError,
|
||||
parseExecShellCommandExitStatus,
|
||||
parseToolResultWithMedia,
|
||||
type ToolResultLine
|
||||
parseToolResultWithMedia
|
||||
} from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
@@ -94,7 +91,7 @@
|
||||
);
|
||||
|
||||
const useFullHeightCodeBlocks = $derived(
|
||||
Boolean(config()[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS])
|
||||
Boolean(settingsStore.config[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS])
|
||||
);
|
||||
|
||||
const autoScroll = $derived(isLive && !useFullHeightCodeBlocks);
|
||||
@@ -223,7 +220,7 @@
|
||||
>
|
||||
{#each outputLines 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.IMAGE}
|
||||
<img
|
||||
src={line.media.base64Url}
|
||||
alt={line.media.name}
|
||||
|
||||
+3
-2
@@ -2,8 +2,9 @@
|
||||
import { parseFileGlobSearchMeta } from './parsers/file-glob-search';
|
||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||
import { XCircle } from '@lucide/svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { abbreviateHome } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Clock, Loader2 } from '@lucide/svelte';
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { Info, Loader2 } from '@lucide/svelte';
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { abbreviateHome } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+3
-2
@@ -2,8 +2,9 @@
|
||||
import { parseGrepSearchMeta } from './parsers/grep-search';
|
||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||
import { XCircle } from '@lucide/svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { abbreviateHome } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+3
-3
@@ -2,8 +2,8 @@
|
||||
import { parseReadFileMeta } from './parsers/read-file';
|
||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { DEFAULT_LANGUAGE, MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||
import { type AgenticSection } from '$lib/utils';
|
||||
import { CODE_BLOCK, MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
@@ -32,7 +32,7 @@
|
||||
{#if section.toolResult}
|
||||
<SyntaxHighlightedCode
|
||||
code={section.toolResult}
|
||||
language={readFileMeta?.language ?? DEFAULT_LANGUAGE}
|
||||
language={readFileMeta?.language ?? CODE_BLOCK.DEFAULT_LANGUAGE}
|
||||
maxHeight={MAX_HEIGHT_CODE_BLOCK}
|
||||
/>
|
||||
{:else}
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { parseReadMediaMeta } from './parsers/read-media';
|
||||
import ToolCallBlock from './ToolCallBlock.svelte';
|
||||
import { ATTACHMENT_SAVED_REGEX } from '$lib/constants/agentic';
|
||||
import { ATTACHMENT_SAVED_REGEX } from '$lib/constants/agentic.constants';
|
||||
import { AttachmentType, MimeTypeAudio } from '$lib/enums';
|
||||
import type { DatabaseMessageExtraAudioFile, DatabaseMessageExtraImageFile } from '$lib/types';
|
||||
import { type AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { createBase64DataUrl } from '$lib/utils/data-url';
|
||||
|
||||
interface Props {
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||
import { FileTypeText } from '$lib/enums';
|
||||
import { type AgenticSection, getBuiltinToolUi } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { getBuiltinToolUi } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+4
-5
@@ -2,16 +2,15 @@
|
||||
import { Globe, Loader2 } from '@lucide/svelte';
|
||||
import { CollapsibleContentBlock } from '$lib/components/app';
|
||||
import * as HoverCard from '$lib/components/ui/hover-card';
|
||||
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { mcpStore } from '$lib/stores';
|
||||
import type { AgenticSection, SearchResult } from '$lib/types';
|
||||
import {
|
||||
type AgenticSection,
|
||||
extractSearchQuery,
|
||||
extractSearchResults,
|
||||
faviconForUrl,
|
||||
sanitizeExternalUrl,
|
||||
type SearchResult
|
||||
sanitizeExternalUrl
|
||||
} from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
|
||||
+3
-2
@@ -4,8 +4,9 @@
|
||||
import { XCircle } from '@lucide/svelte';
|
||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { abbreviateHome, type AgenticSection } from '$lib/utils';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { abbreviateHome } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+4
-4
@@ -11,11 +11,11 @@
|
||||
|
||||
import { Loader2, Wrench } from '@lucide/svelte';
|
||||
import { CollapsibleContentBlock } from '$lib/components/app';
|
||||
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
|
||||
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants';
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import type { AgenticSection, BuiltinToolUiEntry } from '$lib/utils';
|
||||
import { mcpStore } from '$lib/stores';
|
||||
import type { AgenticSection, BuiltinToolUiEntry } from '$lib/types';
|
||||
import { getBuiltinToolUi } from '$lib/utils';
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
|
||||
type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
// stay focused on its own format quirks.
|
||||
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils/agentic';
|
||||
import type { AgenticSection } from '$lib/types/agentic';
|
||||
import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, tryParseToolResultObject } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { tryParseToolResultObject } from '$lib/utils';
|
||||
|
||||
export type EditFileEdit = {
|
||||
oldText: string;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
export type ExecShellCommandMeta = {
|
||||
command: string;
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { splitSearchSummaryList } from '$lib/utils';
|
||||
|
||||
export type FileGlobSearchMeta = {
|
||||
path: string;
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { splitSearchSummaryList } from '$lib/utils';
|
||||
|
||||
export type GrepSearchMatch = {
|
||||
file: string;
|
||||
|
||||
+6
-7
@@ -4,13 +4,10 @@
|
||||
// can render incrementally as the file path streams in.
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
FILE_PATH_SEPARATOR_REGEX,
|
||||
TEXT_LANGUAGE_PREFIX_REGEX
|
||||
} from '$lib/constants';
|
||||
import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, getFileTypeByExtension } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { getFileTypeByExtension } from '$lib/utils';
|
||||
|
||||
export type ReadFileMeta = {
|
||||
fileName: string;
|
||||
@@ -50,7 +47,9 @@ export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null
|
||||
}
|
||||
|
||||
const fileType = getFileTypeByExtension(fileName);
|
||||
const language = fileType ? fileType.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') : DEFAULT_LANGUAGE;
|
||||
const language = fileType
|
||||
? fileType.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '')
|
||||
: CODE_BLOCK.DEFAULT_LANGUAGE;
|
||||
|
||||
return { fileName, language, lineRange };
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import { FILE_PATH_SEPARATOR_REGEX, NEWLINE } from '$lib/constants/code';
|
||||
import { FILE_PATH_SEPARATOR_REGEX, NEWLINE } from '$lib/constants';
|
||||
import {
|
||||
PREFIX_FILE,
|
||||
PREFIX_MIME,
|
||||
PREFIX_SIZE,
|
||||
READ_MEDIA_SIZE_REGEX
|
||||
} from '$lib/constants/read-media';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
export interface ReadMediaMeta {
|
||||
fileName: string;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
export type RunJavascriptMeta = {
|
||||
code: string;
|
||||
|
||||
+5
-7
@@ -4,13 +4,10 @@
|
||||
// result blob.
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import {
|
||||
DEFAULT_LANGUAGE,
|
||||
FILE_PATH_SEPARATOR_REGEX,
|
||||
TEXT_LANGUAGE_PREFIX_REGEX
|
||||
} from '$lib/constants';
|
||||
import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';
|
||||
|
||||
export type WriteFileMeta = {
|
||||
fileName: string;
|
||||
@@ -36,7 +33,8 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
|
||||
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
|
||||
const content = typeof args.content === 'string' ? args.content : '';
|
||||
const language =
|
||||
getFileTypeByExtension(rawPath)?.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') ?? DEFAULT_LANGUAGE;
|
||||
getFileTypeByExtension(rawPath)?.replace(CODE_BLOCK.TEXT_LANGUAGE_PREFIX_REGEX, '') ??
|
||||
CODE_BLOCK.DEFAULT_LANGUAGE;
|
||||
const resultObj = tryParseToolResultObject(section.toolResult);
|
||||
const bytesWritten =
|
||||
resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
|
||||
|
||||
+7
-47
@@ -5,57 +5,31 @@
|
||||
ChatMessageStatistics,
|
||||
ChatMessageUserBubble
|
||||
} from '$lib/components/app/chat';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { ChatMessageStatisticsMode, MessageRole } from '$lib/enums';
|
||||
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
|
||||
import { isLoading } from '$lib/stores/chat.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { chatStore, settingsStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
message: DatabaseMessage;
|
||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||
deletionInfo: {
|
||||
totalCount: number;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
messageTypes: string[];
|
||||
} | null;
|
||||
isLastUserMessage?: boolean;
|
||||
nextAssistantMessage?: DatabaseMessage | null;
|
||||
showDeleteDialog: boolean;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
onConfirmDelete: () => void;
|
||||
onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
|
||||
onShowDeleteDialogChange: (show: boolean) => void;
|
||||
onNavigateToSibling?: (siblingId: string) => void;
|
||||
onCopy: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
deletionInfo,
|
||||
isLastUserMessage = false,
|
||||
message,
|
||||
nextAssistantMessage = null,
|
||||
onConfirmDelete,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onForkConversation,
|
||||
onNavigateToSibling,
|
||||
onShowDeleteDialogChange,
|
||||
showDeleteDialog,
|
||||
siblingInfo = null
|
||||
nextAssistantMessage = null
|
||||
}: Props = $props();
|
||||
|
||||
// Get contexts
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
const processingState = useProcessingState();
|
||||
|
||||
const currentConfig = $derived(config());
|
||||
const isActivelyProcessing = $derived(isLastUserMessage && isLoading());
|
||||
const currentConfig = $derived(settingsStore.config);
|
||||
const isActivelyProcessing = $derived(isLastUserMessage && chatStore.isLoading);
|
||||
|
||||
// For agentic turns, prefer the cumulative agentic.llm totals over per-call timings.
|
||||
let storedReadingStats = $derived.by(() => {
|
||||
@@ -133,21 +107,7 @@
|
||||
|
||||
{#if message.timestamp}
|
||||
<div class="max-w-[80%]">
|
||||
<ChatMessageActionIcons
|
||||
actionsPosition="right"
|
||||
{deletionInfo}
|
||||
justify="end"
|
||||
{onConfirmDelete}
|
||||
{onCopy}
|
||||
{onDelete}
|
||||
{onEdit}
|
||||
{onForkConversation}
|
||||
{onNavigateToSibling}
|
||||
{onShowDeleteDialogChange}
|
||||
{siblingInfo}
|
||||
{showDeleteDialog}
|
||||
role={MessageRole.USER}
|
||||
/>
|
||||
<ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ChatAttachmentsList, MarkdownContent, MentionText } from '$lib/components/app';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { DatabaseMessageExtra } from '$lib/types/database';
|
||||
|
||||
interface Props {
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
let isMultiline = $state(false);
|
||||
let messageElement: HTMLElement | undefined = $state();
|
||||
const currentConfig = config();
|
||||
const currentConfig = settingsStore.config;
|
||||
|
||||
$effect(() => {
|
||||
if (!messageElement || !content.trim()) return;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
|
||||
import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
|
||||
import { useMessageEditContext } from '$lib/hooks/use-message-edit-context.svelte';
|
||||
import { useChatMessageEditContext } from '$lib/hooks/use-chat-message-edit-context.svelte';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
@@ -21,7 +21,7 @@
|
||||
onSendImmediately
|
||||
}: Props = $props();
|
||||
|
||||
const editCtx = useMessageEditContext({
|
||||
const editCtx = useChatMessageEditContext({
|
||||
getContent: () => content,
|
||||
getExtras: () => extras,
|
||||
onSave: (content, extras) => onEdit(content, extras)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import type { Component, Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { TOOL_SERVER_LABELS } from '$lib/constants';
|
||||
import { ToolPermissionDecision, ToolSource } from '$lib/enums';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { toolsStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
toolName: string;
|
||||
|
||||
+25
-45
@@ -9,30 +9,16 @@
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import Label from '$lib/components/ui/label/label.svelte';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { getChatMessageActionsContext, getChatMessageEditContext } from '$lib/contexts';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import { activeConversation } from '$lib/stores/conversations.svelte';
|
||||
import { conversationsStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
role: MessageRole.USER | MessageRole.ASSISTANT;
|
||||
justify: 'start' | 'end';
|
||||
actionsPosition: 'left' | 'right';
|
||||
siblingInfo?: ChatMessageSiblingInfo | null;
|
||||
showDeleteDialog: boolean;
|
||||
deletionInfo: {
|
||||
totalCount: number;
|
||||
userMessages: number;
|
||||
assistantMessages: number;
|
||||
messageTypes: string[];
|
||||
} | null;
|
||||
onCopy: () => void;
|
||||
onEdit?: () => void;
|
||||
onRegenerate?: () => void;
|
||||
onContinue?: () => void;
|
||||
onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
|
||||
onDelete: () => void;
|
||||
onConfirmDelete: () => void;
|
||||
onNavigateToSibling?: (siblingId: string) => void;
|
||||
onShowDeleteDialogChange: (show: boolean) => void;
|
||||
showRawOutputSwitch?: boolean;
|
||||
rawOutputEnabled?: boolean;
|
||||
onRawOutputToggle?: (enabled: boolean) => void;
|
||||
@@ -40,36 +26,29 @@
|
||||
|
||||
let {
|
||||
actionsPosition,
|
||||
deletionInfo,
|
||||
justify,
|
||||
onConfirmDelete,
|
||||
onContinue,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onForkConversation,
|
||||
onNavigateToSibling,
|
||||
onRawOutputToggle,
|
||||
onRegenerate,
|
||||
onShowDeleteDialogChange,
|
||||
rawOutputEnabled = false,
|
||||
role,
|
||||
showDeleteDialog,
|
||||
showRawOutputSwitch = false,
|
||||
siblingInfo = null
|
||||
showRawOutputSwitch = false
|
||||
}: Props = $props();
|
||||
|
||||
const messageActions = getChatMessageActionsContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
let showForkDialog = $state(false);
|
||||
let forkName = $state('');
|
||||
let forkIncludeAttachments = $state(true);
|
||||
|
||||
function handleConfirmDelete() {
|
||||
onConfirmDelete();
|
||||
onShowDeleteDialogChange(false);
|
||||
messageActions.confirmDelete();
|
||||
messageActions.setShowDeleteDialog(false);
|
||||
}
|
||||
|
||||
function handleOpenForkDialog() {
|
||||
const conv = activeConversation();
|
||||
const conv = conversationsStore.activeConversation;
|
||||
|
||||
forkName = `Fork of ${conv?.name ?? 'Conversation'}`;
|
||||
forkIncludeAttachments = true;
|
||||
@@ -77,7 +56,10 @@
|
||||
}
|
||||
|
||||
function handleConfirmFork() {
|
||||
onForkConversation?.({ includeAttachments: forkIncludeAttachments, name: forkName.trim() });
|
||||
messageActions.forkConversation?.({
|
||||
includeAttachments: forkIncludeAttachments,
|
||||
name: forkName.trim()
|
||||
});
|
||||
showForkDialog = false;
|
||||
}
|
||||
</script>
|
||||
@@ -88,18 +70,16 @@
|
||||
? 'left-0'
|
||||
: 'right-0'} flex items-center gap-2 opacity-100 transition-opacity"
|
||||
>
|
||||
{#if siblingInfo && siblingInfo.totalSiblings > 1}
|
||||
<ChatMessageActionIconsBranchingControls {siblingInfo} {onNavigateToSibling} />
|
||||
{#if messageActions.siblingInfo && messageActions.siblingInfo.totalSiblings > 1}
|
||||
<ChatMessageActionIconsBranchingControls />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="pointer-events-auto inset-0 flex items-center gap-1 opacity-100 transition-all duration-150"
|
||||
>
|
||||
<ActionIcon icon={Copy} tooltip="Copy" onclick={onCopy} />
|
||||
<ActionIcon icon={Copy} tooltip="Copy" onclick={messageActions.copy} />
|
||||
|
||||
{#if onEdit}
|
||||
<ActionIcon icon={Edit} tooltip="Edit" onclick={onEdit} />
|
||||
{/if}
|
||||
<ActionIcon icon={Edit} tooltip="Edit" onclick={editCtx.startEdit} />
|
||||
|
||||
{#if role === MessageRole.ASSISTANT && onRegenerate}
|
||||
<ActionIcon icon={RefreshCw} tooltip="Regenerate" onclick={() => onRegenerate()} />
|
||||
@@ -109,11 +89,11 @@
|
||||
<ActionIcon icon={ArrowRight} tooltip="Continue" onclick={onContinue} />
|
||||
{/if}
|
||||
|
||||
{#if onForkConversation}
|
||||
{#if messageActions.forkConversation}
|
||||
<ActionIcon icon={GitBranch} tooltip="Fork conversation" onclick={handleOpenForkDialog} />
|
||||
{/if}
|
||||
|
||||
<ActionIcon icon={Trash2} tooltip="Delete" onclick={onDelete} />
|
||||
<ActionIcon icon={Trash2} tooltip="Delete" onclick={messageActions.requestDelete} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,19 +109,19 @@
|
||||
</div>
|
||||
|
||||
<DialogConfirmation
|
||||
bind:open={showDeleteDialog}
|
||||
open={messageActions.showDeleteDialog}
|
||||
title="Delete Message"
|
||||
description={deletionInfo && deletionInfo.totalCount > 1
|
||||
? `This will delete ${deletionInfo.totalCount} messages including: ${deletionInfo.userMessages} user message${deletionInfo.userMessages > 1 ? 's' : ''} and ${deletionInfo.assistantMessages} assistant response${deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.`
|
||||
description={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1
|
||||
? `This will delete ${messageActions.deletionInfo.totalCount} messages including: ${messageActions.deletionInfo.userMessages} user message${messageActions.deletionInfo.userMessages > 1 ? 's' : ''} and ${messageActions.deletionInfo.assistantMessages} assistant response${messageActions.deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.`
|
||||
: 'Are you sure you want to delete this message? This action cannot be undone.'}
|
||||
confirmText={deletionInfo && deletionInfo.totalCount > 1
|
||||
? `Delete ${deletionInfo.totalCount} Messages`
|
||||
confirmText={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1
|
||||
? `Delete ${messageActions.deletionInfo.totalCount} Messages`
|
||||
: 'Delete'}
|
||||
cancelText="Cancel"
|
||||
variant="destructive"
|
||||
icon={Trash2}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={() => onShowDeleteDialogChange(false)}
|
||||
onCancel={() => messageActions.setShowDeleteDialog(false)}
|
||||
/>
|
||||
|
||||
<DialogConfirmation
|
||||
|
||||
+8
-5
@@ -1,14 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
||||
import { ActionIcon } from '$lib/components/app';
|
||||
import { getChatMessageActionsContext } from '$lib/contexts';
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
siblingInfo: ChatMessageSiblingInfo | null;
|
||||
onNavigateToSibling?: (siblingId: string) => void;
|
||||
}
|
||||
|
||||
let { class: className = '', onNavigateToSibling, siblingInfo }: Props = $props();
|
||||
let { class: className = '' }: Props = $props();
|
||||
|
||||
const messageActions = getChatMessageActionsContext();
|
||||
|
||||
let siblingInfo = $derived(messageActions.siblingInfo);
|
||||
|
||||
let hasPrevious = $derived(siblingInfo && siblingInfo.currentIndex > 0);
|
||||
let hasNext = $derived(siblingInfo && siblingInfo.currentIndex < siblingInfo.totalSiblings - 1);
|
||||
@@ -31,7 +34,7 @@
|
||||
tooltip="Previous version"
|
||||
disabled={!hasPrevious}
|
||||
class="h-5 w-5 p-0 {!hasPrevious ? '!cursor-not-allowed opacity-30' : ''}"
|
||||
onclick={() => onNavigateToSibling?.(previousSiblingId!)}
|
||||
onclick={() => messageActions.navigateToSibling(previousSiblingId!)}
|
||||
/>
|
||||
|
||||
<span class="px-1 font-mono text-xs">
|
||||
@@ -43,7 +46,7 @@
|
||||
tooltip="Next version"
|
||||
disabled={!hasNext}
|
||||
class="h-5 w-5 p-0 {!hasNext ? 'opacity-30' : ''}"
|
||||
onclick={() => onNavigateToSibling?.(nextSiblingId!)}
|
||||
onclick={() => messageActions.navigateToSibling(nextSiblingId!)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+21
-20
@@ -8,21 +8,14 @@
|
||||
MarkdownContent
|
||||
} from '$lib/components/app';
|
||||
import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums';
|
||||
import {
|
||||
agenticExecutingToolCallId,
|
||||
agenticLastError,
|
||||
agenticPendingContinueRequest,
|
||||
agenticPendingPermissionRequest,
|
||||
agenticResolveContinue,
|
||||
agenticResolvePermission
|
||||
} from '$lib/stores/agentic.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { agenticStore, settingsStore } from '$lib/stores';
|
||||
import type {
|
||||
AgenticSection,
|
||||
ChatMessageAgenticTimings,
|
||||
ChatMessageAgenticTurnStats,
|
||||
DatabaseMessage
|
||||
} from '$lib/types';
|
||||
import { type AgenticSection, deriveAgenticSections } from '$lib/utils';
|
||||
import { deriveAgenticSections } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
message: DatabaseMessage;
|
||||
@@ -40,19 +33,25 @@
|
||||
|
||||
let expandedStates: Record<number, boolean> = $state({});
|
||||
|
||||
const showThoughtInProgress = $derived(Boolean(config().showThoughtInProgress));
|
||||
const alwaysShowToolCallContent = $derived(Boolean(config().alwaysShowToolCallContent));
|
||||
const showMessageStats = $derived(Boolean(config().showMessageStats));
|
||||
const showAgenticTurnStats = $derived(showMessageStats && Boolean(config().showAgenticTurnStats));
|
||||
const showThoughtInProgress = $derived(Boolean(settingsStore.config.showThoughtInProgress));
|
||||
const alwaysShowToolCallContent = $derived(
|
||||
Boolean(settingsStore.config.alwaysShowToolCallContent)
|
||||
);
|
||||
const showMessageStats = $derived(Boolean(settingsStore.config.showMessageStats));
|
||||
const showAgenticTurnStats = $derived(
|
||||
showMessageStats && Boolean(settingsStore.config.showAgenticTurnStats)
|
||||
);
|
||||
|
||||
const hasReasoningError = $derived(
|
||||
isLastAssistantMessage ? !!agenticLastError(message.convId) : false
|
||||
isLastAssistantMessage ? !!agenticStore.lastError(message.convId) : false
|
||||
);
|
||||
|
||||
let permissionDismissed = $state(false);
|
||||
|
||||
const pendingPermission = $derived(
|
||||
isStreaming && isLastAssistantMessage ? agenticPendingPermissionRequest(message.convId) : null
|
||||
isStreaming && isLastAssistantMessage
|
||||
? agenticStore.pendingPermissionRequest(message.convId)
|
||||
: null
|
||||
);
|
||||
|
||||
let prevPendingRef: typeof pendingPermission = null;
|
||||
@@ -68,13 +67,15 @@
|
||||
|
||||
function handlePermission(decision: ToolPermissionDecision) {
|
||||
permissionDismissed = true;
|
||||
agenticResolvePermission(message.convId, decision);
|
||||
agenticStore.resolvePermission(message.convId, decision);
|
||||
}
|
||||
|
||||
let continueDismissed = $state(false);
|
||||
|
||||
const pendingContinue = $derived(
|
||||
isStreaming && isLastAssistantMessage ? agenticPendingContinueRequest(message.convId) : false
|
||||
isStreaming && isLastAssistantMessage
|
||||
? agenticStore.pendingContinueRequest(message.convId)
|
||||
: false
|
||||
);
|
||||
|
||||
let prevContinueRef = false;
|
||||
@@ -90,13 +91,13 @@
|
||||
|
||||
function handleContinue(shouldContinue: boolean) {
|
||||
continueDismissed = true;
|
||||
agenticResolveContinue(message.convId, shouldContinue);
|
||||
agenticStore.resolveContinue(message.convId, shouldContinue);
|
||||
}
|
||||
|
||||
const sections = $derived(deriveAgenticSections(message, toolMessages, [], isStreaming));
|
||||
|
||||
const currentlyExecutingToolCallId = $derived(
|
||||
isStreaming ? agenticExecutingToolCallId(message.convId) : null
|
||||
isStreaming ? agenticStore.executingToolCallId(message.convId) : null
|
||||
);
|
||||
|
||||
type TurnGroup = {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { ChatForm, DialogConfirmation } from '$lib/components/app';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import { getMessageEditContext } from '$lib/contexts';
|
||||
import { getChatMessageEditContext } from '$lib/contexts';
|
||||
import { KeyboardKey, MessageRole } from '$lib/enums';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { chatStore } from '$lib/stores';
|
||||
import { processFilesToChatUploaded } from '$lib/utils/browser-only';
|
||||
|
||||
const editCtx = getMessageEditContext();
|
||||
const editCtx = getChatMessageEditContext();
|
||||
|
||||
let saveWithoutRegenerate = $state(false);
|
||||
let showDiscardDialog = $state(false);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { Lightbulb } from '@lucide/svelte';
|
||||
import { CollapsibleContentBlock, MarkdownContent } from '$lib/components/app';
|
||||
import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
|
||||
import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import type { DatabaseMessageExtra } from '$lib/types';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { AgenticSection, DatabaseMessageExtra } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
@@ -25,7 +24,7 @@
|
||||
section
|
||||
}: Props = $props();
|
||||
|
||||
const currentConfig = config();
|
||||
const currentConfig = settingsStore.config;
|
||||
|
||||
const REASONING_HEADER = 'Reasoning';
|
||||
const REASONING_HEADER_PENDING = 'Reasoning...';
|
||||
|
||||
@@ -1,22 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
|
||||
import { setChatActionsContext } from '$lib/contexts';
|
||||
import { MessageRole } from '$lib/enums';
|
||||
import {
|
||||
agenticClearSteeringMessage,
|
||||
agenticInjectSteeringMessage,
|
||||
agenticPendingSteeringMessageContent,
|
||||
agenticPendingSteeringMessageExtras
|
||||
} from '$lib/stores/agentic.svelte';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
chatClearPendingMessage,
|
||||
chatInjectPendingMessage,
|
||||
chatPendingMessageContent,
|
||||
chatPendingMessageExtras
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import { activeConversation, conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
|
||||
import type { ChatMessageActions } from '$lib/types';
|
||||
import {
|
||||
buildSiblingInfoMap,
|
||||
copyToClipboard,
|
||||
@@ -34,9 +20,9 @@
|
||||
|
||||
let allConversationMessages = $state<DatabaseMessage[]>([]);
|
||||
|
||||
const currentConfig = config();
|
||||
const currentConfig = settingsStore.config;
|
||||
|
||||
setChatActionsContext({
|
||||
const chatActions: ChatMessageActions = {
|
||||
continueAssistantMessage: async (message: DatabaseMessage) => {
|
||||
onUserAction?.();
|
||||
await chatStore.continueAssistantMessage(message.id);
|
||||
@@ -105,10 +91,10 @@
|
||||
await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
|
||||
refreshAllMessages();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function refreshAllMessages() {
|
||||
const conversation = activeConversation();
|
||||
const conversation = conversationsStore.activeConversation;
|
||||
|
||||
if (conversation) {
|
||||
conversationsStore.getConversationMessages(conversation.id).then((messages) => {
|
||||
@@ -121,7 +107,7 @@
|
||||
|
||||
// Refresh messages whenever the active conversation changes
|
||||
$effect(() => {
|
||||
if (activeConversation()) {
|
||||
if (conversationsStore.activeConversation) {
|
||||
refreshAllMessages();
|
||||
}
|
||||
});
|
||||
@@ -242,6 +228,7 @@
|
||||
{#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}
|
||||
{isLastAssistantMessage}
|
||||
@@ -251,32 +238,33 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
{#if activeConversation() && agenticPendingSteeringMessageContent(activeConversation()!.id)}
|
||||
{@const convId = activeConversation()!.id}
|
||||
{@const pendingContent = agenticPendingSteeringMessageContent(convId)}
|
||||
{#if conversationsStore.activeConversation && agenticStore.pendingSteeringMessageContent(conversationsStore.activeConversation!.id)}
|
||||
{@const convId = conversationsStore.activeConversation!.id}
|
||||
{@const pendingContent = agenticStore.pendingSteeringMessageContent(convId)}
|
||||
|
||||
{#if pendingContent}
|
||||
<ChatMessageUserPending
|
||||
class="mx-auto mt-12 w-full max-w-[48rem]"
|
||||
content={pendingContent}
|
||||
extras={agenticPendingSteeringMessageExtras(convId)}
|
||||
extras={agenticStore.pendingSteeringMessageExtras(convId)}
|
||||
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
|
||||
onEdit={(newContent, extras) => agenticInjectSteeringMessage(convId, newContent, extras)}
|
||||
onDelete={() => agenticClearSteeringMessage(convId)}
|
||||
onEdit={(newContent, extras) =>
|
||||
agenticStore.injectSteeringMessage(convId, newContent, extras)}
|
||||
onDelete={() => agenticStore.clearSteeringMessage(convId)}
|
||||
/>
|
||||
{/if}
|
||||
{:else if activeConversation() && chatPendingMessageContent(activeConversation()!.id)}
|
||||
{@const convId = activeConversation()!.id}
|
||||
{@const pendingContent = chatPendingMessageContent(convId)}
|
||||
{:else if conversationsStore.activeConversation && chatStore.pendingMessageContent(conversationsStore.activeConversation!.id)}
|
||||
{@const convId = conversationsStore.activeConversation!.id}
|
||||
{@const pendingContent = chatStore.pendingMessageContent(convId)}
|
||||
|
||||
{#if pendingContent}
|
||||
<ChatMessageUserPending
|
||||
class="mx-auto mt-12 w-full max-w-[48rem]"
|
||||
content={pendingContent}
|
||||
extras={chatPendingMessageExtras(convId)}
|
||||
extras={chatStore.pendingMessageExtras(convId)}
|
||||
onSendImmediately={() => chatStore.abortCurrentFlow(convId)}
|
||||
onEdit={(newContent, extras) => chatInjectPendingMessage(convId, newContent, extras)}
|
||||
onDelete={() => chatClearPendingMessage(convId)}
|
||||
onEdit={(newContent, extras) => chatStore.injectPendingMessage(convId, newContent, extras)}
|
||||
onDelete={() => chatStore.clearPendingMessage(convId)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -20,26 +20,20 @@
|
||||
import { useKeyboardShortcuts } from '$lib/hooks/use-keyboard-shortcuts.svelte';
|
||||
import {
|
||||
chatStore,
|
||||
errorDialog,
|
||||
isChatStreaming,
|
||||
isEditing,
|
||||
isLoading
|
||||
} from '$lib/stores/chat.svelte';
|
||||
import {
|
||||
activeConversation,
|
||||
activeMessages,
|
||||
conversationsStore
|
||||
} from '$lib/stores/conversations.svelte';
|
||||
import { device } from '$lib/stores/device.svelte';
|
||||
import { serverError, serverLoading } from '$lib/stores/server.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
conversationsStore,
|
||||
device,
|
||||
isMobile,
|
||||
serverStore,
|
||||
settingsStore
|
||||
} from '$lib/stores';
|
||||
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
|
||||
let { showCenteredEmpty = false } = $props();
|
||||
|
||||
let disableAutoScroll = $derived(Boolean(config().disableAutoScroll) || isMobile.current);
|
||||
let disableAutoScroll = $derived(
|
||||
Boolean(settingsStore.config.disableAutoScroll) || isMobile.current
|
||||
);
|
||||
let isMobileUserScrolledUp = $state(false);
|
||||
let mobileScrollDownHint = $state(false);
|
||||
let mobileScrollDownHintLockedUntil = $state(0);
|
||||
@@ -48,12 +42,15 @@
|
||||
let showDeleteDialog = $state(false);
|
||||
let showEmptyFileDialog = $state(false);
|
||||
let isEmpty = $derived(
|
||||
showCenteredEmpty && !activeConversation() && activeMessages().length === 0 && !isLoading()
|
||||
showCenteredEmpty &&
|
||||
!conversationsStore.activeConversation &&
|
||||
conversationsStore.activeMessages.length === 0 &&
|
||||
!chatStore.isLoading
|
||||
);
|
||||
let activeErrorDialog = $derived(errorDialog());
|
||||
let isServerLoading = $derived(serverLoading());
|
||||
let hasPropsError = $derived(!!serverError());
|
||||
let isCurrentConversationLoading = $derived(isLoading() || isChatStreaming());
|
||||
let activeErrorDialog = $derived(chatStore.errorDialogState);
|
||||
let isServerLoading = $derived(serverStore.loading);
|
||||
let hasPropsError = $derived(!!serverStore.error);
|
||||
let isCurrentConversationLoading = $derived(chatStore.isLoading || chatStore.isStreaming());
|
||||
let chatFormBottomPosition = $derived.by(() => {
|
||||
if (!isMobile.current) return '1rem';
|
||||
|
||||
@@ -80,7 +77,7 @@
|
||||
});
|
||||
const { handleKeydown } = useKeyboardShortcuts({
|
||||
deleteActiveConversation: () => {
|
||||
if (activeConversation()) {
|
||||
if (conversationsStore.activeConversation) {
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
}
|
||||
@@ -100,7 +97,7 @@
|
||||
}
|
||||
|
||||
async function handleDeleteConfirm() {
|
||||
const conversation = activeConversation();
|
||||
const conversation = conversationsStore.activeConversation;
|
||||
|
||||
if (conversation) {
|
||||
await conversationsStore.deleteConversation(conversation.id);
|
||||
@@ -148,7 +145,7 @@
|
||||
async function handleMessagesReady(messageCount: number) {
|
||||
if (messageCount === 0) return;
|
||||
|
||||
const id = activeConversation()?.id ?? null;
|
||||
const id = conversationsStore.activeConversation?.id ?? null;
|
||||
|
||||
if (!id || id === lastScrolledConversationId) return;
|
||||
|
||||
@@ -168,7 +165,7 @@
|
||||
const settle = () => {
|
||||
if (autoScroll.userScrolledUp) return;
|
||||
|
||||
if (activeConversation()?.id !== id) return;
|
||||
if (conversationsStore.activeConversation?.id !== id) return;
|
||||
|
||||
autoScroll.scrollToBottom();
|
||||
const height = container.scrollHeight;
|
||||
@@ -246,7 +243,7 @@
|
||||
|
||||
$effect(() => {
|
||||
const shouldDisableAutoScroll =
|
||||
config().disableAutoScroll || (isMobile.current && isCurrentConversationLoading);
|
||||
settingsStore.config.disableAutoScroll || (isMobile.current && isCurrentConversationLoading);
|
||||
|
||||
autoScroll.setDisabled(shouldDisableAutoScroll);
|
||||
|
||||
@@ -310,7 +307,7 @@
|
||||
>
|
||||
{#if !isEmpty}
|
||||
<ChatMessages
|
||||
messages={activeMessages()}
|
||||
messages={conversationsStore.activeMessages}
|
||||
onMessagesReady={handleMessagesReady}
|
||||
onUserAction={() => {
|
||||
handleSendLikeScroll();
|
||||
@@ -354,7 +351,7 @@
|
||||
|
||||
<ChatScreenForm
|
||||
class="pointer-events-auto conversation-chat-form"
|
||||
disabled={hasPropsError || isEditing()}
|
||||
disabled={hasPropsError || chatStore.isEditing()}
|
||||
{initialMessage}
|
||||
isLoading={isCurrentConversationLoading}
|
||||
onFileRemove={fileUpload.handleFileRemove}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ArrowDown } from '@lucide/svelte';
|
||||
import ActionIcon from '$lib/components/app/actions/ActionIcon.svelte';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
|
||||
let { onclick }: { onclick: (e?: MouseEvent) => void } = $props();
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/state';
|
||||
import { ChatForm } from '$lib/components/app';
|
||||
import { useDraftMessages } from '$lib/hooks/use-draft-messages.svelte';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import { isMobile } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { serverStore } from '$lib/stores/server.svelte';
|
||||
import { serverStore } from '$lib/stores';
|
||||
|
||||
interface Props {
|
||||
isEmpty: boolean;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { AlertTriangle, Loader2, RefreshCw } from '@lucide/svelte';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { serverError, serverLoading, serverStatus, serverStore } from '$lib/stores/server.svelte';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import { serverStore } from '$lib/stores';
|
||||
|
||||
let hasError = $derived(!!serverError());
|
||||
let isLoadingModel = $derived(serverStatus() === 503);
|
||||
let hasError = $derived(!!serverStore.error);
|
||||
let isLoadingModel = $derived(serverStore.status === 503);
|
||||
</script>
|
||||
|
||||
{#if hasError}
|
||||
@@ -23,17 +23,17 @@
|
||||
{#if !isLoadingModel}
|
||||
<button
|
||||
onclick={() => serverStore.fetch()}
|
||||
disabled={serverLoading()}
|
||||
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"
|
||||
>
|
||||
<RefreshCw class="h-3 w-3 {serverLoading() ? 'animate-spin' : ''}" />
|
||||
{serverLoading() ? 'Retrying...' : 'Retry'}
|
||||
<RefreshCw class="h-3 w-3 {serverStore.loading ? 'animate-spin' : ''}" />
|
||||
{serverStore.loading ? 'Retrying...' : 'Retry'}
|
||||
</button>
|
||||
{/if}
|
||||
</Alert.Title>
|
||||
|
||||
{#if !isLoadingModel}
|
||||
<Alert.Description>{serverError()}</Alert.Description>
|
||||
<Alert.Description>{serverStore.error}</Alert.Description>
|
||||
{/if}
|
||||
</Alert.Root>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from '@lucide/svelte';
|
||||
import { StreamConnectionState } from '$lib/enums';
|
||||
import { chatStore } from '$lib/stores/chat.svelte';
|
||||
import { chatStore } from '$lib/stores';
|
||||
|
||||
let state = $derived(chatStore.streamConnectionState);
|
||||
</script>
|
||||
|
||||
@@ -91,7 +91,7 @@ export { default as ChatAttachmentsListItemThumbnailImage } from './ChatAttachme
|
||||
* preview without carousel, or a gallery/carousel view when multiple items exist.
|
||||
* Uses ChatAttachmentPreviewSingle internally for each item's content.
|
||||
*/
|
||||
export { default as ChatAttachmentsPreview } from './ChatAttachments/ChatAttachmentsPreview.svelte';
|
||||
export { default as ChatAttachmentsPreview } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreview.svelte';
|
||||
export { default as ChatAttachmentsPreviewNavButtons } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewNavButtons.svelte';
|
||||
export { default as ChatAttachmentsPreviewFileInfo } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewFileInfo.svelte';
|
||||
export { default as ChatAttachmentsPreviewThumbnailStrip } from './ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewThumbnailStrip.svelte';
|
||||
@@ -120,8 +120,8 @@ export { default as ChatAttachmentsPreviewCurrentItem } from './ChatAttachments/
|
||||
* Used by ChatScreenForm and ChatMessageEditForm for both new conversations and message editing.
|
||||
*
|
||||
* **Architecture:**
|
||||
* - Composes ChatFormTextarea (or ChatFormContenteditable for messages with
|
||||
* file mention links), ChatFormActions, and ChatFormPickerMcpPrompts
|
||||
* - Composes ChatFormInput (a plain textarea, or a ChatFormInputRich for
|
||||
* messages with file mention links), ChatFormActions, and ChatFormPickerMcpPrompts
|
||||
* - Manages file upload state via `uploadedFiles` bindable prop
|
||||
* - Integrates with ModelsSelectorDropdown for model selection in router mode
|
||||
* - Communicates with parent via callbacks (onSubmit, onFilesAdd, onStop, etc.)
|
||||
@@ -258,7 +258,7 @@ export { default as ChatFormContextGauge } from './ChatForm/ChatFormContextGauge
|
||||
/**
|
||||
* Hidden file input element for programmatic file selection.
|
||||
*/
|
||||
export { default as ChatFormFileInputInvisible } from './ChatForm/ChatFormFileInputInvisible.svelte';
|
||||
export { default as ChatFormInputFileInputInvisible } from './ChatForm/ChatFormInput/ChatFormInputFileInputInvisible.svelte';
|
||||
|
||||
/**
|
||||
* Displays MCP Resource attachments as a horizontal carousel.
|
||||
@@ -267,18 +267,13 @@ export { default as ChatFormFileInputInvisible } from './ChatForm/ChatFormFileIn
|
||||
export { default as ChatFormMcpResourcesList } from './ChatForm/ChatFormMcpResourcesList.svelte';
|
||||
|
||||
/**
|
||||
* Auto-resizing contenteditable input that renders `[name](file://...)`
|
||||
* mention links as inline chips while keeping the value as the markdown
|
||||
* source string. ChatForm swaps it in once a mention link lands in the
|
||||
* buffer. Shares the focus()/resetHeight()/caret handle with the textarea.
|
||||
* The message editor. Renders a plain auto-resizing textarea by default,
|
||||
* or a ChatFormInputRich that renders `[name](file://...)` mention links as
|
||||
* inline chips (keeping the value as the markdown source string) once a
|
||||
* mention link lands in the buffer. The variant is selected via the
|
||||
* `useRichInput` prop; both share one imperative handle.
|
||||
*/
|
||||
export { default as ChatFormContenteditable } from './ChatForm/ChatFormContenteditable.svelte';
|
||||
|
||||
/**
|
||||
* Plain auto-resizing textarea with IME composition support. Default input
|
||||
* renderer inside ChatForm until a file mention lands.
|
||||
*/
|
||||
export { default as ChatFormTextarea } from './ChatForm/ChatFormTextarea.svelte';
|
||||
export { default as ChatFormInput } from './ChatForm/ChatFormInput/ChatFormInput.svelte';
|
||||
|
||||
/**
|
||||
* Working directory selector for agent mode. Renders a chip below the chat
|
||||
@@ -288,7 +283,7 @@ export { default as ChatFormTextarea } from './ChatForm/ChatFormTextarea.svelte'
|
||||
* synthetic "Set working directory to ..." user message into chat history
|
||||
* and is enforced on tool calls via the `x-tool-cwd` request header.
|
||||
*/
|
||||
export { default as ChatFormWorkingDirectory } from './ChatForm/ChatFormWorkingDirectory.svelte';
|
||||
export { default as ChatFormCurrentWorkingDirectory } from './ChatForm/ChatFormCurrentWorkingDirectory/ChatFormCurrentWorkingDirectory.svelte';
|
||||
|
||||
/**
|
||||
* **ChatFormPickerMcpPrompts** - MCP prompt selection interface
|
||||
@@ -359,14 +354,14 @@ export { default as ChatFormPickerPopover } from './ChatForm/ChatFormPickers/Cha
|
||||
* Generic scrollable list for picker popovers. Provides search input,
|
||||
* scroll-into-view for keyboard navigation, loading skeletons, empty state,
|
||||
* and optional footer. Uses Svelte 5 snippets for item/skeleton/footer rendering.
|
||||
* Shared by ChatFormPickerMcpPrompts and ChatFormMentionPicker.
|
||||
* Shared by ChatFormPickerMcpPrompts and ChatFormPickerMention.
|
||||
*/
|
||||
export { default as ChatFormPickerList } from './ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerList.svelte';
|
||||
|
||||
/**
|
||||
* Generic button wrapper for picker list items. Provides consistent styling,
|
||||
* hover/selected states, and data-picker-index attribute for scroll-into-view.
|
||||
* Shared by ChatFormPickerMcpPrompts and ChatFormMentionPicker.
|
||||
* Shared by ChatFormPickerMcpPrompts and ChatFormPickerMention.
|
||||
*/
|
||||
export { default as ChatFormPickerListItem } from './ChatForm/ChatFormPickers/ChatFormPicker/ChatFormPickerListItem.svelte';
|
||||
|
||||
@@ -389,14 +384,14 @@ export { default as ChatFormPickerListItemSkeleton } from './ChatForm/ChatFormPi
|
||||
* tool, scoped to the conversation cwd (or server home when unset).
|
||||
* Selection splices a `[name](file:///<abs path>)` link into the input.
|
||||
*/
|
||||
export { default as ChatFormMentionPicker } from './ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte';
|
||||
export { default as ChatFormPickerMention } from './ChatForm/ChatFormPickers/ChatFormPickerMention.svelte';
|
||||
|
||||
/**
|
||||
* `/`-triggered slash-command picker. Lists the available slash commands
|
||||
* (`/prompt`, `/cwd`, `/model`) filtered by the typed query; selection
|
||||
* hands the command to the parent for dispatch.
|
||||
*/
|
||||
export { default as ChatFormCommandPicker } from './ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte';
|
||||
export { default as ChatFormPickerCommand } from './ChatForm/ChatFormPickers/ChatFormPickerCommand.svelte';
|
||||
|
||||
/**
|
||||
* Hosts the chat-form pickers (slash-command, MCP prompt, file mention)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import ChevronDown from '@lucide/svelte/icons/chevron-down';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import '$styles/katex-custom.scss';
|
||||
import '$lib/styles/katex-custom.scss';
|
||||
import {
|
||||
getCodeInfoFromTarget,
|
||||
getHastNodeId,
|
||||
@@ -25,34 +25,25 @@
|
||||
DialogMermaidPreview
|
||||
} from '$lib/components/app';
|
||||
import {
|
||||
BOOL_TRUE_STRING,
|
||||
CODE_BLOCK_HEADER_CLASS,
|
||||
DATA_ERROR_BOUND_ATTR,
|
||||
DATA_ERROR_HANDLED_ATTR,
|
||||
CODE_BLOCK_CLASS,
|
||||
DIAGRAM_VIEW_MODE_ATTR,
|
||||
DIAGRAM_VIEW_RENDERED,
|
||||
DIAGRAM_VIEW_SOURCE,
|
||||
IMAGE_NOT_ERROR_BOUND_SELECTOR,
|
||||
MARKDOWN_DATA_ATTRS,
|
||||
MERMAID_BLOCK_CLASS,
|
||||
MERMAID_LANGUAGE,
|
||||
MERMAID_RENDERED_ATTR,
|
||||
MERMAID_SYNTAX_ATTR,
|
||||
MERMAID_WRAPPER_CLASS,
|
||||
SETTINGS_KEYS,
|
||||
SVG_BLOCK_CLASS,
|
||||
SVG_INLINE_SHADOW_STYLE,
|
||||
SVG_LANGUAGE,
|
||||
SVG_RENDERED_ATTR,
|
||||
SVG_SOURCE_ATTR,
|
||||
SVG_TAG_PREFIX,
|
||||
SVG_WRAPPER_CLASS,
|
||||
TOGGLE_SOURCE_BTN_CLASS,
|
||||
XML_LANGUAGE
|
||||
SVG,
|
||||
TOGGLE_SOURCE_BTN_CLASS
|
||||
} from '$lib/constants';
|
||||
import { ColorMode, UrlProtocol } from '$lib/enums';
|
||||
import { BooleanString, ColorMode, UrlProtocol } from '$lib/enums';
|
||||
import { FileTypeText } from '$lib/enums/files.enums';
|
||||
import { createAutoScrollController } from '$lib/hooks/use-auto-scroll.svelte';
|
||||
import { config } from '$lib/stores/settings.svelte';
|
||||
import { settingsStore } from '$lib/stores';
|
||||
import type { DatabaseMessageExtra } from '$lib/types/database';
|
||||
import {
|
||||
copyCodeToClipboard,
|
||||
@@ -105,9 +96,9 @@
|
||||
|
||||
if (!block) return null;
|
||||
|
||||
if (block.language === SVG_LANGUAGE) return block.code;
|
||||
if (block.language === SVG.LANGUAGE) return block.code;
|
||||
|
||||
if (block.language === XML_LANGUAGE && block.code.trimStart().startsWith(SVG_TAG_PREFIX))
|
||||
if (block.language === SVG.XML_LANGUAGE && block.code.trimStart().startsWith(SVG.TAG_PREFIX))
|
||||
return block.code;
|
||||
|
||||
return null;
|
||||
@@ -137,7 +128,7 @@
|
||||
|
||||
// Mount the streaming svg into its shadow host on every chunk so it renders live
|
||||
$effect(() => {
|
||||
if (streamingSvgHost) mountSvgShadow(streamingSvgHost, liveSvgHtml, SVG_INLINE_SHADOW_STYLE);
|
||||
if (streamingSvgHost) mountSvgShadow(streamingSvgHost, liveSvgHtml, SVG.INLINE_SHADOW_STYLE);
|
||||
});
|
||||
|
||||
let streamingCodeScrollContainer = $state<HTMLDivElement>();
|
||||
@@ -493,13 +484,19 @@
|
||||
const copyButton = wrapper.querySelector<HTMLButtonElement>('.copy-code-btn');
|
||||
const previewButton = wrapper.querySelector<HTMLButtonElement>('.preview-code-btn');
|
||||
|
||||
if (copyButton && copyButton.dataset.listenerBound !== 'true') {
|
||||
copyButton.dataset.listenerBound = 'true';
|
||||
if (
|
||||
copyButton &&
|
||||
copyButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
|
||||
) {
|
||||
copyButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
|
||||
copyButton.addEventListener('click', handleCopyClick);
|
||||
}
|
||||
|
||||
if (previewButton && previewButton.dataset.listenerBound !== 'true') {
|
||||
previewButton.dataset.listenerBound = 'true';
|
||||
if (
|
||||
previewButton &&
|
||||
previewButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
|
||||
) {
|
||||
previewButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
|
||||
previewButton.addEventListener('click', handlePreviewClick);
|
||||
}
|
||||
}
|
||||
@@ -515,7 +512,7 @@
|
||||
const images = containerRef.querySelectorAll<HTMLImageElement>(IMAGE_NOT_ERROR_BOUND_SELECTOR);
|
||||
|
||||
for (const img of images) {
|
||||
img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
|
||||
img.setAttribute(MARKDOWN_DATA_ATTRS.ERROR_BOUND, BooleanString.TRUE);
|
||||
img.addEventListener('error', handleImageError);
|
||||
}
|
||||
}
|
||||
@@ -535,7 +532,7 @@
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG_WRAPPER_CLASS}`);
|
||||
const wrapper = toggleBtn.closest(`.${MERMAID_WRAPPER_CLASS}, .${SVG.WRAPPER_CLASS}`);
|
||||
|
||||
if (!wrapper) return;
|
||||
|
||||
@@ -593,16 +590,16 @@
|
||||
}
|
||||
|
||||
// Check if clicking on copy or preview button in svg block
|
||||
const svgCopyBtn = target.closest(`.${SVG_WRAPPER_CLASS} .copy-code-btn`);
|
||||
const svgPreviewBtn = target.closest(`.${SVG_WRAPPER_CLASS} .preview-code-btn`);
|
||||
const svgCopyBtn = target.closest(`.${SVG.WRAPPER_CLASS} .copy-code-btn`);
|
||||
const svgPreviewBtn = target.closest(`.${SVG.WRAPPER_CLASS} .preview-code-btn`);
|
||||
|
||||
if (svgCopyBtn || svgPreviewBtn) {
|
||||
const wrapper = target.closest(`.${SVG_WRAPPER_CLASS}`);
|
||||
const wrapper = target.closest(`.${SVG.WRAPPER_CLASS}`);
|
||||
|
||||
if (!wrapper) return;
|
||||
|
||||
const preElement = wrapper.querySelector<HTMLElement>(
|
||||
`pre.${SVG_BLOCK_CLASS}[${SVG_SOURCE_ATTR}]`
|
||||
`pre.${SVG.BLOCK_CLASS}[${SVG.SOURCE_ATTR}]`
|
||||
);
|
||||
|
||||
if (!preElement) return;
|
||||
@@ -611,7 +608,7 @@
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await copyToClipboard(preElement.getAttribute(SVG_SOURCE_ATTR) ?? '');
|
||||
await copyToClipboard(preElement.getAttribute(SVG.SOURCE_ATTR) ?? '');
|
||||
} catch (error) {
|
||||
console.error('Failed to copy svg source:', error);
|
||||
}
|
||||
@@ -622,7 +619,7 @@
|
||||
if (svgPreviewBtn) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
mermaidPreviewSvgHtml = sanitizeSvg(preElement.getAttribute(SVG_SOURCE_ATTR) ?? '');
|
||||
mermaidPreviewSvgHtml = sanitizeSvg(preElement.getAttribute(SVG.SOURCE_ATTR) ?? '');
|
||||
svgPreviewLive = false;
|
||||
mermaidPreviewOpen = true;
|
||||
|
||||
@@ -633,14 +630,14 @@
|
||||
// A click on the header chrome targets the action buttons, never the
|
||||
// diagram. Guard so a header click can not fall through to the click to
|
||||
// zoom branches below, whatever the scroll position or stacking.
|
||||
if (target.closest(`.${CODE_BLOCK_HEADER_CLASS}`)) return;
|
||||
if (target.closest(`.${CODE_BLOCK_CLASS.HEADER}`)) return;
|
||||
|
||||
// Open preview when clicking the svg block itself. A final block carries its
|
||||
// source, a streaming block does not and is mirrored live into the dialog.
|
||||
const svgEl = target.closest(`.${SVG_BLOCK_CLASS}`);
|
||||
const svgEl = target.closest(`.${SVG.BLOCK_CLASS}`);
|
||||
|
||||
if (svgEl) {
|
||||
const source = svgEl.getAttribute(SVG_SOURCE_ATTR);
|
||||
const source = svgEl.getAttribute(SVG.SOURCE_ATTR);
|
||||
|
||||
if (source !== null) {
|
||||
mermaidPreviewSvgHtml = sanitizeSvg(source);
|
||||
@@ -698,7 +695,7 @@
|
||||
|
||||
// Mark nodes immediately to prevent duplicate renders if called again during streaming.
|
||||
// This avoids needing a guard that would block node discovery.
|
||||
nodes.forEach((node) => node.setAttribute(MERMAID_RENDERED_ATTR, 'true'));
|
||||
nodes.forEach((node) => node.setAttribute(MERMAID_RENDERED_ATTR, BooleanString.TRUE));
|
||||
|
||||
// Read mode before await so Svelte tracks it reactively.
|
||||
const isDark = mode.current === ColorMode.DARK;
|
||||
@@ -739,15 +736,15 @@
|
||||
if (!containerRef) return;
|
||||
|
||||
const nodes = containerRef.querySelectorAll<HTMLElement>(
|
||||
`pre.${SVG_BLOCK_CLASS}:not([${SVG_RENDERED_ATTR}])`
|
||||
`pre.${SVG.BLOCK_CLASS}:not([${SVG.RENDERED_ATTR}])`
|
||||
);
|
||||
|
||||
if (nodes.length === 0) return;
|
||||
|
||||
nodes.forEach((node) => {
|
||||
node.setAttribute(SVG_RENDERED_ATTR, 'true');
|
||||
node.setAttribute(SVG.RENDERED_ATTR, BooleanString.TRUE);
|
||||
|
||||
const source = node.getAttribute(SVG_SOURCE_ATTR) ?? node.textContent ?? '';
|
||||
const source = node.getAttribute(SVG.SOURCE_ATTR) ?? node.textContent ?? '';
|
||||
const clean = sanitizeSvg(source);
|
||||
|
||||
if (clean) {
|
||||
@@ -755,7 +752,7 @@
|
||||
const host = document.createElement('div');
|
||||
|
||||
node.appendChild(host);
|
||||
mountSvgShadow(host, clean, SVG_INLINE_SHADOW_STYLE);
|
||||
mountSvgShadow(host, clean, SVG.INLINE_SHADOW_STYLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -772,11 +769,11 @@
|
||||
// Don't handle data URLs or already-handled images
|
||||
if (
|
||||
img.src.startsWith(UrlProtocol.DATA) ||
|
||||
img.dataset[DATA_ERROR_HANDLED_ATTR] === BOOL_TRUE_STRING
|
||||
img.getAttribute(MARKDOWN_DATA_ATTRS.ERROR_HANDLED) === BooleanString.TRUE
|
||||
)
|
||||
return;
|
||||
|
||||
img.dataset[DATA_ERROR_HANDLED_ATTR] = BOOL_TRUE_STRING;
|
||||
img.setAttribute(MARKDOWN_DATA_ATTRS.ERROR_HANDLED, BooleanString.TRUE);
|
||||
|
||||
const src = img.src;
|
||||
// Create fallback element
|
||||
@@ -871,18 +868,21 @@
|
||||
<div
|
||||
bind:this={containerRef}
|
||||
onclick={handleMermaidClick}
|
||||
class="markdown-content {className}{config()[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS]
|
||||
class="markdown-content {className}{settingsStore.config[SETTINGS_KEYS.FULL_HEIGHT_CODE_BLOCKS]
|
||||
? ' full-height-code-blocks'
|
||||
: ''}"
|
||||
>
|
||||
{#each renderedBlocks as block (block.id)}
|
||||
<div class="markdown-block" data-block-id={block.id}>
|
||||
<div class="markdown-block" {...{ [MARKDOWN_DATA_ATTRS.BLOCK_ID]: block.id }}>
|
||||
{@html block.html}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if unstableBlockHtml}
|
||||
<div class="markdown-block markdown-block--unstable" data-block-id="unstable">
|
||||
<div
|
||||
class="markdown-block markdown-block--unstable"
|
||||
{...{ [MARKDOWN_DATA_ATTRS.BLOCK_ID]: 'unstable' }}
|
||||
>
|
||||
<!-- eslint-disable-next-line no-at-html-tags -->
|
||||
{@html unstableBlockHtml}
|
||||
</div>
|
||||
@@ -919,7 +919,7 @@
|
||||
</div>
|
||||
{#if liveSvgHtml}
|
||||
<div class="svg-scroll-container">
|
||||
<div class={SVG_BLOCK_CLASS}>
|
||||
<div class={SVG.BLOCK_CLASS}>
|
||||
<div bind:this={streamingSvgHost}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
* Uses dependency injection pattern to avoid direct component state access.
|
||||
*/
|
||||
|
||||
import { MERMAID_BLOCK_CLASS, MERMAID_SYNTAX_ATTR, MERMAID_WRAPPER_CLASS } from '$lib/constants';
|
||||
import {
|
||||
CODE_BLOCK_CLASS,
|
||||
MARKDOWN_DATA_ATTRS,
|
||||
MERMAID_BLOCK_CLASS,
|
||||
MERMAID_SYNTAX_ATTR,
|
||||
MERMAID_WRAPPER_CLASS
|
||||
} from '$lib/constants';
|
||||
import { BooleanString } from '$lib/enums';
|
||||
import { copyCodeToClipboard, copyToClipboard } from '$lib/utils';
|
||||
|
||||
export interface PreviewState {
|
||||
@@ -40,11 +47,11 @@ export function createHandleCopyClick() {
|
||||
|
||||
if (!target) return;
|
||||
|
||||
const wrapper = target.closest('.code-block-wrapper');
|
||||
const wrapper = target.closest(`.${CODE_BLOCK_CLASS.WRAPPER}`);
|
||||
|
||||
if (!wrapper) return;
|
||||
|
||||
const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
|
||||
const codeElement = wrapper.querySelector<HTMLElement>(`code[${MARKDOWN_DATA_ATTRS.CODE_ID}]`);
|
||||
|
||||
if (!codeElement) return;
|
||||
|
||||
@@ -86,16 +93,16 @@ export function createHandlePreviewClick(previewState: PreviewState) {
|
||||
|
||||
if (!target) return;
|
||||
|
||||
const wrapper = target.closest('.code-block-wrapper');
|
||||
const wrapper = target.closest(`.${CODE_BLOCK_CLASS.WRAPPER}`);
|
||||
|
||||
if (!wrapper) return;
|
||||
|
||||
const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
|
||||
const codeElement = wrapper.querySelector<HTMLElement>(`code[${MARKDOWN_DATA_ATTRS.CODE_ID}]`);
|
||||
|
||||
if (!codeElement) return;
|
||||
|
||||
const rawCode = codeElement.textContent ?? '';
|
||||
const languageLabel = wrapper.querySelector<HTMLElement>('.code-language');
|
||||
const languageLabel = wrapper.querySelector<HTMLElement>(`.${CODE_BLOCK_CLASS.LANGUAGE}`);
|
||||
const language = languageLabel?.textContent?.trim() || 'text';
|
||||
|
||||
previewState.setPreviewCode(rawCode);
|
||||
@@ -112,8 +119,8 @@ export function createHandleMermaidClick(mermaidState: MermaidPreviewState) {
|
||||
return async function handleMermaidClick(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement;
|
||||
// Check if clicking on copy or preview button in mermaid block
|
||||
const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .copy-code-btn`);
|
||||
const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .preview-code-btn`);
|
||||
const copyBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .${CODE_BLOCK_CLASS.COPY_BTN}`);
|
||||
const previewBtn = target.closest(`.${MERMAID_WRAPPER_CLASS} .${CODE_BLOCK_CLASS.PREVIEW_BTN}`);
|
||||
|
||||
if (copyBtn || previewBtn) {
|
||||
const wrapper = target.closest(`.${MERMAID_WRAPPER_CLASS}`);
|
||||
@@ -189,15 +196,17 @@ export function createHandleMermaidPreviewOpenChange(mermaidState: MermaidPrevie
|
||||
export function createHandleImageError(
|
||||
renderedBlocksState: RenderedBlocksState,
|
||||
IMAGE_NOT_ERROR_BOUND_SELECTOR: string,
|
||||
DATA_ERROR_BOUND_ATTR: string,
|
||||
BOOL_TRUE_STRING: string
|
||||
errorBoundAttr: string,
|
||||
booleanString: BooleanString
|
||||
) {
|
||||
return async function handleImageError(event: Event) {
|
||||
const img = event.target as HTMLImageElement;
|
||||
|
||||
if (!img) return;
|
||||
|
||||
const blockId = img.closest('[data-block-id]')?.getAttribute('data-block-id');
|
||||
const blockId = img
|
||||
.closest(`[${MARKDOWN_DATA_ATTRS.BLOCK_ID}]`)
|
||||
?.getAttribute(MARKDOWN_DATA_ATTRS.BLOCK_ID);
|
||||
|
||||
if (!blockId) return;
|
||||
|
||||
@@ -206,19 +215,22 @@ export function createHandleImageError(
|
||||
if (!block) return;
|
||||
|
||||
// Skip if already handled
|
||||
if (img.dataset[DATA_ERROR_BOUND_ATTR] === BOOL_TRUE_STRING) return;
|
||||
if (img.getAttribute(errorBoundAttr) === booleanString) return;
|
||||
|
||||
img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
|
||||
img.setAttribute(errorBoundAttr, booleanString);
|
||||
|
||||
// Get the fallback HTML and replace the image
|
||||
const fallbackHtml = `<div class="image-error-placeholder" data-original-src="${img.src}">
|
||||
const fallbackHtml = `<div class="image-error-placeholder" ${MARKDOWN_DATA_ATTRS.ORIGINAL_SRC}="${img.src}">
|
||||
<span class="image-error-icon">⚠️</span>
|
||||
<span class="image-error-text">Failed to load image</span>
|
||||
</div>`;
|
||||
// Replace the img element with fallback in the block's HTML
|
||||
const newHtml = block.html.replace(/img[^>]*src=["']([^"']*)[^>]*>/g, (match, src) => {
|
||||
if (src === img.src) {
|
||||
return fallbackHtml.replace('data-original-src=""', `data-original-src="${src}"`);
|
||||
return fallbackHtml.replace(
|
||||
`${MARKDOWN_DATA_ATTRS.ORIGINAL_SRC}=""`,
|
||||
`${MARKDOWN_DATA_ATTRS.ORIGINAL_SRC}="${src}"`
|
||||
);
|
||||
}
|
||||
|
||||
return match;
|
||||
@@ -243,19 +255,27 @@ export function createSetupCodeBlockActions(
|
||||
return function setupCodeBlockActions(containerRef: HTMLElement | null) {
|
||||
if (!containerRef) return;
|
||||
|
||||
const wrappers = containerRef.querySelectorAll<HTMLElement>('.code-block-wrapper');
|
||||
const wrappers = containerRef.querySelectorAll<HTMLElement>(`.${CODE_BLOCK_CLASS.WRAPPER}`);
|
||||
|
||||
for (const wrapper of wrappers) {
|
||||
const copyButton = wrapper.querySelector<HTMLButtonElement>('.copy-code-btn');
|
||||
const previewButton = wrapper.querySelector<HTMLButtonElement>('.preview-code-btn');
|
||||
const copyButton = wrapper.querySelector<HTMLButtonElement>(`.${CODE_BLOCK_CLASS.COPY_BTN}`);
|
||||
const previewButton = wrapper.querySelector<HTMLButtonElement>(
|
||||
`.${CODE_BLOCK_CLASS.PREVIEW_BTN}`
|
||||
);
|
||||
|
||||
if (copyButton && copyButton.dataset.listenerBound !== 'true') {
|
||||
copyButton.dataset.listenerBound = 'true';
|
||||
if (
|
||||
copyButton &&
|
||||
copyButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
|
||||
) {
|
||||
copyButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
|
||||
copyButton.addEventListener('click', handleCopyClick);
|
||||
}
|
||||
|
||||
if (previewButton && previewButton.dataset.listenerBound !== 'true') {
|
||||
previewButton.dataset.listenerBound = 'true';
|
||||
if (
|
||||
previewButton &&
|
||||
previewButton.getAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND) !== BooleanString.TRUE
|
||||
) {
|
||||
previewButton.setAttribute(MARKDOWN_DATA_ATTRS.LISTENER_BOUND, BooleanString.TRUE);
|
||||
previewButton.addEventListener('click', handlePreviewClick);
|
||||
}
|
||||
}
|
||||
@@ -269,8 +289,8 @@ export function createSetupCodeBlockActions(
|
||||
export function createSetupImageErrorHandlers(
|
||||
handleImageError: (event: Event) => void,
|
||||
IMAGE_NOT_ERROR_BOUND_SELECTOR: string,
|
||||
DATA_ERROR_BOUND_ATTR: string,
|
||||
BOOL_TRUE_STRING: string
|
||||
errorBoundAttr: string,
|
||||
booleanString: BooleanString
|
||||
) {
|
||||
return function setupImageErrorHandlers(containerRef: HTMLElement | null) {
|
||||
if (!containerRef) return;
|
||||
@@ -278,7 +298,7 @@ export function createSetupImageErrorHandlers(
|
||||
const images = containerRef.querySelectorAll<HTMLImageElement>(IMAGE_NOT_ERROR_BOUND_SELECTOR);
|
||||
|
||||
for (const img of images) {
|
||||
img.dataset[DATA_ERROR_BOUND_ATTR] = BOOL_TRUE_STRING;
|
||||
img.setAttribute(errorBoundAttr, booleanString);
|
||||
img.addEventListener('error', handleImageError);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Utility functions for markdown processing in MarkdownContent component.
|
||||
*/
|
||||
|
||||
import { MARKDOWN_DATA_ATTRS } from '$lib/constants';
|
||||
import type { RootContent as HastRootContent } from 'hast';
|
||||
|
||||
/**
|
||||
@@ -69,7 +70,7 @@ export function getCodeInfoFromTarget(target: HTMLElement): CodeInfo | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const codeElement = wrapper.querySelector<HTMLElement>('code[data-code-id]');
|
||||
const codeElement = wrapper.querySelector<HTMLElement>(`code[${MARKDOWN_DATA_ATTRS.CODE_ID}]`);
|
||||
|
||||
if (!codeElement) {
|
||||
console.error('No code element found in wrapper');
|
||||
|
||||
+8
-14
@@ -4,17 +4,11 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
CODE_BLOCK_ACTIONS_CLASS,
|
||||
CODE_BLOCK_HEADER_CLASS,
|
||||
CODE_BLOCK_SCROLL_CONTAINER_CLASS,
|
||||
CODE_BLOCK_CLASS,
|
||||
CODE_ICON_SVG,
|
||||
CODE_LANGUAGE_CLASS,
|
||||
COPY_CODE_BTN_CLASS,
|
||||
COPY_ICON_SVG,
|
||||
DIAGRAM_SOURCE_CLASS,
|
||||
PREVIEW_CODE_BTN_CLASS,
|
||||
PREVIEW_ICON_SVG,
|
||||
RELATIVE_CLASS,
|
||||
TOGGLE_SOURCE_BTN_CLASS
|
||||
} from '$lib/constants';
|
||||
import type { Element, ElementContent } from 'hast';
|
||||
@@ -65,7 +59,7 @@ export function createButton(
|
||||
* Creates a copy button element.
|
||||
*/
|
||||
export function createCopyButton(id: string, idAttribute: string, title: string = 'Copy'): Element {
|
||||
return createButton(COPY_CODE_BTN_CLASS, title, COPY_ICON_SVG, id, idAttribute);
|
||||
return createButton(CODE_BLOCK_CLASS.COPY_BTN, title, COPY_ICON_SVG, id, idAttribute);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +70,7 @@ export function createPreviewButton(
|
||||
idAttribute: string,
|
||||
title: string = 'Preview'
|
||||
): Element {
|
||||
return createButton(PREVIEW_CODE_BTN_CLASS, title, PREVIEW_ICON_SVG, id, idAttribute);
|
||||
return createButton(CODE_BLOCK_CLASS.PREVIEW_BTN, title, PREVIEW_ICON_SVG, id, idAttribute);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +114,7 @@ export function createSourceView(
|
||||
type: 'element'
|
||||
}
|
||||
],
|
||||
properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_SCROLL_CONTAINER_CLASS] },
|
||||
properties: { className: [DIAGRAM_SOURCE_CLASS, CODE_BLOCK_CLASS.SCROLL_CONTAINER] },
|
||||
tagName: 'div',
|
||||
type: 'element'
|
||||
};
|
||||
@@ -134,7 +128,7 @@ export function createBlockHeader(
|
||||
id: string,
|
||||
idAttribute: string,
|
||||
actions: Element[],
|
||||
languageClassName: string = CODE_LANGUAGE_CLASS
|
||||
languageClassName: string = CODE_BLOCK_CLASS.LANGUAGE
|
||||
): Element {
|
||||
return {
|
||||
children: [
|
||||
@@ -146,12 +140,12 @@ export function createBlockHeader(
|
||||
},
|
||||
{
|
||||
children: actions,
|
||||
properties: { className: [CODE_BLOCK_ACTIONS_CLASS] },
|
||||
properties: { className: [CODE_BLOCK_CLASS.ACTIONS] },
|
||||
tagName: 'div',
|
||||
type: 'element'
|
||||
}
|
||||
],
|
||||
properties: { className: [CODE_BLOCK_HEADER_CLASS] },
|
||||
properties: { className: [CODE_BLOCK_CLASS.HEADER] },
|
||||
tagName: 'div',
|
||||
type: 'element'
|
||||
};
|
||||
@@ -185,7 +179,7 @@ export function createWrapper(
|
||||
return {
|
||||
children: [header, createScrollContainer(preElement, scrollContainerClass), ...extraChildren],
|
||||
properties: {
|
||||
className: [wrapperClass, RELATIVE_CLASS],
|
||||
className: [wrapperClass, CODE_BLOCK_CLASS.RELATIVE],
|
||||
...additionalAttributes
|
||||
} as Element['properties'],
|
||||
tagName: 'div',
|
||||
|
||||
+9
-7
@@ -17,7 +17,7 @@ import {
|
||||
createWrapper,
|
||||
generateBlockId
|
||||
} from './code-block-utils';
|
||||
import { CODE_BLOCK_SCROLL_CONTAINER_CLASS, CODE_BLOCK_WRAPPER_CLASS } from '$lib/constants';
|
||||
import { CODE_BLOCK_CLASS, MARKDOWN_DATA_ATTRS } from '$lib/constants';
|
||||
import type { Element, ElementContent, Root } from 'hast';
|
||||
import type { Plugin } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
@@ -65,21 +65,23 @@ export const rehypeEnhanceCodeBlocks: Plugin<[], Root> = () => {
|
||||
|
||||
codeElement.properties = {
|
||||
...codeElement.properties,
|
||||
'data-code-id': codeId
|
||||
[MARKDOWN_DATA_ATTRS.CODE_ID]: codeId
|
||||
};
|
||||
|
||||
const actions: Element[] = [createCopyButton(codeId, 'data-code-id', 'Copy code')];
|
||||
const actions: Element[] = [
|
||||
createCopyButton(codeId, MARKDOWN_DATA_ATTRS.CODE_ID, 'Copy code')
|
||||
];
|
||||
|
||||
if (language.toLowerCase() === 'html') {
|
||||
actions.push(createPreviewButton(codeId, 'data-code-id', 'Preview code'));
|
||||
actions.push(createPreviewButton(codeId, MARKDOWN_DATA_ATTRS.CODE_ID, 'Preview code'));
|
||||
}
|
||||
|
||||
const header = createBlockHeader(language, codeId, 'data-code-id', actions);
|
||||
const header = createBlockHeader(language, codeId, MARKDOWN_DATA_ATTRS.CODE_ID, actions);
|
||||
const wrapper = createWrapper(
|
||||
header,
|
||||
node,
|
||||
CODE_BLOCK_WRAPPER_CLASS,
|
||||
CODE_BLOCK_SCROLL_CONTAINER_CLASS
|
||||
CODE_BLOCK_CLASS.WRAPPER,
|
||||
CODE_BLOCK_CLASS.SCROLL_CONTAINER
|
||||
);
|
||||
|
||||
// Replace pre with wrapper in parent
|
||||
|
||||
+13
-22
@@ -19,16 +19,7 @@ import {
|
||||
generateBlockId
|
||||
} from './code-block-utils';
|
||||
import type { DiagramPreData } from './pre-transform';
|
||||
import {
|
||||
DIAGRAM_VIEW_MODE_ATTR,
|
||||
DIAGRAM_VIEW_RENDERED,
|
||||
SVG_BLOCK_CLASS,
|
||||
SVG_ID_ATTR,
|
||||
SVG_LANGUAGE,
|
||||
SVG_SCROLL_CONTAINER_CLASS,
|
||||
SVG_SOURCE_ATTR,
|
||||
SVG_WRAPPER_CLASS
|
||||
} from '$lib/constants';
|
||||
import { DIAGRAM_VIEW_MODE_ATTR, DIAGRAM_VIEW_RENDERED, SVG } from '$lib/constants';
|
||||
import type { Element, ElementContent, Root } from 'hast';
|
||||
import type { Plugin } from 'unified';
|
||||
import { visit } from 'unist-util-visit';
|
||||
@@ -48,11 +39,11 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
|
||||
|
||||
if (!Array.isArray(className)) return;
|
||||
|
||||
const isSvg = className.some((cls) => typeof cls === 'string' && cls === SVG_BLOCK_CLASS);
|
||||
const isSvg = className.some((cls) => typeof cls === 'string' && cls === SVG.BLOCK_CLASS);
|
||||
|
||||
if (!isSvg) return;
|
||||
|
||||
const svgId = generateBlockId(SVG_LANGUAGE, 'idxSvgBlock');
|
||||
const svgId = generateBlockId(SVG.LANGUAGE, 'idxSvgBlock');
|
||||
// Extract the svg source (text content of the pre element)
|
||||
const svgSource = node.children
|
||||
.map((child) => {
|
||||
@@ -65,26 +56,26 @@ export const rehypeEnhanceSvgBlocks: Plugin<[], Root> = () => {
|
||||
// Store the svg source in data attribute for copy and render
|
||||
node.properties = {
|
||||
...node.properties,
|
||||
[SVG_ID_ATTR]: svgId,
|
||||
[SVG_SOURCE_ATTR]: svgSource
|
||||
[SVG.ID_ATTR]: svgId,
|
||||
[SVG.SOURCE_ATTR]: svgSource
|
||||
};
|
||||
|
||||
const actions = [
|
||||
createCopyButton(svgId, SVG_ID_ATTR, 'Copy svg source'),
|
||||
createToggleSourceButton(svgId, SVG_ID_ATTR, 'Toggle svg source'),
|
||||
createPreviewButton(svgId, SVG_ID_ATTR, 'Preview svg')
|
||||
createCopyButton(svgId, SVG.ID_ATTR, 'Copy svg source'),
|
||||
createToggleSourceButton(svgId, SVG.ID_ATTR, 'Toggle svg source'),
|
||||
createPreviewButton(svgId, SVG.ID_ATTR, 'Preview svg')
|
||||
];
|
||||
const header = createBlockHeader(SVG_LANGUAGE, svgId, SVG_ID_ATTR, actions);
|
||||
const header = createBlockHeader(SVG.LANGUAGE, svgId, SVG.ID_ATTR, actions);
|
||||
const preservedCode = (node.data as DiagramPreData | undefined)?.sourceCode;
|
||||
const sourceView = createSourceView(preservedCode, svgSource, SVG_LANGUAGE);
|
||||
const sourceView = createSourceView(preservedCode, svgSource, SVG.LANGUAGE);
|
||||
const wrapper = createWrapper(
|
||||
header,
|
||||
node,
|
||||
SVG_WRAPPER_CLASS,
|
||||
SVG_SCROLL_CONTAINER_CLASS,
|
||||
SVG.WRAPPER_CLASS,
|
||||
SVG.SCROLL_CONTAINER_CLASS,
|
||||
{
|
||||
[DIAGRAM_VIEW_MODE_ATTR]: DIAGRAM_VIEW_RENDERED,
|
||||
[SVG_ID_ATTR]: svgId
|
||||
[SVG.ID_ATTR]: svgId
|
||||
},
|
||||
[sourceView]
|
||||
);
|
||||
|
||||
+3
-4
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Rehype plugin that rewrites `file://` markdown anchors into the inline
|
||||
* mention chip, sharing the class string with the contenteditable
|
||||
* tokenizer via `$lib/constants/mention-badge`.
|
||||
* mention chip, sharing the class string with the ChatFormInputRich
|
||||
* tokenizer via `$lib/constants`.
|
||||
*
|
||||
* The chip is presentational: `file://` navigation is blocked from
|
||||
* http(s) pages, so the anchor becomes a plain `<span>` (no link role,
|
||||
@@ -16,8 +16,7 @@ import {
|
||||
PATH_SEPARATOR,
|
||||
SETTINGS_KEYS
|
||||
} from '$lib/constants';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { settingsStore, toolsStore } from '$lib/stores';
|
||||
import { decodeFileLinkPath, getMentionBadgeIconPaths, getMentionBadgeLabel } from '$lib/utils';
|
||||
import type { Element, Root } from 'hast';
|
||||
import type { Plugin } from 'unified';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createPreTransform } from './pre-transform';
|
||||
import { SVG_BLOCK_CLASS, SVG_LANGUAGE, SVG_TAG_PREFIX, XML_LANGUAGE } from '$lib/constants';
|
||||
import { SVG } from '$lib/constants';
|
||||
|
||||
/**
|
||||
* Converts svg code blocks to <pre class="svg-block"> for client-side rendering.
|
||||
@@ -7,7 +7,7 @@ import { SVG_BLOCK_CLASS, SVG_LANGUAGE, SVG_TAG_PREFIX, XML_LANGUAGE } from '$li
|
||||
* svg inside an xml fence.
|
||||
*/
|
||||
export const rehypeSvgPre = createPreTransform(
|
||||
[SVG_LANGUAGE, XML_LANGUAGE],
|
||||
SVG_BLOCK_CLASS,
|
||||
(text) => text.startsWith(SVG_TAG_PREFIX)
|
||||
[SVG.LANGUAGE, SVG.XML_LANGUAGE],
|
||||
SVG.BLOCK_CLASS,
|
||||
(text) => text.startsWith(SVG.TAG_PREFIX)
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { SETTINGS_KEYS } from '$lib/constants';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import { settingsStore, toolsStore } from '$lib/stores';
|
||||
import {
|
||||
getMentionBadgeIconPaths,
|
||||
getMentionBadgeLabel,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import MermaidPreviewControls from './MermaidPreviewControls.svelte';
|
||||
import { SVG_DIALOG_SHADOW_STYLE } from '$lib/constants';
|
||||
import { SVG } from '$lib/constants';
|
||||
import { mountSvgShadow } from '$lib/utils/svg-shadow';
|
||||
|
||||
interface Props {
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
// Re-mount on every svgHtml change so a live streaming svg keeps rendering while zoomed
|
||||
$effect(() => {
|
||||
if (svgHost) mountSvgShadow(svgHost, svgHtml, SVG_DIALOG_SHADOW_STYLE);
|
||||
if (svgHost) mountSvgShadow(svgHost, svgHtml, SVG.DIALOG_SHADOW_STYLE);
|
||||
});
|
||||
|
||||
// Zoom and pan state
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
|
||||
import ZoomInIcon from '@lucide/svelte/icons/zoom-in';
|
||||
import ZoomOutIcon from '@lucide/svelte/icons/zoom-out';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
|
||||
interface Props {
|
||||
scale: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
|
||||
import { ColorMode } from '$lib/enums';
|
||||
import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX, UI_DATA_ATTRS } from '$lib/constants';
|
||||
import { BooleanString, ColorMode } from '$lib/enums';
|
||||
import { highlightCode } from '$lib/utils';
|
||||
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
||||
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
||||
@@ -38,13 +38,15 @@
|
||||
function loadHighlightTheme(isDark: boolean) {
|
||||
if (!browser) return;
|
||||
|
||||
const existingThemes = document.querySelectorAll('style[data-highlight-theme-preview]');
|
||||
const existingThemes = document.querySelectorAll(
|
||||
`style[${UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW}]`
|
||||
);
|
||||
|
||||
existingThemes.forEach((style) => style.remove());
|
||||
|
||||
const style = document.createElement('style');
|
||||
|
||||
style.setAttribute('data-highlight-theme-preview', 'true');
|
||||
style.setAttribute(UI_DATA_ATTRS.HIGHLIGHT_THEME_PREVIEW, BooleanString.TRUE);
|
||||
style.textContent = isDark ? githubDarkCss : githubLightCss;
|
||||
|
||||
document.head.appendChild(style);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user