]> git.djapps.eu Git - pkg/ggml/sources/llama.cpp/commitdiff
ui: Clean up contexts, remove prop drilling from Chat Form Actions (#26951)
authorAleksander Grygier <redacted>
Thu, 13 Aug 2026 06:21:15 +0000 (08:21 +0200)
committerGitHub <redacted>
Thu, 13 Aug 2026 06:21:15 +0000 (08:21 +0200)
* refactor: Remove dead context for Chat Settings and create a new one for Chat Messages Actions

* refactor: Contexts & types

33 files changed:
tools/ui/src/app.d.ts
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddDropdown.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionAddSheet.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionAdd/ChatFormActionsAdd.svelte
tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActions.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageAssistant/ChatMessageAssistant.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageMcpPrompt/ChatMessageMcpPrompt.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageSystem/ChatMessageSystem.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlock.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageToolCall/ChatMessageToolCallBlockExecShellCommand.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUser.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessage/ChatMessageUser/ChatMessageUserPending.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionIcons/ChatMessageActionIcons.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageActions/ChatMessageActionIcons/ChatMessageActionIconsBranchingControls.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageAgenticContent.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageEditForm.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessageReasoningBlock.svelte
tools/ui/src/lib/components/app/chat/ChatMessages/ChatMessages.svelte
tools/ui/src/lib/components/app/settings/SettingsChat/SettingsChat.svelte
tools/ui/src/lib/constants/context-keys.constants.ts
tools/ui/src/lib/contexts/chat-actions.context.ts [deleted file]
tools/ui/src/lib/contexts/chat-form-actions.context.ts [new file with mode: 0644]
tools/ui/src/lib/contexts/chat-message-actions.context.ts [new file with mode: 0644]
tools/ui/src/lib/contexts/chat-message-edit.context.ts [new file with mode: 0644]
tools/ui/src/lib/contexts/chat-settings-config.context.ts [deleted file]
tools/ui/src/lib/contexts/index.ts
tools/ui/src/lib/contexts/message-edit.context.ts [deleted file]
tools/ui/src/lib/hooks/use-chat-message-edit-context.svelte.ts [new file with mode: 0644]
tools/ui/src/lib/hooks/use-message-edit-context.svelte.ts [deleted file]
tools/ui/src/lib/types/chat.d.ts
tools/ui/src/lib/types/index.ts
tools/ui/tests/stories/ChatMessage.stories.svelte

index 6f26214e1c6ee8046b5282831ff1769b93192d16..5309dce8f4dcdecf852d10356179ac7f70b4429f 100644 (file)
@@ -32,8 +32,8 @@ import type {
        ApiRouterModelsStatusResponse,
        ApiRouterModelsUnloadRequest,
        ApiRouterModelsUnloadResponse,
-       // Chat types
        ChatAttachmentDisplayItem,
+       // Chat types
        ChatMessagePromptProgress,
        ChatMessageSiblingInfo,
        ChatMessageTimings,
index 99aaee7845f28b9e2609e9fe062e49774a063d47..02bfadb7e4131e078bebeeaad116b4879267ae79 100644 (file)
                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
 
        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}
                                        >
                                                <span class="sr-only">{ATTACHMENT_TOOLTIP_TEXT}</span>
 
                                class="flex cursor-pointer items-center gap-2"
                                onclick={() => {
                                        suppressCloseAutoFocus = true;
-                                       onSystemPromptClick?.();
+                                       chatFormActions.onSystemPromptClick?.();
                                }}
                        >
                                <MessageSquare class={ICON_CLASS_DEFAULT} />
 
                        <ChatFormActionAddMcpServersSubmenu onMcpSettingsClick={handleMcpSettingsClick} />
 
-                       {#if hasMcpPromptsSupport}
+                       {#if chatFormActions.hasMcpPromptsSupport}
                                <DropdownMenu.Separator />
 
                                <DropdownMenu.Item
                                        class="flex cursor-pointer items-center gap-2"
-                                       onclick={onMcpPromptClick}
+                                       onclick={chatFormActions.onMcpPromptClick}
                                >
                                        <Zap class={ICON_CLASS_DEFAULT} />
 
                                </DropdownMenu.Item>
                        {/if}
 
-                       {#if hasMcpResourcesSupport}
+                       {#if chatFormActions.hasMcpResourcesSupport}
                                <DropdownMenu.Item
                                        class="flex cursor-pointer items-center gap-2"
-                                       onclick={onMcpResourcesClick}
+                                       onclick={chatFormActions.onMcpResourcesClick}
                                >
                                        <FolderOpen class={ICON_CLASS_DEFAULT} />
 
index aa5397ca0b301d9b79fa9877b0d303ac8b24d4c2..63a8c267d81b6cda6d9933da436e243b8798035b 100644 (file)
@@ -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';
 
        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);
 
        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 @@
 
 <div class="flex items-center gap-1 {className}">
        <Sheet.Root bind:open={sheetOpen}>
-               {@render trigger({ disabled, onclick: () => (sheetOpen = true) })}
+               {@render trigger({ disabled: chatFormActions.disabled, onclick: () => (sheetOpen = true) })}
 
                <Sheet.Content side="bottom" class="max-h-[85vh] gap-0 overflow-y-auto">
                        <Sheet.Header>
                                        <span>System Message</span>
                                </button>
 
-                               {#if hasMcpPromptsSupport}
+                               {#if chatFormActions.hasMcpPromptsSupport}
                                        <button
                                                type="button"
                                                class={sheetItemClass}
                                        </button>
                                {/if}
 
-                               {#if hasMcpResourcesSupport}
+                               {#if chatFormActions.hasMcpResourcesSupport}
                                        <button
                                                type="button"
                                                class={sheetItemClass}
index a31ffb394dddd65d48526f77780e54c6906d1b51..47bdb47a4711ac7d8a5f0595952af07805c1f45d 100644 (file)
@@ -3,65 +3,14 @@
        import ChatFormActionAddDropdown from './ChatFormActionAddDropdown.svelte';
        import ChatFormActionAddSheet from './ChatFormActionAddSheet.svelte';
        import { isMobile } from '$lib/stores';
-
-       interface Props {
-               disabled?: boolean;
-               hasAudioModality?: boolean;
-               hasVideoModality?: boolean;
-               hasMcpPromptsSupport?: boolean;
-               hasMcpResourcesSupport?: boolean;
-               hasVisionModality?: boolean;
-               onFileUpload?: () => void;
-               onMcpPromptClick?: () => void;
-               onMcpResourcesClick?: () => void;
-               onMcpSettingsClick?: () => void;
-               onSystemPromptClick?: () => void;
-       }
-
-       let {
-               disabled = false,
-               hasAudioModality = false,
-               hasMcpPromptsSupport = false,
-               hasMcpResourcesSupport = false,
-               hasVideoModality = false,
-               hasVisionModality = false,
-               onFileUpload,
-               onMcpPromptClick,
-               onMcpResourcesClick,
-               onMcpSettingsClick,
-               onSystemPromptClick
-       }: Props = $props();
 </script>
 
 {#if isMobile.current}
-       <ChatFormActionAddSheet
-               {disabled}
-               {hasAudioModality}
-               {hasVideoModality}
-               {hasVisionModality}
-               {hasMcpPromptsSupport}
-               {hasMcpResourcesSupport}
-               {onFileUpload}
-               {onSystemPromptClick}
-               {onMcpPromptClick}
-               {onMcpResourcesClick}
-       >
+       <ChatFormActionAddSheet>
                {#snippet trigger({ disabled, onclick })}
                        <ChatFormActionAddButton {disabled} {onclick} />
                {/snippet}
        </ChatFormActionAddSheet>
 {:else}
-       <ChatFormActionAddDropdown
-               {disabled}
-               {hasAudioModality}
-               {hasVideoModality}
-               {hasVisionModality}
-               {hasMcpPromptsSupport}
-               {hasMcpResourcesSupport}
-               {onFileUpload}
-               {onMcpPromptClick}
-               {onMcpResourcesClick}
-               {onMcpSettingsClick}
-               {onSystemPromptClick}
-       />
+       <ChatFormActionAddDropdown />
 {/if}
index 13e97cc73c6fd8a5a79e03c0f20c748c796a8954..d8fad772dd17c500dfe2b1d1cb9117f377b3cc62 100644 (file)
@@ -11,6 +11,7 @@
        } from '$lib/components/app';
        import { Button } from '$lib/components/ui/button';
        import { ICON_CLASS_DEFAULT, ROUTES } from '$lib/constants';
+       import { setChatFormActionsContext } from '$lib/contexts';
        import { FileTypeCategory, MessageRole } from '$lib/enums';
        import { ChatService } from '$lib/services';
        import { chatStore, conversationsStore, mcpStore, settingsStore } from '$lib/stores';
 
                return livePromptTokens > 0 || liveOutputTokens > 0;
        });
+
+       setChatFormActionsContext({
+               get disabled() {
+                       return disabled;
+               },
+               get hasAudioModality() {
+                       return hasAudioModality;
+               },
+               get hasMcpPromptsSupport() {
+                       return hasMcpPromptsSupport;
+               },
+               get hasMcpResourcesSupport() {
+                       return hasMcpResourcesSupport;
+               },
+               get hasVideoModality() {
+                       return hasVideoModality;
+               },
+               get hasVisionModality() {
+                       return hasVisionModality;
+               },
+               get onFileUpload() {
+                       return onFileUpload;
+               },
+               get onMcpPromptClick() {
+                       return onMcpPromptClick;
+               },
+               get onMcpResourcesClick() {
+                       return onMcpResourcesClick;
+               },
+               get onMcpSettingsClick() {
+                       return () => goto(ROUTES.MCP_SERVERS);
+               },
+               get onSystemPromptClick() {
+                       return onSystemPromptClick;
+               }
+       });
 </script>
 
 <div
 >
        {#if showAddButton}
                <div class="mr-auto flex items-center gap-2">
-                       <ChatFormActionsAdd
-                               {disabled}
-                               {hasAudioModality}
-                               {hasVideoModality}
-                               {hasVisionModality}
-                               {hasMcpPromptsSupport}
-                               {hasMcpResourcesSupport}
-                               {onFileUpload}
-                               {onSystemPromptClick}
-                               {onMcpPromptClick}
-                               {onMcpResourcesClick}
-                               onMcpSettingsClick={() => goto(ROUTES.MCP_SERVERS)}
-                       />
+                       <ChatFormActionsAdd />
                </div>
        {/if}
 
index b3e54ada02fd3b3a6d4072c765b28b934070824b..78cb8872173bc71f87127091b064cec18f96704b 100644 (file)
@@ -8,16 +8,21 @@
                ChatMessageUser
        } from '$lib/components/app/chat';
        import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
-       import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
+       import { setChatMessageActionsContext, setChatMessageEditContext } from '$lib/contexts';
        import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
        import { DatabaseService } from '$lib/services/database.service';
        import { chatStore, conversationsStore, isMobile } from '$lib/stores';
-       import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
+       import type {
+               ChatMessageActions,
+               ChatMessageDeletionInfo,
+               DatabaseMessageExtraMcpPrompt
+       } from '$lib/types';
        import { deriveAgenticSections } from '$lib/utils';
        import { parseFilesToMessageExtras } from '$lib/utils/browser-only';
 
        interface Props {
                class?: string;
+               chatActions: ChatMessageActions;
                message: DatabaseMessage;
                toolMessages?: DatabaseMessage[];
                isLastAssistantMessage?: boolean;
@@ -27,6 +32,7 @@
        }
 
        let {
+               chatActions,
                class: className = '',
                isLastAssistantMessage = false,
                isLastUserMessage = false,
                toolMessages = []
        }: Props = $props();
 
-       const chatActions = getChatActionsContext();
-
-       let deletionInfo = $state<{
-               totalCount: number;
-               userMessages: number;
-               assistantMessages: number;
-               messageTypes: string[];
-       } | null>(null);
+       let deletionInfo = $state<ChatMessageDeletionInfo | null>(null);
        // The system message placeholder must never surface as editable content; keeping
        // it in the derived (not just in handleEdit) guards against prop invalidation
        // reverting the override while editing
        let showSaveOnlyOption = $derived(message.role === MessageRole.USER);
        let showBranchAfterEditOption = $derived(message.role === MessageRole.ASSISTANT);
 
-       setMessageEditContext({
+       setChatMessageEditContext({
                cancel: handleCancelEdit,
                get editedContent() {
                        return editedContent;
                startEdit: handleEdit
        });
 
+       setChatMessageActionsContext({
+               confirmDelete: handleConfirmDelete,
+               copy: handleCopy,
+               get deletionInfo() {
+                       return deletionInfo;
+               },
+               get forkConversation() {
+                       const isForkableUser = message.role === MessageRole.USER && !mcpPromptExtra;
+
+                       return isForkableUser || message.role === MessageRole.ASSISTANT
+                               ? handleForkConversation
+                               : undefined;
+               },
+               navigateToSibling: handleNavigateToSibling,
+               requestDelete: handleDelete,
+               setShowDeleteDialog: handleShowDeleteDialogChange,
+               get showDeleteDialog() {
+                       return showDeleteDialog;
+               },
+               get siblingInfo() {
+                       return siblingInfo;
+               }
+       });
+
        let mcpPromptExtra = $derived.by(() => {
                if (message.role !== MessageRole.USER) return null;
 
 
 <div class="chat-message" class:chat-message--synthetic={isSynthetic}>
        {#if message.role === MessageRole.SYSTEM}
-               <ChatMessageSystem
-                       bind:textareaElement
-                       class={className}
-                       {deletionInfo}
-                       {message}
-                       onConfirmDelete={handleConfirmDelete}
-                       onCopy={handleCopy}
-                       onDelete={handleDelete}
-                       onEdit={handleEdit}
-                       onNavigateToSibling={handleNavigateToSibling}
-                       onShowDeleteDialogChange={handleShowDeleteDialogChange}
-                       {showDeleteDialog}
-                       {siblingInfo}
-               />
+               <ChatMessageSystem bind:textareaElement class={className} {message} />
        {:else if mcpPromptExtra}
-               <ChatMessageMcpPrompt
-                       class={className}
-                       {deletionInfo}
-                       {message}
-                       mcpPrompt={mcpPromptExtra}
-                       onConfirmDelete={handleConfirmDelete}
-                       onCopy={handleCopy}
-                       onDelete={handleDelete}
-                       onEdit={handleEdit}
-                       onNavigateToSibling={handleNavigateToSibling}
-                       onShowDeleteDialogChange={handleShowDeleteDialogChange}
-                       {showDeleteDialog}
-                       {siblingInfo}
-               />
+               <ChatMessageMcpPrompt class={className} {message} mcpPrompt={mcpPromptExtra} />
        {:else if isSynthetic}
                <ChatMessageSynthetic {message} class={className} />
        {:else if message.role === MessageRole.USER}
-               <ChatMessageUser
-                       class={className}
-                       {deletionInfo}
-                       {isLastUserMessage}
-                       {message}
-                       {nextAssistantMessage}
-                       onConfirmDelete={handleConfirmDelete}
-                       onCopy={handleCopy}
-                       onDelete={handleDelete}
-                       onEdit={handleEdit}
-                       onForkConversation={handleForkConversation}
-                       onNavigateToSibling={handleNavigateToSibling}
-                       onShowDeleteDialogChange={handleShowDeleteDialogChange}
-                       {showDeleteDialog}
-                       {siblingInfo}
-               />
+               <ChatMessageUser class={className} {isLastUserMessage} {message} {nextAssistantMessage} />
        {:else}
                <ChatMessageAssistant
                        bind:textareaElement
                        class={className}
-                       {deletionInfo}
                        {isLastAssistantMessage}
                        {message}
                        {toolMessages}
-                       onConfirmDelete={handleConfirmDelete}
                        onContinue={handleContinue}
-                       onCopy={handleCopy}
-                       onDelete={handleDelete}
-                       onEdit={handleEdit}
-                       onForkConversation={handleForkConversation}
-                       onNavigateToSibling={handleNavigateToSibling}
                        onRegenerate={handleRegenerate}
-                       onShowDeleteDialogChange={handleShowDeleteDialogChange}
-                       {showDeleteDialog}
-                       {siblingInfo}
                />
        {/if}
 </div>
index 14ea68099c6fcc92705b4061b775655838de3e71..b92be9fbd61f2457341c2e03ceb1054ace9f4e44 100644 (file)
@@ -8,7 +8,7 @@
                ChatMessageAssistantStatistics,
                ChatMessageEditForm
        } from '$lib/components/app';
-       import { getMessageEditContext } from '$lib/contexts';
+       import { getChatMessageEditContext } from '$lib/contexts';
        import { MessageRole } from '$lib/enums';
        import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
        import { chatStore, modelsStore, serverStore, settingsStore } from '$lib/stores';
 
        interface Props {
                class?: string;
-               deletionInfo: {
-                       totalCount: number;
-                       userMessages: number;
-                       assistantMessages: number;
-                       messageTypes: string[];
-               } | null;
                isLastAssistantMessage?: boolean;
                message: DatabaseMessage;
                toolMessages?: DatabaseMessage[];
-               onCopy: () => void;
-               onConfirmDelete: () => void;
                onContinue?: () => void;
-               onDelete: () => void;
-               onEdit?: () => void;
-               onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
-               onNavigateToSibling?: (siblingId: string) => void;
                onRegenerate: (modelOverride?: string) => void;
-               onShowDeleteDialogChange: (show: boolean) => void;
-               showDeleteDialog: boolean;
-               siblingInfo?: ChatMessageSiblingInfo | null;
                textareaElement?: HTMLTextAreaElement;
        }
 
        let {
                class: className = '',
-               deletionInfo,
                isLastAssistantMessage = false,
                message,
-               onConfirmDelete,
                onContinue,
-               onCopy,
-               onDelete,
-               onEdit,
-               onForkConversation,
-               onNavigateToSibling,
                onRegenerate,
-               onShowDeleteDialogChange,
-               showDeleteDialog,
-               siblingInfo = null,
                textareaElement = $bindable(),
                toolMessages = []
        }: Props = $props();
 
        // Get edit context
-       const editCtx = getMessageEditContext();
+       const editCtx = getChatMessageEditContext();
 
        const isAgentic = $derived(hasAgenticContent(message, toolMessages));
        const processingState = useProcessingState();
                        role={MessageRole.ASSISTANT}
                        justify="start"
                        actionsPosition="left"
-                       {siblingInfo}
-                       {showDeleteDialog}
-                       {deletionInfo}
-                       {onCopy}
-                       {onEdit}
                        {onRegenerate}
                        onContinue={currentConfig.enableContinueGeneration ? onContinue : undefined}
-                       {onForkConversation}
-                       {onDelete}
-                       {onConfirmDelete}
-                       {onNavigateToSibling}
-                       {onShowDeleteDialogChange}
                        showRawOutputSwitch={currentConfig.showRawOutputSwitch}
                        rawOutputEnabled={showRawOutput}
                        onRawOutputToggle={(enabled) => (showRawOutput = enabled)}
index d3aa7251e09674044fbd199fd7cc5a348ec7af27..4563b1fa86304a7529e821c2d7d19cfe19ed55c7 100644 (file)
@@ -4,7 +4,7 @@
                ChatMessageEditForm,
                ChatMessageMcpPromptContent
        } from '$lib/components/app';
-       import { getMessageEditContext } from '$lib/contexts';
+       import { getChatMessageEditContext } from '$lib/contexts';
        import { McpPromptVariant, MessageRole } from '$lib/enums';
        import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
 
                class?: string;
                message: DatabaseMessage;
                mcpPrompt: DatabaseMessageExtraMcpPrompt;
-               siblingInfo?: ChatMessageSiblingInfo | null;
-               showDeleteDialog: boolean;
-               deletionInfo: {
-                       totalCount: number;
-                       userMessages: number;
-                       assistantMessages: number;
-                       messageTypes: string[];
-               } | null;
-               onCopy: () => void;
-               onEdit: () => void;
-               onDelete: () => void;
-               onConfirmDelete: () => void;
-               onNavigateToSibling?: (siblingId: string) => void;
-               onShowDeleteDialogChange: (show: boolean) => void;
        }
 
-       let {
-               class: className = '',
-               deletionInfo,
-               mcpPrompt,
-               message,
-               onConfirmDelete,
-               onCopy,
-               onDelete,
-               onEdit,
-               onNavigateToSibling,
-               onShowDeleteDialogChange,
-               showDeleteDialog,
-               siblingInfo = null
-       }: Props = $props();
+       let { class: className = '', mcpPrompt, message }: Props = $props();
 
        // Get edit context
-       const editCtx = getMessageEditContext();
+       const editCtx = getChatMessageEditContext();
 </script>
 
 <div
 
                {#if message.timestamp}
                        <div class="max-w-[80%]">
-                               <ChatMessageActionIcons
-                                       actionsPosition="right"
-                                       {deletionInfo}
-                                       justify="end"
-                                       {onConfirmDelete}
-                                       {onCopy}
-                                       {onDelete}
-                                       {onEdit}
-                                       {onNavigateToSibling}
-                                       {onShowDeleteDialogChange}
-                                       {siblingInfo}
-                                       {showDeleteDialog}
-                                       role={MessageRole.USER}
-                               />
+                               <ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
                        </div>
                {/if}
        {/if}
index 2f919f9eb89cb8627fde1fae07ec83c414d87f3f..c6222f568c4898ef0e98a4eceea5c61738649d3c 100644 (file)
@@ -4,7 +4,7 @@
        import { Button } from '$lib/components/ui/button';
        import { Card } from '$lib/components/ui/card';
        import { INPUT_CLASSES } from '$lib/constants';
-       import { getMessageEditContext } from '$lib/contexts';
+       import { getChatMessageEditContext } from '$lib/contexts';
        import { KeyboardKey, MessageRole } from '$lib/enums';
        import { settingsStore } from '$lib/stores';
        import { autoResizeTextarea, isIMEComposing } from '$lib/utils';
        interface Props {
                class?: string;
                message: DatabaseMessage;
-               siblingInfo?: ChatMessageSiblingInfo | null;
-               showDeleteDialog: boolean;
-               deletionInfo: {
-                       totalCount: number;
-                       userMessages: number;
-                       assistantMessages: number;
-                       messageTypes: string[];
-               } | null;
-               onCopy: () => void;
-               onEdit: () => void;
-               onDelete: () => void;
-               onConfirmDelete: () => void;
-               onNavigateToSibling?: (siblingId: string) => void;
-               onShowDeleteDialogChange: (show: boolean) => void;
                textareaElement?: HTMLTextAreaElement;
        }
 
-       let {
-               class: className = '',
-               deletionInfo,
-               message,
-               onConfirmDelete,
-               onCopy,
-               onDelete,
-               onEdit,
-               onNavigateToSibling,
-               onShowDeleteDialogChange,
-               showDeleteDialog,
-               siblingInfo = null,
-               textareaElement = $bindable()
-       }: Props = $props();
-
-       const editCtx = getMessageEditContext();
+       let { class: className = '', message, textareaElement = $bindable() }: Props = $props();
+
+       const editCtx = getChatMessageEditContext();
 
        function handleEditKeydown(event: KeyboardEvent) {
                if (event.key === KeyboardKey.ENTER && !event.shiftKey && !isIMEComposing(event)) {
 
                {#if message.timestamp}
                        <div class="max-w-[80%]">
-                               <ChatMessageActionIcons
-                                       actionsPosition="right"
-                                       {deletionInfo}
-                                       justify="end"
-                                       {onConfirmDelete}
-                                       {onCopy}
-                                       {onDelete}
-                                       {onEdit}
-                                       {onNavigateToSibling}
-                                       {onShowDeleteDialogChange}
-                                       {siblingInfo}
-                                       {showDeleteDialog}
-                                       role={MessageRole.USER}
-                               />
+                               <ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
                        </div>
                {/if}
        {/if}
index db09bb7235d381da35a9b9c8ee50c438f8a04f8a..a6fa2e250471300f4ec4891e25720340753cc877 100644 (file)
@@ -12,8 +12,7 @@
        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, DatabaseMessageExtra } from '$lib/types';
        import { extractSearchQuery, extractSearchResults, isWebSearchToolName } from '$lib/utils';
 
        interface Props {
index 639bee89784a55f8503f48d996982144076d1520..d7103bf97f27b4d6929ecd4f220c5c9b29f0d307 100644 (file)
@@ -13,8 +13,7 @@
        import { SETTINGS_KEYS, TOOL_RUNTIME_SCROLL_AT_BOTTOM_THRESHOLD_PX } from '$lib/constants';
        import { AttachmentType } from '$lib/enums';
        import { settingsStore, toolsStore } from '$lib/stores';
-       import type { AgenticSection, ToolResultLine } from '$lib/types';
-       import type { DatabaseMessageExtra } from '$lib/types';
+       import type { AgenticSection, DatabaseMessageExtra, ToolResultLine } from '$lib/types';
        import {
                abbreviateHome,
                type ExecShellExitStatus,
index f902ec88d594f9ae7ea1bb2f3476eb5c49023dbc..d217a9c382eeddbbf86e6249a6166ef514cf3b82 100644 (file)
@@ -5,7 +5,7 @@
                ChatMessageStatistics,
                ChatMessageUserBubble
        } from '$lib/components/app/chat';
-       import { getMessageEditContext } from '$lib/contexts';
+       import { getChatMessageEditContext } from '$lib/contexts';
        import { ChatMessageStatisticsMode, MessageRole } from '$lib/enums';
        import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
        import { chatStore, settingsStore } from '$lib/stores';
        interface Props {
                class?: string;
                message: DatabaseMessage;
-               siblingInfo?: ChatMessageSiblingInfo | null;
-               deletionInfo: {
-                       totalCount: number;
-                       userMessages: number;
-                       assistantMessages: number;
-                       messageTypes: string[];
-               } | null;
                isLastUserMessage?: boolean;
                nextAssistantMessage?: DatabaseMessage | null;
-               showDeleteDialog: boolean;
-               onEdit: () => void;
-               onDelete: () => void;
-               onConfirmDelete: () => void;
-               onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
-               onShowDeleteDialogChange: (show: boolean) => void;
-               onNavigateToSibling?: (siblingId: string) => void;
-               onCopy: () => void;
        }
 
        let {
                class: className = '',
-               deletionInfo,
                isLastUserMessage = false,
                message,
-               nextAssistantMessage = null,
-               onConfirmDelete,
-               onCopy,
-               onDelete,
-               onEdit,
-               onForkConversation,
-               onNavigateToSibling,
-               onShowDeleteDialogChange,
-               showDeleteDialog,
-               siblingInfo = null
+               nextAssistantMessage = null
        }: Props = $props();
 
        // Get contexts
-       const editCtx = getMessageEditContext();
+       const editCtx = getChatMessageEditContext();
        const processingState = useProcessingState();
 
        const currentConfig = $derived(settingsStore.config);
 
                {#if message.timestamp}
                        <div class="max-w-[80%]">
-                               <ChatMessageActionIcons
-                                       actionsPosition="right"
-                                       {deletionInfo}
-                                       justify="end"
-                                       {onConfirmDelete}
-                                       {onCopy}
-                                       {onDelete}
-                                       {onEdit}
-                                       {onForkConversation}
-                                       {onNavigateToSibling}
-                                       {onShowDeleteDialogChange}
-                                       {siblingInfo}
-                                       {showDeleteDialog}
-                                       role={MessageRole.USER}
-                               />
+                               <ChatMessageActionIcons actionsPosition="right" justify="end" role={MessageRole.USER} />
                        </div>
                {/if}
        {/if}
index f3e8c4e91d398673a809d5fe742dec2585be5a51..a072f2e84dd224197bbc0dd180b210d8b23d2c29 100644 (file)
@@ -1,7 +1,7 @@
 <script lang="ts">
        import { ArrowUp, Edit, Trash2 } from '@lucide/svelte';
        import { ActionIcon, ChatMessageEditForm, ChatMessageUserBubble } from '$lib/components/app';
-       import { useMessageEditContext } from '$lib/hooks/use-message-edit-context.svelte';
+       import { useChatMessageEditContext } from '$lib/hooks/use-chat-message-edit-context.svelte';
 
        interface Props {
                class?: string;
@@ -21,7 +21,7 @@
                onSendImmediately
        }: Props = $props();
 
-       const editCtx = useMessageEditContext({
+       const editCtx = useChatMessageEditContext({
                getContent: () => content,
                getExtras: () => extras,
                onSave: (content, extras) => onEdit(content, extras)
index 1b3715bb4f31b3193fbc078a23883ef76e59d514..895f391255bb020f3be6d6f421f634d7ca6724c9 100644 (file)
@@ -9,6 +9,7 @@
        import Input from '$lib/components/ui/input/input.svelte';
        import Label from '$lib/components/ui/label/label.svelte';
        import { Switch } from '$lib/components/ui/switch';
+       import { getChatMessageActionsContext, getChatMessageEditContext } from '$lib/contexts';
        import { MessageRole } from '$lib/enums';
        import { conversationsStore } from '$lib/stores';
 
                role: MessageRole.USER | MessageRole.ASSISTANT;
                justify: 'start' | 'end';
                actionsPosition: 'left' | 'right';
-               siblingInfo?: ChatMessageSiblingInfo | null;
-               showDeleteDialog: boolean;
-               deletionInfo: {
-                       totalCount: number;
-                       userMessages: number;
-                       assistantMessages: number;
-                       messageTypes: string[];
-               } | null;
-               onCopy: () => void;
-               onEdit?: () => void;
                onRegenerate?: () => void;
                onContinue?: () => void;
-               onForkConversation?: (options: { name: string; includeAttachments: boolean }) => void;
-               onDelete: () => void;
-               onConfirmDelete: () => void;
-               onNavigateToSibling?: (siblingId: string) => void;
-               onShowDeleteDialogChange: (show: boolean) => void;
                showRawOutputSwitch?: boolean;
                rawOutputEnabled?: boolean;
                onRawOutputToggle?: (enabled: boolean) => void;
 
        let {
                actionsPosition,
-               deletionInfo,
                justify,
-               onConfirmDelete,
                onContinue,
-               onCopy,
-               onDelete,
-               onEdit,
-               onForkConversation,
-               onNavigateToSibling,
                onRawOutputToggle,
                onRegenerate,
-               onShowDeleteDialogChange,
                rawOutputEnabled = false,
                role,
-               showDeleteDialog,
-               showRawOutputSwitch = false,
-               siblingInfo = null
+               showRawOutputSwitch = false
        }: Props = $props();
 
+       const messageActions = getChatMessageActionsContext();
+       const editCtx = getChatMessageEditContext();
+
        let showForkDialog = $state(false);
        let forkName = $state('');
        let forkIncludeAttachments = $state(true);
 
        function handleConfirmDelete() {
-               onConfirmDelete();
-               onShowDeleteDialogChange(false);
+               messageActions.confirmDelete();
+               messageActions.setShowDeleteDialog(false);
        }
 
        function handleOpenForkDialog() {
        }
 
        function handleConfirmFork() {
-               onForkConversation?.({ includeAttachments: forkIncludeAttachments, name: forkName.trim() });
+               messageActions.forkConversation?.({
+                       includeAttachments: forkIncludeAttachments,
+                       name: forkName.trim()
+               });
                showForkDialog = false;
        }
 </script>
                        ? 'left-0'
                        : 'right-0'} flex items-center gap-2 opacity-100 transition-opacity"
        >
-               {#if siblingInfo && siblingInfo.totalSiblings > 1}
-                       <ChatMessageActionIconsBranchingControls {siblingInfo} {onNavigateToSibling} />
+               {#if messageActions.siblingInfo && messageActions.siblingInfo.totalSiblings > 1}
+                       <ChatMessageActionIconsBranchingControls />
                {/if}
 
                <div
                        class="pointer-events-auto inset-0 flex items-center gap-1 opacity-100 transition-all duration-150"
                >
-                       <ActionIcon icon={Copy} tooltip="Copy" onclick={onCopy} />
+                       <ActionIcon icon={Copy} tooltip="Copy" onclick={messageActions.copy} />
 
-                       {#if onEdit}
-                               <ActionIcon icon={Edit} tooltip="Edit" onclick={onEdit} />
-                       {/if}
+                       <ActionIcon icon={Edit} tooltip="Edit" onclick={editCtx.startEdit} />
 
                        {#if role === MessageRole.ASSISTANT && onRegenerate}
                                <ActionIcon icon={RefreshCw} tooltip="Regenerate" onclick={() => onRegenerate()} />
                                <ActionIcon icon={ArrowRight} tooltip="Continue" onclick={onContinue} />
                        {/if}
 
-                       {#if onForkConversation}
+                       {#if messageActions.forkConversation}
                                <ActionIcon icon={GitBranch} tooltip="Fork conversation" onclick={handleOpenForkDialog} />
                        {/if}
 
-                       <ActionIcon icon={Trash2} tooltip="Delete" onclick={onDelete} />
+                       <ActionIcon icon={Trash2} tooltip="Delete" onclick={messageActions.requestDelete} />
                </div>
        </div>
 
 </div>
 
 <DialogConfirmation
-       bind:open={showDeleteDialog}
+       open={messageActions.showDeleteDialog}
        title="Delete Message"
-       description={deletionInfo && deletionInfo.totalCount > 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)}
 />
 
 <DialogConfirmation
index 37a6a7d94aa10a250d50d5fb905a77937c6f5a1f..b7f99f0f3dacf4c03a6619bac91527d475b9239b 100644 (file)
@@ -1,14 +1,17 @@
 <script lang="ts">
        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!)}
                />
 
                <span class="px-1 font-mono text-xs">
@@ -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!)}
                />
        </div>
 {/if}
index 8ce1ead01e9fc5fa22c54713ca32759007570d99..5849799794f556ee8380ff204548b208e7430bdd 100644 (file)
@@ -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
index feacd0ddde340e7b485d22d64ba8d3c3983af614..369b6137b4235cbc179b754402a3c057ec92ac24 100644 (file)
@@ -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);
index 78c9dcb25a74eea6ddfc37dae3251ea30fc1e245..a0861fb9619faf74e5ff300ed8116b9adbb3134b 100644 (file)
@@ -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;
index 33d971f93188b3aa0dad9843d503a32e662b1648..2a8f45ba5388f6536e819146030aa3f985ffd8d2 100644 (file)
@@ -1,8 +1,8 @@
 <script lang="ts">
        import { ChatMessage, ChatMessageUserPending } from '$lib/components/app';
-       import { setChatActionsContext } from '$lib/contexts';
        import { MessageRole } from '$lib/enums';
        import { agenticStore, chatStore, conversationsStore, settingsStore } from '$lib/stores';
+       import type { ChatMessageActions } from '$lib/types';
        import {
                buildSiblingInfoMap,
                copyToClipboard,
@@ -22,7 +22,7 @@
 
        const currentConfig = settingsStore.config;
 
-       setChatActionsContext({
+       const chatActions: ChatMessageActions = {
                continueAssistantMessage: async (message: DatabaseMessage) => {
                        onUserAction?.();
                        await chatStore.continueAssistantMessage(message.id);
@@ -91,7 +91,7 @@
                        await chatStore.regenerateMessageWithBranching(message.id, modelOverride);
                        refreshAllMessages();
                }
-       });
+       };
 
        function refreshAllMessages() {
                const conversation = conversationsStore.activeConversation;
        {#each displayMessages as { isLastAssistantMessage, isLastUserMessage, message, nextAssistantMessage, siblingInfo, toolMessages } (message.id)}
                <ChatMessage
                        class="mx-auto mt-12 w-full max-w-3xl"
+                       {chatActions}
                        {message}
                        {toolMessages}
                        {isLastAssistantMessage}
index 83ceec93a4be8245b4409e822129d4a11d77fb60..c8b2c814cde9efb53e91adea14201dbf684bf845 100644 (file)
@@ -17,7 +17,6 @@
                SETTINGS_CHAT_SECTIONS,
                SETTINGS_SECTION_TITLES
        } from '$lib/constants';
-       import { setChatSettingsConfigContext } from '$lib/contexts';
        import { ColorMode } from '$lib/enums/ui.enums';
        import { RouterService } from '$lib/services/router.service';
        import { modelsStore, serverStore, settingsReferrer, settingsStore } from '$lib/stores';
        export function reset() {
                localConfig = { ...settingsStore.config };
        }
-
-       setChatSettingsConfigContext({
-               handleConfigChange,
-               handleThemeChange,
-               get localConfig() {
-                       return localConfig;
-               }
-       });
 </script>
 
 <div class="mx-auto flex h-full w-full flex-col md:pl-8" in:fade={{ duration: 150 }}>
index 0bd733b37066bf9dd8ab18b4b67f083483baddd2..62ff5413e13448b3f76d9505b645364b26123b46 100644 (file)
@@ -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 (file)
index dffb5f3..0000000
+++ /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 (file)
index 0000000..a49f174
--- /dev/null
@@ -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 (file)
index 0000000..fb075b3
--- /dev/null
@@ -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 (file)
index 0000000..e9c0530
--- /dev/null
@@ -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 (file)
index a90f709..0000000
+++ /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);
-}
index c6719fa9e47728f87b076764b9697f3b0e9267c3..4aaec8148c298603ce6cdfec4d4dd44259ce34be 100644 (file)
@@ -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 (file)
index 80f3e6e..0000000
+++ /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<AssistantEditActions>;
-
-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 (file)
index 0000000..de2994e
--- /dev/null
@@ -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<DatabaseMessageExtra[]>([]);
+       let editedUploadedFiles = $state<ChatUploadedFile[]>([]);
+
+       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 (file)
index 2716754..0000000
+++ /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<DatabaseMessageExtra[]>([]);
-       let editedUploadedFiles = $state<ChatUploadedFile[]>([]);
-
-       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;
-               }
-       };
-}
index cb93e7795150023d3dcbd071b9b4a5e7b3ce64be..f0f3a297e8e18b584d8c15f1fcd5c12cd542df94 100644 (file)
@@ -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<ChatMessageAssistantEditActions>;
+
+/**
+ * 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;
+}
index 917c9516b578014eb54734988004b8aab43af55b..34767e2ae7df2e8ef27cc2a776388b362b0251a9 100644 (file)
@@ -44,6 +44,14 @@ export type {
        ChatUploadedFile,
        ChatAttachmentDisplayItem,
        ChatMessageSiblingInfo,
+       ChatMessageActions,
+       ChatMessageActionsContext,
+       ChatMessageDeletionInfo,
+       ChatMessageEditContext,
+       ChatMessageEditState,
+       ChatMessageEditActions,
+       ChatMessageAssistantEditActions,
+       ChatFormActionsContext,
        ChatMessagePromptProgress,
        ChatMessageTimings,
        ChatMessageAgenticTimings,
index 023ba1ac4fc7e3b490120f8deec7ab333bf79a00..84fee2ea1c7895b9efe8b7bf1bbc099e47fb8c30 100644 (file)
@@ -1,6 +1,7 @@
 <script module lang="ts">
        import { defineMeta } from '@storybook/addon-svelte-csf';
        import ChatMessage from '$lib/components/app/chat/ChatMessages/ChatMessage/ChatMessage.svelte';
+       import type { ChatMessageActions } from '$lib/types';
 
        const { Story } = defineMeta({
                component: ChatMessage,
                title: 'Components/ChatScreen/ChatMessage'
        });
 
+       const chatActions: ChatMessageActions = {
+               continueAssistantMessage: () => {},
+               copy: () => {},
+               delete: () => {},
+               editUserMessagePreserveResponses: () => {},
+               editWithBranching: () => {},
+               editWithReplacement: () => {},
+               forkConversation: () => {},
+               navigateToSibling: () => {},
+               regenerateWithBranching: () => {}
+       };
+
        // Mock messages for different scenarios
        const userMessage: DatabaseMessage = {
                children: [],
        }}
 >
        <div class="w-[56rem]">
-               <ChatMessage message={streamingMessage} />
+               <ChatMessage message={streamingMessage} {chatActions} />
        </div>
 </Story>