*storybook.log
storybook-static
+*.code-workspace
\ No newline at end of file
}
},
"node_modules/@sveltejs/kit": {
- "version": "2.48.4",
- "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.4.tgz",
- "integrity": "sha512-TGFX1pZUt9qqY20Cv5NyYvy0iLWHf2jXi8s+eCGsig7jQMdwZWKUFMR6TbvFNhfDSUpc1sH/Y5EHv20g3HHA3g==",
+ "version": "2.48.5",
+ "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.5.tgz",
+ "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==",
"dev": true,
"license": "MIT",
"dependencies": {
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
class?: string;
message: DatabaseMessage;
onCopy?: (message: DatabaseMessage) => void;
+ onContinueAssistantMessage?: (message: DatabaseMessage) => void;
onDelete?: (message: DatabaseMessage) => void;
onEditWithBranching?: (message: DatabaseMessage, newContent: string) => void;
onEditWithReplacement?: (
newContent: string,
shouldBranch: boolean
) => void;
+ onEditUserMessagePreserveResponses?: (message: DatabaseMessage, newContent: string) => void;
onNavigateToSibling?: (siblingId: string) => void;
onRegenerateWithBranching?: (message: DatabaseMessage) => void;
siblingInfo?: ChatMessageSiblingInfo | null;
class: className = '',
message,
onCopy,
+ onContinueAssistantMessage,
onDelete,
onEditWithBranching,
onEditWithReplacement,
+ onEditUserMessagePreserveResponses,
onNavigateToSibling,
onRegenerateWithBranching,
siblingInfo = null
onRegenerateWithBranching?.(message);
}
+ function handleContinue() {
+ onContinueAssistantMessage?.(message);
+ }
+
function handleSaveEdit() {
if (message.role === 'user') {
+ // For user messages, trim to avoid accidental whitespace
onEditWithBranching?.(message, editedContent.trim());
} else {
- onEditWithReplacement?.(message, editedContent.trim(), shouldBranchAfterEdit);
+ // For assistant messages, preserve exact content including trailing whitespace
+ // This is important for the Continue feature to work properly
+ onEditWithReplacement?.(message, editedContent, shouldBranchAfterEdit);
}
isEditing = false;
shouldBranchAfterEdit = false;
}
+ function handleSaveEditOnly() {
+ if (message.role === 'user') {
+ // For user messages, trim to avoid accidental whitespace
+ onEditUserMessagePreserveResponses?.(message, editedContent.trim());
+ }
+
+ isEditing = false;
+ }
+
function handleShowDeleteDialogChange(show: boolean) {
showDeleteDialog = show;
}
onEditedContentChange={handleEditedContentChange}
{onNavigateToSibling}
onSaveEdit={handleSaveEdit}
+ onSaveEditOnly={handleSaveEditOnly}
onShowDeleteDialogChange={handleShowDeleteDialogChange}
{showDeleteDialog}
{siblingInfo}
messageContent={message.content}
onCancelEdit={handleCancelEdit}
onConfirmDelete={handleConfirmDelete}
+ onContinue={handleContinue}
onCopy={handleCopy}
onDelete={handleDelete}
onEdit={handleEdit}
<script lang="ts">
- import { Edit, Copy, RefreshCw, Trash2 } from '@lucide/svelte';
+ import { Edit, Copy, RefreshCw, Trash2, ArrowRight } from '@lucide/svelte';
import { ActionButton, ConfirmationDialog } from '$lib/components/app';
import ChatMessageBranchingControls from './ChatMessageBranchingControls.svelte';
onCopy: () => void;
onEdit?: () => void;
onRegenerate?: () => void;
+ onContinue?: () => void;
onDelete: () => void;
onConfirmDelete: () => void;
onNavigateToSibling?: (siblingId: string) => void;
onCopy,
onEdit,
onConfirmDelete,
+ onContinue,
onDelete,
onNavigateToSibling,
onShowDeleteDialogChange,
<ActionButton icon={RefreshCw} tooltip="Regenerate" onclick={onRegenerate} />
{/if}
+ {#if role === 'assistant' && onContinue}
+ <ActionButton icon={ArrowRight} tooltip="Continue" onclick={onContinue} />
+ {/if}
+
<ActionButton icon={Trash2} tooltip="Delete" onclick={onDelete} />
</div>
</div>
import { ChatMessageThinkingBlock, MarkdownContent } from '$lib/components/app';
import { useProcessingState } from '$lib/hooks/use-processing-state.svelte';
import { isLoading } from '$lib/stores/chat.svelte';
+ import autoResizeTextarea from '$lib/utils/autoresize-textarea';
import { fade } from 'svelte/transition';
import {
Check,
onCancelEdit?: () => void;
onCopy: () => void;
onConfirmDelete: () => void;
+ onContinue?: () => void;
onDelete: () => void;
onEdit?: () => void;
onEditKeydown?: (event: KeyboardEvent) => void;
messageContent,
onCancelEdit,
onConfirmDelete,
+ onContinue,
onCopy,
onDelete,
onEdit,
void copyToClipboard(model ?? '');
}
+ $effect(() => {
+ if (isEditing && textareaElement) {
+ autoResizeTextarea(textareaElement);
+ }
+ });
+
function formatToolCallBadge(toolCall: ApiChatCompletionToolCall, index: number) {
const callNumber = index + 1;
const functionName = toolCall.function?.name?.trim();
bind:value={editedContent}
class="min-h-[50vh] w-full resize-y rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
onkeydown={onEditKeydown}
- oninput={(e) => onEditedContentChange?.(e.currentTarget.value)}
+ oninput={(e) => {
+ autoResizeTextarea(e.currentTarget);
+ onEditedContentChange?.(e.currentTarget.value);
+ }}
placeholder="Edit assistant message..."
></textarea>
{onCopy}
{onEdit}
{onRegenerate}
+ onContinue={currentConfig.enableContinueGeneration && !thinkingContent
+ ? onContinue
+ : undefined}
{onDelete}
{onConfirmDelete}
{onNavigateToSibling}
<script lang="ts">
- import { Check, X } from '@lucide/svelte';
+ import { Check, X, Send } from '@lucide/svelte';
import { Card } from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { ChatAttachmentsList, MarkdownContent } from '$lib/components/app';
import { INPUT_CLASSES } from '$lib/constants/input-classes';
import { config } from '$lib/stores/settings.svelte';
+ import autoResizeTextarea from '$lib/utils/autoresize-textarea';
import ChatMessageActions from './ChatMessageActions.svelte';
interface Props {
} | null;
onCancelEdit: () => void;
onSaveEdit: () => void;
+ onSaveEditOnly?: () => void;
onEditKeydown: (event: KeyboardEvent) => void;
onEditedContentChange: (content: string) => void;
onCopy: () => void;
deletionInfo,
onCancelEdit,
onSaveEdit,
+ onSaveEditOnly,
onEditKeydown,
onEditedContentChange,
onCopy,
let messageElement: HTMLElement | undefined = $state();
const currentConfig = config();
+ $effect(() => {
+ if (isEditing && textareaElement) {
+ autoResizeTextarea(textareaElement);
+ }
+ });
+
$effect(() => {
if (!messageElement || !message.content.trim()) return;
bind:value={editedContent}
class="min-h-[60px] w-full resize-none rounded-2xl px-3 py-2 text-sm {INPUT_CLASSES}"
onkeydown={onEditKeydown}
- oninput={(e) => onEditedContentChange(e.currentTarget.value)}
+ oninput={(e) => {
+ autoResizeTextarea(e.currentTarget);
+ onEditedContentChange(e.currentTarget.value);
+ }}
placeholder="Edit your message..."
></textarea>
<div class="mt-2 flex justify-end gap-2">
- <Button class="h-8 px-3" onclick={onCancelEdit} size="sm" variant="outline">
+ <Button class="h-8 px-3" onclick={onCancelEdit} size="sm" variant="ghost">
<X class="mr-1 h-3 w-3" />
-
Cancel
</Button>
- <Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent.trim()} size="sm">
- <Check class="mr-1 h-3 w-3" />
+ {#if onSaveEditOnly}
+ <Button
+ class="h-8 px-3"
+ onclick={onSaveEditOnly}
+ disabled={!editedContent.trim()}
+ size="sm"
+ variant="outline"
+ >
+ <Check class="mr-1 h-3 w-3" />
+ Save
+ </Button>
+ {/if}
+ <Button class="h-8 px-3" onclick={onSaveEdit} disabled={!editedContent.trim()} size="sm">
+ <Send class="mr-1 h-3 w-3" />
Send
</Button>
</div>
import { DatabaseStore } from '$lib/stores/database';
import {
activeConversation,
+ continueAssistantMessage,
deleteMessage,
- navigateToSibling,
- editMessageWithBranching,
editAssistantMessage,
+ editMessageWithBranching,
+ editUserMessagePreserveResponses,
+ navigateToSibling,
regenerateMessageWithBranching
} from '$lib/stores/chat.svelte';
import { getMessageSiblings } from '$lib/utils/branching';
refreshAllMessages();
}
+
+ async function handleContinueAssistantMessage(message: DatabaseMessage) {
+ onUserAction?.();
+
+ await continueAssistantMessage(message.id);
+
+ refreshAllMessages();
+ }
+
+ async function handleEditUserMessagePreserveResponses(
+ message: DatabaseMessage,
+ newContent: string
+ ) {
+ onUserAction?.();
+
+ await editUserMessagePreserveResponses(message.id, newContent);
+
+ refreshAllMessages();
+ }
+
async function handleDeleteMessage(message: DatabaseMessage) {
await deleteMessage(message.id);
onNavigateToSibling={handleNavigateToSibling}
onEditWithBranching={handleEditWithBranching}
onEditWithReplacement={handleEditWithReplacement}
+ onEditUserMessagePreserveResponses={handleEditUserMessagePreserveResponses}
onRegenerateWithBranching={handleRegenerateWithBranching}
+ onContinueAssistantMessage={handleContinueAssistantMessage}
/>
{/each}
</div>
{ value: 'dark', label: 'Dark', icon: Moon }
]
},
+ {
+ key: 'pasteLongTextToFileLen',
+ label: 'Paste long text to file length',
+ type: 'input'
+ },
{
key: 'showMessageStats',
label: 'Show message generation statistics',
type: 'checkbox'
},
{
- key: 'askForTitleConfirmation',
- label: 'Ask for confirmation before changing conversation title',
+ key: 'showModelInfo',
+ label: 'Show model information',
type: 'checkbox'
},
{
- key: 'pasteLongTextToFileLen',
- label: 'Paste long text to file length',
- type: 'input'
+ key: 'enableContinueGeneration',
+ label: 'Enable "Continue" button',
+ type: 'checkbox',
+ isExperimental: true
},
{
key: 'pdfAsImage',
type: 'checkbox'
},
{
- key: 'showModelInfo',
- label: 'Show model information',
+ key: 'renderUserContentAsMarkdown',
+ label: 'Render user content as Markdown',
type: 'checkbox'
},
{
- key: 'renderUserContentAsMarkdown',
- label: 'Render user content as Markdown',
+ key: 'askForTitleConfirmation',
+ label: 'Ask for confirmation before changing conversation title',
type: 'checkbox'
}
]
<script lang="ts">
- import { RotateCcw } from '@lucide/svelte';
+ import { RotateCcw, FlaskConical } from '@lucide/svelte';
import { Checkbox } from '$lib/components/ui/checkbox';
import { Input } from '$lib/components/ui/input';
import Label from '$lib/components/ui/label/label.svelte';
})()}
<div class="flex items-center gap-2">
- <Label for={field.key} class="text-sm font-medium">
+ <Label for={field.key} class="flex items-center gap-1.5 text-sm font-medium">
{field.label}
+
+ {#if field.isExperimental}
+ <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
+ {/if}
</Label>
{#if isCustomRealTime}
<ParameterSourceIndicator />
</p>
{/if}
{:else if field.type === 'textarea'}
- <Label for={field.key} class="block text-sm font-medium">
+ <Label for={field.key} class="block flex items-center gap-1.5 text-sm font-medium">
{field.label}
+
+ {#if field.isExperimental}
+ <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
+ {/if}
</Label>
<Textarea
})()}
<div class="flex items-center gap-2">
- <Label for={field.key} class="text-sm font-medium">
+ <Label for={field.key} class="flex items-center gap-1.5 text-sm font-medium">
{field.label}
+
+ {#if field.isExperimental}
+ <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
+ {/if}
</Label>
{#if isCustomRealTime}
<ParameterSourceIndicator />
for={field.key}
class="cursor-pointer text-sm leading-none font-medium {isDisabled
? 'text-muted-foreground'
- : ''}"
+ : ''} flex items-center gap-1.5"
>
{field.label}
+
+ {#if field.isExperimental}
+ <FlaskConical class="h-3.5 w-3.5 text-muted-foreground" />
+ {/if}
</label>
{#if field.help || SETTING_CONFIG_INFO[field.key]}
max_tokens: -1,
custom: '', // custom json-stringified object
// experimental features
- pyInterpreterEnabled: false
+ pyInterpreterEnabled: false,
+ enableContinueGeneration: false
};
export const SETTING_CONFIG_INFO: Record<string, string> = {
modelSelectorEnabled:
'Enable the model selector in the chat input to choose the inference model. Sends the associated model field in API requests.',
pyInterpreterEnabled:
- 'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.'
+ 'Enable Python interpreter using Pyodide. Allows running Python code in markdown code blocks.',
+ enableContinueGeneration:
+ 'Enable "Continue" button for assistant messages. Currently works only with non-reasoning models.'
};
let aggregatedContent = '';
let fullReasoningContent = '';
let aggregatedToolCalls: ApiChatCompletionToolCall[] = [];
- let hasReceivedData = false;
let lastTimings: ChatMessageTimings | undefined;
let streamFinished = false;
let modelEmitted = false;
return;
}
- hasReceivedData = true;
-
if (!abortSignal?.aborted) {
onToolCallChunk?.(serializedToolCalls);
}
if (content) {
finalizeOpenToolCallBatch();
- hasReceivedData = true;
aggregatedContent += content;
if (!abortSignal?.aborted) {
onChunk?.(content);
if (reasoningContent) {
finalizeOpenToolCallBatch();
- hasReceivedData = true;
fullReasoningContent += reasoningContent;
if (!abortSignal?.aborted) {
onReasoningChunk?.(reasoningContent);
if (streamFinished) {
finalizeOpenToolCallBatch();
- if (
- !hasReceivedData &&
- aggregatedContent.length === 0 &&
- aggregatedToolCalls.length === 0
- ) {
- const noResponseError = new Error('No response received from server. Please try again.');
- throw noResponseError;
- }
-
const finalToolCalls =
aggregatedToolCalls.length > 0 ? JSON.stringify(aggregatedToolCalls) : undefined;
timestamp: Date.now()
});
+ // Ensure currNode points to the edited message to maintain correct path
+ await DatabaseStore.updateCurrentNode(this.activeConversation.id, messageToEdit.id);
+ this.activeConversation.currNode = messageToEdit.id;
+
this.updateMessageAtIndex(messageIndex, {
content: newContent,
timestamp: Date.now()
}
}
+ /**
+ * Edits a user message and preserves all responses below
+ * Updates the message content in-place without deleting or regenerating responses
+ *
+ * **Use Case**: When you want to fix a typo or rephrase a question without losing the assistant's response
+ *
+ * **Important Behavior:**
+ * - Does NOT create a branch (unlike editMessageWithBranching)
+ * - Does NOT regenerate assistant responses
+ * - Only updates the user message content in the database
+ * - Preserves the entire conversation tree below the edited message
+ * - Updates conversation title if this is the first user message
+ *
+ * @param messageId - The ID of the user message to edit
+ * @param newContent - The new content for the message
+ */
+ async editUserMessagePreserveResponses(messageId: string, newContent: string): Promise<void> {
+ if (!this.activeConversation) return;
+
+ try {
+ const messageIndex = this.findMessageIndex(messageId);
+ if (messageIndex === -1) {
+ console.error('Message not found for editing');
+ return;
+ }
+
+ const messageToEdit = this.activeMessages[messageIndex];
+ if (messageToEdit.role !== 'user') {
+ console.error('Only user messages can be edited with this method');
+ return;
+ }
+
+ // Simply update the message content in-place
+ await DatabaseStore.updateMessage(messageId, {
+ content: newContent,
+ timestamp: Date.now()
+ });
+
+ this.updateMessageAtIndex(messageIndex, {
+ content: newContent,
+ timestamp: Date.now()
+ });
+
+ // Check if first user message for title update
+ const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
+ const rootMessage = allMessages.find((m) => m.type === 'root' && m.parent === null);
+ const isFirstUserMessage =
+ rootMessage && messageToEdit.parent === rootMessage.id && messageToEdit.role === 'user';
+
+ if (isFirstUserMessage && newContent.trim()) {
+ await this.updateConversationTitleWithConfirmation(
+ this.activeConversation.id,
+ newContent.trim(),
+ this.titleUpdateConfirmationCallback
+ );
+ }
+
+ this.updateConversationTimestamp();
+ } catch (error) {
+ console.error('Failed to edit user message:', error);
+ }
+ }
+
/**
* Edits a message by creating a new branch with the edited content
* @param messageId - The ID of the message to edit
}
}
+ /**
+ * Continues generation for an existing assistant message
+ * @param messageId - The ID of the assistant message to continue
+ */
+ async continueAssistantMessage(messageId: string): Promise<void> {
+ if (!this.activeConversation || this.isLoading) return;
+
+ try {
+ const messageIndex = this.findMessageIndex(messageId);
+ if (messageIndex === -1) {
+ console.error('Message not found for continuation');
+ return;
+ }
+
+ const messageToContinue = this.activeMessages[messageIndex];
+ if (messageToContinue.role !== 'assistant') {
+ console.error('Only assistant messages can be continued');
+ return;
+ }
+
+ // Race condition protection: Check if this specific conversation is already loading
+ // This prevents multiple rapid clicks on "Continue" from creating concurrent operations
+ if (this.isConversationLoading(this.activeConversation.id)) {
+ console.warn('Continuation already in progress for this conversation');
+ return;
+ }
+
+ this.errorDialogState = null;
+ this.setConversationLoading(this.activeConversation.id, true);
+ this.clearConversationStreaming(this.activeConversation.id);
+
+ // IMPORTANT: Fetch the latest content from the database to ensure we have
+ // the most up-to-date content, especially after a stopped generation
+ // This prevents issues where the in-memory state might be stale
+ const allMessages = await DatabaseStore.getConversationMessages(this.activeConversation.id);
+ const dbMessage = allMessages.find((m) => m.id === messageId);
+
+ if (!dbMessage) {
+ console.error('Message not found in database for continuation');
+ this.setConversationLoading(this.activeConversation.id, false);
+
+ return;
+ }
+
+ // Use content from database as the source of truth
+ const originalContent = dbMessage.content;
+ const originalThinking = dbMessage.thinking || '';
+
+ // Get conversation context up to (but not including) the message to continue
+ const conversationContext = this.activeMessages.slice(0, messageIndex);
+
+ const contextWithContinue = [
+ ...conversationContext.map((msg) => {
+ if ('id' in msg && 'convId' in msg && 'timestamp' in msg) {
+ return msg as DatabaseMessage & { extra?: DatabaseMessageExtra[] };
+ }
+ return msg as ApiChatMessageData;
+ }),
+ {
+ role: 'assistant' as const,
+ content: originalContent
+ }
+ ];
+
+ let appendedContent = '';
+ let appendedThinking = '';
+ let hasReceivedContent = false;
+
+ await chatService.sendMessage(
+ contextWithContinue,
+ {
+ ...this.getApiOptions(),
+
+ onChunk: (chunk: string) => {
+ hasReceivedContent = true;
+ appendedContent += chunk;
+ // Preserve originalContent exactly as-is, including any trailing whitespace
+ // The concatenation naturally preserves any whitespace at the end of originalContent
+ const fullContent = originalContent + appendedContent;
+
+ this.setConversationStreaming(
+ messageToContinue.convId,
+ fullContent,
+ messageToContinue.id
+ );
+
+ this.updateMessageAtIndex(messageIndex, {
+ content: fullContent
+ });
+ },
+
+ onReasoningChunk: (reasoningChunk: string) => {
+ hasReceivedContent = true;
+ appendedThinking += reasoningChunk;
+
+ const fullThinking = originalThinking + appendedThinking;
+
+ this.updateMessageAtIndex(messageIndex, {
+ thinking: fullThinking
+ });
+ },
+
+ onComplete: async (
+ finalContent?: string,
+ reasoningContent?: string,
+ timings?: ChatMessageTimings
+ ) => {
+ const fullContent = originalContent + (finalContent || appendedContent);
+ const fullThinking = originalThinking + (reasoningContent || appendedThinking);
+
+ const updateData: {
+ content: string;
+ thinking: string;
+ timestamp: number;
+ timings?: ChatMessageTimings;
+ } = {
+ content: fullContent,
+ thinking: fullThinking,
+ timestamp: Date.now(),
+ timings: timings
+ };
+
+ await DatabaseStore.updateMessage(messageToContinue.id, updateData);
+
+ this.updateMessageAtIndex(messageIndex, updateData);
+
+ this.updateConversationTimestamp();
+
+ this.setConversationLoading(messageToContinue.convId, false);
+ this.clearConversationStreaming(messageToContinue.convId);
+ slotsService.clearConversationState(messageToContinue.convId);
+ },
+
+ onError: async (error: Error) => {
+ if (this.isAbortError(error)) {
+ // User cancelled - save partial continuation if any content was received
+ if (hasReceivedContent && appendedContent) {
+ const partialContent = originalContent + appendedContent;
+ const partialThinking = originalThinking + appendedThinking;
+
+ await DatabaseStore.updateMessage(messageToContinue.id, {
+ content: partialContent,
+ thinking: partialThinking,
+ timestamp: Date.now()
+ });
+
+ this.updateMessageAtIndex(messageIndex, {
+ content: partialContent,
+ thinking: partialThinking,
+ timestamp: Date.now()
+ });
+ }
+
+ this.setConversationLoading(messageToContinue.convId, false);
+ this.clearConversationStreaming(messageToContinue.convId);
+ slotsService.clearConversationState(messageToContinue.convId);
+
+ return;
+ }
+
+ // Non-abort error - rollback to original content
+ console.error('Continue generation error:', error);
+
+ // Rollback: Restore original content in UI
+ this.updateMessageAtIndex(messageIndex, {
+ content: originalContent,
+ thinking: originalThinking
+ });
+
+ // Ensure database has original content (in case of partial writes)
+ await DatabaseStore.updateMessage(messageToContinue.id, {
+ content: originalContent,
+ thinking: originalThinking
+ });
+
+ this.setConversationLoading(messageToContinue.convId, false);
+ this.clearConversationStreaming(messageToContinue.convId);
+ slotsService.clearConversationState(messageToContinue.convId);
+
+ const dialogType = error.name === 'TimeoutError' ? 'timeout' : 'server';
+ this.showErrorDialog(dialogType, error.message);
+ }
+ },
+ messageToContinue.convId
+ );
+ } catch (error) {
+ if (this.isAbortError(error)) return;
+ console.error('Failed to continue message:', error);
+ if (this.activeConversation) {
+ this.setConversationLoading(this.activeConversation.id, false);
+ }
+ }
+ }
+
/**
* Public methods for accessing per-conversation states
*/
export const navigateToSibling = chatStore.navigateToSibling.bind(chatStore);
export const editAssistantMessage = chatStore.editAssistantMessage.bind(chatStore);
export const editMessageWithBranching = chatStore.editMessageWithBranching.bind(chatStore);
+export const editUserMessagePreserveResponses =
+ chatStore.editUserMessagePreserveResponses.bind(chatStore);
export const regenerateMessageWithBranching =
chatStore.regenerateMessageWithBranching.bind(chatStore);
+export const continueAssistantMessage = chatStore.continueAssistantMessage.bind(chatStore);
export const deleteMessage = chatStore.deleteMessage.bind(chatStore);
export const getDeletionInfo = chatStore.getDeletionInfo.bind(chatStore);
export const updateConversationName = chatStore.updateConversationName.bind(chatStore);
key: string;
label: string;
type: 'input' | 'textarea' | 'checkbox' | 'select';
+ isExperimental?: boolean;
help?: string;
options?: Array<{ value: string; label: string; icon?: typeof import('@lucide/svelte').Icon }>;
}