diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
index eb713e0913..5360d7eb81 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageAssistant.svelte
@@ -7,10 +7,11 @@
ModelBadge,
ModelsSelector
} from '$lib/components/app';
+ import { getMessageEditContext } from '$lib/contexts';
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
import { isLoading, isChatStreaming } from '$lib/stores/chat.svelte';
import { agenticStreamingToolCall } from '$lib/stores/agentic.svelte';
- import { autoResizeTextarea, copyToClipboard } from '$lib/utils';
+ import { autoResizeTextarea, copyToClipboard, isIMEComposing } from '$lib/utils';
import { fade } from 'svelte/transition';
import { Check, X } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
@@ -32,25 +33,17 @@
assistantMessages: number;
messageTypes: string[];
} | null;
- editedContent?: string;
- isEditing?: boolean;
message: DatabaseMessage;
messageContent: string | undefined;
- onCancelEdit?: () => void;
onCopy: () => void;
onConfirmDelete: () => void;
onContinue?: () => void;
onDelete: () => void;
onEdit?: () => void;
- onEditKeydown?: (event: KeyboardEvent) => void;
- onEditedContentChange?: (content: string) => void;
onNavigateToSibling?: (siblingId: string) => void;
onRegenerate: (modelOverride?: string) => void;
- onSaveEdit?: () => void;
onShowDeleteDialogChange: (show: boolean) => void;
- onShouldBranchAfterEditChange?: (value: boolean) => void;
showDeleteDialog: boolean;
- shouldBranchAfterEdit?: boolean;
siblingInfo?: ChatMessageSiblingInfo | null;
textareaElement?: HTMLTextAreaElement;
}
@@ -58,29 +51,37 @@
let {
class: className = '',
deletionInfo,
- editedContent = '',
- isEditing = false,
message,
messageContent,
- onCancelEdit,
onConfirmDelete,
onContinue,
onCopy,
onDelete,
onEdit,
- onEditKeydown,
- onEditedContentChange,
onNavigateToSibling,
onRegenerate,
- onSaveEdit,
onShowDeleteDialogChange,
- onShouldBranchAfterEditChange,
showDeleteDialog,
- shouldBranchAfterEdit = false,
siblingInfo = null,
textareaElement = $bindable()
}: Props = $props();
+ // Get edit context
+ const editCtx = getMessageEditContext();
+
+ // Local state for assistant-specific editing
+ let shouldBranchAfterEdit = $state(false);
+
+ function handleEditKeydown(event: KeyboardEvent) {
+ if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
+ event.preventDefault();
+ editCtx.save();
+ } else if (event.key === 'Escape') {
+ event.preventDefault();
+ editCtx.cancel();
+ }
+ }
+
const hasAgenticMarkers = $derived(
messageContent?.includes(AGENTIC_TAGS.TOOL_CALL_START) ?? false
);
@@ -104,7 +105,7 @@
}
$effect(() => {
- if (isEditing && textareaElement) {
+ if (editCtx.isEditing && textareaElement) {
autoResizeTextarea(textareaElement);
}
});
@@ -131,16 +132,16 @@
{/if}
- {#if isEditing}
+ {#if editCtx.isEditing}
@@ -150,19 +151,24 @@
onShouldBranchAfterEditChange?.(checked === true)}
+ onCheckedChange={(checked) => (shouldBranchAfterEdit = checked === true)}
/>
-
- {#if message.timestamp && !isEditing}
+ {#if message.timestamp && !editCtx.isEditing}
void;
- onSaveEdit: () => void;
- onSaveEditOnly?: () => void;
- onEditedContentChange: (content: string) => void;
- onEditedExtrasChange?: (extras: DatabaseMessageExtra[]) => void;
- onEditedUploadedFilesChange?: (files: ChatUploadedFile[]) => void;
- }
-
- let {
- editedContent,
- editedExtras = [],
- editedUploadedFiles = [],
- originalContent,
- originalExtras = [],
- showSaveOnlyOption = false,
- onCancelEdit,
- onSaveEdit,
- onSaveEditOnly,
- onEditedContentChange,
- onEditedExtrasChange,
- onEditedUploadedFilesChange
- }: Props = $props();
+ const editCtx = getMessageEditContext();
let inputAreaRef: ChatForm | undefined = $state(undefined);
let saveWithoutRegenerate = $state(false);
let showDiscardDialog = $state(false);
let hasUnsavedChanges = $derived.by(() => {
- if (editedContent !== originalContent) return true;
- if (editedUploadedFiles.length > 0) return true;
+ if (editCtx.editedContent !== editCtx.originalContent) return true;
+ if (editCtx.editedUploadedFiles.length > 0) return true;
const extrasChanged =
- editedExtras.length !== originalExtras.length ||
- editedExtras.some((extra, i) => extra !== originalExtras[i]);
+ editCtx.editedExtras.length !== editCtx.originalExtras.length ||
+ editCtx.editedExtras.some((extra, i) => extra !== editCtx.originalExtras[i]);
if (extrasChanged) return true;
@@ -54,11 +27,11 @@
});
let hasAttachments = $derived(
- (editedExtras && editedExtras.length > 0) ||
- (editedUploadedFiles && editedUploadedFiles.length > 0)
+ (editCtx.editedExtras && editCtx.editedExtras.length > 0) ||
+ (editCtx.editedUploadedFiles && editCtx.editedUploadedFiles.length > 0)
);
- let canSubmit = $derived(editedContent.trim().length > 0 || hasAttachments);
+ let canSubmit = $derived(editCtx.editedContent.trim().length > 0 || hasAttachments);
function handleGlobalKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
@@ -71,47 +44,40 @@
if (hasUnsavedChanges) {
showDiscardDialog = true;
} else {
- onCancelEdit();
+ editCtx.cancel();
}
}
function handleSubmit() {
if (!canSubmit) return;
- if (saveWithoutRegenerate && onSaveEditOnly) {
- onSaveEditOnly();
+ if (saveWithoutRegenerate && editCtx.showSaveOnlyOption) {
+ editCtx.saveOnly();
} else {
- onSaveEdit();
+ editCtx.save();
}
saveWithoutRegenerate = false;
}
function handleAttachmentRemove(index: number) {
- if (!onEditedExtrasChange) return;
-
- const newExtras = [...editedExtras];
+ const newExtras = [...editCtx.editedExtras];
newExtras.splice(index, 1);
- onEditedExtrasChange(newExtras);
+ editCtx.setExtras(newExtras);
}
function handleUploadedFileRemove(fileId: string) {
- if (!onEditedUploadedFilesChange) return;
-
- const newFiles = editedUploadedFiles.filter((f) => f.id !== fileId);
- onEditedUploadedFilesChange(newFiles);
+ const newFiles = editCtx.editedUploadedFiles.filter((f) => f.id !== fileId);
+ editCtx.setUploadedFiles(newFiles);
}
async function handleFilesAdd(files: File[]) {
- if (!onEditedUploadedFilesChange) return;
-
const processed = await processFilesToChatUploaded(files);
-
- onEditedUploadedFilesChange([...editedUploadedFiles, processed].flat());
+ editCtx.setUploadedFiles([...editCtx.editedUploadedFiles, ...processed]);
}
function handleUploadedFilesChange(files: ChatUploadedFile[]) {
- onEditedUploadedFilesChange?.(files);
+ editCtx.setUploadedFiles(files);
}
$effect(() => {
@@ -128,11 +94,11 @@
- {#if showSaveOnlyOption && onSaveEditOnly}
+ {#if editCtx.showSaveOnlyOption}
@@ -169,6 +135,6 @@
cancelText="Keep editing"
variant="destructive"
icon={AlertTriangle}
- onConfirm={onCancelEdit}
+ onConfirm={editCtx.cancel}
onCancel={() => (showDiscardDialog = false)}
/>
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPrompt.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPrompt.svelte
index b29f706339..4fbaf02f96 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPrompt.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageMcpPrompt.svelte
@@ -1,5 +1,6 @@
- {#if isEditing}
-
+ {#if editCtx.isEditing}
+
{:else}
void;
- onSaveEdit: () => void;
- onEditKeydown: (event: KeyboardEvent) => void;
- onEditedContentChange: (content: string) => void;
onCopy: () => void;
onEdit: () => void;
onDelete: () => void;
@@ -37,15 +33,9 @@
let {
class: className = '',
message,
- isEditing,
- editedContent,
siblingInfo = null,
showDeleteDialog,
deletionInfo,
- onCancelEdit,
- onSaveEdit,
- onEditKeydown,
- onEditedContentChange,
onCopy,
onEdit,
onDelete,
@@ -55,10 +45,25 @@
textareaElement = $bindable()
}: Props = $props();
+ const editCtx = getMessageEditContext();
+
+ function handleEditKeydown(event: KeyboardEvent) {
+ if (event.key === 'Enter' && !event.shiftKey && !isIMEComposing(event)) {
+ event.preventDefault();
+
+ editCtx.save();
+ } else if (event.key === 'Escape') {
+ event.preventDefault();
+
+ editCtx.cancel();
+ }
+ }
+
let isMultiline = $state(false);
let messageElement: HTMLElement | undefined = $state();
let isExpanded = $state(false);
let contentHeight = $state(0);
+
const MAX_HEIGHT = 200; // pixels
const currentConfig = config();
@@ -98,24 +103,29 @@
class="group flex flex-col items-end gap-3 md:gap-2 {className}"
role="group"
>
- {#if isEditing}
+ {#if editCtx.isEditing}
-
+
Cancel
-
+
Save
diff --git a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte
index 565fcef8fa..afc79dd2ef 100644
--- a/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte
+++ b/tools/server/webui/src/lib/components/app/chat/ChatMessages/ChatMessageUser.svelte
@@ -1,6 +1,7 @@