refactor: Cleanup
This commit is contained in:
parent
853f711896
commit
e55ee82f07
|
|
@ -1,14 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { FileText, Database, Image, Code, File } from '@lucide/svelte';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { getResourceIcon } from '$lib/utils';
|
||||
import type { DatabaseMessageExtraMcpResource } from '$lib/types';
|
||||
import {
|
||||
IMAGE_FILE_EXTENSION_REGEX,
|
||||
CODE_FILE_EXTENSION_REGEX,
|
||||
TEXT_FILE_EXTENSION_REGEX
|
||||
} from '$lib/constants/mcp-resource';
|
||||
import { MimeTypePrefix, MimeTypeIncludes, UriPattern } from '$lib/enums';
|
||||
import * as Tooltip from '$lib/components/ui/tooltip';
|
||||
import { ActionIconRemove } from '$lib/components/app';
|
||||
|
||||
|
|
@ -22,34 +16,6 @@
|
|||
|
||||
let { extra, readonly = true, onRemove, onClick, class: className }: Props = $props();
|
||||
|
||||
function getResourceIcon(mimeType?: string, uri?: string) {
|
||||
const mime = mimeType?.toLowerCase() || '';
|
||||
const u = uri?.toLowerCase() || '';
|
||||
|
||||
if (mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) {
|
||||
return Image;
|
||||
}
|
||||
|
||||
if (
|
||||
mime.includes(MimeTypeIncludes.JSON) ||
|
||||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
|
||||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
|
||||
CODE_FILE_EXTENSION_REGEX.test(u)
|
||||
) {
|
||||
return Code;
|
||||
}
|
||||
|
||||
if (mime.includes(MimeTypePrefix.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) {
|
||||
return FileText;
|
||||
}
|
||||
|
||||
if (u.includes(UriPattern.DATABASE_KEYWORD) || u.includes(UriPattern.DATABASE_SCHEME)) {
|
||||
return Database;
|
||||
}
|
||||
|
||||
return File;
|
||||
}
|
||||
|
||||
const ResourceIcon = $derived(getResourceIcon(extra.mimeType, extra.uri));
|
||||
const serverName = $derived(mcpStore.getServerDisplayName(extra.serverName));
|
||||
const favicon = $derived(mcpStore.getServerFavicon(extra.serverName));
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@
|
|||
selectorModelRef?.open();
|
||||
}
|
||||
|
||||
let showMcpDialog = $state(false);
|
||||
let showChatSettingsDialogWithMcpSection = $state(false);
|
||||
|
||||
let hasMcpPromptsSupport = $derived.by(() => {
|
||||
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
|
||||
|
|
@ -189,10 +189,13 @@
|
|||
{onSystemPromptClick}
|
||||
{onMcpPromptClick}
|
||||
{onMcpResourcesClick}
|
||||
onMcpSettingsClick={() => (showMcpDialog = true)}
|
||||
onMcpSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
|
||||
/>
|
||||
|
||||
<McpServersSelector {disabled} onSettingsClick={() => (showMcpDialog = true)} />
|
||||
<McpServersSelector
|
||||
{disabled}
|
||||
onSettingsClick={() => (showChatSettingsDialogWithMcpSection = true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-1.5">
|
||||
|
|
@ -232,7 +235,7 @@
|
|||
</div>
|
||||
|
||||
<DialogChatSettings
|
||||
open={showMcpDialog}
|
||||
onOpenChange={(open) => (showMcpDialog = open)}
|
||||
open={showChatSettingsDialogWithMcpSection}
|
||||
onOpenChange={(open) => (showChatSettingsDialogWithMcpSection = open)}
|
||||
initialSection={SETTINGS_SECTION_TITLES.MCP}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@
|
|||
import { KeyboardKey } from '$lib/enums';
|
||||
import type { MCPPromptInfo, GetPromptResult, MCPServerSettingsEntry } from '$lib/types';
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
import ChatFormPromptPickerList from './ChatFormPromptPickerList.svelte';
|
||||
import ChatFormPromptPickerHeader from './ChatFormPromptPickerHeader.svelte';
|
||||
import ChatFormPromptPickerArgumentForm from './ChatFormPromptPickerArgumentForm.svelte';
|
||||
import {
|
||||
ChatFormPromptPickerList,
|
||||
ChatFormPromptPickerHeader,
|
||||
ChatFormPromptPickerArgumentForm
|
||||
} from '$lib/components/app/chat';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
|
||||
interface Props {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
import { Wrench, Loader2, AlertTriangle, Brain } from '@lucide/svelte';
|
||||
import { AgenticSectionType, AttachmentType, FileTypeText } from '$lib/enums';
|
||||
import { formatJsonPretty } from '$lib/utils';
|
||||
import { ATTACHMENT_SAVED_REGEX } from '$lib/constants/agentic-ui';
|
||||
import { ATTACHMENT_SAVED_REGEX, NEWLINE_SEPARATOR } from '$lib/constants/agentic';
|
||||
import { parseAgenticContent, type AgenticSection } from '$lib/utils/agentic';
|
||||
import type { DatabaseMessage, DatabaseMessageExtraImageFile } from '$lib/types/database';
|
||||
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
toolResult: string,
|
||||
extras?: DatabaseMessage['extra']
|
||||
): ToolResultLine[] {
|
||||
const lines = toolResult.split('\n');
|
||||
const lines = toolResult.split(NEWLINE_SEPARATOR);
|
||||
|
||||
return lines.map((line) => {
|
||||
const match = line.match(ATTACHMENT_SAVED_REGEX);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,13 @@
|
|||
import { Button } from '$lib/components/ui/button';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { SyntaxHighlightedCode, ActionIconCopyToClipboard } from '$lib/components/app';
|
||||
import { getLanguageFromFilename } from '$lib/utils';
|
||||
import { MimeTypePrefix, MimeTypeIncludes } from '$lib/enums';
|
||||
import {
|
||||
getLanguageFromFilename,
|
||||
isCodeResource,
|
||||
isImageResource,
|
||||
downloadResourceContent
|
||||
} from '$lib/utils';
|
||||
import { MimeTypeIncludes, MimeTypeText } from '$lib/enums';
|
||||
import type { DatabaseMessageExtraMcpResource } from '$lib/types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -19,23 +24,6 @@
|
|||
const serverName = $derived(mcpStore.getServerDisplayName(extra.serverName));
|
||||
const favicon = $derived(mcpStore.getServerFavicon(extra.serverName));
|
||||
|
||||
function isCode(mimeType?: string, uri?: string): boolean {
|
||||
const mime = mimeType?.toLowerCase() || '';
|
||||
const u = uri?.toLowerCase() || '';
|
||||
return (
|
||||
mime.includes(MimeTypeIncludes.JSON) ||
|
||||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
|
||||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
|
||||
/\.(js|ts|json|yaml|yml|xml|html|css|py|rs|go|java|cpp|c|h|rb|sh|toml)$/i.test(u)
|
||||
);
|
||||
}
|
||||
|
||||
function isImage(mimeType?: string, uri?: string): boolean {
|
||||
const mime = mimeType?.toLowerCase() || '';
|
||||
const u = uri?.toLowerCase() || '';
|
||||
return mime.startsWith(MimeTypePrefix.IMAGE) || /\.(png|jpg|jpeg|gif|svg|webp)$/i.test(u);
|
||||
}
|
||||
|
||||
function getLanguage(): string {
|
||||
if (extra.mimeType?.includes(MimeTypeIncludes.JSON)) return 'json';
|
||||
if (extra.mimeType?.includes(MimeTypeIncludes.JAVASCRIPT)) return 'javascript';
|
||||
|
|
@ -47,17 +35,11 @@
|
|||
|
||||
function handleDownload() {
|
||||
if (!extra.content) return;
|
||||
|
||||
const blob = new Blob([extra.content], { type: extra.mimeType || 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
|
||||
a.href = url;
|
||||
a.download = extra.name || 'resource.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
downloadResourceContent(
|
||||
extra.content,
|
||||
extra.mimeType || MimeTypeText.PLAIN,
|
||||
extra.name || 'resource.txt'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -113,7 +95,7 @@
|
|||
</div>
|
||||
|
||||
<div class="overflow-auto">
|
||||
{#if isImage(extra.mimeType, extra.uri) && extra.content}
|
||||
{#if isImageResource(extra.mimeType, extra.uri) && extra.content}
|
||||
<div class="flex items-center justify-center">
|
||||
<img
|
||||
src={extra.content.startsWith('data:')
|
||||
|
|
@ -123,7 +105,7 @@
|
|||
class="max-h-[70vh] max-w-full rounded object-contain"
|
||||
/>
|
||||
</div>
|
||||
{:else if isCode(extra.mimeType, extra.uri) && extra.content}
|
||||
{:else if isCodeResource(extra.mimeType, extra.uri) && extra.content}
|
||||
<SyntaxHighlightedCode code={extra.content} language={getLanguage()} maxHeight="70vh" />
|
||||
{:else if extra.content}
|
||||
<pre
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { conversationsStore } from '$lib/stores/conversations.svelte';
|
||||
import { mcpResources, mcpTotalResourceCount } from '$lib/stores/mcp-resources.svelte';
|
||||
import { McpResourceBrowser, McpResourcePreview } from '$lib/components/app';
|
||||
import { getResourceDisplayName } from '$lib/utils';
|
||||
import type { MCPResourceInfo } from '$lib/types';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
|
|
@ -88,20 +89,7 @@
|
|||
lastSelectedUri = resource.uri;
|
||||
}
|
||||
|
||||
function getResourceDisplayName(resource: MCPResourceInfo): string {
|
||||
// Extract the display name from the resource URI (last path segment)
|
||||
try {
|
||||
const uriWithoutProtocol = resource.uri.replace(/^[a-z]+:\/\//, '');
|
||||
const parts = uriWithoutProtocol.split('/');
|
||||
return parts[parts.length - 1] || resource.name || resource.uri;
|
||||
} catch {
|
||||
return resource.name || resource.uri;
|
||||
}
|
||||
}
|
||||
|
||||
function getAllResourcesFlatInTreeOrder(): MCPResourceInfo[] {
|
||||
// Get resources in the same order as displayed in McpResourceBrowser tree
|
||||
// Sort by: folders first (if applicable), then alphabetically by display name
|
||||
const allResources: MCPResourceInfo[] = [];
|
||||
const resourcesMap = mcpResources();
|
||||
|
||||
|
|
@ -111,7 +99,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Sort to match tree display order: folders first, then alphabetically
|
||||
return allResources.sort((a, b) => {
|
||||
const aName = getResourceDisplayName(a);
|
||||
const bName = getResourceDisplayName(b);
|
||||
|
|
@ -119,18 +106,13 @@
|
|||
});
|
||||
}
|
||||
|
||||
function getAllResourcesFlat(): MCPResourceInfo[] {
|
||||
// Fallback for other uses (like attaching)
|
||||
return getAllResourcesFlatInTreeOrder();
|
||||
}
|
||||
|
||||
async function handleAttach() {
|
||||
if (selectedResources.size === 0) return;
|
||||
|
||||
isAttaching = true;
|
||||
|
||||
try {
|
||||
const allResources = getAllResourcesFlat();
|
||||
const allResources = getAllResourcesFlatInTreeOrder();
|
||||
const resourcesToAttach = allResources.filter((r) => selectedResources.has(r.uri));
|
||||
|
||||
for (const resource of resourcesToAttach) {
|
||||
|
|
@ -185,7 +167,7 @@
|
|||
|
||||
<div class="min-w-0 flex-1 overflow-auto p-4">
|
||||
{#if selectedResources.size === 1}
|
||||
{@const allResources = getAllResourcesFlat()}
|
||||
{@const allResources = getAllResourcesFlatInTreeOrder()}
|
||||
{@const selectedResource = allResources.find((r) => selectedResources.has(r.uri))}
|
||||
|
||||
<McpResourcePreview resource={selectedResource ?? null} />
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import { mcpResources, mcpResourcesLoading } from '$lib/stores/mcp-resources.svelte';
|
||||
import type { MCPServerResources, MCPResourceInfo } from '$lib/types';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import { parseResourcePath } from './mcp-resource-browser';
|
||||
import { parseResourcePath } from '$lib/utils';
|
||||
import McpResourceBrowserHeader from './McpResourceBrowserHeader.svelte';
|
||||
import McpResourceBrowserEmptyState from './McpResourceBrowserEmptyState.svelte';
|
||||
import McpResourceBrowserServerItem from './McpResourceBrowserServerItem.svelte';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { RefreshCw, Loader2 } from '@lucide/svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import SearchInput from '../../forms/SearchInput.svelte';
|
||||
import { SearchInput } from '$lib/components/app/forms';
|
||||
|
||||
interface Props {
|
||||
isLoading: boolean;
|
||||
|
|
|
|||
|
|
@ -10,10 +10,9 @@
|
|||
type ResourceTreeNode,
|
||||
buildResourceTree,
|
||||
countTreeResources,
|
||||
getDisplayName,
|
||||
getResourceIcon,
|
||||
sortTreeChildren
|
||||
} from './mcp-resource-browser';
|
||||
import { getDisplayName, getResourceIcon } from '$lib/utils';
|
||||
|
||||
interface Props {
|
||||
serverName: string;
|
||||
|
|
@ -93,7 +92,7 @@
|
|||
</Collapsible.Root>
|
||||
{:else if node.resource}
|
||||
{@const resource = node.resource}
|
||||
{@const ResourceIcon = getResourceIcon(resource)}
|
||||
{@const ResourceIcon = getResourceIcon(resource.mimeType, resource.uri)}
|
||||
{@const isSelected = isResourceSelected(resource)}
|
||||
{@const resourceDisplayName = resource.title || getDisplayName(node.name)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Database, File, FileText, Image, Code } from '@lucide/svelte';
|
||||
import type { MCPResource, MCPResourceInfo } from '$lib/types';
|
||||
import { parseResourcePath } from '$lib/utils';
|
||||
|
||||
export interface ResourceTreeNode {
|
||||
name: string;
|
||||
|
|
@ -8,25 +8,6 @@ export interface ResourceTreeNode {
|
|||
isFiltered?: boolean;
|
||||
}
|
||||
|
||||
export function parseResourcePath(uri: string): string[] {
|
||||
try {
|
||||
const withoutProtocol = uri.replace(/^[a-z]+:\/\//, '');
|
||||
|
||||
return withoutProtocol.split('/').filter((p) => p.length > 0);
|
||||
} catch {
|
||||
return [uri];
|
||||
}
|
||||
}
|
||||
|
||||
export function getDisplayName(pathPart: string): string {
|
||||
const withoutExt = pathPart.replace(/\.[^.]+$/, '');
|
||||
|
||||
return withoutExt
|
||||
.split(/[-_]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function resourceMatchesSearch(resource: MCPResource, query: string): boolean {
|
||||
return (
|
||||
resource.title?.toLowerCase().includes(query) || resource.uri.toLowerCase().includes(query)
|
||||
|
|
@ -124,34 +105,6 @@ export function countTreeResources(node: ResourceTreeNode): number {
|
|||
return count;
|
||||
}
|
||||
|
||||
export function getResourceIcon(resource: MCPResourceInfo) {
|
||||
const mimeType = resource.mimeType?.toLowerCase() || '';
|
||||
const uri = resource.uri.toLowerCase();
|
||||
|
||||
if (mimeType.startsWith('image/') || /\.(png|jpg|jpeg|gif|svg|webp)$/.test(uri)) {
|
||||
return Image;
|
||||
}
|
||||
|
||||
if (
|
||||
mimeType.includes('json') ||
|
||||
mimeType.includes('javascript') ||
|
||||
mimeType.includes('typescript') ||
|
||||
/\.(js|ts|json|yaml|yml|xml|html|css)$/.test(uri)
|
||||
) {
|
||||
return Code;
|
||||
}
|
||||
|
||||
if (mimeType.includes('text') || /\.(txt|md|log)$/.test(uri)) {
|
||||
return FileText;
|
||||
}
|
||||
|
||||
if (uri.includes('database') || uri.includes('db://')) {
|
||||
return Database;
|
||||
}
|
||||
|
||||
return File;
|
||||
}
|
||||
|
||||
export function sortTreeChildren(children: ResourceTreeNode[]): ResourceTreeNode[] {
|
||||
return children.sort((a, b) => {
|
||||
const aIsFolder = !a.resource && a.children.size > 0;
|
||||
|
|
|
|||
|
|
@ -3,8 +3,14 @@
|
|||
import { Button } from '$lib/components/ui/button';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { mcpStore } from '$lib/stores/mcp.svelte';
|
||||
import { isImageMimeType, createBase64DataUrl } from '$lib/utils';
|
||||
import { MimeTypeApplication } from '$lib/enums';
|
||||
import {
|
||||
isImageMimeType,
|
||||
createBase64DataUrl,
|
||||
getResourceTextContent,
|
||||
getResourceBlobContent,
|
||||
downloadResourceContent
|
||||
} from '$lib/utils';
|
||||
import { MimeTypeApplication, MimeTypeText } from '$lib/enums';
|
||||
import { ActionIconCopyToClipboard } from '$lib/components/app';
|
||||
import type { MCPResourceInfo, MCPResourceContent } from '$lib/types';
|
||||
|
||||
|
|
@ -46,34 +52,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
function getTextContent(): string {
|
||||
if (!content) return '';
|
||||
return content
|
||||
.filter((c): c is { uri: string; mimeType?: string; text: string } => 'text' in c)
|
||||
.map((c) => c.text)
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
function getBlobContent(): Array<{ uri: string; mimeType?: string; blob: string }> {
|
||||
if (!content) return [];
|
||||
return content.filter(
|
||||
(c): c is { uri: string; mimeType?: string; blob: string } => 'blob' in c
|
||||
);
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
const text = getTextContent();
|
||||
const text = getResourceTextContent(content);
|
||||
if (!text || !resource) return;
|
||||
|
||||
const blob = new Blob([text], { type: resource.mimeType || 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = resource.name || 'resource.txt';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
downloadResourceContent(
|
||||
text,
|
||||
resource.mimeType || MimeTypeText.PLAIN,
|
||||
resource.name || 'resource.txt'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -98,8 +84,8 @@
|
|||
|
||||
<div class="flex items-center gap-1">
|
||||
<ActionIconCopyToClipboard
|
||||
text={getTextContent()}
|
||||
canCopy={!isLoading && !!getTextContent()}
|
||||
text={getResourceTextContent(content)}
|
||||
canCopy={!isLoading && !!getResourceTextContent(content)}
|
||||
ariaLabel="Copy content"
|
||||
/>
|
||||
|
||||
|
|
@ -108,7 +94,7 @@
|
|||
size="sm"
|
||||
class="h-7 w-7 p-0"
|
||||
onclick={handleDownload}
|
||||
disabled={isLoading || !getTextContent()}
|
||||
disabled={isLoading || !getResourceTextContent(content)}
|
||||
title="Download content"
|
||||
>
|
||||
<Download class="h-3.5 w-3.5" />
|
||||
|
|
@ -128,8 +114,8 @@
|
|||
<span class="text-sm">{error}</span>
|
||||
</div>
|
||||
{:else if content}
|
||||
{@const textContent = getTextContent()}
|
||||
{@const blobContent = getBlobContent()}
|
||||
{@const textContent = getResourceTextContent(content)}
|
||||
{@const blobContent = getResourceBlobContent(content)}
|
||||
|
||||
{#if textContent}
|
||||
<pre class="font-mono text-xs break-words whitespace-pre-wrap">{textContent}</pre>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { Cable, ExternalLink, Globe, Zap, Radio } from '@lucide/svelte';
|
||||
import { Cable, ExternalLink } from '@lucide/svelte';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import type { MCPServerInfo, MCPCapabilitiesInfo } from '$lib/types';
|
||||
import { MCPTransportType } from '$lib/enums';
|
||||
import { MCP_TRANSPORT_LABELS, MCP_TRANSPORT_ICONS } from '$lib/constants/mcp';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { McpCapabilitiesBadges } from '$lib/components/app/mcp';
|
||||
|
||||
|
|
@ -27,21 +28,6 @@
|
|||
capabilities,
|
||||
transportType
|
||||
}: Props = $props();
|
||||
|
||||
const transportLabels: Record<MCPTransportType, string> = {
|
||||
[MCPTransportType.WEBSOCKET]: 'WebSocket',
|
||||
[MCPTransportType.STREAMABLE_HTTP]: 'HTTP',
|
||||
[MCPTransportType.SSE]: 'SSE'
|
||||
};
|
||||
|
||||
const transportIcons: Record<
|
||||
MCPTransportType,
|
||||
typeof Cable | typeof Zap | typeof Globe | typeof Radio
|
||||
> = {
|
||||
[MCPTransportType.WEBSOCKET]: Zap,
|
||||
[MCPTransportType.STREAMABLE_HTTP]: Globe,
|
||||
[MCPTransportType.SSE]: Radio
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="space-y-3">
|
||||
|
|
@ -87,13 +73,13 @@
|
|||
{#if capabilities || transportType}
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
{#if transportType}
|
||||
{@const TransportIcon = transportIcons[transportType]}
|
||||
{@const TransportIcon = MCP_TRANSPORT_ICONS[transportType]}
|
||||
<Badge variant="outline" class="h-5 gap-1 px-1.5 text-[10px]">
|
||||
{#if TransportIcon}
|
||||
<TransportIcon class="h-3 w-3" />
|
||||
{/if}
|
||||
|
||||
{transportLabels[transportType] || transportType}
|
||||
{MCP_TRANSPORT_LABELS[transportType] || transportType}
|
||||
</Badge>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
import type { AgenticConfig } from '$lib/types/agentic';
|
||||
|
||||
export const ATTACHMENT_SAVED_REGEX = /\[Attachment saved: ([^\]]+)\]/;
|
||||
|
||||
export const NEWLINE_SEPARATOR = '\n';
|
||||
|
||||
export const DEFAULT_AGENTIC_CONFIG: AgenticConfig = {
|
||||
enabled: true,
|
||||
maxTurns: 100,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const FILE_EXTENSION_REGEX = /\.[^.]+$/;
|
|||
*/
|
||||
export const IMAGE_MIME_TO_EXTENSION: Record<string, string> = {
|
||||
[MimeTypeImage.JPEG]: 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
[MimeTypeImage.JPG]: 'jpg',
|
||||
[MimeTypeImage.PNG]: 'png',
|
||||
[MimeTypeImage.GIF]: 'gif',
|
||||
[MimeTypeImage.WEBP]: 'webp'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
import { Zap, Globe, Radio } from '@lucide/svelte';
|
||||
import { MCPTransportType } from '$lib/enums';
|
||||
import type { ClientCapabilities, Implementation } from '$lib/types';
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
export const DEFAULT_MCP_CONFIG = {
|
||||
protocolVersion: '2025-06-18',
|
||||
|
|
@ -15,3 +18,17 @@ export const DEFAULT_IMAGE_MIME_TYPE = 'image/png';
|
|||
export const MCP_RECONNECT_INITIAL_DELAY = 1000;
|
||||
export const MCP_RECONNECT_BACKOFF_MULTIPLIER = 2;
|
||||
export const MCP_RECONNECT_MAX_DELAY = 30000;
|
||||
|
||||
/** Human-readable labels for MCP transport types */
|
||||
export const MCP_TRANSPORT_LABELS: Record<MCPTransportType, string> = {
|
||||
[MCPTransportType.WEBSOCKET]: 'WebSocket',
|
||||
[MCPTransportType.STREAMABLE_HTTP]: 'HTTP',
|
||||
[MCPTransportType.SSE]: 'SSE'
|
||||
};
|
||||
|
||||
/** Icon components for MCP transport types */
|
||||
export const MCP_TRANSPORT_ICONS: Record<MCPTransportType, Component> = {
|
||||
[MCPTransportType.WEBSOCKET]: Zap,
|
||||
[MCPTransportType.STREAMABLE_HTTP]: Globe,
|
||||
[MCPTransportType.SSE]: Radio
|
||||
};
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ export enum MimeTypeAudio {
|
|||
|
||||
export enum MimeTypeImage {
|
||||
JPEG = 'image/jpeg',
|
||||
JPG = 'image/jpg',
|
||||
PNG = 'image/png',
|
||||
GIF = 'image/gif',
|
||||
WEBP = 'image/webp',
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { mcpStore } from '$lib/stores/mcp.svelte';
|
|||
import { modelsStore } from '$lib/stores/models.svelte';
|
||||
import { isAbortError } from '$lib/utils';
|
||||
import { DEFAULT_AGENTIC_CONFIG, AGENTIC_TAGS } from '$lib/constants/agentic';
|
||||
import { IMAGE_MIME_TO_EXTENSION } from '$lib/constants/mcp-resource';
|
||||
import { AttachmentType, ContentPartType, MessageRole } from '$lib/enums';
|
||||
import type {
|
||||
AgenticFlowParams,
|
||||
|
|
@ -658,14 +659,7 @@ class AgenticStore {
|
|||
}
|
||||
|
||||
private buildAttachmentName(mimeType: string, index: number): string {
|
||||
const extensionMap: Record<string, string> = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp'
|
||||
};
|
||||
const extension = extensionMap[mimeType] ?? 'img';
|
||||
const extension = IMAGE_MIME_TO_EXTENSION[mimeType] ?? 'img';
|
||||
return `mcp-attachment-${Date.now()}-${index}.${extension}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,3 +59,5 @@ export interface ParsedClipboardContent {
|
|||
textAttachments: ClipboardTextAttachment[];
|
||||
mcpPromptAttachments: ClipboardMcpPromptAttachment[];
|
||||
}
|
||||
|
||||
export type MimeTypeUnion = MimeTypeAudio | MimeTypeImage | MimeTypeApplication | MimeTypeText;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type {
|
|||
PromptMessage,
|
||||
Transport
|
||||
} from '@modelcontextprotocol/sdk';
|
||||
import type { MimeTypeUnion } from './common';
|
||||
|
||||
export type { Tool, CallToolResult, Prompt, GetPromptResult, PromptMessage };
|
||||
export type ClientCapabilities = SDKClientCapabilities;
|
||||
|
|
@ -37,7 +38,7 @@ export interface MCPServerInfo {
|
|||
title?: string;
|
||||
description?: string;
|
||||
websiteUrl?: string;
|
||||
icons?: Array<{ src: string; mimeType?: string; sizes?: string[] }>;
|
||||
icons?: Array<{ src: string; mimeType?: MimeTypeUnion; sizes?: string[] }>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -281,7 +282,7 @@ export interface MCPResourceAnnotations {
|
|||
*/
|
||||
export interface MCPResourceIcon {
|
||||
src: string;
|
||||
mimeType?: string;
|
||||
mimeType?: MimeTypeUnion;
|
||||
sizes?: string[];
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
|
|
@ -294,7 +295,7 @@ export interface MCPResource {
|
|||
name: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
mimeType?: MimeTypeUnion;
|
||||
annotations?: MCPResourceAnnotations;
|
||||
icons?: MCPResourceIcon[];
|
||||
_meta?: Record<string, unknown>;
|
||||
|
|
@ -308,7 +309,7 @@ export interface MCPResourceTemplate {
|
|||
name: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
mimeType?: MimeTypeUnion;
|
||||
annotations?: MCPResourceAnnotations;
|
||||
icons?: MCPResourceIcon[];
|
||||
_meta?: Record<string, unknown>;
|
||||
|
|
@ -319,7 +320,7 @@ export interface MCPResourceTemplate {
|
|||
*/
|
||||
export interface MCPTextResourceContent {
|
||||
uri: string;
|
||||
mimeType?: string;
|
||||
mimeType?: MimeTypeUnion;
|
||||
text: string;
|
||||
}
|
||||
|
||||
|
|
@ -328,7 +329,7 @@ export interface MCPTextResourceContent {
|
|||
*/
|
||||
export interface MCPBlobResourceContent {
|
||||
uri: string;
|
||||
mimeType?: string;
|
||||
mimeType?: MimeTypeUnion;
|
||||
/** Base64-encoded binary data */
|
||||
blob: string;
|
||||
}
|
||||
|
|
@ -354,7 +355,7 @@ export interface MCPResourceInfo {
|
|||
name: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
mimeType?: MimeTypeUnion;
|
||||
serverName: string;
|
||||
annotations?: MCPResourceAnnotations;
|
||||
icons?: MCPResourceIcon[];
|
||||
|
|
@ -368,7 +369,7 @@ export interface MCPResourceTemplateInfo {
|
|||
name: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
mimeType?: string;
|
||||
mimeType?: MimeTypeUnion;
|
||||
serverName: string;
|
||||
annotations?: MCPResourceAnnotations;
|
||||
icons?: MCPResourceIcon[];
|
||||
|
|
|
|||
|
|
@ -109,7 +109,16 @@ export {
|
|||
parseMcpServerSettings,
|
||||
getMcpLogLevelIcon,
|
||||
getMcpLogLevelClass,
|
||||
isImageMimeType
|
||||
isImageMimeType,
|
||||
parseResourcePath,
|
||||
getDisplayName,
|
||||
getResourceDisplayName,
|
||||
isCodeResource,
|
||||
isImageResource,
|
||||
getResourceIcon,
|
||||
getResourceTextContent,
|
||||
getResourceBlobContent,
|
||||
downloadResourceContent
|
||||
} from './mcp';
|
||||
|
||||
// Data URL utilities
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import {
|
|||
UrlPrefix,
|
||||
MimeTypePrefix,
|
||||
MimeTypeIncludes,
|
||||
UriPattern
|
||||
UriPattern,
|
||||
MimeTypeText
|
||||
} from '$lib/enums';
|
||||
import { DEFAULT_MCP_CONFIG, MCP_SERVER_ID_PREFIX } from '$lib/constants/mcp';
|
||||
import {
|
||||
|
|
@ -15,8 +16,18 @@ import {
|
|||
PROTOCOL_PREFIX_REGEX,
|
||||
FILE_EXTENSION_REGEX
|
||||
} from '$lib/constants/mcp-resource';
|
||||
import { Database, File, FileText, Image, Code, Info, AlertTriangle, XCircle } from '@lucide/svelte';
|
||||
import {
|
||||
Database,
|
||||
File,
|
||||
FileText,
|
||||
Image,
|
||||
Code,
|
||||
Info,
|
||||
AlertTriangle,
|
||||
XCircle
|
||||
} from '@lucide/svelte';
|
||||
import type { Component } from 'svelte';
|
||||
import type { MimeTypeUnion } from '$lib/types/common';
|
||||
|
||||
/**
|
||||
* Detects the MCP transport type from a URL.
|
||||
|
|
@ -119,6 +130,170 @@ export function getMcpLogLevelClass(level: MCPLogLevel): string {
|
|||
* @param mimeType - The MIME type to check
|
||||
* @returns True if the MIME type starts with 'image/'
|
||||
*/
|
||||
export function isImageMimeType(mimeType?: string): boolean {
|
||||
export function isImageMimeType(mimeType?: MimeTypeUnion): boolean {
|
||||
return mimeType?.startsWith(MimeTypePrefix.IMAGE) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a resource URI into path segments, stripping the protocol prefix.
|
||||
*
|
||||
* @param uri - The resource URI to parse
|
||||
* @returns Array of non-empty path segments
|
||||
*/
|
||||
export function parseResourcePath(uri: string): string[] {
|
||||
try {
|
||||
const withoutProtocol = uri.replace(PROTOCOL_PREFIX_REGEX, '');
|
||||
return withoutProtocol.split('/').filter((p) => p.length > 0);
|
||||
} catch {
|
||||
return [uri];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a path part into a human-readable display name.
|
||||
* Strips file extensions and converts kebab-case/snake_case to Title Case.
|
||||
*
|
||||
* @param pathPart - The path segment to convert
|
||||
* @returns Human-readable display name
|
||||
*/
|
||||
export function getDisplayName(pathPart: string): string {
|
||||
const withoutExt = pathPart.replace(FILE_EXTENSION_REGEX, '');
|
||||
return withoutExt
|
||||
.split(/[-_]/)
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the display name from a resource, extracting the last path segment from the URI.
|
||||
*
|
||||
* @param resource - The MCP resource info
|
||||
* @returns Display name string
|
||||
*/
|
||||
export function getResourceDisplayName(resource: MCPResourceInfo): string {
|
||||
try {
|
||||
const parts = parseResourcePath(resource.uri);
|
||||
return parts[parts.length - 1] || resource.name || resource.uri;
|
||||
} catch {
|
||||
return resource.name || resource.uri;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a MIME type and/or URI represents code content.
|
||||
*
|
||||
* @param mimeType - Optional MIME type string
|
||||
* @param uri - Optional URI string
|
||||
* @returns True if the content is code
|
||||
*/
|
||||
export function isCodeResource(mimeType?: MimeTypeUnion, uri?: string): boolean {
|
||||
const mime = mimeType?.toLowerCase() || '';
|
||||
const u = uri?.toLowerCase() || '';
|
||||
return (
|
||||
mime.includes(MimeTypeIncludes.JSON) ||
|
||||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
|
||||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
|
||||
CODE_FILE_EXTENSION_REGEX.test(u)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a MIME type and/or URI represents image content.
|
||||
*
|
||||
* @param mimeType - Optional MIME type string
|
||||
* @param uri - Optional URI string
|
||||
* @returns True if the content is an image
|
||||
*/
|
||||
export function isImageResource(mimeType?: MimeTypeUnion, uri?: string): boolean {
|
||||
const mime = mimeType?.toLowerCase() || '';
|
||||
const u = uri?.toLowerCase() || '';
|
||||
return mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the appropriate Lucide icon component for an MCP resource based on its MIME type and URI.
|
||||
*
|
||||
* @param mimeType - Optional MIME type of the resource
|
||||
* @param uri - Optional URI of the resource
|
||||
* @returns Lucide icon component
|
||||
*/
|
||||
export function getResourceIcon(mimeType?: MimeTypeUnion, uri?: string): Component {
|
||||
const mime = mimeType?.toLowerCase() || '';
|
||||
const u = uri?.toLowerCase() || '';
|
||||
|
||||
if (mime.startsWith(MimeTypePrefix.IMAGE) || IMAGE_FILE_EXTENSION_REGEX.test(u)) {
|
||||
return Image;
|
||||
}
|
||||
|
||||
if (
|
||||
mime.includes(MimeTypeIncludes.JSON) ||
|
||||
mime.includes(MimeTypeIncludes.JAVASCRIPT) ||
|
||||
mime.includes(MimeTypeIncludes.TYPESCRIPT) ||
|
||||
CODE_FILE_EXTENSION_REGEX.test(u)
|
||||
) {
|
||||
return Code;
|
||||
}
|
||||
|
||||
if (mime.includes(MimeTypePrefix.TEXT) || TEXT_FILE_EXTENSION_REGEX.test(u)) {
|
||||
return FileText;
|
||||
}
|
||||
|
||||
if (u.includes(UriPattern.DATABASE_KEYWORD) || u.includes(UriPattern.DATABASE_SCHEME)) {
|
||||
return Database;
|
||||
}
|
||||
|
||||
return File;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from MCP resource content array.
|
||||
*
|
||||
* @param content - Array of MCP resource content items
|
||||
* @returns Joined text content string
|
||||
*/
|
||||
export function getResourceTextContent(content: MCPResourceContent[] | null | undefined): string {
|
||||
if (!content) return '';
|
||||
return content
|
||||
.filter((c): c is { uri: string; mimeType?: MimeTypeUnion; text: string } => 'text' in c)
|
||||
.map((c) => c.text)
|
||||
.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract blob content from MCP resource content array.
|
||||
*
|
||||
* @param content - Array of MCP resource content items
|
||||
* @returns Array of blob content items
|
||||
*/
|
||||
export function getResourceBlobContent(
|
||||
content: MCPResourceContent[] | null | undefined
|
||||
): Array<{ uri: string; mimeType?: MimeTypeUnion; blob: string }> {
|
||||
if (!content) return [];
|
||||
|
||||
return content.filter(
|
||||
(c): c is { uri: string; mimeType?: MimeTypeUnion; blob: string } => 'blob' in c
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a file download from text content.
|
||||
*
|
||||
* @param text - The text content to download
|
||||
* @param mimeType - MIME type for the blob
|
||||
* @param filename - Suggested filename
|
||||
*/
|
||||
export function downloadResourceContent(
|
||||
text: string,
|
||||
mimeType: MimeTypeUnion = MimeTypeText.PLAIN,
|
||||
filename: string = 'resource.txt'
|
||||
): void {
|
||||
const blob = new Blob([text], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue