diff --git a/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview.svelte b/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreview.svelte similarity index 100% rename from tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview.svelte rename to tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreview.svelte 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 bf56e9108..49c3dfe20 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatForm.svelte @@ -3,12 +3,11 @@ import { ChatAttachmentsList, ChatFormActions, - ChatFormContentEditable, - ChatFormFileInputInvisible, + ChatFormCurrentWorkingDirectory, + ChatFormInput, + ChatFormInputFileInputInvisible, ChatFormMcpResourcesList, ChatFormPickers, - ChatFormTextarea, - ChatFormWorkingDirectory, DialogMcpResourcesBrowser } from '$lib/components/app'; import { @@ -121,7 +120,7 @@ let audioRecorder: AudioRecorder | undefined; let chatFormActionsRef: ChatFormActions | undefined = $state(undefined); - let fileInputRef: ChatFormFileInputInvisible | undefined = $state(undefined); + let fileInputRef: ChatFormInputFileInputInvisible | undefined = $state(undefined); let pickersRef: { handleKeydown: (event: KeyboardEvent) => boolean } | undefined = $state(undefined); let inputRef: ChatInputHandle | undefined = $state(undefined); @@ -544,7 +543,7 @@ } - +
- {#if useContenteditable} - { - pickers.handleInput(); - onValueChange?.(value); - }} - onPaste={handlePaste} - {disabled} - {placeholder} - /> - {:else} - { - pickers.handleInput(); - onValueChange?.(value); - }} - onPaste={handlePaste} - {disabled} - {placeholder} - /> - {/if} + { + pickers.handleInput(); + onValueChange?.(value); + }} + onPaste={handlePaste} + {disabled} + {placeholder} + {useContenteditable} + /> {#if mcpResourceStore.hasAttachments} {#if toolsStore.hasEnabledCwdTools} - - import ChatFormWorkingDirectoryChip from './ChatFormWorkingDirectoryChip.svelte'; - import ChatFormWorkingDirectoryResultsList from './ChatFormWorkingDirectoryResultsList.svelte'; + import ChatFormCurrentWorkingDirectoryChip from './ChatFormCurrentWorkingDirectoryChip.svelte'; + import ChatFormCurrentWorkingDirectoryResultsList from './ChatFormCurrentWorkingDirectoryResultsList.svelte'; import { FolderOpen } from '@lucide/svelte'; import SearchInput from '$lib/components/app/forms/SearchInput.svelte'; import * as Popover from '$lib/components/ui/popover'; @@ -250,7 +250,7 @@ // user cancelled - silently ignore; other errors are logged if (err instanceof DOMException && err.name === 'AbortError') return; - console.error('[ChatFormWorkingDirectory] showDirectoryPicker failed:', err); + console.error('[ChatFormCurrentWorkingDirectory] showDirectoryPicker failed:', err); } } @@ -331,7 +331,7 @@ onclick={onOpen} {disabled} > - {searchUnavailableMessage} {:else if query.trim() && (search.isSearching || queryResults.length > 0 || searchError)} - + import ChatFormInputBasic from './ChatFormInputBasic.svelte'; + import ChatFormInputRich from './ChatFormInputRich.svelte'; + + interface Props { + class?: string; + disabled?: boolean; + onInput?: () => void; + onKeydown?: (event: KeyboardEvent) => void; + onPaste?: (event: ClipboardEvent) => void; + placeholder?: string; + value?: string; + useContenteditable?: boolean; + } + + let { + class: className = '', + disabled = false, + onInput, + onKeydown, + onPaste, + placeholder = 'Ask anything...', + useContenteditable = false, + value = $bindable('') + }: Props = $props(); + + let basicRef: ChatFormInputBasic | undefined = $state(); + let richRef: ChatFormInputRich | undefined = $state(); + + // The two renderers share one imperative handle (focus/caret/height), so + // the parent can drive whichever variant is mounted through this one. + export function getElement() { + return useContenteditable ? richRef?.getElement() : basicRef?.getElement(); + } + + export function focus() { + if (useContenteditable) richRef?.focus(); + else basicRef?.focus(); + } + + export function resetHeight() { + if (useContenteditable) richRef?.resetHeight(); + else basicRef?.resetHeight(); + } + + export function getCaretOffset(): number { + return useContenteditable + ? (richRef?.getCaretOffset() ?? 0) + : (basicRef?.getCaretOffset() ?? 0); + } + + export function setCaretOffset(offset: number) { + if (useContenteditable) richRef?.setCaretOffset(offset); + else basicRef?.setCaretOffset(offset); + } + + +{#if useContenteditable} + +{:else} + +{/if} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormTextarea.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormInput/ChatFormInputBasic.svelte similarity index 100% rename from tools/ui/src/lib/components/app/chat/ChatForm/ChatFormTextarea.svelte rename to tools/ui/src/lib/components/app/chat/ChatForm/ChatFormInput/ChatFormInputBasic.svelte diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormFileInputInvisible.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormInput/ChatFormInputFileInputInvisible.svelte similarity index 100% rename from tools/ui/src/lib/components/app/chat/ChatForm/ChatFormFileInputInvisible.svelte rename to tools/ui/src/lib/components/app/chat/ChatForm/ChatFormInput/ChatFormInputFileInputInvisible.svelte diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContentEditable.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormInput/ChatFormInputRich.svelte similarity index 97% rename from tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContentEditable.svelte rename to tools/ui/src/lib/components/app/chat/ChatForm/ChatFormInput/ChatFormInputRich.svelte index ba4430401..f25c3c95c 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormContentEditable.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormInput/ChatFormInputRich.svelte @@ -2,7 +2,7 @@ import { CODE_BLOCK } from '$lib/constants'; import { ColorMode } from '$lib/enums'; import { isMobile } from '$lib/stores'; - import type { ContentEditableToken } from '$lib/types'; + import type { ChatFormInputRichToken } from '$lib/types'; import type { SourceHistoryEntry } from '$lib/utils'; import { badgeAwareWordJump, @@ -64,7 +64,7 @@ rootElement.dataset.empty = source.length === 0 ? 'true' : 'false'; } - function renderTokens(tokens: ContentEditableToken[]) { + function renderTokens(tokens: ChatFormInputRichToken[]) { if (!rootElement) return; const caret = rangeToTextOffset(rootElement, safeRange()); @@ -127,7 +127,7 @@ } function highlightCodeBlocks(root: HTMLElement) { - for (const el of root.querySelectorAll('code[data-code-token="block"]')) { + for (const el of root.querySelectorAll('code[data-code-token="code_block"]')) { highlightCodeBlockElement(el); } } @@ -151,7 +151,7 @@ } while (node && node !== rootElement) { - if (node instanceof HTMLElement && node.dataset.codeToken === 'block') { + if (node instanceof HTMLElement && node.dataset.codeToken === 'code_block') { const caret = rangeToTextOffset(rootElement, range); if (highlightCodeBlockElement(node)) { @@ -404,7 +404,7 @@ let node: Node | null = container.parentNode; while (node && node !== rootElement) { - if (node instanceof HTMLElement && node.dataset.codeToken === 'block') { + if (node instanceof HTMLElement && node.dataset.codeToken === 'code_block') { const tail = document.createRange(); tail.setStart(container, offset); @@ -462,7 +462,7 @@ const first = rootElement.firstChild; - if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'block') return false; + if (!(first instanceof HTMLElement) || first.dataset.codeToken !== 'code_block') return false; const range = safeRange(); @@ -507,7 +507,7 @@ const second = first.nextSibling; - if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'block') return; + if (!(second instanceof HTMLElement) || second.dataset.codeToken !== 'code_block') return; const range = safeRange(); const onHatch = @@ -787,7 +787,7 @@ } -
+
/* pre-wrap is load-bearing: without it Chromium collapses \n in text nodes and converts them to spaces while typing */ - .chat-form-contenteditable { + .chat-form-input-rich { white-space: pre-wrap; } - .chat-form-contenteditable:global([data-empty='true'])::before { + .chat-form-input-rich:global([data-empty='true'])::before { content: attr(data-placeholder); color: var(--muted-foreground); pointer-events: none; } /* Inline code - mirrors markdown-content.css */ - .chat-form-contenteditable :global(code[data-code-token='inline']) { + .chat-form-input-rich :global(code[data-code-token='code_inline']) { background: var(--muted); color: var(--muted-foreground); padding: 0.125rem 0.375rem; @@ -835,7 +835,7 @@ } /* Fenced code block - mirrors .code-block-wrapper in markdown-content.css */ - .chat-form-contenteditable :global(code[data-code-token='block']) { + .chat-form-input-rich :global(code[data-code-token='code_block']) { display: block; margin: 0.25rem 0; padding: 0.75rem 1rem; diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerCommand.svelte similarity index 100% rename from tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormCommandPicker.svelte rename to tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerCommand.svelte diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMention.svelte similarity index 100% rename from tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormMentionPicker.svelte rename to tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMention.svelte diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte index 7ba97cf9b..dbe03e2e0 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickers.svelte @@ -1,7 +1,7 @@ - -