ui: Linting & Formatting scripts (#26819)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+11
-9
@@ -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 () => {
|
||||
|
||||
+4
-3
@@ -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;
|
||||
}}
|
||||
/>
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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;
|
||||
|
||||
+9
-9
@@ -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
|
||||
|
||||
+16
-13
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+6
-6
@@ -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();
|
||||
|
||||
|
||||
+9
-9
@@ -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));
|
||||
|
||||
+7
-7
@@ -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));
|
||||
|
||||
+5
-5
@@ -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);
|
||||
|
||||
+19
-11
@@ -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();
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+4
-1
@@ -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 {
|
||||
|
||||
+7
-4
@@ -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 {
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+6
-6
@@ -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 ?? '');
|
||||
|
||||
+11
-7
@@ -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;
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
+14
-11
@@ -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>
|
||||
|
||||
+4
-1
@@ -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);
|
||||
|
||||
+18
-9
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
+4
-1
@@ -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 };
|
||||
}
|
||||
|
||||
+10
-3
@@ -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 };
|
||||
}
|
||||
|
||||
+24
-12
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
+11
-7
@@ -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 };
|
||||
}
|
||||
|
||||
+10
-2
@@ -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 };
|
||||
}
|
||||
|
||||
+9
-8
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
+11
-10
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
+6
-5
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user