From: Aleksander Grygier Date: Thu, 13 Aug 2026 06:21:15 +0000 (+0200) Subject: ui: Clean up contexts, remove prop drilling from Chat Form Actions (#26951) X-Git-Tag: upstream/0.0.10438~34 X-Git-Url: https://git.djapps.eu/?a=commitdiff_plain;h=d86c7d62dfa7fb7227f0b699e3af58c11b295549;p=pkg%2Fggml%2Fsources%2Fllama.cpp ui: Clean up contexts, remove prop drilling from Chat Form Actions (#26951) * refactor: Remove dead context for Chat Settings and create a new one for Chat Messages Actions * refactor: Contexts & types --- diff --git a/tools/ui/src/app.d.ts b/tools/ui/src/app.d.ts index 6f26214e1..5309dce8f 100644 --- a/tools/ui/src/app.d.ts +++ b/tools/ui/src/app.d.ts @@ -32,8 +32,8 @@ import type { ApiRouterModelsStatusResponse, ApiRouterModelsUnloadRequest, ApiRouterModelsUnloadResponse, - // Chat types ChatAttachmentDisplayItem, + // Chat types ChatMessagePromptProgress, ChatMessageSiblingInfo, ChatMessageTimings, diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte index 99aaee784..02bfadb7e 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte @@ -15,37 +15,16 @@ ICON_CLASS_DEFAULT, TOOLTIP_DELAY_DURATION } from '$lib/constants'; + import { getChatFormActionsContext } from '$lib/contexts'; import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte'; interface Props { class?: string; - disabled?: boolean; - hasAudioModality?: boolean; - hasVideoModality?: boolean; - hasVisionModality?: boolean; - hasMcpPromptsSupport?: boolean; - hasMcpResourcesSupport?: boolean; - onFileUpload?: () => void; - onSystemPromptClick?: () => void; - onMcpPromptClick?: () => void; - onMcpSettingsClick?: () => void; - onMcpResourcesClick?: () => void; } - let { - class: className = '', - disabled = false, - hasAudioModality = false, - hasMcpPromptsSupport = false, - hasMcpResourcesSupport = false, - hasVideoModality = false, - hasVisionModality = false, - onFileUpload, - onMcpPromptClick, - onMcpResourcesClick, - onMcpSettingsClick, - onSystemPromptClick - }: Props = $props(); + let { class: className = '' }: Props = $props(); + + const chatFormActions = getChatFormActionsContext(); let dropdownOpen = $state(false); // The system message action moves focus to the message editor, so the menu @@ -54,18 +33,23 @@ function handleMcpSettingsClick() { dropdownOpen = false; - onMcpSettingsClick?.(); + chatFormActions.onMcpSettingsClick?.(); } const attachmentMenu = useAttachmentMenu( () => ({ - hasAudioModality, - hasMcpPromptsSupport, - hasMcpResourcesSupport, - hasVideoModality, - hasVisionModality + hasAudioModality: chatFormActions.hasAudioModality, + hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport, + hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport, + hasVideoModality: chatFormActions.hasVideoModality, + hasVisionModality: chatFormActions.hasVisionModality + }), + () => ({ + onFileUpload: chatFormActions.onFileUpload, + onMcpPromptClick: chatFormActions.onMcpPromptClick, + onMcpResourcesClick: chatFormActions.onMcpResourcesClick, + onSystemPromptClick: chatFormActions.onSystemPromptClick }), - () => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }), () => { dropdownOpen = false; } @@ -85,7 +69,7 @@ buttonVariants({ variant: 'secondary' }), 'file-upload-button h-8 w-8 cursor-pointer rounded-full p-0' )} - {disabled} + disabled={chatFormActions.disabled} > {ATTACHMENT_TOOLTIP_TEXT} @@ -162,7 +146,7 @@ class="flex cursor-pointer items-center gap-2" onclick={() => { suppressCloseAutoFocus = true; - onSystemPromptClick?.(); + chatFormActions.onSystemPromptClick?.(); }} > @@ -174,12 +158,12 @@ - {#if hasMcpPromptsSupport} + {#if chatFormActions.hasMcpPromptsSupport} @@ -187,10 +171,10 @@ {/if} - {#if hasMcpResourcesSupport} + {#if chatFormActions.hasMcpResourcesSupport} diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte index aa5397ca0..63a8c267d 100644 --- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte @@ -19,6 +19,7 @@ ICON_CLASS_DEFAULT, TOOLTIP_DELAY_DURATION } from '$lib/constants'; + import { getChatFormActionsContext } from '$lib/contexts'; import { HealthCheckStatus } from '$lib/enums'; import { AttachmentAction } from '$lib/enums/attachment.enums'; import { useAttachmentMenu } from '$lib/hooks/use-attachment-menu.svelte'; @@ -29,33 +30,12 @@ interface Props { class?: string; - disabled?: boolean; - hasAudioModality?: boolean; - hasVideoModality?: boolean; - hasVisionModality?: boolean; - hasMcpPromptsSupport?: boolean; - hasMcpResourcesSupport?: boolean; - onFileUpload?: () => void; - onSystemPromptClick?: () => void; - onMcpPromptClick?: () => void; - onMcpResourcesClick?: () => void; trigger: Snippet<[{ disabled: boolean; onclick?: () => void }]>; } - let { - class: className = '', - disabled = false, - hasAudioModality = false, - hasMcpPromptsSupport = false, - hasMcpResourcesSupport = false, - hasVideoModality = false, - hasVisionModality = false, - onFileUpload, - onMcpPromptClick, - onMcpResourcesClick, - onSystemPromptClick, - trigger - }: Props = $props(); + let { class: className = '', trigger }: Props = $props(); + + const chatFormActions = getChatFormActionsContext(); let sheetOpen = $state(false); let reasoningExpanded = $state(false); @@ -65,13 +45,18 @@ const attachmentMenu = useAttachmentMenu( () => ({ - hasAudioModality, - hasMcpPromptsSupport, - hasMcpResourcesSupport, - hasVideoModality, - hasVisionModality + hasAudioModality: chatFormActions.hasAudioModality, + hasMcpPromptsSupport: chatFormActions.hasMcpPromptsSupport, + hasMcpResourcesSupport: chatFormActions.hasMcpResourcesSupport, + hasVideoModality: chatFormActions.hasVideoModality, + hasVisionModality: chatFormActions.hasVisionModality + }), + () => ({ + onFileUpload: chatFormActions.onFileUpload, + onMcpPromptClick: chatFormActions.onMcpPromptClick, + onMcpResourcesClick: chatFormActions.onMcpResourcesClick, + onSystemPromptClick: chatFormActions.onSystemPromptClick }), - () => ({ onFileUpload, onMcpPromptClick, onMcpResourcesClick, onSystemPromptClick }), () => { sheetOpen = false; } @@ -91,7 +76,7 @@
- {@render trigger({ disabled, onclick: () => (sheetOpen = true) })} + {@render trigger({ disabled: chatFormActions.disabled, onclick: () => (sheetOpen = true) })} @@ -350,7 +335,7 @@ System Message - {#if hasMcpPromptsSupport} + {#if chatFormActions.hasMcpPromptsSupport}
1 - ? `This will delete ${deletionInfo.totalCount} messages including: ${deletionInfo.userMessages} user message${deletionInfo.userMessages > 1 ? 's' : ''} and ${deletionInfo.assistantMessages} assistant response${deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.` + description={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1 + ? `This will delete ${messageActions.deletionInfo.totalCount} messages including: ${messageActions.deletionInfo.userMessages} user message${messageActions.deletionInfo.userMessages > 1 ? 's' : ''} and ${messageActions.deletionInfo.assistantMessages} assistant response${messageActions.deletionInfo.assistantMessages > 1 ? 's' : ''}. All messages in this branch and their responses will be permanently removed. This action cannot be undone.` : 'Are you sure you want to delete this message? This action cannot be undone.'} - confirmText={deletionInfo && deletionInfo.totalCount > 1 - ? `Delete ${deletionInfo.totalCount} Messages` + confirmText={messageActions.deletionInfo && messageActions.deletionInfo.totalCount > 1 + ? `Delete ${messageActions.deletionInfo.totalCount} Messages` : 'Delete'} cancelText="Cancel" variant="destructive" icon={Trash2} onConfirm={handleConfirmDelete} - onCancel={() => onShowDeleteDialogChange(false)} + onCancel={() => messageActions.setShowDeleteDialog(false)} /> import { ChevronLeft, ChevronRight } from '@lucide/svelte'; import { ActionIcon } from '$lib/components/app'; + import { getChatMessageActionsContext } from '$lib/contexts'; interface Props { class?: string; - siblingInfo: ChatMessageSiblingInfo | null; - onNavigateToSibling?: (siblingId: string) => void; } - let { class: className = '', onNavigateToSibling, siblingInfo }: Props = $props(); + let { class: className = '' }: Props = $props(); + + const messageActions = getChatMessageActionsContext(); + + let siblingInfo = $derived(messageActions.siblingInfo); let hasPrevious = $derived(siblingInfo && siblingInfo.currentIndex > 0); let hasNext = $derived(siblingInfo && siblingInfo.currentIndex < siblingInfo.totalSiblings - 1); @@ -31,7 +34,7 @@ tooltip="Previous version" disabled={!hasPrevious} class="h-5 w-5 p-0 {!hasPrevious ? '!cursor-not-allowed opacity-30' : ''}" - onclick={() => onNavigateToSibling?.(previousSiblingId!)} + onclick={() => messageActions.navigateToSibling(previousSiblingId!)} /> @@ -43,7 +46,7 @@ tooltip="Next version" disabled={!hasNext} class="h-5 w-5 p-0 {!hasNext ? 'opacity-30' : ''}" - onclick={() => onNavigateToSibling?.(nextSiblingId!)} + onclick={() => messageActions.navigateToSibling(nextSiblingId!)} /> {/if} 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 8ce1ead01..584979979 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte @@ -9,8 +9,8 @@ } from '$lib/components/app'; import { AgenticSectionType, ChatMessageStatsView, ToolPermissionDecision } from '$lib/enums'; import { agenticStore, settingsStore } from '$lib/stores'; - import type { AgenticSection } from '$lib/types'; import type { + AgenticSection, ChatMessageAgenticTimings, ChatMessageAgenticTurnStats, DatabaseMessage diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte index feacd0ddd..369b6137b 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte @@ -3,12 +3,12 @@ import { ChatForm, DialogConfirmation } from '$lib/components/app'; import { Button } from '$lib/components/ui/button'; import { Switch } from '$lib/components/ui/switch'; - import { getMessageEditContext } from '$lib/contexts'; + import { getChatMessageEditContext } from '$lib/contexts'; import { KeyboardKey, MessageRole } from '$lib/enums'; import { chatStore } from '$lib/stores'; import { processFilesToChatUploaded } from '$lib/utils/browser-only'; - const editCtx = getMessageEditContext(); + const editCtx = getChatMessageEditContext(); let saveWithoutRegenerate = $state(false); let showDiscardDialog = $state(false); 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 78c9dcb25..a0861fb96 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte @@ -4,8 +4,7 @@ import { REASONING_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants'; import { AgenticSectionType } from '$lib/enums'; import { settingsStore } from '$lib/stores'; - import type { DatabaseMessageExtra } from '$lib/types'; - import type { AgenticSection } from '$lib/types'; + import type { AgenticSection, DatabaseMessageExtra } from '$lib/types'; interface Props { section: AgenticSection; diff --git a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte index 33d971f93..2a8f45ba5 100644 --- a/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte +++ b/tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte @@ -1,8 +1,8 @@
diff --git a/tools/ui/src/lib/constants/context-keys.constants.ts b/tools/ui/src/lib/constants/context-keys.constants.ts index 0bd733b37..62ff5413e 100644 --- a/tools/ui/src/lib/constants/context-keys.constants.ts +++ b/tools/ui/src/lib/constants/context-keys.constants.ts @@ -1,3 +1,3 @@ -export const CONTEXT_KEY_MESSAGE_EDIT = 'chat-message-edit'; -export const CONTEXT_KEY_CHAT_ACTIONS = 'chat-actions'; -export const CONTEXT_KEY_CHAT_SETTINGS_CONFIG = 'chat-settings-config'; +export const CONTEXT_KEY_CHAT_MESSAGE_EDIT = 'chat-message-edit'; +export const CONTEXT_KEY_CHAT_MESSAGE_ACTIONS = 'chat-message-actions'; +export const CONTEXT_KEY_CHAT_FORM_ACTIONS = 'chat-form-actions'; diff --git a/tools/ui/src/lib/contexts/chat-actions.context.ts b/tools/ui/src/lib/contexts/chat-actions.context.ts deleted file mode 100644 index dffb5f3a3..000000000 --- a/tools/ui/src/lib/contexts/chat-actions.context.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { CONTEXT_KEY_CHAT_ACTIONS } from '$lib/constants'; -import { getContext, setContext } from 'svelte'; - -export interface ChatActionsContext { - copy: (message: DatabaseMessage) => void; - delete: (message: DatabaseMessage) => void; - navigateToSibling: (siblingId: string) => void; - editWithBranching: ( - message: DatabaseMessage, - newContent: string, - newExtras?: DatabaseMessageExtra[] - ) => void; - editWithReplacement: ( - message: DatabaseMessage, - newContent: string, - shouldBranch: boolean - ) => void; - editUserMessagePreserveResponses: ( - message: DatabaseMessage, - newContent: string, - newExtras?: DatabaseMessageExtra[] - ) => void; - regenerateWithBranching: (message: DatabaseMessage, modelOverride?: string) => void; - continueAssistantMessage: (message: DatabaseMessage) => void; - forkConversation: ( - message: DatabaseMessage, - options: { name: string; includeAttachments: boolean } - ) => void; -} - -const CHAT_ACTIONS_KEY = Symbol.for(CONTEXT_KEY_CHAT_ACTIONS); - -export function setChatActionsContext(ctx: ChatActionsContext): ChatActionsContext { - return setContext(CHAT_ACTIONS_KEY, ctx); -} - -export function getChatActionsContext(): ChatActionsContext { - return getContext(CHAT_ACTIONS_KEY); -} diff --git a/tools/ui/src/lib/contexts/chat-form-actions.context.ts b/tools/ui/src/lib/contexts/chat-form-actions.context.ts new file mode 100644 index 000000000..a49f17447 --- /dev/null +++ b/tools/ui/src/lib/contexts/chat-form-actions.context.ts @@ -0,0 +1,19 @@ +import { CONTEXT_KEY_CHAT_FORM_ACTIONS } from '$lib/constants'; +import type { ChatFormActionsContext } from '$lib/types'; +import { getContext, setContext } from 'svelte'; + +const CHAT_FORM_ACTIONS_KEY = Symbol.for(CONTEXT_KEY_CHAT_FORM_ACTIONS); + +/** + * Sets the chat form actions context. Call in the parent component (ChatFormActions.svelte). + */ +export function setChatFormActionsContext(ctx: ChatFormActionsContext): ChatFormActionsContext { + return setContext(CHAT_FORM_ACTIONS_KEY, ctx); +} + +/** + * Gets the chat form actions context. Call in child components. + */ +export function getChatFormActionsContext(): ChatFormActionsContext { + return getContext(CHAT_FORM_ACTIONS_KEY); +} diff --git a/tools/ui/src/lib/contexts/chat-message-actions.context.ts b/tools/ui/src/lib/contexts/chat-message-actions.context.ts new file mode 100644 index 000000000..fb075b3b0 --- /dev/null +++ b/tools/ui/src/lib/contexts/chat-message-actions.context.ts @@ -0,0 +1,21 @@ +import { CONTEXT_KEY_CHAT_MESSAGE_ACTIONS } from '$lib/constants'; +import type { ChatMessageActionsContext } from '$lib/types'; +import { getContext, setContext } from 'svelte'; + +const CHAT_MESSAGE_ACTIONS_KEY = Symbol.for(CONTEXT_KEY_CHAT_MESSAGE_ACTIONS); + +/** + * Sets the per-message actions context. Call this in the parent component (ChatMessage.svelte). + */ +export function setChatMessageActionsContext( + ctx: ChatMessageActionsContext +): ChatMessageActionsContext { + return setContext(CHAT_MESSAGE_ACTIONS_KEY, ctx); +} + +/** + * Gets the per-message actions context. Call this in child components. + */ +export function getChatMessageActionsContext(): ChatMessageActionsContext { + return getContext(CHAT_MESSAGE_ACTIONS_KEY); +} diff --git a/tools/ui/src/lib/contexts/chat-message-edit.context.ts b/tools/ui/src/lib/contexts/chat-message-edit.context.ts new file mode 100644 index 000000000..e9c053036 --- /dev/null +++ b/tools/ui/src/lib/contexts/chat-message-edit.context.ts @@ -0,0 +1,19 @@ +import { CONTEXT_KEY_CHAT_MESSAGE_EDIT } from '$lib/constants'; +import type { ChatMessageEditContext } from '$lib/types'; +import { getContext, setContext } from 'svelte'; + +const CHAT_MESSAGE_EDIT_KEY = Symbol.for(CONTEXT_KEY_CHAT_MESSAGE_EDIT); + +/** + * Sets the message edit context. Call this in the parent component (ChatMessage.svelte). + */ +export function setChatMessageEditContext(ctx: ChatMessageEditContext): ChatMessageEditContext { + return setContext(CHAT_MESSAGE_EDIT_KEY, ctx); +} + +/** + * Gets the message edit context. Call this in child components. + */ +export function getChatMessageEditContext(): ChatMessageEditContext { + return getContext(CHAT_MESSAGE_EDIT_KEY); +} diff --git a/tools/ui/src/lib/contexts/chat-settings-config.context.ts b/tools/ui/src/lib/contexts/chat-settings-config.context.ts deleted file mode 100644 index a90f709b4..000000000 --- a/tools/ui/src/lib/contexts/chat-settings-config.context.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { CONTEXT_KEY_CHAT_SETTINGS_CONFIG } from '$lib/constants'; -import { getContext, setContext } from 'svelte'; - -export interface ChatSettingsConfigContext { - readonly localConfig: SettingsConfigType; - handleConfigChange: (key: string, value: string | boolean) => void; - handleThemeChange: (theme: string) => void; -} - -const CHAT_SETTINGS_CONFIG_KEY = Symbol.for(CONTEXT_KEY_CHAT_SETTINGS_CONFIG); - -export function setChatSettingsConfigContext( - ctx: ChatSettingsConfigContext -): ChatSettingsConfigContext { - return setContext(CHAT_SETTINGS_CONFIG_KEY, ctx); -} - -export function getChatSettingsConfigContext(): ChatSettingsConfigContext { - return getContext(CHAT_SETTINGS_CONFIG_KEY); -} diff --git a/tools/ui/src/lib/contexts/index.ts b/tools/ui/src/lib/contexts/index.ts index c6719fa9e..4aaec8148 100644 --- a/tools/ui/src/lib/contexts/index.ts +++ b/tools/ui/src/lib/contexts/index.ts @@ -1,19 +1,8 @@ -export { - getMessageEditContext, - setMessageEditContext, - type MessageEditContext, - type MessageEditState, - type MessageEditActions -} from './message-edit.context'; +export { getChatMessageEditContext, setChatMessageEditContext } from './chat-message-edit.context'; export { - getChatActionsContext, - setChatActionsContext, - type ChatActionsContext -} from './chat-actions.context'; + getChatMessageActionsContext, + setChatMessageActionsContext +} from './chat-message-actions.context'; -export { - getChatSettingsConfigContext, - setChatSettingsConfigContext, - type ChatSettingsConfigContext -} from './chat-settings-config.context'; +export { getChatFormActionsContext, setChatFormActionsContext } from './chat-form-actions.context'; diff --git a/tools/ui/src/lib/contexts/message-edit.context.ts b/tools/ui/src/lib/contexts/message-edit.context.ts deleted file mode 100644 index 80f3e6eee..000000000 --- a/tools/ui/src/lib/contexts/message-edit.context.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { CONTEXT_KEY_MESSAGE_EDIT } from '$lib/constants'; -import { MessageRole } from '$lib/enums'; -import { getContext, setContext } from 'svelte'; - -export interface MessageEditState { - readonly isEditing: boolean; - readonly editedContent: string; - readonly editedExtras: DatabaseMessageExtra[]; - readonly editedUploadedFiles: ChatUploadedFile[]; - readonly originalContent: string; - readonly originalExtras: DatabaseMessageExtra[]; - readonly showSaveOnlyOption: boolean; - readonly showBranchAfterEditOption: boolean; - readonly shouldBranchAfterEdit: boolean; - readonly messageRole: MessageRole; - readonly rawEditContent?: string; -} - -export interface MessageEditActions { - setContent: (content: string) => void; - setExtras: (extras: DatabaseMessageExtra[]) => void; - setUploadedFiles: (files: ChatUploadedFile[]) => void; - save: () => void; - saveOnly: () => void; - cancel: () => void; - startEdit: () => void; -} - -export interface AssistantEditActions { - setShouldBranchAfterEdit: (value: boolean) => void; -} - -export type MessageEditContext = MessageEditState & - MessageEditActions & - Partial; - -const MESSAGE_EDIT_KEY = Symbol.for(CONTEXT_KEY_MESSAGE_EDIT); - -/** - * Sets the message edit context. Call this in the parent component (ChatMessage.svelte). - */ -export function setMessageEditContext(ctx: MessageEditContext): MessageEditContext { - return setContext(MESSAGE_EDIT_KEY, ctx); -} - -/** - * Gets the message edit context. Call this in child components. - */ -export function getMessageEditContext(): MessageEditContext { - return getContext(MESSAGE_EDIT_KEY); -} diff --git a/tools/ui/src/lib/hooks/use-chat-message-edit-context.svelte.ts b/tools/ui/src/lib/hooks/use-chat-message-edit-context.svelte.ts new file mode 100644 index 000000000..de2994e73 --- /dev/null +++ b/tools/ui/src/lib/hooks/use-chat-message-edit-context.svelte.ts @@ -0,0 +1,102 @@ +import { setChatMessageEditContext } from '$lib/contexts'; +import { MessageRole } from '$lib/enums'; +import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra'; + +interface UseChatMessageEditContextOptions { + getContent: () => string; + getExtras: () => DatabaseMessageExtra[]; + showSaveOnlyOption?: boolean; + onSave: (content: string, extras?: DatabaseMessageExtra[]) => void; +} + +export function useChatMessageEditContext(options: UseChatMessageEditContextOptions) { + let isEditing = $state(false); + let editedContent = $state(''); + let editedExtras = $state([]); + let editedUploadedFiles = $state([]); + + function handleEdit() { + editedContent = options.getContent(); + editedExtras = [...options.getExtras()]; + editedUploadedFiles = []; + isEditing = true; + } + + async function handleSaveEdit() { + const trimmed = editedContent.trim(); + + if (!trimmed && editedExtras.length === 0 && editedUploadedFiles.length === 0) return; + + let finalExtras: DatabaseMessageExtra[] = $state.snapshot(editedExtras); + + if (editedUploadedFiles.length > 0) { + const plainFiles = $state.snapshot(editedUploadedFiles); + const result = await parseFilesToMessageExtras(plainFiles); + const newExtras = result?.extras || []; + + finalExtras = [...finalExtras, ...newExtras]; + } + + options.onSave(trimmed, finalExtras.length > 0 ? finalExtras : undefined); + isEditing = false; + } + + function handleCancelEdit() { + isEditing = false; + } + + setChatMessageEditContext({ + cancel: handleCancelEdit, + get editedContent() { + return editedContent; + }, + get editedExtras() { + return editedExtras; + }, + get editedUploadedFiles() { + return editedUploadedFiles; + }, + get isEditing() { + return isEditing; + }, + get messageRole() { + return MessageRole.USER; + }, + get originalContent() { + return options.getContent(); + }, + get originalExtras() { + return options.getExtras(); + }, + save: handleSaveEdit, + saveOnly: handleSaveEdit, + setContent: (c: string) => { + editedContent = c; + }, + setExtras: (e: DatabaseMessageExtra[]) => { + editedExtras = e; + }, + setUploadedFiles: (f: ChatUploadedFile[]) => { + editedUploadedFiles = f; + }, + get shouldBranchAfterEdit() { + return false; + }, + get showBranchAfterEditOption() { + return false; + }, + get showSaveOnlyOption() { + return options.showSaveOnlyOption ?? false; + }, + startEdit: handleEdit + }); + + return { + handleCancelEdit, + handleEdit, + handleSaveEdit, + get isEditing() { + return isEditing; + } + }; +} diff --git a/tools/ui/src/lib/hooks/use-message-edit-context.svelte.ts b/tools/ui/src/lib/hooks/use-message-edit-context.svelte.ts deleted file mode 100644 index 271675404..000000000 --- a/tools/ui/src/lib/hooks/use-message-edit-context.svelte.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { setMessageEditContext } from '$lib/contexts'; -import { MessageRole } from '$lib/enums'; -import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra'; - -interface UseMessageEditContextOptions { - getContent: () => string; - getExtras: () => DatabaseMessageExtra[]; - showSaveOnlyOption?: boolean; - onSave: (content: string, extras?: DatabaseMessageExtra[]) => void; -} - -export function useMessageEditContext(options: UseMessageEditContextOptions) { - let isEditing = $state(false); - let editedContent = $state(''); - let editedExtras = $state([]); - let editedUploadedFiles = $state([]); - - function handleEdit() { - editedContent = options.getContent(); - editedExtras = [...options.getExtras()]; - editedUploadedFiles = []; - isEditing = true; - } - - async function handleSaveEdit() { - const trimmed = editedContent.trim(); - - if (!trimmed && editedExtras.length === 0 && editedUploadedFiles.length === 0) return; - - let finalExtras: DatabaseMessageExtra[] = $state.snapshot(editedExtras); - - if (editedUploadedFiles.length > 0) { - const plainFiles = $state.snapshot(editedUploadedFiles); - const result = await parseFilesToMessageExtras(plainFiles); - const newExtras = result?.extras || []; - - finalExtras = [...finalExtras, ...newExtras]; - } - - options.onSave(trimmed, finalExtras.length > 0 ? finalExtras : undefined); - isEditing = false; - } - - function handleCancelEdit() { - isEditing = false; - } - - setMessageEditContext({ - cancel: handleCancelEdit, - get editedContent() { - return editedContent; - }, - get editedExtras() { - return editedExtras; - }, - get editedUploadedFiles() { - return editedUploadedFiles; - }, - get isEditing() { - return isEditing; - }, - get messageRole() { - return MessageRole.USER; - }, - get originalContent() { - return options.getContent(); - }, - get originalExtras() { - return options.getExtras(); - }, - save: handleSaveEdit, - saveOnly: handleSaveEdit, - setContent: (c: string) => { - editedContent = c; - }, - setExtras: (e: DatabaseMessageExtra[]) => { - editedExtras = e; - }, - setUploadedFiles: (f: ChatUploadedFile[]) => { - editedUploadedFiles = f; - }, - get shouldBranchAfterEdit() { - return false; - }, - get showBranchAfterEditOption() { - return false; - }, - get showSaveOnlyOption() { - return options.showSaveOnlyOption ?? false; - }, - startEdit: handleEdit - }); - - return { - handleCancelEdit, - handleEdit, - handleSaveEdit, - get isEditing() { - return isEditing; - } - }; -} diff --git a/tools/ui/src/lib/types/chat.d.ts b/tools/ui/src/lib/types/chat.d.ts index cb93e7795..f0f3a297e 100644 --- a/tools/ui/src/lib/types/chat.d.ts +++ b/tools/ui/src/lib/types/chat.d.ts @@ -7,7 +7,8 @@ import type { AttachmentMenuItemId, ChatFormCommandAction, ErrorDialogType, - FileMentionEntryType + FileMentionEntryType, + MessageRole } from '$lib/enums'; import type { Component } from 'svelte'; @@ -235,3 +236,111 @@ export interface ChatFormCommand { action: ChatFormCommandAction; disabled: boolean; } + +/** + * Data shown in the message delete confirmation dialog. + */ +export interface ChatMessageDeletionInfo { + totalCount: number; + userMessages: number; + assistantMessages: number; + messageTypes: string[]; +} + +/** + * Conversation-level message operations owned by ChatMessages (store calls + list + * refresh + user-action notification), passed to each ChatMessage as a prop. + */ +export interface ChatMessageActions { + copy: (message: DatabaseMessage) => void; + delete: (message: DatabaseMessage) => void; + navigateToSibling: (siblingId: string) => void; + editWithBranching: ( + message: DatabaseMessage, + newContent: string, + newExtras?: DatabaseMessageExtra[] + ) => void; + editWithReplacement: ( + message: DatabaseMessage, + newContent: string, + shouldBranch: boolean + ) => void; + editUserMessagePreserveResponses: ( + message: DatabaseMessage, + newContent: string, + newExtras?: DatabaseMessageExtra[] + ) => void; + regenerateWithBranching: (message: DatabaseMessage, modelOverride?: string) => void; + continueAssistantMessage: (message: DatabaseMessage) => void; + forkConversation: ( + message: DatabaseMessage, + options: { name: string; includeAttachments: boolean } + ) => void; +} + +/** + * Per-message actions and state. Set once per message in ChatMessage.svelte and + * consumed by its descendants (action icons, branching controls). + */ +export interface ChatMessageActionsContext { + readonly siblingInfo: ChatMessageSiblingInfo | null; + readonly deletionInfo: ChatMessageDeletionInfo | null; + readonly showDeleteDialog: boolean; + copy: () => void; + requestDelete: () => void; + confirmDelete: () => void; + setShowDeleteDialog: (show: boolean) => void; + navigateToSibling: (siblingId: string) => void; + forkConversation?: (options: { name: string; includeAttachments: boolean }) => void; +} + +export interface ChatMessageEditState { + readonly isEditing: boolean; + readonly editedContent: string; + readonly editedExtras: DatabaseMessageExtra[]; + readonly editedUploadedFiles: ChatUploadedFile[]; + readonly originalContent: string; + readonly originalExtras: DatabaseMessageExtra[]; + readonly showSaveOnlyOption: boolean; + readonly showBranchAfterEditOption: boolean; + readonly shouldBranchAfterEdit: boolean; + readonly messageRole: MessageRole; + readonly rawEditContent?: string; +} + +export interface ChatMessageEditActions { + setContent: (content: string) => void; + setExtras: (extras: DatabaseMessageExtra[]) => void; + setUploadedFiles: (files: ChatUploadedFile[]) => void; + save: () => void; + saveOnly: () => void; + cancel: () => void; + startEdit: () => void; +} + +export interface ChatMessageAssistantEditActions { + setShouldBranchAfterEdit: (value: boolean) => void; +} + +export type ChatMessageEditContext = ChatMessageEditState & + ChatMessageEditActions & + Partial; + +/** + * Actions and capability flags for the ChatForm add-menu. Set once in + * ChatFormActions.svelte and consumed by its deep descendants (the add sheet, + * dropdown and MCP servers submenu) to avoid relaying them through props. + */ +export interface ChatFormActionsContext { + readonly disabled: boolean; + readonly hasAudioModality: boolean; + readonly hasVideoModality: boolean; + readonly hasVisionModality: boolean; + readonly hasMcpPromptsSupport: boolean; + readonly hasMcpResourcesSupport: boolean; + onFileUpload?: () => void; + onSystemPromptClick?: () => void; + onMcpPromptClick?: () => void; + onMcpResourcesClick?: () => void; + onMcpSettingsClick?: () => void; +} diff --git a/tools/ui/src/lib/types/index.ts b/tools/ui/src/lib/types/index.ts index 917c9516b..34767e2ae 100644 --- a/tools/ui/src/lib/types/index.ts +++ b/tools/ui/src/lib/types/index.ts @@ -44,6 +44,14 @@ export type { ChatUploadedFile, ChatAttachmentDisplayItem, ChatMessageSiblingInfo, + ChatMessageActions, + ChatMessageActionsContext, + ChatMessageDeletionInfo, + ChatMessageEditContext, + ChatMessageEditState, + ChatMessageEditActions, + ChatMessageAssistantEditActions, + ChatFormActionsContext, ChatMessagePromptProgress, ChatMessageTimings, ChatMessageAgenticTimings, diff --git a/tools/ui/tests/stories/ChatMessage.stories.svelte b/tools/ui/tests/stories/ChatMessage.stories.svelte index 023ba1ac4..84fee2ea1 100644 --- a/tools/ui/tests/stories/ChatMessage.stories.svelte +++ b/tools/ui/tests/stories/ChatMessage.stories.svelte @@ -1,6 +1,7 @@