From d86c7d62dfa7fb7227f0b699e3af58c11b295549 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Thu, 13 Aug 2026 08:21:15 +0200 Subject: [PATCH] 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 --- tools/ui/src/app.d.ts | 2 +- .../ChatFormActionAddDropdown.svelte | 60 ++++------ .../ChatFormActionAddSheet.svelte | 51 +++----- .../ChatFormActionsAdd.svelte | 55 +-------- .../ChatFormActions/ChatFormActions.svelte | 51 ++++++-- .../ChatMessage/ChatMessage.svelte | 102 ++++++---------- .../ChatMessageAssistant.svelte | 39 +----- .../ChatMessageMcpPrompt.svelte | 48 +------- .../ChatMessageSystem.svelte | 48 +------- .../ChatMessageToolCallBlock.svelte | 3 +- ...essageToolCallBlockExecShellCommand.svelte | 3 +- .../ChatMessageUser/ChatMessageUser.svelte | 47 +------- .../ChatMessageUserPending.svelte | 4 +- .../ChatMessageActionIcons.svelte | 66 ++++------- ...MessageActionIconsBranchingControls.svelte | 13 +- .../ChatMessageAgenticContent.svelte | 2 +- .../ChatMessages/ChatMessageEditForm.svelte | 4 +- .../ChatMessageReasoningBlock.svelte | 3 +- .../app/chat/ChatMessages/ChatMessages.svelte | 7 +- .../settings/SettingsChat/SettingsChat.svelte | 9 -- .../lib/constants/context-keys.constants.ts | 6 +- .../src/lib/contexts/chat-actions.context.ts | 39 ------ .../lib/contexts/chat-form-actions.context.ts | 19 +++ .../contexts/chat-message-actions.context.ts | 21 ++++ .../lib/contexts/chat-message-edit.context.ts | 19 +++ .../contexts/chat-settings-config.context.ts | 20 ---- tools/ui/src/lib/contexts/index.ts | 21 +--- .../src/lib/contexts/message-edit.context.ts | 51 -------- ...> use-chat-message-edit-context.svelte.ts} | 8 +- tools/ui/src/lib/types/chat.d.ts | 111 +++++++++++++++++- tools/ui/src/lib/types/index.ts | 8 ++ .../tests/stories/ChatMessage.stories.svelte | 15 ++- 32 files changed, 378 insertions(+), 577 deletions(-) delete mode 100644 tools/ui/src/lib/contexts/chat-actions.context.ts create mode 100644 tools/ui/src/lib/contexts/chat-form-actions.context.ts create mode 100644 tools/ui/src/lib/contexts/chat-message-actions.context.ts create mode 100644 tools/ui/src/lib/contexts/chat-message-edit.context.ts delete mode 100644 tools/ui/src/lib/contexts/chat-settings-config.context.ts delete mode 100644 tools/ui/src/lib/contexts/message-edit.context.ts rename tools/ui/src/lib/hooks/{use-message-edit-context.svelte.ts => use-chat-message-edit-context.svelte.ts} (91%) 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-message-edit-context.svelte.ts b/tools/ui/src/lib/hooks/use-chat-message-edit-context.svelte.ts similarity index 91% rename from tools/ui/src/lib/hooks/use-message-edit-context.svelte.ts rename to tools/ui/src/lib/hooks/use-chat-message-edit-context.svelte.ts index 271675404..de2994e73 100644 --- a/tools/ui/src/lib/hooks/use-message-edit-context.svelte.ts +++ b/tools/ui/src/lib/hooks/use-chat-message-edit-context.svelte.ts @@ -1,15 +1,15 @@ -import { setMessageEditContext } from '$lib/contexts'; +import { setChatMessageEditContext } from '$lib/contexts'; import { MessageRole } from '$lib/enums'; import { parseFilesToMessageExtras } from '$lib/utils/convert-files-to-extra'; -interface UseMessageEditContextOptions { +interface UseChatMessageEditContextOptions { getContent: () => string; getExtras: () => DatabaseMessageExtra[]; showSaveOnlyOption?: boolean; onSave: (content: string, extras?: DatabaseMessageExtra[]) => void; } -export function useMessageEditContext(options: UseMessageEditContextOptions) { +export function useChatMessageEditContext(options: UseChatMessageEditContextOptions) { let isEditing = $state(false); let editedContent = $state(''); let editedExtras = $state([]); @@ -45,7 +45,7 @@ export function useMessageEditContext(options: UseMessageEditContextOptions) { isEditing = false; } - setMessageEditContext({ + setChatMessageEditContext({ cancel: handleCancelEdit, get editedContent() { return editedContent; 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 @@