mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-12 22:31:11 +04:00
refactor: Clean up UI types
This commit is contained in:
@@ -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}
|
||||
<ChatFormContenteditable
|
||||
<ChatFormContentEditable
|
||||
class="px-5 py-1.5 md:pt-0 mb-0.5"
|
||||
bind:this={inputRef}
|
||||
bind:value
|
||||
|
||||
+3
-2
@@ -2,7 +2,8 @@
|
||||
import { CODE_BLOCK } from '$lib/constants';
|
||||
import { ColorMode } from '$lib/enums';
|
||||
import { isMobile } from '$lib/stores/viewport.svelte';
|
||||
import type { ContentToken, SourceHistoryEntry } from '$lib/utils';
|
||||
import type { ContentEditableToken } from '$lib/types';
|
||||
import type { SourceHistoryEntry } from '$lib/utils';
|
||||
import {
|
||||
badgeAwareWordJump,
|
||||
buildFragment,
|
||||
@@ -63,7 +64,7 @@
|
||||
rootElement.dataset.empty = source.length === 0 ? 'true' : 'false';
|
||||
}
|
||||
|
||||
function renderTokens(tokens: ContentToken[]) {
|
||||
function renderTokens(tokens: ContentEditableToken[]) {
|
||||
if (!rootElement) return;
|
||||
|
||||
const caret = rangeToTextOffset(rootElement, safeRange());
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ColorLevel } from './context-gauge';
|
||||
import { colorLevelTextClass } from './context-gauge';
|
||||
import type { ColorLevel } from '$lib/enums';
|
||||
|
||||
interface Props {
|
||||
percent: number | null;
|
||||
|
||||
+11
-11
@@ -1,25 +1,25 @@
|
||||
export type ColorLevel = 'ok' | 'warning' | 'critical' | 'neutral';
|
||||
import { ColorLevel } from '$lib/enums';
|
||||
|
||||
const WARNING_THRESHOLD = 80;
|
||||
const CRITICAL_THRESHOLD = 95;
|
||||
|
||||
export function colorLevelFromPercent(percent: number | null): ColorLevel {
|
||||
if (percent === null) return 'neutral';
|
||||
if (percent === null) return ColorLevel.NEUTRAL;
|
||||
|
||||
if (percent >= CRITICAL_THRESHOLD) return 'critical';
|
||||
if (percent >= CRITICAL_THRESHOLD) return ColorLevel.CRITICAL;
|
||||
|
||||
if (percent >= WARNING_THRESHOLD) return 'warning';
|
||||
if (percent >= WARNING_THRESHOLD) return ColorLevel.WARNING;
|
||||
|
||||
return 'ok';
|
||||
return ColorLevel.OK;
|
||||
}
|
||||
|
||||
export function colorLevelTextClass(level: ColorLevel): string {
|
||||
switch (level) {
|
||||
case 'critical':
|
||||
case ColorLevel.CRITICAL:
|
||||
return 'text-red-400';
|
||||
case 'warning':
|
||||
case ColorLevel.WARNING:
|
||||
return 'text-amber-400';
|
||||
case 'ok':
|
||||
case ColorLevel.OK:
|
||||
return 'text-muted-foreground';
|
||||
default:
|
||||
return 'text-muted-foreground';
|
||||
@@ -28,11 +28,11 @@ export function colorLevelTextClass(level: ColorLevel): string {
|
||||
|
||||
export function colorLevelBgClass(level: ColorLevel): string {
|
||||
switch (level) {
|
||||
case 'critical':
|
||||
case ColorLevel.CRITICAL:
|
||||
return 'bg-red-500';
|
||||
case 'warning':
|
||||
case ColorLevel.WARNING:
|
||||
return 'bg-amber-500';
|
||||
case 'ok':
|
||||
case ColorLevel.OK:
|
||||
return 'bg-green-500';
|
||||
default:
|
||||
return 'bg-muted';
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-6
@@ -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;
|
||||
|
||||
+2
-4
@@ -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';
|
||||
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+4
-4
@@ -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)}
|
||||
<div class="font-mono text-[11px] leading-relaxed whitespace-pre-wrap">{line.text}</div>
|
||||
{#if line.media}
|
||||
{#if line.media?.type === AttachmentType.IMAGE}
|
||||
<img
|
||||
src={line.media.base64Url}
|
||||
alt={line.media.name}
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Clock, Loader2 } from '@lucide/svelte';
|
||||
import { AgenticSectionType } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+2
-1
@@ -5,7 +5,8 @@
|
||||
import { SyntaxHighlightedCode } from '$lib/components/app';
|
||||
import { MAX_HEIGHT_CODE_BLOCK } from '$lib/constants';
|
||||
import { FileTypeText } from '$lib/enums';
|
||||
import { type AgenticSection, getBuiltinToolUi } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { getBuiltinToolUi } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
section: AgenticSection;
|
||||
|
||||
+2
-3
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+2
-1
@@ -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 };
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
// stay focused on its own format quirks.
|
||||
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils/agentic';
|
||||
import type { AgenticSection } from '$lib/types/agentic';
|
||||
import { parsePartialJsonArgs } from '$lib/utils/parse-partial-json-args';
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { FILE_PATH_SEPARATOR_REGEX } from '$lib/constants';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, tryParseToolResultObject } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { tryParseToolResultObject } from '$lib/utils';
|
||||
|
||||
export type EditFileEdit = {
|
||||
oldText: string;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
export type ExecShellCommandMeta = {
|
||||
command: string;
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { splitSearchSummaryList } from '$lib/utils';
|
||||
|
||||
export type FileGlobSearchMeta = {
|
||||
path: string;
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import { type AgenticSection, splitSearchSummaryList } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
import { splitSearchSummaryList } from '$lib/utils';
|
||||
|
||||
export type GrepSearchMatch = {
|
||||
file: string;
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
import { parseToolArgs } from './_shared';
|
||||
import { BuiltInTool } from '$lib/enums';
|
||||
import type { AgenticSection } from '$lib/utils';
|
||||
import type { AgenticSection } from '$lib/types';
|
||||
|
||||
export type RunJavascriptMeta = {
|
||||
code: string;
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+59
-2
@@ -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 };
|
||||
|
||||
+11
@@ -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 };
|
||||
Vendored
+67
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Vendored
+10
@@ -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;
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string, CacheEntry>();
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <div> 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 <div> 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 <br> 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 <br> (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 <div><br></div>', 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 <div><br></div>-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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ChatFormContenteditable from '$lib/components/app/chat/ChatForm/ChatFormContenteditable.svelte';
|
||||
import ChatFormContentEditable from '$lib/components/app/chat/ChatForm/ChatFormContentEditable.svelte';
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -9,7 +9,7 @@
|
||||
let { value: initial = '' }: Props = $props();
|
||||
|
||||
let value = $state(untrack(() => initial));
|
||||
let inputRef: ChatFormContenteditable | undefined = $state(undefined);
|
||||
let inputRef: ChatFormContentEditable | undefined = $state(undefined);
|
||||
|
||||
export function getValue() {
|
||||
return value;
|
||||
@@ -24,4 +24,4 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ChatFormContenteditable bind:this={inputRef} bind:value />
|
||||
<ChatFormContentEditable bind:this={inputRef} bind:value />
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user