Compare commits

..

5 Commits

Author SHA1 Message Date
Aleksander Grygier f2efd64141 ui: Move styles/ to $lib scope (#26950)
* refactor: Move `styles/` to `src/lib` and remove legacy alias

* chore: Add newline
2026-08-13 08:13:52 +02:00
Aleksander Grygier 094e53db1c ui: Stores architecture improvements (#26910)
* refactor: Stores barrel imports + SSR gates

* refactor: Drop agenticStore wrapper exports

* refactor: Drop chatStore wrapper exports

* refactor: Drop modelsStore wrapper exports

* refactor: Drop serverStore wrapper exports

* refactor: Drop unused mcpStore wrapper exports

* refactor: Drop mcpResourceStore wrapper exports

* refactor: Drop conversationsStore wrapper exports + move buildConversationTree to utils

* refactor: Drop settingsStore wrapper exports

* refactor: Drop unused toolsStore wrapper exports

* refactor: Fix lint errors from store wrapper removal

* fix: Missing change

* refactor: Cleanup

* refactor: Context Stats store
2026-08-13 08:11:30 +02:00
Aleksander Grygier a6040c925c refactor: Clean up UI types (#26909) 2026-08-13 08:07:34 +02:00
Georgi Gerganov 1f368f354d ggml : fix arm builds, unused var (#26991) 2026-08-13 07:57:24 +03:00
Aleksander Grygier e21152dc96 ui: Constants refactor (#26908)
* refactor: Constants

* refactor: Constants/Enums cleanup

* refactor: Constant objects instead of multiple single value constants

* refactor: Cleanup constants
2026-08-13 06:53:56 +02:00
307 changed files with 2424 additions and 2375 deletions
+2 -1
View File
@@ -5,7 +5,8 @@
static int ggml_backend_cpu_aarch64_score() {
int score = 1;
ggml_feats_arch64_runtime_t af = ggml_get_aarch64_runtime_features();
const ggml_feats_arch64_runtime_t af = ggml_feats_get_arch64_runtime();
GGML_UNUSED(af);
#ifdef GGML_USE_DOTPROD
if (!af.has_dotprod) { return 0; }
+2 -2
View File
@@ -184,7 +184,7 @@ static void add_smcus_from_smidr(uint64_t smidr, size_t & num_private, std::map<
}
static size_t detect_num_smcus() {
auto runtime_feat = ggml_get_aarch64_runtime_features();
const auto runtime_feat = ggml_feats_get_arch64_runtime();
if (!runtime_feat.has_sme) {
return 0;
}
@@ -310,7 +310,7 @@ static void init_kleidiai_context(void) {
const char *env_threads = getenv("GGML_TOTAL_THREADS");
const char *env_chunk_mult = getenv("GGML_KLEIDIAI_CHUNK_MULTIPLIER");
auto runtime_feat = ggml_get_aarch64_runtime_features();
const auto runtime_feat = ggml_feats_get_arch64_runtime();
size_t detected_smcus = 0;
+1 -1
View File
@@ -92,7 +92,7 @@ typedef struct ggml_feats_arch64_runtime {
int sve_cnt;
} ggml_feats_arch64_runtime_t;
static inline ggml_feats_arch64_runtime_t ggml_get_aarch64_runtime_features(void) {
static inline ggml_feats_arch64_runtime_t ggml_feats_get_arch64_runtime(void) {
ggml_feats_arch64_runtime_t runtime_feat = {};
#if defined(__linux__)
+1 -1
View File
@@ -1,5 +1,5 @@
import { writeThemeFavicons } from './scripts/favicon-colorize';
import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa.constants';
import { defineConfig } from '@vite-pwa/assets-generator/config';
writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
+1 -1
View File
@@ -4,7 +4,7 @@ import {
PWA_ASSET_GENERATOR,
PWA_GENERATOR_DEVICES,
THEME_COLORS
} from './src/lib/constants/pwa';
} from './src/lib/constants/pwa.constants';
import { SplashOrientation } from './src/lib/enums/splash.enums';
import {
combinePresetAndAppleSplashScreens,
+1 -1
View File
@@ -1,4 +1,4 @@
import { BUILD_CONFIG } from '../src/lib/constants/pwa';
import { BUILD_CONFIG } from '../src/lib/constants/pwa.constants';
import { existsSync, writeFileSync } from 'node:fs';
import { resolve } from 'path';
import type { Plugin } from 'vite';
@@ -1,4 +1,4 @@
import { BUILD_CONFIG } from '../src/lib/constants/pwa';
import { BUILD_CONFIG } from '../src/lib/constants/pwa.constants';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'path';
import type { Plugin } from 'vite';
@@ -1,5 +1,10 @@
import { NEWLINE, TAB } from '../src/lib/constants/code';
import { APPLE_DEVICES, BUILD_CONFIG, REGEX_PATTERNS, SPLASH_LINK } from '../src/lib/constants/pwa';
import {
APPLE_DEVICES,
BUILD_CONFIG,
REGEX_PATTERNS,
SPLASH_LINK
} from '../src/lib/constants/pwa.constants';
import { NEWLINE, TAB } from '../src/lib/constants/special-characters.constants';
import { SplashOrientation } from '../src/lib/enums/splash.enums';
import type { SplashDimensions } from '../src/lib/types';
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
+1
View File
@@ -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';
@@ -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,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,
@@ -5,7 +5,7 @@
ChatAttachmentsPreviewNavButtons,
ChatAttachmentsPreviewThumbnailStrip
} from '$lib/components/app';
import { modelsStore } from '$lib/stores/models.svelte';
import { modelsStore } from '$lib/stores';
import {
createBase64DataUrl,
formatFileSize,
@@ -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';
@@ -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 } from '$lib/constants';
interface PreviewItem {
id: string;
@@ -3,7 +3,7 @@
import {
ChatAttachmentsList,
ChatFormActions,
ChatFormContenteditable,
ChatFormContentEditable,
ChatFormFileInputInvisible,
ChatFormMcpResourcesList,
ChatFormPickers,
@@ -26,19 +26,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,
@@ -143,7 +140,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 +181,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 +189,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 +217,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)
@@ -605,7 +604,7 @@
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
<ChatFormContentEditable
class="px-5 py-1.5 md:pt-0 mb-0.5"
bind:this={inputRef}
bind:value
@@ -634,7 +633,7 @@
/>
{/if}
{#if mcpHasResourceAttachments()}
{#if mcpResourceStore.hasAttachments}
<ChatFormMcpResourcesList
class="mb-3"
onResourceClick={(uri) => {
@@ -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;
@@ -12,9 +12,9 @@
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 { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte';
interface Props {
@@ -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 {
@@ -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();
@@ -14,16 +14,17 @@
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 { 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 {
@@ -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);
@@ -2,7 +2,7 @@
import ChatFormActionAddButton from './ChatFormActionAddButton.svelte';
import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
import { isMobile } from '$lib/stores/viewport.svelte';
import { isMobile } from '$lib/stores';
interface Props {
disabled?: boolean;
@@ -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,21 +131,23 @@
});
$effect(() => {
hasModelSelected = !isRouter || !!conversationModel || !!selectedModelId();
hasModelSelected = !isRouter || !!conversationModel || !!modelsStore.selectedModelId;
});
$effect(() => {
if (!isRouter) {
isSelectedModelInCache = true;
} else if (conversationModel) {
isSelectedModelInCache = modelOptions().some((option) => option.model === conversationModel);
isSelectedModelInCache = modelsStore.models.some(
(option) => option.model === conversationModel
);
} else {
const currentModelId = selectedModelId();
const currentModelId = modelsStore.selectedModelId;
if (!currentModelId) {
isSelectedModelInCache = false;
} else {
isSelectedModelInCache = modelOptions().some((option) => option.id === currentModelId);
isSelectedModelInCache = modelsStore.models.some((option) => option.id === currentModelId);
}
}
});
@@ -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;
@@ -10,18 +10,10 @@
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 { 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 +54,7 @@
uploadedFiles = []
}: Props = $props();
let currentConfig = $derived(config());
let currentConfig = $derived(settingsStore.config);
let hasMcpPromptsSupport = $derived.by(() => {
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
@@ -104,7 +96,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 +118,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;
@@ -1,8 +1,9 @@
<script lang="ts">
import { TRIM_LEADING_PADDING_REGEX, TRIM_TRAILING_PADDING_REGEX } from '$lib/constants';
import { CODE_BLOCK } from '$lib/constants';
import { ColorMode } from '$lib/enums';
import { isMobile } from '$lib/stores/viewport.svelte';
import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
import { isMobile } from '$lib/stores';
import type { ContentEditableToken } from '$lib/types';
import type { SourceHistoryEntry } from '$lib/utils';
import {
badgeAwareWordJump,
buildFragment,
@@ -63,7 +64,7 @@
rootElement.dataset.empty = source.length === 0 ? 'true' : 'false';
}
function renderTokens(tokens: ContentToken[]) {
function renderTokens(tokens: ContentEditableToken[]) {
if (!rootElement) return;
const caret = rangeToTextOffset(rootElement, safeRange());
@@ -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
@@ -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,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;
@@ -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}
@@ -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';
@@ -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);
@@ -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 {
@@ -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';
@@ -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';
@@ -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';
@@ -4,26 +4,17 @@
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 } 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
@@ -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
});
@@ -7,15 +7,11 @@
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 { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
import { getChatActionsContext, setMessageEditContext } 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 { chatStore, conversationsStore, isMobile } from '$lib/stores';
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
import { deriveAgenticSections } from '$lib/utils';
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
@@ -187,7 +183,7 @@
});
$effect(() => {
const pendingId = pendingEditMessageId();
const pendingId = chatStore.pendingEditMessageId;
if (pendingId && pendingId === message.id && !isEditing) {
handleEdit();
@@ -11,10 +11,7 @@
import { getMessageEditContext } 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';
@@ -69,15 +66,15 @@
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 +172,7 @@
<ChatMessageAgenticContent
{message}
{toolMessages}
isStreaming={isChatStreaming()}
isStreaming={chatStore.isStreaming()}
{isLastAssistantMessage}
/>
{/if}
@@ -190,14 +187,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}
/>
@@ -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 {
@@ -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>
@@ -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}
@@ -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,7 +6,7 @@
import { INPUT_CLASSES } from '$lib/constants';
import { getMessageEditContext } 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 {
@@ -64,7 +64,7 @@
let contentHeight = $state(0);
const MAX_HEIGHT = 200; // pixels
const currentConfig = config();
const currentConfig = settingsStore.config;
let showExpandButton = $derived(contentHeight > MAX_HEIGHT);
@@ -12,13 +12,9 @@
import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte';
import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte';
import { BuiltInTool } from '$lib/enums';
import type { AgenticSection } from '$lib/types';
import type { DatabaseMessageExtra } from '$lib/types';
import {
type AgenticSection,
extractSearchQuery,
extractSearchResults,
isWebSearchToolName
} from '$lib/utils';
import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
interface Props {
section: AgenticSection;
@@ -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,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;
@@ -10,21 +10,19 @@
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 { 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, ToolResultLine } from '$lib/types';
import type { DatabaseMessageExtra } from '$lib/types';
import {
abbreviateHome,
type AgenticSection,
type ExecShellExitStatus,
highlightCode,
isExitCodeSummaryLine,
parseExecShellCommandError,
parseExecShellCommandExitStatus,
parseToolResultWithMedia,
type ToolResultLine
parseToolResultWithMedia
} from '$lib/utils';
interface Props {
@@ -94,7 +92,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 +221,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}
@@ -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,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;
@@ -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;
@@ -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;
@@ -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}
@@ -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 {
@@ -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;
@@ -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 {
@@ -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;
@@ -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 };
@@ -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';
/**
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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 };
}
@@ -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;
@@ -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;
@@ -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;
@@ -8,8 +8,7 @@
import { getMessageEditContext } 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;
@@ -54,8 +53,8 @@
const editCtx = getMessageEditContext();
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(() => {
@@ -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;
@@ -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 {
@@ -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;
@@ -10,7 +10,7 @@
import Label from '$lib/components/ui/label/label.svelte';
import { Switch } from '$lib/components/ui/switch';
import { MessageRole } from '$lib/enums';
import { activeConversation } from '$lib/stores/conversations.svelte';
import { conversationsStore } from '$lib/stores';
interface Props {
role: MessageRole.USER | MessageRole.ASSISTANT;
@@ -69,7 +69,7 @@
}
function handleOpenForkDialog() {
const conv = activeConversation();
const conv = conversationsStore.activeConversation;
forkName = `Fork of ${conv?.name ?? 'Conversation'}`;
forkIncludeAttachments = true;
@@ -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 } from '$lib/types';
import type {
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 = {
@@ -5,7 +5,7 @@
import { Switch } from '$lib/components/ui/switch';
import { getMessageEditContext } 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();
@@ -1,11 +1,11 @@
<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 { settingsStore } from '$lib/stores';
import type { DatabaseMessageExtra } from '$lib/types';
import type { AgenticSection } from '$lib/utils';
import type { AgenticSection } from '$lib/types';
interface Props {
section: AgenticSection;
@@ -25,7 +25,7 @@
section
}: Props = $props();
const currentConfig = config();
const currentConfig = settingsStore.config;
const REASONING_HEADER = 'Reasoning';
const REASONING_HEADER_PENDING = 'Reasoning...';
@@ -2,21 +2,7 @@
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 {
buildSiblingInfoMap,
copyToClipboard,
@@ -34,7 +20,7 @@
let allConversationMessages = $state<DatabaseMessage[]>([]);
const currentConfig = config();
const currentConfig = settingsStore.config;
setChatActionsContext({
continueAssistantMessage: async (message: DatabaseMessage) => {
@@ -108,7 +94,7 @@
});
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();
}
});
@@ -251,32 +237,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>
@@ -120,7 +120,7 @@ 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
* - Composes ChatFormTextarea (or ChatFormContentEditable 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
@@ -272,7 +272,7 @@ export { default as ChatFormMcpResourcesList } from './ChatForm/ChatFormMcpResou
* source string. ChatForm swaps it in once a mention link lands in the
* buffer. Shares the focus()/resetHeight()/caret handle with the textarea.
*/
export { default as ChatFormContenteditable } from './ChatForm/ChatFormContenteditable.svelte';
export { default as ChatFormContentEditable } from './ChatForm/ChatFormContentEditable.svelte';
/**
* Plain auto-resizing textarea with IME composition support. Default input
@@ -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,
@@ -26,7 +26,7 @@
} from '$lib/components/app';
import {
BOOL_TRUE_STRING,
CODE_BLOCK_HEADER_CLASS,
CODE_BLOCK_CLASS,
DATA_ERROR_BOUND_ATTR,
DATA_ERROR_HANDLED_ATTR,
DIAGRAM_VIEW_MODE_ATTR,
@@ -39,20 +39,13 @@
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 { 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 +98,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 +130,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>();
@@ -535,7 +528,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 +586,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 +604,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 +615,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 +626,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);
@@ -739,15 +732,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, '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 +748,7 @@
const host = document.createElement('div');
node.appendChild(host);
mountSvgShadow(host, clean, SVG_INLINE_SHADOW_STYLE);
mountSvgShadow(host, clean, SVG.INLINE_SHADOW_STYLE);
}
});
}
@@ -871,7 +864,7 @@
<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'
: ''}"
>
@@ -919,7 +912,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>
@@ -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',
@@ -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 } from '$lib/constants';
import type { Element, ElementContent, Root } from 'hast';
import type { Plugin } from 'unified';
import { visit } from 'unist-util-visit';
@@ -78,8 +78,8 @@ export const rehypeEnhanceCodeBlocks: Plugin<[], Root> = () => {
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
@@ -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]
);
@@ -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`.
* 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,6 +1,6 @@
<script lang="ts">
import { browser } from '$app/environment';
import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants/auto-scroll';
import { SYNTAX_CODE_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
import { ColorMode } from '$lib/enums';
import { highlightCode } from '$lib/utils';
import githubLightCss from 'highlight.js/styles/github.css?inline';
@@ -3,9 +3,9 @@
import { ActionIconCopyToClipboard, SyntaxHighlightedCode } from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { DEFAULT_RESOURCE_FILENAME } from '$lib/constants';
import { MimeTypeIncludes, MimeTypeText } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { DEFAULT_RESOURCE_FILENAME, MIME_TYPE_SUBSTRINGS } from '$lib/constants';
import { MimeTypeText } from '$lib/enums';
import { mcpStore } from '$lib/stores';
import type { DatabaseMessageExtraMcpResource } from '$lib/types';
import {
downloadResourceContent,
@@ -26,11 +26,13 @@
const favicon = $derived(mcpStore.getServerFavicon(extra.serverName));
function getLanguage(): string {
if (extra.mimeType?.includes(MimeTypeIncludes.JSON)) return MimeTypeIncludes.JSON;
if (extra.mimeType?.includes(MIME_TYPE_SUBSTRINGS.JSON)) return MIME_TYPE_SUBSTRINGS.JSON;
if (extra.mimeType?.includes(MimeTypeIncludes.JAVASCRIPT)) return MimeTypeIncludes.JAVASCRIPT;
if (extra.mimeType?.includes(MIME_TYPE_SUBSTRINGS.JAVASCRIPT))
return MIME_TYPE_SUBSTRINGS.JAVASCRIPT;
if (extra.mimeType?.includes(MimeTypeIncludes.TYPESCRIPT)) return MimeTypeIncludes.TYPESCRIPT;
if (extra.mimeType?.includes(MIME_TYPE_SUBSTRINGS.TYPESCRIPT))
return MIME_TYPE_SUBSTRINGS.TYPESCRIPT;
const name = extra.name || extra.uri || '';
@@ -7,14 +7,8 @@
} from '$lib/components/app';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import {
mcpResources,
mcpResourceStore,
mcpTotalResourceCount
} from '$lib/stores/mcp-resources.svelte';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
import { conversationsStore, mcpResourceStore, mcpStore } from '$lib/stores';
import type { MCPResourceContent, MCPResourceInfo, MCPResourceTemplateInfo } from '$lib/types';
import { getResourceDisplayName } from '$lib/utils';
import { SvelteSet } from 'svelte/reactivity';
@@ -39,7 +33,7 @@
let templatePreviewLoading = $state(false);
let templatePreviewError = $state<string | null>(null);
const totalCount = $derived(mcpTotalResourceCount());
const totalCount = $derived(mcpResourceStore.totalResourceCount);
$effect(() => {
if (open) {
@@ -205,7 +199,7 @@
function getAllResourcesFlatInTreeOrder(): MCPResourceInfo[] {
const allResources: MCPResourceInfo[] = [];
const resourcesMap = mcpResources();
const resourcesMap = mcpResourceStore.serverResources;
for (const [serverName, serverRes] of resourcesMap.entries()) {
for (const resource of serverRes.resources) {
@@ -4,17 +4,15 @@
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import {
BEARER_PREFIX,
BOOL_FALSE_STRING,
BOOL_TRUE_STRING,
DISMISSED_RECOMMENDED_MCP_SERVERS_LOCALSTORAGE_KEY,
HEADERS,
MCP_SERVER_ID_PREFIX,
RECOMMENDED_MCP_SERVERS,
REDACTED_HEADERS
RECOMMENDED_MCP_SERVERS
} 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 { canonicalizeServerUrl, parseHeadersToArray, uuid } from '$lib/utils';
interface Props {
@@ -61,10 +59,10 @@
let bearerTokenFilled = $derived.by(() => {
const pairs = parseHeadersToArray(newServerHeaders);
const bearerPrefix = BEARER_PREFIX.toLowerCase();
const bearerPrefix = HEADERS.BEARER.toLowerCase();
const bearer = pairs.find(
(p) =>
REDACTED_HEADERS.has(p.key.trim().toLowerCase()) &&
HEADERS.REDACTED.has(p.key.trim().toLowerCase()) &&
p.value.trim().toLowerCase().startsWith(bearerPrefix)
);
@@ -2,8 +2,7 @@
import { ActionIconCopyToClipboard, BadgesModality } from '$lib/components/app';
import * as Dialog from '$lib/components/ui/dialog';
import * as Table from '$lib/components/ui/table';
import { modelOptions, modelsLoading, modelsStore } from '$lib/stores/models.svelte';
import { serverStore } from '$lib/stores/server.svelte';
import { modelsStore, serverStore } from '$lib/stores';
import type { ApiLlamaCppServerProps } from '$lib/types';
import { formatFileSize, formatNumber, formatParameters } from '$lib/utils';
@@ -26,8 +25,8 @@
let serverProps = $derived(isRouter && modelId ? routerModelProps : serverStore.props);
let modelName = $derived(isRouter && modelId ? modelId : modelsStore.singleModelName);
let models = $derived(modelOptions());
let isLoadingModels = $derived(modelsLoading());
let models = $derived(modelsStore.models);
let isLoadingModels = $derived(modelsStore.loading);
// in router mode, find the model option matching modelId
// in single mode, use the first model as before
@@ -3,8 +3,7 @@
import { goto } from '$app/navigation';
import { page } from '$app/state';
import * as AlertDialog from '$lib/components/ui/alert-dialog';
import { URL_PARAMS } from '$lib/constants';
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
import { ICON_CLASS_DEFAULT, URL_PARAMS } from '$lib/constants';
interface Props {
open: boolean;
@@ -1,7 +1,7 @@
<script lang="ts">
import { Search, X } from '@lucide/svelte';
import { Input } from '$lib/components/ui/input';
import { ICON_CLASS_DEFAULT } from '$lib/constants/css-classes';
import { ICON_CLASS_DEFAULT } from '$lib/constants';
interface Props {
autofocus?: boolean;

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