refactor: Cleanup

This commit is contained in:
Aleksander Grygier 2026-01-08 12:46:10 +01:00
parent b0ba550928
commit 223c6333e9
24 changed files with 147 additions and 104 deletions

View File

@ -5,7 +5,7 @@ import type {
} from '$lib/types/api';
import type { ChatMessagePromptProgress, ChatMessageTimings } from '$lib/types/chat';
import { mergeToolCallDeltas, extractModelName } from '$lib/utils/chat-stream';
import type { AgenticChatCompletionRequest } from './types';
import type { AgenticChatCompletionRequest } from '$lib/types/agentic';
export type OpenAISseCallbacks = {
onChunk?: (chunk: string) => void;

View File

@ -14,7 +14,7 @@
} from '$lib/components/app';
import { config } from '$lib/stores/settings.svelte';
import { Wrench, Loader2 } from '@lucide/svelte';
import { AgenticSectionType } from '$lib/types/agentic';
import { AgenticSectionType } from '$lib/enums';
import { AGENTIC_TAGS, AGENTIC_REGEX } from '$lib/constants/agentic';
import { formatJsonPretty } from '$lib/utils/formatters';
@ -38,16 +38,20 @@
const showToolCallInProgress = $derived(config().showToolCallInProgress as boolean);
function isExpanded(index: number, isPending: boolean): boolean {
if (showToolCallInProgress && isPending) {
return true;
}
function getDefaultExpanded(isPending: boolean): boolean {
return showToolCallInProgress && isPending;
}
return expandedStates[index] ?? showToolCallInProgress;
function isExpanded(index: number, isPending: boolean): boolean {
if (expandedStates[index] !== undefined) {
return expandedStates[index];
}
return getDefaultExpanded(isPending);
}
function toggleExpanded(index: number, isPending: boolean) {
expandedStates[index] = !isExpanded(index, isPending);
const currentState = isExpanded(index, isPending);
expandedStates[index] = !currentState;
}
function parseAgenticContent(rawContent: string): AgenticSection[] {

View File

@ -10,6 +10,7 @@
import { DatabaseService } from '$lib/services';
import { config } from '$lib/stores/settings.svelte';
import { SYSTEM_MESSAGE_PLACEHOLDER } from '$lib/constants/ui';
import { MessageRole } from '$lib/enums';
import { copyToClipboard, isIMEComposing, formatMessageForClipboard } from '$lib/utils';
import ChatMessageAssistant from './ChatMessageAssistant.svelte';
import ChatMessageUser from './ChatMessageUser.svelte';
@ -70,7 +71,7 @@
let textareaElement: HTMLTextAreaElement | undefined = $state();
let thinkingContent = $derived.by(() => {
if (message.role === 'assistant') {
if (message.role === MessageRole.ASSISTANT) {
const trimmedThinking = message.thinking?.trim();
return trimmedThinking ? trimmedThinking : null;
@ -92,7 +93,7 @@
isEditing = false;
// If canceling a new system message with placeholder content, remove it without deleting children
if (message.role === 'system') {
if (message.role === MessageRole.SYSTEM) {
const conversationDeleted = await removeSystemPromptPlaceholder(message.id);
if (conversationDeleted) {
@ -136,7 +137,7 @@
isEditing = true;
// Clear temporary placeholder content for system messages
editedContent =
message.role === 'system' && message.content === SYSTEM_MESSAGE_PLACEHOLDER
message.role === MessageRole.SYSTEM && message.content === SYSTEM_MESSAGE_PLACEHOLDER
? ''
: message.content;
textareaElement?.focus();
@ -179,7 +180,7 @@
}
async function handleSaveEdit() {
if (message.role === 'system') {
if (message.role === MessageRole.SYSTEM) {
// System messages: update in place without branching
const newContent = editedContent.trim();
@ -198,7 +199,7 @@
if (index !== -1) {
conversationsStore.updateMessageAtIndex(index, { content: newContent });
}
} else if (message.role === 'user') {
} else if (message.role === MessageRole.USER) {
const finalExtras = await getMergedExtras();
onEditWithBranching?.(message, editedContent.trim(), finalExtras);
} else {
@ -213,7 +214,7 @@
}
async function handleSaveEditOnly() {
if (message.role === 'user') {
if (message.role === MessageRole.USER) {
// For user messages, trim to avoid accidental whitespace
const finalExtras = await getMergedExtras();
onEditUserMessagePreserveResponses?.(message, editedContent.trim(), finalExtras);
@ -240,7 +241,7 @@
}
</script>
{#if message.role === 'system'}
{#if message.role === MessageRole.SYSTEM}
<ChatMessageSystem
bind:textareaElement
class={className}
@ -261,7 +262,7 @@
{showDeleteDialog}
{siblingInfo}
/>
{:else if message.role === 'user'}
{:else if message.role === MessageRole.USER}
<ChatMessageUser
bind:textareaElement
class={className}

View File

@ -6,9 +6,10 @@
DialogConfirmation
} from '$lib/components/app';
import { Switch } from '$lib/components/ui/switch';
import { MessageRole } from '$lib/enums';
interface Props {
role: 'user' | 'assistant';
role: MessageRole.USER | MessageRole.ASSISTANT;
justify: 'start' | 'end';
actionsPosition: 'left' | 'right';
siblingInfo?: ChatMessageSiblingInfo | null;
@ -77,11 +78,11 @@
<ActionButton icon={Edit} tooltip="Edit" onclick={onEdit} />
{/if}
{#if role === 'assistant' && onRegenerate}
{#if role === MessageRole.ASSISTANT && onRegenerate}
<ActionButton icon={RefreshCw} tooltip="Regenerate" onclick={() => onRegenerate()} />
{/if}
{#if role === 'assistant' && onContinue}
{#if role === MessageRole.ASSISTANT && onContinue}
<ActionButton icon={ArrowRight} tooltip="Continue" onclick={onContinue} />
{/if}

View File

@ -17,6 +17,7 @@
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { INPUT_CLASSES } from '$lib/constants/css-classes';
import { MessageRole } from '$lib/enums';
import Label from '$lib/components/ui/label/label.svelte';
import { config } from '$lib/stores/settings.svelte';
import { conversationsStore } from '$lib/stores/conversations.svelte';
@ -245,7 +246,7 @@
{#if message.timestamp && !isEditing}
<ChatMessageActions
role="assistant"
role={MessageRole.ASSISTANT}
justify="start"
actionsPosition="left"
{siblingInfo}

View File

@ -6,6 +6,7 @@
import { INPUT_CLASSES } from '$lib/constants/css-classes';
import { config } from '$lib/stores/settings.svelte';
import ChatMessageActions from './ChatMessageActions.svelte';
import { MessageRole } from '$lib/enums';
interface Props {
class?: string;
@ -211,7 +212,7 @@
{onShowDeleteDialogChange}
{siblingInfo}
{showDeleteDialog}
role="user"
role={MessageRole.USER}
/>
</div>
{/if}

View File

@ -4,6 +4,7 @@
import { config } from '$lib/stores/settings.svelte';
import ChatMessageActions from './ChatMessageActions.svelte';
import ChatMessageEditForm from './ChatMessageEditForm.svelte';
import { MessageRole } from '$lib/enums';
interface Props {
class?: string;
@ -156,7 +157,7 @@
{onShowDeleteDialogChange}
{siblingInfo}
{showDeleteDialog}
role="user"
role={MessageRole.USER}
/>
</div>
{/if}

View File

@ -1,9 +1,8 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import McpSettingsSection from '../mcp/McpSettingsSection.svelte';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { Button } from '$lib/components/ui/button';
import McpLogo from '../misc/McpLogo.svelte';
import { McpLogo, McpSettingsSection } from '$lib/components/app';
interface Props {
onOpenChange?: (open: boolean) => void;

View File

@ -60,7 +60,7 @@ export { default as ActionButton } from './misc/ActionButton.svelte';
export { default as ActionDropdown } from './misc/ActionDropdown.svelte';
export { default as BadgeChatStatistic } from './misc/BadgeChatStatistic.svelte';
export { default as BadgeInfo } from './misc/BadgeInfo.svelte';
export { default as ModelBadge } from './models/ModelBadge.svelte';
export { default as McpLogo } from './misc/McpLogo.svelte';
export { default as BadgeModality } from './misc/BadgeModality.svelte';
export { default as ConversationSelection } from './misc/ConversationSelection.svelte';
export { default as CopyToClipboardIcon } from './misc/CopyToClipboardIcon.svelte';
@ -70,6 +70,10 @@ export { default as RemoveButton } from './misc/RemoveButton.svelte';
export { default as SearchInput } from './misc/SearchInput.svelte';
export { default as SearchableDropdownMenu } from './misc/SearchableDropdownMenu.svelte';
export { default as SyntaxHighlightedCode } from './misc/SyntaxHighlightedCode.svelte';
// Models
export { default as ModelBadge } from './models/ModelBadge.svelte';
export { default as ModelsSelector } from './models/ModelsSelector.svelte';
// MCP

View File

@ -1,13 +1,3 @@
/**
* Agentic orchestration configuration.
*/
export interface AgenticConfig {
enabled: boolean;
maxTurns: number;
maxToolPreviewLines: number;
filterReasoningAfterFirstTurn: boolean;
}
/**
* Types of sections in agentic content display.
*/

View File

@ -2,3 +2,22 @@ export enum ChatMessageStatsView {
GENERATION = 'generation',
READING = 'reading'
}
/**
* Message roles for chat messages.
*/
export enum MessageRole {
USER = 'user',
ASSISTANT = 'assistant',
SYSTEM = 'system'
}
/**
* Message types for different content kinds.
*/
export enum MessageType {
ROOT = 'root',
TEXT = 'text',
THINK = 'think',
SYSTEM = 'system'
}

View File

@ -1,6 +1,8 @@
export { AttachmentType } from './attachment';
export { ChatMessageStatsView } from './chat';
export { AgenticSectionType } from './agentic';
export { ChatMessageStatsView, MessageRole, MessageType } from './chat';
export {
FileTypeCategory,

View File

@ -0,0 +1 @@
export { MCPError } from './mcp';

View File

@ -0,0 +1,14 @@
/**
* MCP-specific error class with error code and optional data.
*/
export class MCPError extends Error {
code: number;
data?: unknown;
constructor(message: string, code: number, data?: unknown) {
super(message);
this.name = 'MCPError';
this.code = code;
this.data = data;
}
}

View File

@ -28,7 +28,7 @@ import type {
ClientCapabilities,
Implementation
} from '$lib/types/mcp';
import { MCPError } from '$lib/types/mcp';
import { MCPError } from '$lib/errors';
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
export interface MCPHostManagerConfig {

View File

@ -8,8 +8,10 @@ export type {
ToolExecutionResult
} from './server-connection';
// Errors
export { MCPError } from '$lib/errors';
// Types
export { MCPError } from '$lib/types/mcp';
export type {
MCPClientConfig,
MCPServerConfig,

View File

@ -23,7 +23,7 @@ import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/webso
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import type { MCPServerConfig, ClientCapabilities, Implementation } from '$lib/types/mcp';
import { MCPError } from '$lib/types/mcp';
import { MCPError } from '$lib/errors';
import { DEFAULT_MCP_CONFIG } from '$lib/constants/mcp';
// Type for tool call result content item

View File

@ -21,13 +21,13 @@
*/
import { mcpStore } from '$lib/stores/mcp.svelte';
import { OpenAISseClient, type OpenAISseTurnResult } from '$lib/agentic/openai-sse-client';
import {
toAgenticMessages,
type AgenticMessage,
type AgenticChatCompletionRequest,
type AgenticToolCallList
} from '$lib/agentic/types';
import { OpenAISseClient, type OpenAISseTurnResult } from '$lib/clients/openai-sse';
import type {
AgenticMessage,
AgenticChatCompletionRequest,
AgenticToolCallList
} from '$lib/types/agentic';
import { toAgenticMessages } from '$lib/utils';
import type { ApiChatCompletionToolCall, ApiChatMessageData } from '$lib/types/api';
import type { ChatMessagePromptProgress, ChatMessageTimings } from '$lib/types/chat';
import type { MCPToolCall } from '$lib/types/mcp';

View File

@ -0,0 +1,51 @@
import type { ApiChatCompletionRequest, ApiChatMessageContentPart } from './api';
/**
* Agentic orchestration configuration.
*/
export interface AgenticConfig {
enabled: boolean;
maxTurns: number;
maxToolPreviewLines: number;
filterReasoningAfterFirstTurn: boolean;
}
/**
* Tool call payload for agentic messages.
*/
export type AgenticToolCallPayload = {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
};
/**
* Agentic message types for different roles.
*/
export type AgenticMessage =
| {
role: 'system' | 'user';
content: string | ApiChatMessageContentPart[];
}
| {
role: 'assistant';
content?: string | ApiChatMessageContentPart[];
tool_calls?: AgenticToolCallPayload[];
}
| {
role: 'tool';
tool_call_id: string;
content: string;
};
export type AgenticAssistantMessage = Extract<AgenticMessage, { role: 'assistant' }>;
export type AgenticToolCallList = NonNullable<AgenticAssistantMessage['tool_calls']>;
export type AgenticChatCompletionRequest = Omit<ApiChatCompletionRequest, 'messages'> & {
messages: AgenticMessage[];
stream: true;
tools?: ApiChatCompletionRequest['tools'];
};

View File

@ -1,6 +1,3 @@
export type ChatMessageType = 'root' | 'text' | 'think' | 'system';
export type ChatRole = 'user' | 'assistant' | 'system';
export interface ChatUploadedFile {
id: string;
name: string;

View File

@ -32,17 +32,15 @@ export type {
ApiRouterModelsUnloadResponse
} from './api';
// Chat types
// Chat types - interfaces only (enums are in $lib/enums)
export type {
ChatMessageType,
ChatRole,
ChatUploadedFile,
ChatAttachmentDisplayItem,
ChatAttachmentPreviewItem,
ChatMessageSiblingInfo,
ChatMessagePromptProgress,
ChatMessageTimings
} from './chat';
} from './chat.d';
// Database types
export type {

View File

@ -10,18 +10,6 @@ export type { Tool, CallToolResult };
export type ClientCapabilities = SDKClientCapabilities;
export type Implementation = SDKImplementation;
export class MCPError extends Error {
code: number;
data?: unknown;
constructor(message: string, code: number, data?: unknown) {
super(message);
this.name = 'MCPError';
this.code = code;
this.data = data;
}
}
export type MCPTransportType = 'websocket' | 'streamable_http';
export type MCPServerConfig = {

View File

@ -1,43 +1,9 @@
import type {
ApiChatCompletionRequest,
ApiChatMessageContentPart,
ApiChatMessageData
} from '$lib/types/api';
export type AgenticToolCallPayload = {
id: string;
type: 'function';
function: {
name: string;
arguments: string;
};
};
export type AgenticMessage =
| {
role: 'system' | 'user';
content: string | ApiChatMessageContentPart[];
}
| {
role: 'assistant';
content?: string | ApiChatMessageContentPart[];
tool_calls?: AgenticToolCallPayload[];
}
| {
role: 'tool';
tool_call_id: string;
content: string;
};
export type AgenticAssistantMessage = Extract<AgenticMessage, { role: 'assistant' }>;
export type AgenticToolCallList = NonNullable<AgenticAssistantMessage['tool_calls']>;
export type AgenticChatCompletionRequest = Omit<ApiChatCompletionRequest, 'messages'> & {
messages: AgenticMessage[];
stream: true;
tools?: ApiChatCompletionRequest['tools'];
};
import type { ApiChatMessageData } from '$lib/types/api';
import type { AgenticMessage } from '$lib/types/agentic';
/**
* Converts API messages to agentic format.
*/
export function toAgenticMessages(messages: ApiChatMessageData[]): AgenticMessage[] {
return messages.map((message) => {
if (message.role === 'assistant' && message.tool_calls && message.tool_calls.length > 0) {

View File

@ -94,3 +94,6 @@ export { getLanguageFromFilename } from './syntax-highlight-language';
// Text file utilities
export { isTextFileByName, readFileAsText, isLikelyTextFile } from './text-files';
// Agentic utilities
export { toAgenticMessages } from './agentic';