refactor: Remove dead context for Chat Settings and create a new one for Chat Messages Actions

This commit is contained in:
Aleksander Grygier
2026-08-12 00:45:22 +02:00
parent bd542727fc
commit adf5150556
12 changed files with 116 additions and 303 deletions
@@ -8,7 +8,12 @@
ChatMessageUser
} from '$lib/components/app/chat';
import { REASONING_TAGS, ROUTES, SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants';
import { getChatActionsContext, setMessageEditContext } from '$lib/contexts';
import {
type ChatMessageDeletionInfo,
getChatActionsContext,
setChatMessageActionsContext,
setMessageEditContext
} from '$lib/contexts';
import { AgenticSectionType, AttachmentType, MessageRole } from '$lib/enums';
import { DatabaseService } from '$lib/services/database.service';
import { chatStore, conversationsStore, isMobile } from '$lib/stores';
@@ -38,12 +43,7 @@
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
@@ -166,6 +166,30 @@
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;
@@ -360,73 +384,22 @@
<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>
@@ -17,45 +17,20 @@
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();
@@ -207,18 +182,8 @@
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)}
@@ -12,36 +12,9 @@
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();
@@ -63,20 +36,7 @@
{#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}
@@ -12,37 +12,10 @@
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();
let { class: className = '', message, textareaElement = $bindable() }: Props = $props();
const editCtx = getMessageEditContext();
@@ -218,20 +191,7 @@
{#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}
@@ -13,40 +13,15 @@
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
@@ -132,21 +107,7 @@
{#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}
@@ -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, getMessageEditContext } from '$lib/contexts';
import { MessageRole } from '$lib/enums';
import { conversationsStore } from '$lib/stores';
@@ -16,23 +17,8 @@
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;
@@ -40,32 +26,25 @@
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 = getMessageEditContext();
let showForkDialog = $state(false);
let forkName = $state('');
let forkIncludeAttachments = $state(true);
function handleConfirmDelete() {
onConfirmDelete();
onShowDeleteDialogChange(false);
messageActions.confirmDelete();
messageActions.setShowDeleteDialog(false);
}
function handleOpenForkDialog() {
@@ -77,7 +56,10 @@
}
function handleConfirmFork() {
onForkConversation?.({ includeAttachments: forkIncludeAttachments, name: forkName.trim() });
messageActions.forkConversation?.({
includeAttachments: forkIncludeAttachments,
name: forkName.trim()
});
showForkDialog = false;
}
</script>
@@ -88,18 +70,16 @@
? '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()} />
@@ -109,11 +89,11 @@
<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>
@@ -129,19 +109,19 @@
</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
@@ -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}
@@ -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';
@@ -122,14 +121,6 @@
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 }}>
@@ -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_ACTIONS = 'chat-message-actions';
@@ -0,0 +1,39 @@
import { CONTEXT_KEY_CHAT_MESSAGE_ACTIONS } from '$lib/constants';
import { getContext, setContext } from 'svelte';
export interface ChatMessageDeletionInfo {
totalCount: number;
userMessages: number;
assistantMessages: number;
messageTypes: string[];
}
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;
}
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);
}
@@ -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);
}
+5 -4
View File
@@ -13,7 +13,8 @@ export {
} from './chat-actions.context';
export {
getChatSettingsConfigContext,
setChatSettingsConfigContext,
type ChatSettingsConfigContext
} from './chat-settings-config.context';
getChatMessageActionsContext,
setChatMessageActionsContext,
type ChatMessageActionsContext,
type ChatMessageDeletionInfo
} from './chat-message-actions.context';