From 6175281c78a7baab09bbedfae95672039d55cba6 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 11 Aug 2026 16:37:18 +0200 Subject: [PATCH] refactor: Clean up UI types --- .../app/chat/ChatForm/ChatForm.svelte | 4 +- ....svelte => ChatFormContentEditable.svelte} | 5 +- .../ContextGaugeDial.svelte | 2 +- .../ChatFormContextGauge/context-gauge.ts | 22 +++--- .../ChatFormMentionPicker.svelte | 4 +- .../ChatForm/ChatFormWorkingDirectory.svelte | 2 +- .../ChatMessageToolCallBlock.svelte | 8 +- .../ChatMessageToolCallBlockDefault.svelte | 6 +- .../ChatMessageToolCallBlockEditFile.svelte | 3 +- ...essageToolCallBlockExecShellCommand.svelte | 8 +- ...tMessageToolCallBlockFileGlobSearch.svelte | 3 +- ...ChatMessageToolCallBlockGetDatetime.svelte | 2 +- .../ChatMessageToolCallBlockGetInfo.svelte | 3 +- .../ChatMessageToolCallBlockGrepSearch.svelte | 3 +- .../ChatMessageToolCallBlockReadFile.svelte | 2 +- .../ChatMessageToolCallBlockReadMedia.svelte | 2 +- ...atMessageToolCallBlockRunJavascript.svelte | 3 +- ...atMessageToolCallBlockSearchResults.svelte | 5 +- .../ChatMessageToolCallBlockWriteFile.svelte | 3 +- .../ChatMessageToolCall/ToolCallBlock.svelte | 3 +- .../ChatMessageToolCall/parsers/_shared.ts | 2 +- .../ChatMessageToolCall/parsers/edit-file.ts | 3 +- .../parsers/exec-shell-command.ts | 2 +- .../parsers/file-glob-search.ts | 3 +- .../parsers/grep-search.ts | 3 +- .../ChatMessageToolCall/parsers/read-file.ts | 3 +- .../ChatMessageToolCall/parsers/read-media.ts | 2 +- .../parsers/run-javascript.ts | 2 +- .../ChatMessageToolCall/parsers/write-file.ts | 3 +- .../ChatMessageAgenticContent.svelte | 3 +- .../ChatMessageReasoningBlock.svelte | 2 +- tools/ui/src/lib/components/app/chat/index.ts | 4 +- tools/ui/src/lib/enums/chat.enums.ts | 10 +++ tools/ui/src/lib/enums/index.ts | 12 ++- tools/ui/src/lib/enums/ui.enums.ts | 10 +++ .../src/lib/hooks/use-context-gauge.svelte.ts | 7 +- tools/ui/src/lib/types/agentic.d.ts | 61 ++++++++++++++- tools/ui/src/lib/types/contenteditable.d.ts | 11 +++ tools/ui/src/lib/types/glob.d.ts | 67 +++++++++++++++++ tools/ui/src/lib/types/index.ts | 21 +++++- tools/ui/src/lib/types/search.d.ts | 10 +++ tools/ui/src/lib/utils/agentic.ts | 53 +------------ .../lib/utils/contenteditable-tokenizer.ts | 71 ++++++++++-------- tools/ui/src/lib/utils/glob-search.ts | 46 +++--------- tools/ui/src/lib/utils/index.ts | 20 +---- tools/ui/src/lib/utils/search-results.ts | 10 +-- tools/ui/src/lib/utils/working-directory.ts | 17 +---- ...form-contenteditable-blocks.svelte.test.ts | 18 ++--- ...t-form-contenteditable-undo.svelte.test.ts | 20 ++--- .../chat-form-contenteditable.svelte.test.ts | 74 +++++++++---------- ... => ChatFormContentEditableHarness.svelte} | 6 +- .../tests/unit/assistant-raw-output.test.ts | 3 +- tools/ui/tests/unit/tool-calls.test.ts | 2 +- 53 files changed, 384 insertions(+), 290 deletions(-) rename tools/ui/src/lib/components/app/chat/ChatForm/{ChatFormContenteditable.svelte => ChatFormContentEditable.svelte} (99%) create mode 100644 tools/ui/src/lib/types/contenteditable.d.ts create mode 100644 tools/ui/src/lib/types/glob.d.ts create mode 100644 tools/ui/src/lib/types/search.d.ts rename tools/ui/tests/client/components/{ChatFormContenteditableHarness.svelte => ChatFormContentEditableHarness.svelte} (65%) diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte index bc1befd633..9921d8d88d 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -3,7 +3,7 @@ import { ChatAttachmentsList, ChatFormActions, - ChatFormContenteditable, + ChatFormContentEditable, ChatFormFileInputInvisible, ChatFormMcpResourcesList, ChatFormPickers, @@ -605,7 +605,7 @@ class="flex-column relative min-h-12 items-center rounded-4xl md:rounded-3xl py-2 pb-2.25 shadow-sm transition-all focus-within:shadow-md md:py-3!" > {#if useContenteditable} - - import type { ColorLevel } from './context-gauge'; import { colorLevelTextClass } from './context-gauge'; + import type { ColorLevel } from '$lib/enums'; interface Props { percent: number | null; diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge.ts b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge.ts index 42aab668e6..e0a7f74780 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge.ts +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge.ts @@ -1,25 +1,25 @@ -export type ColorLevel = 'ok' | 'warning' | 'critical' | 'neutral'; +import { ColorLevel } from '$lib/enums'; const WARNING_THRESHOLD = 80; const CRITICAL_THRESHOLD = 95; export function colorLevelFromPercent(percent: number | null): ColorLevel { - if (percent === null) return 'neutral'; + if (percent === null) return ColorLevel.NEUTRAL; - if (percent >= CRITICAL_THRESHOLD) return 'critical'; + if (percent >= CRITICAL_THRESHOLD) return ColorLevel.CRITICAL; - if (percent >= WARNING_THRESHOLD) return 'warning'; + if (percent >= WARNING_THRESHOLD) return ColorLevel.WARNING; - return 'ok'; + return ColorLevel.OK; } export function colorLevelTextClass(level: ColorLevel): string { switch (level) { - case 'critical': + case ColorLevel.CRITICAL: return 'text-red-400'; - case 'warning': + case ColorLevel.WARNING: return 'text-amber-400'; - case 'ok': + case ColorLevel.OK: return 'text-muted-foreground'; default: return 'text-muted-foreground'; @@ -28,11 +28,11 @@ export function colorLevelTextClass(level: ColorLevel): string { export function colorLevelBgClass(level: ColorLevel): string { switch (level) { - case 'critical': + case ColorLevel.CRITICAL: return 'bg-red-500'; - case 'warning': + case ColorLevel.WARNING: return 'bg-amber-500'; - case 'ok': + case ColorLevel.OK: return 'bg-green-500'; default: return 'bg-muted'; diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte index 86a9fb81cd..6a2be8cffd 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte @@ -11,8 +11,8 @@ import { config } from '$lib/stores/settings.svelte'; import { toolsStore } from '$lib/stores/tools.svelte'; import { isMobile } from '$lib/stores/viewport.svelte'; - import type { FileMentionEntry } from '$lib/types'; - import { abbreviateHome, type GlobEntryResult, runGlobSearchWithChildren } from '$lib/utils'; + import type { FileMentionEntry, GlobEntryResult } from '$lib/types'; + import { abbreviateHome, runGlobSearchWithChildren } from '$lib/utils'; /** * Floating file/folder mention picker. The chat input is the search diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte index 27bc40b24a..639ddb5247 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormWorkingDirectory.svelte @@ -11,10 +11,10 @@ import { useScrollActiveRow } from '$lib/hooks/use-scroll-active-row.svelte'; import { ToolsService } from '$lib/services/tools.service'; import { toolsStore } from '$lib/stores/tools.svelte'; + import type { GlobEntry } from '$lib/types'; import { abbreviateHome, buildCaseInsensitiveGlob, - type GlobEntry, joinPath, lastPathSegment, runGlobSearchWithChildren diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte index 7a00f1a8ea..db09bb7235 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte @@ -12,13 +12,9 @@ import ChatMessageToolCallBlockSearchResults from './ChatMessageToolCallBlockSearchResults.svelte'; import ChatMessageToolCallBlockWriteFile from './ChatMessageToolCallBlockWriteFile.svelte'; import { BuiltInTool } from '$lib/enums'; + import type { AgenticSection } from '$lib/types'; import type { DatabaseMessageExtra } from '$lib/types'; - import { - type AgenticSection, - extractSearchQuery, - extractSearchResults, - isWebSearchToolName - } from '$lib/utils'; + import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockDefault.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockDefault.svelte index 79120cb175..c45ec55843 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockDefault.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockDefault.svelte @@ -8,14 +8,12 @@ import { MarkdownContent, SyntaxHighlightedCode } from '$lib/components/app'; import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants'; import { AttachmentType, FileTypeText, MimeTypeAudio, ToolResultKind } from '$lib/enums'; - import type { DatabaseMessageExtra } from '$lib/types'; + import type { AgenticSection, DatabaseMessageExtra, ToolResultLine } from '$lib/types'; import { - type AgenticSection, classifyToolResult, formatJsonPretty, getBuiltinToolUi, - parseToolResultWithMedia, - type ToolResultLine + parseToolResultWithMedia } from '$lib/utils'; import { createBase64DataUrl } from '$lib/utils/data-url'; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte index 5ee9d4936e..6b95a92fb2 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockEditFile.svelte @@ -4,7 +4,8 @@ import { XCircle } from '@lucide/svelte'; import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants'; import { toolsStore } from '$lib/stores/tools.svelte'; - import { abbreviateHome, type AgenticSection, computeLineDiff, prefixFor } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; + import { abbreviateHome, computeLineDiff, prefixFor } from '$lib/utils'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte index ef2bb74336..caf6d832b1 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte @@ -11,19 +11,19 @@ import { AlertTriangle, Check, Loader2, XCircle } from '@lucide/svelte'; import { CollapsibleTerminalBlock } from '$lib/components/app'; import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants'; + import { AttachmentType } from '$lib/enums'; import { config } from '$lib/stores/settings.svelte'; import { toolsStore } from '$lib/stores/tools.svelte'; + import type { AgenticSection, ToolResultLine } from '$lib/types'; import type { DatabaseMessageExtra } from '$lib/types'; import { abbreviateHome, - type AgenticSection, type ExecShellExitStatus, highlightCode, isExitCodeSummaryLine, parseExecShellCommandError, parseExecShellCommandExitStatus, - parseToolResultWithMedia, - type ToolResultLine + parseToolResultWithMedia } from '$lib/utils'; interface Props { @@ -222,7 +222,7 @@ > {#each outputLines as line, i (i)}
{line.text}
- {#if line.media} + {#if line.media?.type === AttachmentType.IMAGE} {line.media.name} import { Clock, Loader2 } from '@lucide/svelte'; import { AgenticSectionType } from '$lib/enums'; - import type { AgenticSection } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGetInfo.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGetInfo.svelte index 7a975dc1b5..9e6ef10919 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGetInfo.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGetInfo.svelte @@ -2,7 +2,8 @@ import { Info, Loader2 } from '@lucide/svelte'; import { AgenticSectionType } from '$lib/enums'; import { toolsStore } from '$lib/stores/tools.svelte'; - import { abbreviateHome, type AgenticSection } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; + import { abbreviateHome } from '$lib/utils'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGrepSearch.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGrepSearch.svelte index 86db568dea..c650318adb 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGrepSearch.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockGrepSearch.svelte @@ -3,7 +3,8 @@ import ToolCallBlock from './ToolCallBlock.svelte'; import { XCircle } from '@lucide/svelte'; import { toolsStore } from '$lib/stores/tools.svelte'; - import { abbreviateHome, type AgenticSection } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; + import { abbreviateHome } from '$lib/utils'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadFile.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadFile.svelte index a655d86a5f..ad29e31d8f 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadFile.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadFile.svelte @@ -3,7 +3,7 @@ import ToolCallBlock from './ToolCallBlock.svelte'; import { SyntaxHighlightedCode } from '$lib/components/app'; import { CODE_BLOCK, MAX_HEIGHT_CODE_BLOCK } from '$lib/constants'; - import { type AgenticSection } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadMedia.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadMedia.svelte index 426fb45440..c6b2615c02 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadMedia.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockReadMedia.svelte @@ -4,7 +4,7 @@ import { ATTACHMENT_SAVED_REGEX } from '$lib/constants/agentic.constants'; import { AttachmentType, MimeTypeAudio } from '$lib/enums'; import type { DatabaseMessageExtraAudioFile, DatabaseMessageExtraImageFile } from '$lib/types'; - import { type AgenticSection } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; import { createBase64DataUrl } from '$lib/utils/data-url'; interface Props { diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockRunJavascript.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockRunJavascript.svelte index 38724e8d77..a566f9be4e 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockRunJavascript.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockRunJavascript.svelte @@ -5,7 +5,8 @@ import { SyntaxHighlightedCode } from '$lib/components/app'; import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants'; import { FileTypeText } from '$lib/enums'; - import { type AgenticSection, getBuiltinToolUi } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; + import { getBuiltinToolUi } from '$lib/utils'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockSearchResults.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockSearchResults.svelte index 142ca84374..67a66d7da8 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockSearchResults.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockSearchResults.svelte @@ -5,13 +5,12 @@ import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants'; import { AgenticSectionType } from '$lib/enums'; import { mcpStore } from '$lib/stores/mcp.svelte'; + import type { AgenticSection, SearchResult } from '$lib/types'; import { - type AgenticSection, extractSearchQuery, extractSearchResults, faviconForUrl, - sanitizeExternalUrl, - type SearchResult + sanitizeExternalUrl } from '$lib/utils'; interface Props { diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte index 742685fcc9..0f3917f6ab 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockWriteFile.svelte @@ -5,7 +5,8 @@ import { SyntaxHighlightedCode } from '$lib/components/app'; import { MAX_HEIGHT_CODE_BLOCK, RESULT_STAT_SEPARATOR } from '$lib/constants'; import { toolsStore } from '$lib/stores/tools.svelte'; - import { abbreviateHome, type AgenticSection } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; + import { abbreviateHome } from '$lib/utils'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ToolCallBlock.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ToolCallBlock.svelte index c43495b6c7..ea08f01357 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ToolCallBlock.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ToolCallBlock.svelte @@ -14,7 +14,8 @@ import { ICON_CLASS_DEFAULT, ICON_CLASS_SPIN } from '$lib/constants'; import { AgenticSectionType } from '$lib/enums'; import { mcpStore } from '$lib/stores/mcp.svelte'; - import { type AgenticSection, type BuiltinToolUiEntry, getBuiltinToolUi } from '$lib/utils'; + import type { AgenticSection, BuiltinToolUiEntry } from '$lib/types'; + import { getBuiltinToolUi } from '$lib/utils'; import type { Component, Snippet } from 'svelte'; type ToolCallBlockMetaWithError = TMeta & { errorMessage?: string }; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts index d8ca812b64..073e03de27 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/_shared.ts @@ -5,7 +5,7 @@ // stay focused on its own format quirks. import { BuiltInTool } from '$lib/enums'; -import type { AgenticSection } from '$lib/utils/agentic'; +import type { AgenticSection } from '$lib/types/agentic'; import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args'; /** diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts index 0c1fb9e432..dadb33a499 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/edit-file.ts @@ -6,7 +6,8 @@ import { parseToolArgs } from './_shared'; import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants'; import { BuiltInTool } from '$lib/enums'; -import { type AgenticSection, tryParseToolResultObject } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; +import { tryParseToolResultObject } from '$lib/utils'; export type EditFileEdit = { oldText: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command.ts index aa7cd70493..496fcdde66 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/exec-shell-command.ts @@ -7,7 +7,7 @@ import { parseToolArgs } from './_shared'; import { BuiltInTool } from '$lib/enums'; -import type { AgenticSection } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; export type ExecShellCommandMeta = { command: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search.ts index bbd50ce891..0acd53d775 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/file-glob-search.ts @@ -6,7 +6,8 @@ import { parseToolArgs } from './_shared'; import { BuiltInTool } from '$lib/enums'; -import { type AgenticSection, splitSearchSummaryList } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; +import { splitSearchSummaryList } from '$lib/utils'; export type FileGlobSearchMeta = { path: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search.ts index 69c95210f3..c25d76b1a6 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/grep-search.ts @@ -7,7 +7,8 @@ import { parseToolArgs } from './_shared'; import { BuiltInTool } from '$lib/enums'; -import { type AgenticSection, splitSearchSummaryList } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; +import { splitSearchSummaryList } from '$lib/utils'; export type GrepSearchMatch = { file: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file.ts index 0b275f9732..9ee748ed78 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-file.ts @@ -6,7 +6,8 @@ import { parseToolArgs } from './_shared'; import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants'; import { BuiltInTool } from '$lib/enums'; -import { type AgenticSection, getFileTypeByExtension } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; +import { getFileTypeByExtension } from '$lib/utils'; export type ReadFileMeta = { fileName: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-media.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-media.ts index fbfdb33626..e973a2f99a 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-media.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/read-media.ts @@ -5,7 +5,7 @@ import { PREFIX_SIZE, READ_MEDIA_SIZE_REGEX } from '$lib/constants/read-media'; -import type { AgenticSection } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; export interface ReadMediaMeta { fileName: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts index a74153d4dc..a524478ab4 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/run-javascript.ts @@ -7,7 +7,7 @@ import { parseToolArgs } from './_shared'; import { BuiltInTool } from '$lib/enums'; -import type { AgenticSection } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; export type RunJavascriptMeta = { code: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts index df91d5cae0..53ba38e127 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file.ts @@ -6,7 +6,8 @@ import { parseToolArgs } from './_shared'; import { CODE_BLOCK, FILE_PATH_SEPARATOR_REGEX } from '$lib/constants'; import { BuiltInTool } from '$lib/enums'; -import { type AgenticSection, getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; +import { getFileTypeByExtension, tryParseToolResultObject } from '$lib/utils'; export type WriteFileMeta = { fileName: string; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte index 2b4a22dd44..7fa29a8995 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -17,12 +17,13 @@ agenticResolvePermission } from '$lib/stores/agentic.svelte'; import { config } from '$lib/stores/settings.svelte'; + import type { AgenticSection } from '$lib/types'; import type { ChatMessageAgenticTimings, ChatMessageAgenticTurnStats, DatabaseMessage } from '$lib/types'; - import { type AgenticSection, deriveAgenticSections } from '$lib/utils'; + import { deriveAgenticSections } from '$lib/utils'; interface Props { message: DatabaseMessage; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte index ebcc7279a9..fbd1567615 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte @@ -5,7 +5,7 @@ import { AgenticSectionType } from '$lib/enums'; import { config } from '$lib/stores/settings.svelte'; import type { DatabaseMessageExtra } from '$lib/types'; - import type { AgenticSection } from '$lib/utils'; + import type { AgenticSection } from '$lib/types'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/index.ts b/tools/ui/src/lib/components/app/chat/index.ts index 04b1d915f6..5c88482648 100644 --- a/tools/ui/src/lib/components/app/chat/index.ts +++ b/tools/ui/src/lib/components/app/chat/index.ts @@ -120,7 +120,7 @@ export { default as ChatAttachmentsPreviewCurrentItem } from './ChatAttachments/ * Used by ChatScreenForm and ChatMessageEditForm for both new conversations and message editing. * * **Architecture:** - * - Composes ChatFormTextarea (or ChatFormContenteditable for messages with + * - Composes ChatFormTextarea (or ChatFormContentEditable for messages with * file mention links), ChatFormActions, and ChatFormPickerMcpPrompts * - Manages file upload state via `uploadedFiles` bindable prop * - Integrates with ModelsSelectorDropdown for model selection in router mode @@ -272,7 +272,7 @@ export { default as ChatFormMcpResourcesList } from './ChatForm/ChatFormMcpResou * source string. ChatForm swaps it in once a mention link lands in the * buffer. Shares the focus()/resetHeight()/caret handle with the textarea. */ -export { default as ChatFormContenteditable } from './ChatForm/ChatFormContenteditable.svelte'; +export { default as ChatFormContentEditable } from './ChatForm/ChatFormContentEditable.svelte'; /** * Plain auto-resizing textarea with IME composition support. Default input diff --git a/tools/ui/src/lib/enums/chat.enums.ts b/tools/ui/src/lib/enums/chat.enums.ts index 198ff4dd7c..ac39695361 100644 --- a/tools/ui/src/lib/enums/chat.enums.ts +++ b/tools/ui/src/lib/enums/chat.enums.ts @@ -89,3 +89,13 @@ export enum FileMentionEntryType { FILE = 'file', DIRECTORY = 'directory' } + +/** + * Kinds of tokens the chat-form contenteditable produces. + */ +export enum ContentEditableTokenKind { + TEXT = 'text', + BADGE = 'badge', + INLINE_CODE = 'inlineCode', + CODE_BLOCK = 'codeBlock' +} diff --git a/tools/ui/src/lib/enums/index.ts b/tools/ui/src/lib/enums/index.ts index 217ecc2fe1..226cc134d0 100644 --- a/tools/ui/src/lib/enums/index.ts +++ b/tools/ui/src/lib/enums/index.ts @@ -27,7 +27,8 @@ export { PdfViewMode, ReasoningFormat, ChatFormCommandAction, - FileMentionEntryType + FileMentionEntryType, + ContentEditableTokenKind } from './chat.enums'; export { SessionRecordType } from './conversation-import.enums'; @@ -71,7 +72,14 @@ export { ServerRole, ServerModelStatus, ServerModelsSseEventType } from './serve export { ParameterSource, SyncableParameterType, SettingsFieldType } from './settings.enums'; -export { ColorMode, HtmlInputType, McpPromptVariant, TooltipSide, UrlProtocol } from './ui.enums'; +export { + ColorLevel, + ColorMode, + HtmlInputType, + McpPromptVariant, + TooltipSide, + UrlProtocol +} from './ui.enums'; export { KeyboardKey } from './keyboard.enums'; diff --git a/tools/ui/src/lib/enums/ui.enums.ts b/tools/ui/src/lib/enums/ui.enums.ts index 6ba0222e97..5ed4c1edbc 100644 --- a/tools/ui/src/lib/enums/ui.enums.ts +++ b/tools/ui/src/lib/enums/ui.enums.ts @@ -34,3 +34,13 @@ export enum UrlProtocol { export enum HtmlInputType { FILE = 'file' } + +/** + * Alert level that drives the context gauge dial color. + */ +export enum ColorLevel { + OK = 'ok', + WARNING = 'warning', + CRITICAL = 'critical', + NEUTRAL = 'neutral' +} diff --git a/tools/ui/src/lib/hooks/use-context-gauge.svelte.ts b/tools/ui/src/lib/hooks/use-context-gauge.svelte.ts index 7ab3f83df6..372f314aae 100644 --- a/tools/ui/src/lib/hooks/use-context-gauge.svelte.ts +++ b/tools/ui/src/lib/hooks/use-context-gauge.svelte.ts @@ -5,12 +5,9 @@ */ import { useProcessingState } from './use-processing-state.svelte'; -import { - type ColorLevel, - colorLevelFromPercent -} from '$lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge'; +import { colorLevelFromPercent } from '$lib/components/app/chat/ChatForm/ChatFormContextGauge/context-gauge'; import { STATS_UNITS } from '$lib/constants'; -import { MessageRole } from '$lib/enums'; +import { ColorLevel, MessageRole } from '$lib/enums'; import { chatStore } from '$lib/stores/chat.svelte'; import { activeMessages } from '$lib/stores/conversations.svelte'; import { diff --git a/tools/ui/src/lib/types/agentic.d.ts b/tools/ui/src/lib/types/agentic.d.ts index e4ab61035b..cf939371ec 100644 --- a/tools/ui/src/lib/types/agentic.d.ts +++ b/tools/ui/src/lib/types/agentic.d.ts @@ -5,9 +5,14 @@ import type { ApiChatMessageData } from './api'; import type { ChatMessagePromptProgress, ChatMessageTimings } from './chat'; -import type { DatabaseMessage, DatabaseMessageExtra, McpServerOverride } from './database'; +import type { + DatabaseMessage, + DatabaseMessageExtra, + DatabaseMessageExtraAudioFile, + DatabaseMessageExtraImageFile +} from './database'; import type { MessageRole } from '$lib/enums'; -import { ToolCallType } from '$lib/enums'; +import { AgenticSectionType, ContinueIntentKind, ToolCallType } from '$lib/enums'; /** * Agentic orchestration configuration. @@ -171,3 +176,55 @@ export interface SteeringMessage { content: string; extras?: DatabaseMessageExtra[]; } + +/** + * Represents a parsed section of agentic content for display + */ +export interface AgenticSection { + type: AgenticSectionType; + content: string; + toolName?: string; + toolArgs?: string; + toolResult?: string; + toolResultExtras?: DatabaseMessageExtra[]; + /** Working directory the tool call ran with (from the tool result + * message), shown by the exec_shell_command renderer. */ + toolCwd?: string; + /** ID of the model-side tool call (matches tool_calls[i].id). Lets + * downstream consumers correlate a section with the agentic loop's + * currently-executing tool, e.g. to drive live-streaming UI state + * by matching against agenticStore.executingToolCallId. */ + toolCallId?: string; + wasInterrupted?: boolean; +} + +/** + * Represents a tool result line that may reference an image attachment + */ +export type ToolResultLine = { + text: string; + media?: DatabaseMessageExtraImageFile | DatabaseMessageExtraAudioFile; +}; + +/** + * Classification of how a Continue click on an assistant message should resume + * generation. The caller dispatches the resume path based on this value. + * + * append_text -> the target is a plain text turn, resume with + * continue_final_message and rehydrate the persisted + * tool_calls and attachments through the regular DB to API + * message converter. + * rerun_turn -> the target carries tool_calls that were never resolved by + * tool result messages. The agentic stream was cut mid turn, + * so we drop the target and rerun the loop from the previous + * history. truncateAfter is the last kept index, inclusive. + * next_turn -> the target's tool_calls were already resolved by trailing + * tool results. Hand the history up to and including the + * last consecutive tool result back to the agentic loop so it + * starts the next turn naturally. truncateAfter points at + * that last tool result. + */ +export type ContinueIntent = + | { kind: ContinueIntentKind.APPEND_TEXT } + | { kind: ContinueIntentKind.RERUN_TURN; truncateAfter: number } + | { kind: ContinueIntentKind.NEXT_TURN; truncateAfter: number }; diff --git a/tools/ui/src/lib/types/contenteditable.d.ts b/tools/ui/src/lib/types/contenteditable.d.ts new file mode 100644 index 0000000000..8c610bfede --- /dev/null +++ b/tools/ui/src/lib/types/contenteditable.d.ts @@ -0,0 +1,11 @@ +import { ContentEditableTokenKind } from '$lib/enums'; + +/** + * A single token produced by the chat-form contenteditable tokenizer: + * plain text, a file/folder mention badge, or an inline/fenced code span. + */ +export type ContentEditableToken = + | { kind: ContentEditableTokenKind.TEXT; text: string } + | { kind: ContentEditableTokenKind.BADGE; name: string; path: string } + | { kind: ContentEditableTokenKind.INLINE_CODE; text: string } + | { kind: ContentEditableTokenKind.CODE_BLOCK; text: string }; diff --git a/tools/ui/src/lib/types/glob.d.ts b/tools/ui/src/lib/types/glob.d.ts new file mode 100644 index 0000000000..d863bce4d6 --- /dev/null +++ b/tools/ui/src/lib/types/glob.d.ts @@ -0,0 +1,67 @@ +import type { GlobSearchType } from '$lib/enums'; + +/** + * A single directory entry returned by the server's `file_glob_search` + * tool. + */ +export interface GlobEntry { + path: string; + type: string; +} + +/** + * Query arguments for a `file_glob_search` run. + */ +export interface GlobSearchArgs { + path: string; + include: string; + maxDepth: number; + rankQuery: string; + /** Last segment of a path-navigation query (`~/dir/sub`), undefined for + * a plain home-relative glob. Lets callers act on the exact targeted + * segment (e.g. the WD picker "entering" a directory). */ + last?: string; +} + +/** + * Ranked result of a glob search against a base path. + */ +export interface GlobSearchResult { + base: string; + entries: GlobEntry[]; + error?: string; +} + +/** + * A glob entry resolved to an absolute path with its display name. + */ +export interface GlobEntryResult { + path: string; + name: string; + type: string; +} + +/** + * Options controlling how a search descends into a matched directory. + */ +export interface GlobSearchChildOptions { + type?: GlobSearchType; + /** Descend only on a trailing path separator (mention picker); off for + * the WD picker, which descends on any exact match. */ + descendOnTrailingSeparator?: boolean; + childMaxDepth?: number; +} + +/** + * Result of a glob search that may also list a matched directory's + * children. + */ +export interface GlobSearchChildResult { + base: string; + args: GlobSearchArgs; + /** Outer ranked entries plus the walked directory's children (absolute). */ + entries: GlobEntryResult[]; + /** Absolute path of the directory whose children were appended. */ + exactDir?: string; + error?: string; +} diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts index a7e755040c..917c9516b5 100644 --- a/tools/ui/src/lib/types/index.ts +++ b/tools/ui/src/lib/types/index.ts @@ -161,6 +161,22 @@ export type { MCPServerResources } from './mcp'; +// Search result types +export type { SearchResult } from './search'; + +// Glob search types (working-directory / mention pickers) +export type { + GlobEntry, + GlobSearchArgs, + GlobSearchResult, + GlobEntryResult, + GlobSearchChildOptions, + GlobSearchChildResult +} from './glob'; + +// Contenteditable token types (chat form) +export type { ContentEditableToken } from './contenteditable'; + // Agentic types export type { AgenticConfig, @@ -174,7 +190,10 @@ export type { AgenticFlowOptions, AgenticFlowParams, AgenticFlowResult, - SteeringMessage + SteeringMessage, + AgenticSection, + ToolResultLine, + ContinueIntent } from './agentic'; // Navigation types diff --git a/tools/ui/src/lib/types/search.d.ts b/tools/ui/src/lib/types/search.d.ts new file mode 100644 index 0000000000..1ce091e84a --- /dev/null +++ b/tools/ui/src/lib/types/search.d.ts @@ -0,0 +1,10 @@ +/** + * A single parsed entry from a web-search MCP tool result. + */ +export type SearchResult = { + title: string; + url: string; + published?: string; + author?: string; + highlights?: string; +}; diff --git a/tools/ui/src/lib/utils/agentic.ts b/tools/ui/src/lib/utils/agentic.ts index 2be4dc7147..cd150c5efb 100644 --- a/tools/ui/src/lib/utils/agentic.ts +++ b/tools/ui/src/lib/utils/agentic.ts @@ -13,6 +13,7 @@ import { MessageRole, ToolResultKind } from '$lib/enums'; +import type { AgenticSection, ContinueIntent, ToolResultLine } from '$lib/types/agentic'; import type { ApiChatCompletionToolCall } from '$lib/types/api'; import type { DatabaseMessage, @@ -20,35 +21,6 @@ import type { DatabaseMessageExtraImageFile } from '$lib/types/database'; -/** - * Represents a parsed section of agentic content for display - */ -export interface AgenticSection { - type: AgenticSectionType; - content: string; - toolName?: string; - toolArgs?: string; - toolResult?: string; - toolResultExtras?: DatabaseMessageExtra[]; - /** Working directory the tool call ran with (from the tool result - * message), shown by the exec_shell_command renderer. */ - toolCwd?: string; - /** ID of the model-side tool call (matches tool_calls[i].id). Lets - * downstream consumers correlate a section with the agentic loop's - * currently-executing tool, e.g. to drive live-streaming UI state - * by matching against agenticStore.executingToolCallId. */ - toolCallId?: string; - wasInterrupted?: boolean; -} - -/** - * Represents a tool result line that may reference a media attachment (image or audio) - */ -export type ToolResultLine = { - text: string; - media?: DatabaseMessageExtraImageFile | DatabaseMessageExtraAudioFile; -}; - /** * Derives display sections from a single assistant message and its direct tool results. * @@ -485,29 +457,6 @@ export function hasAgenticContent( return toolMessages.length > 0; } -/** - * Classification of how a Continue click on an assistant message should resume - * generation. The caller dispatches the resume path based on this value. - * - * append_text -> the target is a plain text turn, resume with - * continue_final_message and rehydrate the persisted - * tool_calls and attachments through the regular DB to API - * message converter. - * rerun_turn -> the target carries tool_calls that were never resolved by - * tool result messages. The agentic stream was cut mid turn, - * so we drop the target and rerun the loop from the previous - * history. truncateAfter is the last kept index, inclusive. - * next_turn -> the target's tool_calls were already resolved by trailing - * tool results. Hand the history up to and including the - * last consecutive tool result back to the agentic loop so it - * starts the next turn naturally. truncateAfter points at - * that last tool result. - */ -export type ContinueIntent = - | { kind: ContinueIntentKind.APPEND_TEXT } - | { kind: ContinueIntentKind.RERUN_TURN; truncateAfter: number } - | { kind: ContinueIntentKind.NEXT_TURN; truncateAfter: number }; - /** * Decide how a Continue click on messages[idx] should resume generation. * Pure function over the persisted history snapshot. diff --git a/tools/ui/src/lib/utils/contenteditable-tokenizer.ts b/tools/ui/src/lib/utils/contenteditable-tokenizer.ts index ea6939b23a..db271398c4 100644 --- a/tools/ui/src/lib/utils/contenteditable-tokenizer.ts +++ b/tools/ui/src/lib/utils/contenteditable-tokenizer.ts @@ -35,14 +35,10 @@ import { MENTION_BADGE_SVG_ATTRIBUTES, SETTINGS_KEYS } from '$lib/constants'; +import { ContentEditableTokenKind } from '$lib/enums'; import { settingsStore } from '$lib/stores/settings.svelte'; import { toolsStore } from '$lib/stores/tools.svelte'; - -export type ContentToken = - | { kind: 'text'; text: string } - | { kind: 'badge'; name: string; path: string } - | { kind: 'inlineCode'; text: string } - | { kind: 'codeBlock'; text: string }; +import type { ContentEditableToken } from '$lib/types/contenteditable'; // Block wrappers browsers insert for newlines; each folds back into a // single `\n` during serialization. @@ -112,8 +108,8 @@ export function isOffsetInCodeBlock(source: string, offset: number): boolean { * interleave in the remaining gaps. Any whitespace after a badge * stays in a plain text token so the round trip is byte-exact. */ -export function tokenizeContent(input: string): ContentToken[] { - const tokens: ContentToken[] = []; +export function tokenizeContent(input: string): ContentEditableToken[] { + const tokens: ContentEditableToken[] = []; let cursor = 0; @@ -130,8 +126,8 @@ export function tokenizeContent(input: string): ContentToken[] { tokens.push( match[1] !== undefined - ? { kind: 'codeBlock', text: match[1] } - : { kind: 'inlineCode', text: match[2] } + ? { kind: ContentEditableTokenKind.CODE_BLOCK, text: match[1] } + : { kind: ContentEditableTokenKind.INLINE_CODE, text: match[2] } ); cursor = start + match[0].length; } @@ -146,7 +142,7 @@ export function tokenizeContent(input: string): ContentToken[] { /** * Tokenize a code-free segment into text and badge tokens. */ -function pushTextAndBadgeTokens(input: string, tokens: ContentToken[]) { +function pushTextAndBadgeTokens(input: string, tokens: ContentEditableToken[]) { let cursor = 0; MENTION_BADGE_RE.lastIndex = 0; @@ -158,15 +154,15 @@ function pushTextAndBadgeTokens(input: string, tokens: ContentToken[]) { const start = match.index; if (start > cursor) { - tokens.push({ kind: 'text', text: input.slice(cursor, start) }); + tokens.push({ kind: ContentEditableTokenKind.TEXT, text: input.slice(cursor, start) }); } - tokens.push({ kind: 'badge', name, path }); + tokens.push({ kind: ContentEditableTokenKind.BADGE, name, path }); cursor = start + whole.length; } if (cursor < input.length) { - tokens.push({ kind: 'text', text: input.slice(cursor) }); + tokens.push({ kind: ContentEditableTokenKind.TEXT, text: input.slice(cursor) }); } } @@ -289,8 +285,8 @@ export function serializeContent(root: HTMLElement): string { * A mismatch means token boundaries shifted (a code span was just * completed or broken) and the DOM needs a rebuild to restyle. */ -export function domMatchesTokens(root: HTMLElement, tokens: ContentToken[]): boolean { - const expected = tokens.filter((token) => token.kind !== 'text'); +export function domMatchesTokens(root: HTMLElement, tokens: ContentEditableToken[]): boolean { + const expected = tokens.filter((token) => token.kind !== ContentEditableTokenKind.TEXT); let index = 0; @@ -313,7 +309,7 @@ export function domMatchesTokens(root: HTMLElement, tokens: ContentToken[]): boo if (!token) return false; if (isBadge) { - if (token.kind !== 'badge') return false; + if (token.kind !== ContentEditableTokenKind.BADGE) return false; if (token.name !== (el.dataset.mentionName ?? '')) return false; @@ -322,15 +318,18 @@ export function domMatchesTokens(root: HTMLElement, tokens: ContentToken[]): boo continue; } - const codeKind = el.dataset.codeToken === 'block' ? 'codeBlock' : 'inlineCode'; + const codeKind: ContentEditableTokenKind = + el.dataset.codeToken === 'block' + ? ContentEditableTokenKind.CODE_BLOCK + : ContentEditableTokenKind.INLINE_CODE; if (token.kind !== codeKind) return false; if ( - (token.kind === 'inlineCode' || token.kind === 'codeBlock') && - token.text !== (el.textContent ?? '') + token.kind === ContentEditableTokenKind.INLINE_CODE || + token.kind === ContentEditableTokenKind.CODE_BLOCK ) { - return false; + if (token.text !== (el.textContent ?? '')) return false; } } @@ -522,23 +521,26 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe * string + inline SVG are shared with the rehype plugin via * `$lib/constants`. */ -export function buildFragment(tokens: ContentToken[]): DocumentFragment { +export function buildFragment(tokens: ContentEditableToken[]): DocumentFragment { const fragment = document.createDocumentFragment(); for (let index = 0; index < tokens.length; index++) { const token = tokens[index]; - if (token.kind === 'text') { + if (token.kind === ContentEditableTokenKind.TEXT) { let text = token.text; // The separator \n at a fenced-block boundary is synthesized // at serialization time; keeping it in the DOM would render a // phantom empty line next to the block. - if (tokens[index - 1]?.kind === 'codeBlock' && text.startsWith('\n')) { + if ( + tokens[index - 1]?.kind === ContentEditableTokenKind.CODE_BLOCK && + text.startsWith('\n') + ) { text = text.slice(1); } - if (tokens[index + 1]?.kind === 'codeBlock' && text.endsWith('\n')) { + if (tokens[index + 1]?.kind === ContentEditableTokenKind.CODE_BLOCK && text.endsWith('\n')) { text = text.slice(0, -1); } @@ -549,10 +551,14 @@ export function buildFragment(tokens: ContentToken[]): DocumentFragment { continue; } - if (token.kind === 'inlineCode' || token.kind === 'codeBlock') { + if ( + token.kind === ContentEditableTokenKind.INLINE_CODE || + token.kind === ContentEditableTokenKind.CODE_BLOCK + ) { const code = document.createElement('code'); - code.dataset.codeToken = token.kind === 'codeBlock' ? 'block' : 'inline'; + code.dataset.codeToken = + token.kind === ContentEditableTokenKind.CODE_BLOCK ? 'block' : 'inline'; code.textContent = token.text; fragment.appendChild(code); @@ -728,11 +734,14 @@ export function badgeAwareWordJump( for (const token of tokenizeContent(source)) { const len = - token.kind === 'badge' ? badgeSourceLength(token.name, token.path) : token.text.length; + token.kind === ContentEditableTokenKind.BADGE + ? badgeSourceLength(token.name, token.path) + : token.text.length; - if (token.kind === 'badge') badgeSpans.push([masked.length, masked.length + len]); + if (token.kind === ContentEditableTokenKind.BADGE) + badgeSpans.push([masked.length, masked.length + len]); - masked += token.kind === 'badge' ? 'a'.repeat(len) : token.text; + masked += token.kind === ContentEditableTokenKind.BADGE ? 'a'.repeat(len) : token.text; } if (badgeSpans.length === 0) return null; @@ -795,7 +804,7 @@ export function badgeAwareWordJump( export function leadingBadgeEdgeOffset(source: string, caret: number): number | null { const [first] = tokenizeContent(source); - if (!first || first.kind !== 'badge') return null; + if (!first || first.kind !== ContentEditableTokenKind.BADGE) return null; return caret === badgeSourceLength(first.name, first.path) ? 0 : null; } diff --git a/tools/ui/src/lib/utils/glob-search.ts b/tools/ui/src/lib/utils/glob-search.ts index ded32793b6..7580f32867 100644 --- a/tools/ui/src/lib/utils/glob-search.ts +++ b/tools/ui/src/lib/utils/glob-search.ts @@ -5,16 +5,18 @@ */ import { lastPathSegment } from './path-display'; -import { - buildGlobSearchArgs, - type GlobEntry, - type GlobSearchArgs, - joinPath, - rankEntries -} from './working-directory'; +import { buildGlobSearchArgs, joinPath, rankEntries } from './working-directory'; import { GLOB, PATH_SEPARATOR, SEARCH } from '$lib/constants'; import { BuiltInTool, GlobSearchType } from '$lib/enums'; import { ToolsService } from '$lib/services/tools.service'; +import type { + GlobEntry, + GlobEntryResult, + GlobSearchArgs, + GlobSearchChildOptions, + GlobSearchChildResult, + GlobSearchResult +} from '$lib/types/glob'; const SEARCH_CACHE_TTL_MS = 2000; @@ -26,12 +28,6 @@ interface CacheEntry { const searchCache = new Map(); -export interface GlobSearchResult { - base: string; - entries: GlobEntry[]; - error?: string; -} - export async function runGlobSearch( args: GlobSearchArgs, type: GlobSearchType, @@ -66,30 +62,6 @@ export async function runGlobSearch( return { base, entries }; } -export interface GlobEntryResult { - path: string; - name: string; - type: string; -} - -export interface GlobSearchChildOptions { - type?: GlobSearchType; - /** Descend only on a trailing path separator (mention picker); off for - * the WD picker, which descends on any exact match. */ - descendOnTrailingSeparator?: boolean; - childMaxDepth?: number; -} - -export interface GlobSearchChildResult { - base: string; - args: GlobSearchArgs; - /** Outer ranked entries plus the walked directory's children (absolute). */ - entries: GlobEntryResult[]; - /** Absolute path of the directory whose children were appended. */ - exactDir?: string; - error?: string; -} - function toEntryResult(e: GlobEntry, base: string): GlobEntryResult { return { name: lastPathSegment(e.path), path: joinPath(base, e.path), type: e.type }; } diff --git a/tools/ui/src/lib/utils/index.ts b/tools/ui/src/lib/utils/index.ts index eef5be9bfe..35e20aa168 100644 --- a/tools/ui/src/lib/utils/index.ts +++ b/tools/ui/src/lib/utils/index.ts @@ -179,18 +179,11 @@ export { rankEntries, joinPath, highlightMatch, - type GlobEntry, - type GlobSearchArgs, type PathQuery } from './working-directory'; // Shared `file_glob_search` runner with a short-lived result cache -export { - runGlobSearch, - runGlobSearchWithChildren, - type GlobEntryResult, - type GlobSearchResult -} from './glob-search'; +export { runGlobSearch, runGlobSearchWithChildren } from './glob-search'; // Mention-token detection (for the `@`-triggered file/folder mention picker) export { @@ -219,8 +212,7 @@ export { rangeToTextOffset, textOffsetToRange, badgeAwareWordJump, - leadingBadgeEdgeOffset, - type ContentToken + leadingBadgeEdgeOffset } from './contenteditable-tokenizer'; // Source-space undo/redo history for the chat-form contenteditable @@ -251,9 +243,7 @@ export { parseToolResultWithMedia, splitSearchSummaryList, hasAgenticContent, - classifyToolResult, - type AgenticSection, - type ToolResultLine + classifyToolResult } from './agentic'; // Line-level unified diff for tool result rendering (`edit_file` block) @@ -276,8 +266,7 @@ export { extractSearchResults, extractSearchQuery, faviconForUrl, - isWebSearchToolName, - type SearchResult + isWebSearchToolName } from './search-results'; // Cache utilities @@ -318,7 +307,6 @@ export { tryParseToolResultObject } from './tool-call-meta'; // Re-exported through $lib/utils so renderer components can read the // label without depending on $lib/constants directly. export { getBuiltinToolUi } from './built-in-tools'; -export type { BuiltinToolUiEntry } from '$lib/types'; // Chat command picker diff --git a/tools/ui/src/lib/utils/search-results.ts b/tools/ui/src/lib/utils/search-results.ts index af04e7b07a..db825c3eba 100644 --- a/tools/ui/src/lib/utils/search-results.ts +++ b/tools/ui/src/lib/utils/search-results.ts @@ -1,3 +1,5 @@ +import type { SearchResult } from '$lib/types/search'; + /** * Parsers for MCP web-search tool responses shaped like: * @@ -16,14 +18,6 @@ * servers without hardcoding tool names. */ -export type SearchResult = { - title: string; - url: string; - published?: string; - author?: string; - highlights?: string; -}; - const SEPARATOR_LINE_RE = /^\s*---\s*$/; const URL_SCHEME_RE = /^https?:\/\//i; // Match either Unix or Windows line endings so chunking/parsing handles diff --git a/tools/ui/src/lib/utils/working-directory.ts b/tools/ui/src/lib/utils/working-directory.ts index 09ee87ecc4..906142d1c1 100644 --- a/tools/ui/src/lib/utils/working-directory.ts +++ b/tools/ui/src/lib/utils/working-directory.ts @@ -14,11 +14,7 @@ import { SEARCH, TRAILING_SLASHES_REGEX } from '$lib/constants'; - -export interface GlobEntry { - path: string; - type: string; -} +import type { GlobEntry, GlobSearchArgs } from '$lib/types/glob'; export interface PathQuery { parent: string; @@ -93,17 +89,6 @@ export function buildCaseInsensitiveGlob(query: string): string { return out + GLOB.WILDCARD; } -export interface GlobSearchArgs { - path: string; - include: string; - maxDepth: number; - rankQuery: string; - /** Last segment of a path-navigation query (`~/dir/sub`), undefined for - * a plain home-relative glob. Lets callers act on the exact targeted - * segment (e.g. the WD picker "entering" a directory). */ - last?: string; -} - export function buildGlobSearchArgs( query: string, scopePath: string, diff --git a/tools/ui/tests/client/chat-form-contenteditable-blocks.svelte.test.ts b/tools/ui/tests/client/chat-form-contenteditable-blocks.svelte.test.ts index 602879b991..0b719c43f8 100644 --- a/tools/ui/tests/client/chat-form-contenteditable-blocks.svelte.test.ts +++ b/tools/ui/tests/client/chat-form-contenteditable-blocks.svelte.test.ts @@ -3,7 +3,7 @@ // serialization must fold those back into `\n` so the emitted value never // diverges from what is on screen. -import ChatFormContenteditableHarness from './components/ChatFormContenteditableHarness.svelte'; +import ChatFormContentEditableHarness from './components/ChatFormContentEditableHarness.svelte'; import { tick } from 'svelte'; import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; @@ -35,9 +35,9 @@ function setCaret(node: Node, offset: number) { selection.addRange(range); } -describe('ChatFormContenteditable browser newline shapes', () => { +describe('ChatFormContentEditable browser newline shapes', () => { it('serializes a Chromium Enter
wrapper as a newline', async () => { - const screen = render(ChatFormContenteditableHarness, { value: SOURCE }); + const screen = render(ChatFormContentEditableHarness, { value: SOURCE }); await tick(); @@ -53,7 +53,7 @@ describe('ChatFormContenteditable browser newline shapes', () => { }); it('serializes a Firefox full
wrap as lines, badge included', async () => { - const screen = render(ChatFormContenteditableHarness, { value: SOURCE }); + const screen = render(ChatFormContentEditableHarness, { value: SOURCE }); await tick(); @@ -73,7 +73,7 @@ describe('ChatFormContenteditable browser newline shapes', () => { }); it('serializes a
as a newline', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'here' }); + const screen = render(ChatFormContentEditableHarness, { value: 'here' }); await tick(); @@ -88,7 +88,7 @@ describe('ChatFormContenteditable browser newline shapes', () => { }); it('ignores a trailing
(browser caret placeholder)', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc' }); await tick(); @@ -102,7 +102,7 @@ describe('ChatFormContenteditable browser newline shapes', () => { }); it('serializes one newline per empty-line

', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc' }); await tick(); @@ -121,7 +121,7 @@ describe('ChatFormContenteditable browser newline shapes', () => { }); it('treats a

-only buffer as empty for the placeholder', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc' }); await tick(); @@ -138,7 +138,7 @@ describe('ChatFormContenteditable browser newline shapes', () => { }); it('maps the caret across block boundaries in both directions', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc\ndef' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc\ndef' }); await tick(); diff --git a/tools/ui/tests/client/chat-form-contenteditable-undo.svelte.test.ts b/tools/ui/tests/client/chat-form-contenteditable-undo.svelte.test.ts index 17406f95ea..7ed01fd99b 100644 --- a/tools/ui/tests/client/chat-form-contenteditable-undo.svelte.test.ts +++ b/tools/ui/tests/client/chat-form-contenteditable-undo.svelte.test.ts @@ -3,7 +3,7 @@ // the native undo stack), and Tab is NOT intercepted (WCAG 2.1.2 no // keyboard trap), matching the plain textarea. -import ChatFormContenteditableHarness from './components/ChatFormContenteditableHarness.svelte'; +import ChatFormContentEditableHarness from './components/ChatFormContentEditableHarness.svelte'; import { tick } from 'svelte'; import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; @@ -31,9 +31,9 @@ function keydown(root: HTMLElement, init: KeyboardEventInit) { return event; } -describe('ChatFormContenteditable undo/redo', () => { +describe('ChatFormContentEditable undo/redo', () => { it('undoes and redoes an edit across a badge-containing buffer', async () => { - const screen = render(ChatFormContenteditableHarness, { value: SOURCE }); + const screen = render(ChatFormContentEditableHarness, { value: SOURCE }); await tick(); @@ -57,7 +57,7 @@ describe('ChatFormContenteditable undo/redo', () => { }); it('redoes with Ctrl+Y as well', async () => { - const screen = render(ChatFormContenteditableHarness, { value: SOURCE }); + const screen = render(ChatFormContentEditableHarness, { value: SOURCE }); await tick(); @@ -75,7 +75,7 @@ describe('ChatFormContenteditable undo/redo', () => { }); it('coalesces a typing burst into one undo step', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc' }); await tick(); @@ -92,7 +92,7 @@ describe('ChatFormContenteditable undo/redo', () => { }); it('keeps a newline as its own undo step', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc' }); await tick(); @@ -113,7 +113,7 @@ describe('ChatFormContenteditable undo/redo', () => { }); it('is a no-op when there is nothing to undo', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc' }); await tick(); @@ -127,7 +127,7 @@ describe('ChatFormContenteditable undo/redo', () => { }); it('abandons the redo branch after a fresh edit', async () => { - const screen = render(ChatFormContenteditableHarness, { value: 'abc' }); + const screen = render(ChatFormContentEditableHarness, { value: 'abc' }); await tick(); @@ -147,9 +147,9 @@ describe('ChatFormContenteditable undo/redo', () => { }); }); -describe('ChatFormContenteditable Tab key', () => { +describe('ChatFormContentEditable Tab key', () => { it('does not trap Tab (focus can leave the editable)', async () => { - const screen = render(ChatFormContenteditableHarness, { value: SOURCE }); + const screen = render(ChatFormContentEditableHarness, { value: SOURCE }); await tick(); diff --git a/tools/ui/tests/client/chat-form-contenteditable.svelte.test.ts b/tools/ui/tests/client/chat-form-contenteditable.svelte.test.ts index d10b15c827..0767fa715f 100644 --- a/tools/ui/tests/client/chat-form-contenteditable.svelte.test.ts +++ b/tools/ui/tests/client/chat-form-contenteditable.svelte.test.ts @@ -3,7 +3,7 @@ // contributes its full `[name](file://...)` link) and pasting such // markdown re-renders the badges. -import ChatFormContenteditable from '$lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte'; +import ChatFormContentEditable from '$lib/components/app/chat/ChatForm/ChatFormContentEditable.svelte'; import { rangeToTextOffset, serializeContent, textOffsetToRange } from '$lib/utils'; import { tick } from 'svelte'; import { describe, expect, it, vi } from 'vitest'; @@ -43,9 +43,9 @@ function clipboardEvent(type: 'copy' | 'cut' | 'paste', text = '') { return { data, event }; } -describe('ChatFormContenteditable clipboard', () => { +describe('ChatFormContentEditable clipboard', () => { it('copy exposes the markdown source of the selection', async () => { - const { container } = render(ChatFormContenteditable, { value: SOURCE }); + const { container } = render(ChatFormContentEditable, { value: SOURCE }); await tick(); @@ -62,7 +62,7 @@ describe('ChatFormContenteditable clipboard', () => { }); it('cut exposes the markdown source and removes the slice', async () => { - const { container } = render(ChatFormContenteditable, { value: SOURCE }); + const { container } = render(ChatFormContentEditable, { value: SOURCE }); await tick(); @@ -88,7 +88,7 @@ describe('ChatFormContenteditable clipboard', () => { }); it('paste of markdown mention links re-renders badges', async () => { - const { container } = render(ChatFormContenteditable, { value: 'hello ' }); + const { container } = render(ChatFormContentEditable, { value: 'hello ' }); await tick(); @@ -114,7 +114,7 @@ describe('ChatFormContenteditable clipboard', () => { }); it('paste without mention links keeps the DOM untouched', async () => { - const { container } = render(ChatFormContenteditable, { value: 'hello ' }); + const { container } = render(ChatFormContentEditable, { value: 'hello ' }); await tick(); @@ -138,9 +138,9 @@ describe('ChatFormContenteditable clipboard', () => { }); }); -describe('ChatFormContenteditable code spans', () => { +describe('ChatFormContentEditable code spans', () => { it('renders inline code from the initial value', async () => { - const { container } = render(ChatFormContenteditable, { value: 'run `npm test` now' }); + const { container } = render(ChatFormContentEditable, { value: 'run `npm test` now' }); await tick(); @@ -153,7 +153,7 @@ describe('ChatFormContenteditable code spans', () => { it('renders a fenced code block with a language', async () => { const source = 'before\n```js\nconst a = 1;\n```\nafter'; - const { container } = render(ChatFormContenteditable, { value: source }); + const { container } = render(ChatFormContentEditable, { value: source }); await tick(); @@ -166,7 +166,7 @@ describe('ChatFormContenteditable code spans', () => { it('copy exposes the markdown source of a selection spanning code', async () => { const source = 'run `npm test` now'; - const { container } = render(ChatFormContenteditable, { value: source }); + const { container } = render(ChatFormContentEditable, { value: source }); await tick(); @@ -183,7 +183,7 @@ describe('ChatFormContenteditable code spans', () => { }); it('paste of a code span renders the styled element', async () => { - const { container } = render(ChatFormContenteditable, { value: 'run ' }); + const { container } = render(ChatFormContentEditable, { value: 'run ' }); await tick(); @@ -210,7 +210,7 @@ describe('ChatFormContenteditable code spans', () => { it('highlights a fenced block content and stays byte-exact', async () => { const source = '```js\nconst a = 1;\n```'; - const { container } = render(ChatFormContenteditable, { value: source }); + const { container } = render(ChatFormContentEditable, { value: source }); await tick(); @@ -223,7 +223,7 @@ describe('ChatFormContenteditable code spans', () => { }); it('does not highlight inline code', async () => { - const { container } = render(ChatFormContenteditable, { value: 'run `const` now' }); + const { container } = render(ChatFormContentEditable, { value: 'run `const` now' }); await tick(); @@ -233,7 +233,7 @@ describe('ChatFormContenteditable code spans', () => { }); }); -describe('ChatFormContenteditable code block escape hatches', () => { +describe('ChatFormContentEditable code block escape hatches', () => { const BLOCK_SOURCE = '```js\nconst a = 1;\n```'; const BLOCK_SELECTOR = 'code[data-code-token="block"]'; @@ -273,7 +273,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { } it('pads a trailing code block with a br hatch that stays invisible to copy', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -292,7 +292,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('escapes a trailing code block with ArrowDown and types after it', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -318,7 +318,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('escapes a leading code block with ArrowUp and types before it', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -343,7 +343,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('escapes a leading code block with ArrowLeft from its first character', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -358,7 +358,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('removes the transient leading hatch when the caret moves back into the block', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -378,7 +378,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('extends the selection out of the block with Shift+ArrowDown', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -397,7 +397,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('line-separates text typed right after the closing fence', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -419,7 +419,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('does not double the newline when Shift+Enter already added one', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -437,7 +437,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('moves a caret stuck before the inserted newline onto the new line', async () => { - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE + '\ntext after the code block' }); @@ -469,7 +469,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('appends the artificial trailing newline when the browser did not add one', async () => { - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE + '\ntext after the code block' }); @@ -501,7 +501,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('lands the caret on the new line with a single Shift+Enter after text below a block', async () => { - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE + '\ntext after the code block' }); @@ -534,7 +534,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('lets Backspace at the text start move into the block without a source fight', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -560,7 +560,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('lets forward Delete eat the text after a block normally', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -581,7 +581,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('renders text after a block without a phantom empty line', async () => { - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE + '\nhello' }); @@ -594,7 +594,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('keeps an intentional blank line after a block out of the separator', async () => { - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE + '\n\nhello' }); @@ -607,7 +607,7 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); it('re-highlights while typing inside a block and keeps the caret', async () => { - const { container } = render(ChatFormContenteditable, { value: BLOCK_SOURCE }); + const { container } = render(ChatFormContentEditable, { value: BLOCK_SOURCE }); await tick(); @@ -639,12 +639,12 @@ describe('ChatFormContenteditable code block escape hatches', () => { }); }); -describe('ChatFormContenteditable Enter in code blocks', () => { +describe('ChatFormContentEditable Enter in code blocks', () => { const BLOCK_SOURCE = '```js\nconst a = 1;\n```'; it('adds a line instead of submitting on plain Enter inside a block', async () => { const onKeydown = vi.fn(); - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { onKeydown, value: BLOCK_SOURCE }); @@ -679,7 +679,7 @@ describe('ChatFormContenteditable Enter in code blocks', () => { it('adds a line after a still-open fence (no closing ``` yet)', async () => { const onKeydown = vi.fn(); - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { onKeydown, value: '```js\nconst a = 1;' }); @@ -707,7 +707,7 @@ describe('ChatFormContenteditable Enter in code blocks', () => { it('forwards plain Enter to the parent when the caret is outside a block', async () => { const onKeydown = vi.fn(); - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { onKeydown, value: BLOCK_SOURCE + '\nafter' }); @@ -730,7 +730,7 @@ describe('ChatFormContenteditable Enter in code blocks', () => { it('forwards plain Enter on the trailing hatch line after a block', async () => { const onKeydown = vi.fn(); - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { onKeydown, value: BLOCK_SOURCE }); @@ -753,7 +753,7 @@ describe('ChatFormContenteditable Enter in code blocks', () => { it('forwards Ctrl+Enter inside a block so explicit submit survives', async () => { const onKeydown = vi.fn(); - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { onKeydown, value: BLOCK_SOURCE }); @@ -780,7 +780,7 @@ describe('ChatFormContenteditable Enter in code blocks', () => { it('forwards Enter inside an inline code span', async () => { const onKeydown = vi.fn(); - const { container } = render(ChatFormContenteditable, { + const { container } = render(ChatFormContentEditable, { onKeydown, value: 'run `npm test` now' }); diff --git a/tools/ui/tests/client/components/ChatFormContenteditableHarness.svelte b/tools/ui/tests/client/components/ChatFormContentEditableHarness.svelte similarity index 65% rename from tools/ui/tests/client/components/ChatFormContenteditableHarness.svelte rename to tools/ui/tests/client/components/ChatFormContentEditableHarness.svelte index fac773756c..21dddf5ba5 100644 --- a/tools/ui/tests/client/components/ChatFormContenteditableHarness.svelte +++ b/tools/ui/tests/client/components/ChatFormContentEditableHarness.svelte @@ -1,5 +1,5 @@ - + diff --git a/tools/ui/tests/unit/assistant-raw-output.test.ts b/tools/ui/tests/unit/assistant-raw-output.test.ts index ec75a0e492..d13b796c43 100644 --- a/tools/ui/tests/unit/assistant-raw-output.test.ts +++ b/tools/ui/tests/unit/assistant-raw-output.test.ts @@ -1,6 +1,7 @@ import { REASONING_TAGS } from '$lib/constants'; import { AgenticSectionType } from '$lib/enums'; -import { type AgenticSection, buildAssistantRawOutput } from '$lib/utils/agentic'; +import type { AgenticSection } from '$lib/types/agentic'; +import { buildAssistantRawOutput } from '$lib/utils/agentic'; import { describe, expect, it } from 'vitest'; function makeSection( diff --git a/tools/ui/tests/unit/tool-calls.test.ts b/tools/ui/tests/unit/tool-calls.test.ts index 3349b7ab6e..b0ec76be41 100644 --- a/tools/ui/tests/unit/tool-calls.test.ts +++ b/tools/ui/tests/unit/tool-calls.test.ts @@ -10,7 +10,7 @@ import { type WriteFileMeta } from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/parsers/write-file'; import { AgenticSectionType, BuiltInTool } from '$lib/enums'; -import type { AgenticSection } from '$lib/utils'; +import type { AgenticSection } from '$lib/types'; import { abbreviateHome, formatCwdMessage, lastPathSegment, parseCwdMessage } from '$lib/utils'; import { describe, expect, it } from 'vitest';