* refactor: Remove dead context for Chat Settings and create a new one for Chat Messages Actions
* refactor: Contexts & types
ApiRouterModelsStatusResponse,
ApiRouterModelsUnloadRequest,
ApiRouterModelsUnloadResponse,
- // Chat types
ChatAttachmentDisplayItem,
+ // Chat types
ChatMessagePromptProgress,
ChatMessageSiblingInfo,
ChatMessageTimings,
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;
}
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} />
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;
}
<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}
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}
} 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}
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;
}
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>
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)}
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}
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}
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 {
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,
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}
<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;
onSendImmediately
}: Props = $props();
- const editCtx = useMessageEditContext({
+ const editCtx = useChatMessageEditContext({
getContent: () => content,
getExtras: () => extras,
onSave: (content, extras) => onEdit(content, extras)
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
<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);
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">
tooltip="Next version"
disabled={!hasNext}
class="h-5 w-5 p-0 {!hasNext ? 'opacity-30' : ''}"
- onclick={() => onNavigateToSibling?.(nextSiblingId!)}
+ onclick={() => messageActions.navigateToSibling(nextSiblingId!)}
/>
</div>
{/if}
} 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
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);
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;
<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,
const currentConfig = settingsStore.config;
- setChatActionsContext({
+ const chatActions: ChatMessageActions = {
continueAssistantMessage: async (message: DatabaseMessage) => {
onUserAction?.();
await chatStore.continueAssistantMessage(message.id);
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}
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 }}>
-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';
+++ /dev/null
-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);
-}
--- /dev/null
+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);
+}
--- /dev/null
+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);
+}
--- /dev/null
+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);
+}
+++ /dev/null
-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);
-}
-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';
+++ /dev/null
-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);
-}
--- /dev/null
+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;
+ }
+ };
+}
+++ /dev/null
-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;
- }
- };
-}
AttachmentMenuItemId,
ChatFormCommandAction,
ErrorDialogType,
- FileMentionEntryType
+ FileMentionEntryType,
+ MessageRole
} from '$lib/enums';
import type { Component } from 'svelte';
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;
+}
ChatUploadedFile,
ChatAttachmentDisplayItem,
ChatMessageSiblingInfo,
+ ChatMessageActions,
+ ChatMessageActionsContext,
+ ChatMessageDeletionInfo,
+ ChatMessageEditContext,
+ ChatMessageEditState,
+ ChatMessageEditActions,
+ ChatMessageAssistantEditActions,
+ ChatFormActionsContext,
ChatMessagePromptProgress,
ChatMessageTimings,
ChatMessageAgenticTimings,
<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>