ui: Linting & Formatting scripts (#26819)

This commit is contained in:
Aleksander Grygier
2026-08-10 08:38:37 +02:00
committed by GitHub
parent 1e396e72a8
commit 92d1bb0c99
538 changed files with 8806 additions and 6036 deletions
@@ -1,24 +1,24 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
import { chatStore, pendingEditMessageId } from '$lib/stores/chat.svelte';
import { isMobile } from '$lib/stores/viewport.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
import { DatabaseService } from '$lib/services/database.service';
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
import { REASONING_TAGS } from '$lib/constants/agentic';
import { MessageRole, AttachmentType, AgenticSectionType } from '$lib/enums';
import {
ChatMessageAssistant,
ChatMessageUser,
ChatMessageSystem,
ChatMessageMcpPrompt,
ChatMessageSynthetic,
ChatMessageMcpPrompt
ChatMessageSystem,
ChatMessageUser
} from '$lib/components/app/chat';
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
import { deriveAgenticSections } from '$lib/utils';
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
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 { 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 { deriveAgenticSections } from '$lib/utils';
import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
interface Props {
class?: string;
@@ -32,12 +32,12 @@
let {
class: className = '',
message,
toolMessages = [],
isLastAssistantMessage = false,
isLastUserMessage = false,
message,
nextAssistantMessage = null,
siblingInfo = null
siblingInfo = null,
toolMessages = []
}: Props = $props();
const chatActions = getChatActionsContext();
@@ -72,10 +72,12 @@
case AgenticSectionType.REASONING:
case AgenticSectionType.REASONING_PENDING:
parts.push(`${REASONING_TAGS.START}\n${section.content}\n${REASONING_TAGS.END}`);
break;
case AgenticSectionType.TEXT:
parts.push(section.content);
break;
case AgenticSectionType.TOOL_CALL:
@@ -115,9 +117,7 @@
let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
setMessageEditContext({
get isEditing() {
return isEditing;
},
cancel: handleCancelEdit,
get editedContent() {
return editedContent;
},
@@ -127,6 +127,12 @@
get editedUploadedFiles() {
return editedUploadedFiles;
},
get isEditing() {
return isEditing;
},
get messageRole() {
return message.role;
},
get originalContent() {
return message.role === MessageRole.ASSISTANT
? (rawEditContent ?? message.content)
@@ -135,42 +141,40 @@
get originalExtras() {
return message.extra || [];
},
get showSaveOnlyOption() {
return showSaveOnlyOption;
},
get showBranchAfterEditOption() {
return showBranchAfterEditOption;
},
get shouldBranchAfterEdit() {
return shouldBranchAfterEdit;
},
get messageRole() {
return message.role;
},
get rawEditContent() {
return rawEditContent;
},
save: handleSaveEdit,
saveOnly: handleSaveEditOnly,
setContent: (content: string) => {
editedContent = content;
},
setExtras: (extras: DatabaseMessageExtra[]) => {
editedExtras = extras;
},
setUploadedFiles: (files: ChatUploadedFile[]) => {
editedUploadedFiles = files;
},
setShouldBranchAfterEdit: (value: boolean) => {
shouldBranchAfterEdit = value;
},
save: handleSaveEdit,
saveOnly: handleSaveEditOnly,
cancel: handleCancelEdit,
setUploadedFiles: (files: ChatUploadedFile[]) => {
editedUploadedFiles = files;
},
get shouldBranchAfterEdit() {
return shouldBranchAfterEdit;
},
get showBranchAfterEditOption() {
return showBranchAfterEditOption;
},
get showSaveOnlyOption() {
return showSaveOnlyOption;
},
startEdit: handleEdit
});
let mcpPromptExtra = $derived.by(() => {
if (message.role !== MessageRole.USER) return null;
if (message.content.trim()) return null;
if (!message.extra || message.extra.length !== 1) return null;
const extra = message.extra[0];
@@ -238,6 +242,7 @@
function handleEdit() {
isEditing = true;
// Clear temporary placeholder content for system messages
if (message.role === MessageRole.SYSTEM && message.content === SYSTEM_MESSAGE_PLACEHOLDER) {
editedContent = '';
@@ -281,6 +286,7 @@
// After the system message flow ends, hand focus to the main chat form
function focusMainChatForm() {
if (isMobile.current) return;
document.querySelector<HTMLTextAreaElement>('.chat-screen-form-wrapper textarea')?.focus();
}
@@ -292,23 +298,29 @@
// If content is empty, remove without deleting children
if (!newContent) {
const conversationDeleted = await chatStore.removeSystemPromptPlaceholder(message.id);
isEditing = false;
if (conversationDeleted) {
goto(ROUTES.START);
} else {
focusMainChatForm();
}
return;
}
await DatabaseService.updateMessage(message.id, { content: newContent });
const index = conversationsStore.findMessageIndex(message.id);
if (index !== -1) {
conversationsStore.updateMessageAtIndex(index, { content: newContent });
}
focusMainChatForm();
} else if (message.role === MessageRole.USER) {
const finalExtras = await getMergedExtras();
chatActions.editWithBranching(message, editedContent.trim(), finalExtras);
} else {
// For assistant messages, preserve exact content including trailing whitespace
@@ -325,6 +337,7 @@
if (message.role === MessageRole.USER) {
// For user messages, trim to avoid accidental whitespace
const finalExtras = await getMergedExtras();
chatActions.editUserMessagePreserveResponses(message, editedContent.trim(), finalExtras);
}
@@ -1,7 +1,7 @@
<script lang="ts">
import {
ChatMessageAgenticContent,
ChatMessageActionIcons,
ChatMessageAgenticContent,
ChatMessageAssistantModel,
ChatMessageAssistantProcessingInfo,
ChatMessageAssistantRawOutput,
@@ -9,14 +9,13 @@
ChatMessageEditForm
} from '$lib/components/app';
import { getMessageEditContext } from '$lib/contexts';
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
import { chatStore, isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
import { modelLoadProgressText } from '$lib/utils';
import { MessageRole } from '$lib/enums';
import { config } from '$lib/stores/settings.svelte';
import { isRouterMode } from '$lib/stores/server.svelte';
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 { modelLoadProgressText } from '$lib/utils';
import { hasAgenticContent } from '$lib/utils';
interface Props {
@@ -49,7 +48,6 @@
deletionInfo,
isLastAssistantMessage = false,
message,
toolMessages = [],
onConfirmDelete,
onContinue,
onCopy,
@@ -61,7 +59,8 @@
onShowDeleteDialogChange,
showDeleteDialog,
siblingInfo = null,
textareaElement = $bindable()
textareaElement = $bindable(),
toolMessages = []
}: Props = $props();
// Get edit context
@@ -124,18 +123,21 @@
if (!userMessageEl) {
lastUserMessageHeight = 0;
return;
}
const updateHeight = () => {
const rect = userMessageEl.getBoundingClientRect();
const marginTop = Math.round(parseFloat(getComputedStyle(userMessageEl).marginTop));
lastUserMessageHeight = Math.round(rect.height + marginTop);
};
updateHeight();
const resizeObserver = new ResizeObserver(updateHeight);
resizeObserver.observe(userMessageEl);
return () => {
@@ -1,8 +1,8 @@
<script lang="ts">
import { ModelBadge, ModelsSelectorDropdown } from '$lib/components/app';
import { copyToClipboard } from '$lib/utils';
import { modelsStore } from '$lib/stores/models.svelte';
import { ServerModelStatus } from '$lib/enums';
import { modelsStore } from '$lib/stores/models.svelte';
import { copyToClipboard } from '$lib/utils';
interface Props {
displayedModel: string | null;
@@ -11,7 +11,7 @@
onRegenerate: (modelOverride?: string) => void;
}
let { displayedModel, isRouter, isLoading, onRegenerate }: Props = $props();
let { displayedModel, isLoading, isRouter, onRegenerate }: Props = $props();
let pendingModel = $state<string | null>(null);
@@ -38,6 +38,7 @@
}
onRegenerate(modelName);
return true;
}}
/>
@@ -1,6 +1,6 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import type { UseProcessingStateReturn } from '$lib/hooks/use-processing-state.svelte';
import { fade } from 'svelte/transition';
interface Props {
modelLoadingText: string | null;
@@ -8,7 +8,7 @@
position: 'top' | 'bottom';
}
let { modelLoadingText, processingState, position }: Props = $props();
let { modelLoadingText, position, processingState }: Props = $props();
const marginClass = position === 'top' ? 'mt-6' : 'mt-4';
</script>
@@ -1,5 +1,5 @@
<script lang="ts">
import { deriveAgenticSections, buildAssistantRawOutput } from '$lib/utils';
import { buildAssistantRawOutput, deriveAgenticSections } from '$lib/utils';
interface Props {
message: DatabaseMessage;
@@ -10,6 +10,7 @@
let rawOutputContent = $derived.by(() => {
const sections = deriveAgenticSections(message, toolMessages, [], false);
return buildAssistantRawOutput(sections);
});
</script>
@@ -10,7 +10,7 @@
showMessageStats: boolean;
}
let { message, isLoading, processingState, showMessageStats }: Props = $props();
let { isLoading, message, processingState, showMessageStats }: Props = $props();
</script>
{#if showMessageStats && message.timings && message.timings.predicted_n && message.timings.predicted_ms}
@@ -1,7 +1,7 @@
<script lang="ts">
import { Folder, FolderX } from '@lucide/svelte';
import { parseCwdMessage } from '$lib/utils';
import type { DatabaseMessage } from '$lib/types';
import { parseCwdMessage } from '$lib/utils';
interface Props {
class?: string;
@@ -5,7 +5,7 @@
ChatMessageMcpPromptContent
} from '$lib/components/app';
import { getMessageEditContext } from '$lib/contexts';
import { MessageRole, McpPromptVariant } from '$lib/enums';
import { McpPromptVariant, MessageRole } from '$lib/enums';
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
interface Props {
@@ -30,17 +30,17 @@
let {
class: className = '',
message,
mcpPrompt,
siblingInfo = null,
showDeleteDialog,
deletionInfo,
onCopy,
onEdit,
onDelete,
mcpPrompt,
message,
onConfirmDelete,
onCopy,
onDelete,
onEdit,
onNavigateToSibling,
onShowDeleteDialogChange
onShowDeleteDialogChange,
showDeleteDialog,
siblingInfo = null
}: Props = $props();
// Get edit context
@@ -1,11 +1,11 @@
<script lang="ts">
import { Card } from '$lib/components/ui/card';
import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { SvelteMap } from 'svelte/reactivity';
import { McpPromptVariant } from '$lib/enums';
import { TruncatedText } from '$lib/components/app/misc';
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 type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
import { SvelteMap } from 'svelte/reactivity';
interface ContentPart {
text: string;
@@ -22,10 +22,10 @@
let {
class: className = '',
prompt,
variant = McpPromptVariant.MESSAGE,
isLoading = false,
loadError
loadError,
prompt,
variant = McpPromptVariant.MESSAGE
}: Props = $props();
let hoveredArgKey = $state<string | null>(null);
@@ -35,13 +35,15 @@
let contentParts = $derived.by((): ContentPart[] => {
if (!prompt.content || !hasArguments) {
return [{ text: prompt.content || '', argKey: null }];
return [{ argKey: null, text: prompt.content || '' }];
}
const parts: ContentPart[] = [];
let remaining = prompt.content;
const valueToKey = new SvelteMap<string, string>();
for (const [key, value] of argumentEntries) {
if (value && value.trim()) {
valueToKey.set(value, key);
@@ -55,20 +57,21 @@
for (const value of sortedValues) {
const index = remaining.indexOf(value);
if (index !== -1 && (earliestMatch === null || index < earliestMatch.index)) {
earliestMatch = { index, value, key: valueToKey.get(value)! };
earliestMatch = { index, key: valueToKey.get(value)!, value };
}
}
if (earliestMatch) {
if (earliestMatch.index > 0) {
parts.push({ text: remaining.slice(0, earliestMatch.index), argKey: null });
parts.push({ argKey: null, text: remaining.slice(0, earliestMatch.index) });
}
parts.push({ text: earliestMatch.value, argKey: earliestMatch.key });
parts.push({ argKey: earliestMatch.key, text: earliestMatch.value });
remaining = remaining.slice(earliestMatch.index + earliestMatch.value.length);
} else {
parts.push({ text: remaining, argKey: null });
parts.push({ argKey: null, text: remaining });
break;
}
@@ -1,7 +1,7 @@
<script lang="ts">
import { parseCwdMessage } from '$lib/utils';
import type { DatabaseMessage } from '$lib/types';
import ChatMessageCwdChange from './ChatMessageCwdChange.svelte';
import type { DatabaseMessage } from '$lib/types';
import { parseCwdMessage } from '$lib/utils';
interface Props {
class?: string;
@@ -31,16 +31,16 @@
let {
class: className = '',
message,
siblingInfo = null,
showDeleteDialog,
deletionInfo,
onCopy,
onEdit,
onDelete,
message,
onConfirmDelete,
onCopy,
onDelete,
onEdit,
onNavigateToSibling,
onShowDeleteDialogChange,
showDeleteDialog,
siblingInfo = null,
textareaElement = $bindable()
}: Props = $props();
@@ -1,12 +1,4 @@
<script lang="ts">
import { BuiltInTool } from '$lib/enums';
import {
extractSearchQuery,
extractSearchResults,
isWebSearchToolName,
type AgenticSection
} from '$lib/utils';
import type { DatabaseMessageExtra } from '$lib/types';
import ChatMessageToolCallBlockDefault from './ChatMessageToolCallBlockDefault.svelte';
import ChatMessageToolCallBlockEditFile from './ChatMessageToolCallBlockEditFile.svelte';
import ChatMessageToolCallBlockExecShellCommand from './ChatMessageToolCallBlockExecShellCommand.svelte';
@@ -18,6 +10,14 @@
import ChatMessageToolCallBlockRunJavascript from './ChatMessageToolCallBlockRunJavascript.svelte';
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';
interface Props {
section: AgenticSection;
@@ -28,7 +28,7 @@
onToggle?: () => void;
}
let { section, attachments, open, isStreaming, isExecuting, onToggle }: Props = $props();
let { attachments, isExecuting, isStreaming, onToggle, open, section }: Props = $props();
const searchResults = $derived(extractSearchResults(section.toolResult));
const searchQuery = $derived(extractSearchQuery(section.toolArgs));
@@ -3,19 +3,19 @@
// Renders section.toolArgs / section.toolResult directly using the
// shared chrome shell.
import ToolCallBlock from './ToolCallBlock.svelte';
import { Loader2 } from '@lucide/svelte';
import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app';
import { FileTypeText, ToolResultKind } from '$lib/enums';
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
import { FileTypeText, ToolResultKind } from '$lib/enums';
import type { DatabaseMessageExtra } from '$lib/types';
import {
type AgenticSection,
classifyToolResult,
formatJsonPretty,
parseToolResultWithImages,
type AgenticSection
parseToolResultWithImages
} from '$lib/utils';
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
import type { DatabaseMessageExtra } from '$lib/types';
import ToolCallBlock from './ToolCallBlock.svelte';
interface Props {
section: AgenticSection;
@@ -25,7 +25,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, attachments, onToggle }: Props = $props();
let { attachments, isStreaming, onToggle, open, section }: Props = $props();
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
const outputKind = $derived(classifyToolResult(section.toolResult));
@@ -1,10 +1,10 @@
<script lang="ts">
import { XCircle } from '@lucide/svelte';
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
import { computeLineDiff, prefixFor, abbreviateHome, type AgenticSection } from '$lib/utils';
import { toolsStore } from '$lib/stores/tools.svelte';
import { parseEditFileMeta } from './parsers/edit-file';
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';
interface Props {
section: AgenticSection;
@@ -13,7 +13,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, onToggle }: Props = $props();
let { isStreaming, onToggle, open, section }: Props = $props();
const editFileMeta = $derived(parseEditFileMeta(section));
const home = $derived(toolsStore.serverHome);
@@ -6,26 +6,26 @@
// The scroll-to-bottom auto-scroll logic mirrors what was here
// before extraction.
import { Check, Loader2, XCircle, AlertTriangle } from '@lucide/svelte';
import { parseExecShellCommandMeta } from './parsers/exec-shell-command';
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 { config } from '$lib/stores/settings.svelte';
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 {
abbreviateHome,
type AgenticSection,
type ExecShellExitStatus,
highlightCode,
isExitCodeSummaryLine,
parseExecShellCommandError,
parseExecShellCommandExitStatus,
parseToolResultWithImages,
type AgenticSection,
type ExecShellExitStatus,
type ToolResultLine
} from '$lib/utils';
import { toolsStore } from '$lib/stores/tools.svelte';
import { parseExecShellCommandMeta } from './parsers/exec-shell-command';
import type { DatabaseMessageExtra } from '$lib/types';
import ToolCallBlock from './ToolCallBlock.svelte';
interface Props {
section: AgenticSection;
@@ -39,7 +39,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, isExecuting = false, attachments, onToggle }: Props = $props();
let { attachments, isExecuting = false, isStreaming, onToggle, open, section }: Props = $props();
// `isLive` covers all in-flight phases: pre-chunk spinner and
// streaming itself. Frozen output (tool done while agent continues)
@@ -108,6 +108,7 @@
function isAtBottom(): boolean {
if (!scrollEl) return false;
return (
scrollEl.scrollHeight - scrollEl.clientHeight - scrollEl.scrollTop <=
SCROLL_BOTTOM_THRESHOLD_PX
@@ -116,6 +117,7 @@
function scrollToBottomOnFrame() {
if (pendingFrame !== null || !scrollEl || userScrolledUp) return;
pendingFrame = requestAnimationFrame(() => {
pendingFrame = null;
@@ -128,18 +130,23 @@
function handleScrollEvent() {
if (!scrollEl) return;
const isScrollingUp = scrollEl.scrollTop < lastScrollTop;
if (isScrollingUp && !isAtBottom()) {
userScrolledUp = true;
} else if (isAtBottom()) {
userScrolledUp = false;
}
lastScrollTop = scrollEl.scrollTop;
}
$effect(() => {
void section.toolResult;
if (!scrollEl || !autoScroll) return;
scrollToBottomOnFrame();
});
@@ -149,10 +156,11 @@
if (!scrollEl || !autoScroll) return;
const observer = new MutationObserver(() => scrollToBottomOnFrame());
observer.observe(scrollEl, {
characterData: true,
childList: true,
subtree: true,
characterData: true
subtree: true
});
return () => observer.disconnect();
@@ -1,9 +1,9 @@
<script lang="ts">
import { XCircle } from '@lucide/svelte';
import { abbreviateHome, type AgenticSection } from '$lib/utils';
import { toolsStore } from '$lib/stores/tools.svelte';
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';
interface Props {
section: AgenticSection;
@@ -12,7 +12,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, onToggle }: Props = $props();
let { isStreaming, onToggle, open, section }: Props = $props();
const fileGlobMeta = $derived(parseFileGlobSearchMeta(section));
const home = $derived(toolsStore.serverHome);
@@ -8,7 +8,7 @@
isStreaming?: boolean;
}
let { section, isStreaming = false }: Props = $props();
let { isStreaming = false, section }: Props = $props();
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
@@ -24,9 +24,12 @@
try {
const parsed: unknown = JSON.parse(toolResultString);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const obj = parsed as Record<string, unknown>;
if (typeof obj.error === 'string') return { errorMessage: obj.error };
if (typeof obj.result === 'string') return { dateString: obj.result.trim() };
}
} catch {
@@ -1,15 +1,15 @@
<script lang="ts">
import { Info, Loader2 } from '@lucide/svelte';
import { AgenticSectionType } from '$lib/enums';
import { abbreviateHome, type AgenticSection } from '$lib/utils';
import { toolsStore } from '$lib/stores/tools.svelte';
import { abbreviateHome, type AgenticSection } from '$lib/utils';
interface Props {
section: AgenticSection;
isStreaming?: boolean;
}
let { section, isStreaming = false }: Props = $props();
let { isStreaming = false, section }: Props = $props();
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
@@ -26,12 +26,15 @@
try {
const parsed: unknown = JSON.parse(toolResultString);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const obj = parsed as Record<string, unknown>;
if (typeof obj.error === 'string') return { errorMessage: obj.error };
return {
os: typeof obj.os === 'string' ? obj.os : undefined,
cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined
cwd: typeof obj.cwd === 'string' ? obj.cwd : undefined,
os: typeof obj.os === 'string' ? obj.os : undefined
};
}
} catch {
@@ -1,9 +1,9 @@
<script lang="ts">
import { XCircle } from '@lucide/svelte';
import { abbreviateHome, type AgenticSection } from '$lib/utils';
import { toolsStore } from '$lib/stores/tools.svelte';
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';
interface Props {
section: AgenticSection;
@@ -12,7 +12,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, onToggle }: Props = $props();
let { isStreaming, onToggle, open, section }: Props = $props();
const grepMeta = $derived(parseGrepSearchMeta(section));
const home = $derived(toolsStore.serverHome);
@@ -1,9 +1,9 @@
<script lang="ts">
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 { parseReadFileMeta } from './parsers/read-file';
import ToolCallBlock from './ToolCallBlock.svelte';
interface Props {
section: AgenticSection;
@@ -12,7 +12,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, onToggle }: Props = $props();
let { isStreaming, onToggle, open, section }: Props = $props();
const readFileMeta = $derived(parseReadFileMeta(section));
</script>
@@ -1,11 +1,11 @@
<script lang="ts">
import { XCircle, Terminal } from '@lucide/svelte';
import { SyntaxHighlightedCode } from '$lib/components/app';
import { FileTypeText } from '$lib/enums';
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
import { getBuiltinToolUi, type AgenticSection } from '$lib/utils';
import { parseRunJavascriptMeta } from './parsers/run-javascript';
import ToolCallBlock from './ToolCallBlock.svelte';
import { Terminal, XCircle } from '@lucide/svelte';
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';
interface Props {
section: AgenticSection;
@@ -14,7 +14,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, onToggle }: Props = $props();
let { isStreaming, onToggle, open, section }: Props = $props();
const runJsMeta = $derived(parseRunJavascriptMeta(section));
const title = $derived(getBuiltinToolUi(section.toolName)?.label ?? section.toolName ?? '');
@@ -1,17 +1,17 @@
<script lang="ts">
import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants/css-classes';
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 { AgenticSectionType } from '$lib/enums';
import { mcpStore } from '$lib/stores/mcp.svelte';
import {
extractSearchResults,
type AgenticSection,
extractSearchQuery,
extractSearchResults,
faviconForUrl,
sanitizeExternalUrl,
type SearchResult,
type AgenticSection
type SearchResult
} from '$lib/utils';
interface Props {
@@ -21,7 +21,7 @@
onToggle?: () => void;
}
let { section, open = $bindable(false), isStreaming = false, onToggle }: Props = $props();
let { isStreaming = false, onToggle, open = $bindable(false), section }: Props = $props();
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
const isStreamingCall = $derived(section.type === AgenticSectionType.TOOL_CALL_STREAMING);
@@ -43,6 +43,7 @@
// retrospective.
const title = $derived.by(() => {
const verb = showSpinner ? 'Searching' : 'Searched';
return query ? `${verb} web for "${query}"` : `${verb} web`;
});
@@ -52,13 +53,16 @@
function formatPublishDate(iso: string | undefined): string | null {
if (!iso) return null;
try {
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return iso;
return date.toLocaleDateString(undefined, {
year: 'numeric',
day: 'numeric',
month: 'short',
day: 'numeric'
year: 'numeric'
});
} catch {
return iso;
@@ -1,11 +1,11 @@
<script lang="ts">
import { parseWriteFileMeta } from './parsers/write-file';
import ToolCallBlock from './ToolCallBlock.svelte';
import { XCircle } from '@lucide/svelte';
import { SyntaxHighlightedCode } from '$lib/components/app';
import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants';
import { abbreviateHome, type AgenticSection } from '$lib/utils';
import { toolsStore } from '$lib/stores/tools.svelte';
import { parseWriteFileMeta } from './parsers/write-file';
import ToolCallBlock from './ToolCallBlock.svelte';
import { abbreviateHome, type AgenticSection } from '$lib/utils';
interface Props {
section: AgenticSection;
@@ -14,7 +14,7 @@
onToggle?: () => void;
}
let { section, open, isStreaming, onToggle }: Props = $props();
let { isStreaming, onToggle, open, section }: Props = $props();
const writeFileMeta = $derived(parseWriteFileMeta(section));
const home = $derived(toolsStore.serverHome);
@@ -11,12 +11,12 @@
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 { AgenticSectionType } from '$lib/enums';
import { getBuiltinToolUi } from '$lib/constants/built-in-tools';
import { mcpStore } from '$lib/stores/mcp.svelte';
import type { Component, Snippet } from 'svelte';
import type { AgenticSection, BuiltinToolUiEntry } from '$lib/utils';
import type { Component, Snippet } from 'svelte';
type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string };
@@ -64,17 +64,17 @@
}
let {
section,
open,
children,
extraLiveStreaming = false,
isStreaming,
meta,
extraLiveStreaming = false,
onToggle,
open,
section,
spinIconWhenActive = false,
wrapper: Wrapper = CollapsibleContentBlock,
title,
titleSnippet,
onToggle,
children
wrapper: Wrapper = CollapsibleContentBlock
}: Props = $props();
const isPending = $derived(section.type === AgenticSectionType.TOOL_CALL_PENDING);
@@ -102,8 +102,11 @@
// signals activity; only terminal states get a pill.
function subtitleFor(errorMessage?: string): string | undefined {
if (showSpinner) return undefined;
if (errorMessage) return 'failed';
if (isStreamingCall && !isStreaming) return 'incomplete';
return undefined;
}
@@ -122,9 +125,9 @@
{onToggle}
>
{@render children(meta, {
isStreaming,
isCodeStreaming,
isPending,
isStreamingCall,
isCodeStreaming
isStreaming,
isStreamingCall
})}
</Wrapper>
@@ -5,8 +5,8 @@
// stay focused on its own format quirks.
import { BuiltInTool } from '$lib/enums';
import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
import type { AgenticSection } from '$lib/utils/agentic';
import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
/**
* Strict (final-state) JSON parser for a tool-args blob. Mirrors the
@@ -17,9 +17,11 @@ import type { AgenticSection } from '$lib/utils/agentic';
function parseFinalToolArgs(blob: string): Record<string, unknown> | null {
try {
const parsed: unknown = JSON.parse(blob);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
return null;
} catch {
return null;
@@ -43,6 +45,7 @@ export function parseToolArgs(
options: { partial?: boolean } = {}
): Record<string, unknown> | null {
if (section.toolName !== expected || !section.toolArgs) return null;
return options.partial
? parsePartialJsonArgs(section.toolArgs)
: parseFinalToolArgs(section.toolArgs);
@@ -3,10 +3,10 @@
// rendering), plus the result blob for `result` / `edits_applied` /
// `error` fields.
import { BuiltInTool } from '$lib/enums';
import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
import { tryParseToolResultObject, type AgenticSection } from '$lib/utils';
import { parseToolArgs } from './_shared';
import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
import { BuiltInTool } from '$lib/enums';
import { type AgenticSection, tryParseToolResultObject } from '$lib/utils';
export type EditFileEdit = {
oldText: string;
@@ -24,48 +24,57 @@ export type EditFileMeta = {
export function parseEditFileMeta(section: AgenticSection): EditFileMeta | null {
const args = parseToolArgs(BuiltInTool.EDIT_FILE, section, { partial: true });
if (!args) return null;
const rawPath = args.path ?? args.file_path ?? args.filePath;
if (typeof rawPath !== 'string' || !rawPath) return null;
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
// Filter the streamed edits array strictly: each entry must be an
// object with a non-empty `old_text`. Edits without an old_text
// would diff against empty and render as a full re-write.
const rawEdits = Array.isArray(args.edits) ? args.edits : [];
const edits: EditFileEdit[] = [];
for (const e of rawEdits) {
if (!e || typeof e !== 'object' || Array.isArray(e)) continue;
const obj = e as Record<string, unknown>;
const oldText = typeof obj.old_text === 'string' ? obj.old_text : '';
if (!oldText) continue;
const newText = typeof obj.new_text === 'string' ? obj.new_text : '';
edits.push({ oldText, newText });
edits.push({ newText, oldText });
}
const resultObj = tryParseToolResultObject(section.toolResult);
let resultMessage: string | undefined;
let editsApplied: number | undefined;
let errorMessage: string | undefined;
if (typeof resultObj?.error === 'string') {
errorMessage = resultObj.error;
} else if (resultObj) {
if (typeof resultObj.result === 'string') {
resultMessage = resultObj.result;
}
if (Number.isFinite(Number(resultObj.edits_applied))) {
editsApplied = Number(resultObj.edits_applied);
}
}
return {
edits,
editsApplied,
errorMessage,
fileName,
filePath: rawPath,
edits,
resultMessage,
editsApplied,
errorMessage
resultMessage
};
}
@@ -5,9 +5,9 @@
// file only deals with what's strictly about *calling* the tool, since
// the error / exit status elide from call-section to result-section.
import { parseToolArgs } from './_shared';
import { BuiltInTool } from '$lib/enums';
import type { AgenticSection } from '$lib/utils';
import { parseToolArgs } from './_shared';
export type ExecShellCommandMeta = {
command: string;
@@ -15,9 +15,12 @@ export type ExecShellCommandMeta = {
export function parseExecShellCommandMeta(section: AgenticSection): ExecShellCommandMeta | null {
const args = parseToolArgs(BuiltInTool.EXEC_SHELL_COMMAND, section);
if (!args) return null;
const commandRaw = args.command ?? args.cmd ?? args.shell_command;
if (typeof commandRaw !== 'string' || !commandRaw) return null;
return { command: commandRaw };
}
@@ -4,9 +4,9 @@
// parser keeps the original raw-text fallback for MCP servers that
// emit unparseable output.
import { BuiltInTool } from '$lib/enums';
import { splitSearchSummaryList, type AgenticSection } from '$lib/utils';
import { parseToolArgs } from './_shared';
import { BuiltInTool } from '$lib/enums';
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
export type FileGlobSearchMeta = {
path: string;
@@ -19,11 +19,13 @@ export type FileGlobSearchMeta = {
export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearchMeta | null {
const args = parseToolArgs(BuiltInTool.FILE_GLOB_SEARCH, section);
if (!args) return null;
const path = typeof args.path === 'string' ? args.path : '';
const include = typeof args.include === 'string' && args.include ? args.include : '**';
const exclude = typeof args.exclude === 'string' && args.exclude ? args.exclude : undefined;
if (!path) return null;
let matches: string[] = [];
@@ -31,17 +33,21 @@ export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearch
let errorMessage: string | undefined;
const toolResultString = section.toolResult;
if (toolResultString) {
try {
const parsed: unknown = JSON.parse(toolResultString);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const obj = parsed as Record<string, unknown>;
if (typeof obj.error === 'string') {
errorMessage = obj.error;
} else if (typeof obj.plain_text_response === 'string') {
const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
totalMatches = total;
});
matches = split.lines;
}
}
@@ -50,9 +56,10 @@ export function parseFileGlobSearchMeta(section: AgenticSection): FileGlobSearch
const split = splitSearchSummaryList(toolResultString, (total) => {
totalMatches = total;
});
matches = split.lines;
}
}
return { path, include, exclude, matches, totalMatches, errorMessage };
return { errorMessage, exclude, include, matches, path, totalMatches };
}
@@ -5,9 +5,9 @@
// fallback so MCP servers that return unparseable output still get
// surfaced.
import { BuiltInTool } from '$lib/enums';
import { splitSearchSummaryList, type AgenticSection } from '$lib/utils';
import { parseToolArgs } from './_shared';
import { BuiltInTool } from '$lib/enums';
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
export type GrepSearchMatch = {
file: string;
@@ -28,10 +28,12 @@ export type GrepSearchMeta = {
export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | null {
const args = parseToolArgs(BuiltInTool.GREP_SEARCH, section);
if (!args) return null;
const path = typeof args.path === 'string' ? args.path : '';
const pattern = typeof args.pattern === 'string' ? args.pattern : '';
if (!path || !pattern) return null;
const include = typeof args.include === 'string' && args.include ? args.include : '**';
@@ -43,17 +45,21 @@ export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | n
let errorMessage: string | undefined;
const toolResultString = section.toolResult;
if (toolResultString) {
try {
const parsed: unknown = JSON.parse(toolResultString);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
const obj = parsed as Record<string, unknown>;
if (typeof obj.error === 'string') {
errorMessage = obj.error;
} else if (typeof obj.plain_text_response === 'string') {
const split = splitSearchSummaryList(obj.plain_text_response, (total) => {
totalMatches = total;
});
matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
}
}
@@ -64,19 +70,20 @@ export function parseGrepSearchMeta(section: AgenticSection): GrepSearchMeta | n
const split = splitSearchSummaryList(toolResultString, (total) => {
totalMatches = total;
});
matches = split.lines.map((line) => parseGrepLine(line, showLineNumbers));
}
}
return {
errorMessage,
exclude,
include,
matches,
path,
pattern,
include,
exclude,
showLineNumbers,
matches,
totalMatches,
errorMessage
totalMatches
};
}
@@ -85,24 +92,29 @@ function parseGrepLine(line: string, showLineNumbers: boolean): GrepSearchMatch
// <file>:<content> when return_line_numbers=false
// <file>:<lineno>:<content> when return_line_numbers=true
const firstColon = line.indexOf(':');
if (firstColon === -1) {
return { file: line, content: '' };
return { content: '', file: line };
}
const file = line.slice(0, firstColon);
const tail = line.slice(firstColon + 1);
if (!showLineNumbers) {
return { file, content: tail };
return { content: tail, file };
}
const secondColon = tail.indexOf(':');
if (secondColon === -1) {
return { file, content: tail };
return { content: tail, file };
}
const lineNum = parseInt(tail.slice(0, secondColon), 10);
return {
content: tail.slice(secondColon + 1),
file,
line: Number.isFinite(lineNum) ? lineNum : undefined,
content: tail.slice(secondColon + 1)
line: Number.isFinite(lineNum) ? lineNum : undefined
};
}
@@ -3,14 +3,14 @@
// `start_line`+`line_count`). Args are parsed partially so a header
// can render incrementally as the file path streams in.
import { BuiltInTool } from '$lib/enums';
import { parseToolArgs } from './_shared';
import {
DEFAULT_LANGUAGE,
FILE_PATH_SEPARATOR_REGEX,
TEXT_LANGUAGE_PREFIX_REGEX
} from '$lib/constants';
import { getFileTypeByExtension, type AgenticSection } from '$lib/utils';
import { parseToolArgs } from './_shared';
import { BuiltInTool } from '$lib/enums';
import { type AgenticSection, getFileTypeByExtension } from '$lib/utils';
export type ReadFileMeta = {
fileName: string;
@@ -20,13 +20,14 @@ export type ReadFileMeta = {
export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null {
const args = parseToolArgs(BuiltInTool.READ_FILE, section, { partial: true });
if (!args) return null;
const rawPath = args.path ?? args.file_path ?? args.filePath;
if (typeof rawPath !== 'string' || !rawPath) return null;
const fileName = rawPath.split(FILE_PATH_SEPARATOR_REGEX).pop() || rawPath;
// Models emit range arguments under several aliases. Accept all to
// stay forgiving across prompt variations.
const startRaw = args.start_line ?? args.line_start ?? args.startLine ?? args.from_line;
@@ -34,19 +35,22 @@ export function parseReadFileMeta(section: AgenticSection): ReadFileMeta | null
const countRaw = args.line_count ?? args.count ?? args.num_lines;
let lineRange: { start: number; end: number } | null = null;
const sNum = Number(startRaw);
const eNum = Number(endRaw);
if (startRaw != null && endRaw != null && Number.isFinite(sNum) && Number.isFinite(eNum)) {
lineRange = { start: sNum, end: eNum };
lineRange = { end: eNum, start: sNum };
} else if (startRaw != null && countRaw != null) {
const cNum = Number(countRaw);
if (Number.isFinite(sNum) && Number.isFinite(cNum)) {
lineRange = { start: sNum, end: sNum + cNum - 1 };
lineRange = { end: sNum + cNum - 1, start: sNum };
}
}
const fileType = getFileTypeByExtension(fileName);
const language = fileType ? fileType.replace(TEXT_LANGUAGE_PREFIX_REGEX, '') : DEFAULT_LANGUAGE;
return { fileName, lineRange, language };
return { fileName, language, lineRange };
}
@@ -5,9 +5,9 @@
// failure renders as a flat line beginning with `Error:`. Both shapes
// are handled.
import { parseToolArgs } from './_shared';
import { BuiltInTool } from '$lib/enums';
import type { AgenticSection } from '$lib/utils';
import { parseToolArgs } from './_shared';
export type RunJavascriptMeta = {
code: string;
@@ -17,30 +17,37 @@ export type RunJavascriptMeta = {
export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMeta | null {
const args = parseToolArgs(BuiltInTool.RUN_JAVASCRIPT, section);
if (!args) return null;
const code = typeof args.code === 'string' ? args.code : '';
if (!code) return null;
const timeoutRaw = Number(args.timeout_ms);
const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : undefined;
let errorMessage: string | undefined;
const toolResultString = section.toolResult;
if (toolResultString) {
// Branches matter here: a JSON object can carry `error`, but a
// JSON array always represents successful output (sandbox returns
// the array of values). Only when the result isn't a JSON object
// do we scan raw lines for the `Error:` prefix.
let parsedObject: Record<string, unknown> | null = null;
try {
const parsed: unknown = JSON.parse(toolResultString);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
parsedObject = parsed as Record<string, unknown>;
}
} catch {
parsedObject = null;
}
if (typeof parsedObject?.error === 'string') {
errorMessage = parsedObject.error;
} else if (!parsedObject) {
@@ -48,9 +55,10 @@ export function parseRunJavascriptMeta(section: AgenticSection): RunJavascriptMe
.split('\n')
.map((line) => line.trim())
.find((line) => line.startsWith('Error:'));
if (errorLine) errorMessage = errorLine.slice('Error:'.length).trim();
}
}
return { code, timeoutMs, errorMessage };
return { code, errorMessage, timeoutMs };
}
@@ -3,14 +3,14 @@
// finishes) and surfaces `bytes`, `result`, and `error` from the
// result blob.
import { BuiltInTool } from '$lib/enums';
import { parseToolArgs } from './_shared';
import {
DEFAULT_LANGUAGE,
FILE_PATH_SEPARATOR_REGEX,
TEXT_LANGUAGE_PREFIX_REGEX
} from '$lib/constants';
import { getFileTypeByExtension, tryParseToolResultObject, type AgenticSection } from '$lib/utils';
import { parseToolArgs } from './_shared';
import { BuiltInTool } from '$lib/enums';
import { type AgenticSection, getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils';
export type WriteFileMeta = {
fileName: string;
@@ -24,18 +24,19 @@ export type WriteFileMeta = {
export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | null {
const args = parseToolArgs(BuiltInTool.WRITE_FILE, section, { partial: true });
if (!args) return null;
// Tool contracts drifted over time: some models emit `path`,
// others `file_path` / `filePath`. Accept all three.
const rawPath = args.path ?? args.file_path ?? args.filePath;
if (typeof rawPath !== 'string' || !rawPath) return null;
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;
const resultObj = tryParseToolResultObject(section.toolResult);
const bytesWritten =
resultObj && Number.isFinite(Number(resultObj.bytes)) ? Number(resultObj.bytes) : undefined;
@@ -43,12 +44,12 @@ export function parseWriteFileMeta(section: AgenticSection): WriteFileMeta | nul
const errorMessage = typeof resultObj?.error === 'string' ? resultObj.error : undefined;
return {
bytesWritten,
content,
errorMessage,
fileName,
filePath: rawPath,
language,
content,
bytesWritten,
resultMessage,
errorMessage
resultMessage
};
}
@@ -6,9 +6,9 @@
ChatMessageUserBubble
} from '$lib/components/app/chat';
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 { MessageRole, ChatMessageStatisticsMode } from '$lib/enums';
import { config } from '$lib/stores/settings.svelte';
interface Props {
@@ -35,19 +35,19 @@
let {
class: className = '',
message,
siblingInfo = null,
deletionInfo,
isLastUserMessage = false,
message,
nextAssistantMessage = null,
showDeleteDialog,
onEdit,
onDelete,
onConfirmDelete,
onCopy,
onDelete,
onEdit,
onForkConversation,
onShowDeleteDialogChange,
onNavigateToSibling,
onCopy
onShowDeleteDialogChange,
showDeleteDialog,
siblingInfo = null
}: Props = $props();
// Get contexts
@@ -60,13 +60,14 @@
// For agentic turns, prefer the cumulative agentic.llm totals over per-call timings.
let storedReadingStats = $derived.by(() => {
const timings = nextAssistantMessage?.timings;
if (!timings?.prompt_n || !timings?.prompt_ms) return null;
const agentic = timings.agentic;
return {
promptTokens: agentic ? agentic.llm.prompt_n : timings.prompt_n,
promptMs: agentic ? agentic.llm.prompt_ms : timings.prompt_ms
promptMs: agentic ? agentic.llm.prompt_ms : timings.prompt_ms,
promptTokens: agentic ? agentic.llm.prompt_n : timings.prompt_n
};
});
@@ -1,6 +1,6 @@
<script lang="ts">
import { Card } from '$lib/components/ui/card';
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
import { Card } from '$lib/components/ui/card';
import { config } from '$lib/stores/settings.svelte';
import type { DatabaseMessageExtra } from '$lib/types/database';
@@ -14,12 +14,12 @@
}
let {
content,
attachments = [],
renderMarkdown = false,
textColorClass = 'text-foreground',
cardBgClass = 'dark:bg-primary/15',
maxHeightStyle = ''
content,
maxHeightStyle = '',
renderMarkdown = false,
textColorClass = 'text-foreground'
}: Props = $props();
let isMultiline = $state(false);
@@ -31,6 +31,7 @@
if (content.includes('\n')) {
isMultiline = true;
return;
}
@@ -1,6 +1,6 @@
<script lang="ts">
import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
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';
interface Props {
@@ -16,9 +16,9 @@
class: className = '',
content,
extras = [],
onSendImmediately,
onDelete,
onEdit,
onDelete
onSendImmediately
}: Props = $props();
const editCtx = useMessageEditContext({