feat: Integrate Resource Attachments into Chat Form UI

This commit is contained in:
Aleksander Grygier 2026-01-28 18:28:03 +01:00
parent 23e4ef7495
commit aa7089d598
4 changed files with 102 additions and 5 deletions

View File

@ -4,8 +4,10 @@
ChatFormActions,
ChatFormFileInputInvisible,
ChatFormPromptPicker,
ChatFormTextarea
ChatFormTextarea,
McpResourcePicker
} from '$lib/components/app';
import ChatFormResourceAttachments from '../ChatFormResourceAttachments.svelte';
import { INPUT_CLASSES } from '$lib/constants/css-classes';
import { SETTING_CONFIG_DEFAULT } from '$lib/constants/settings-config';
import { MimeTypeText, SpecialFileType } from '$lib/enums';
@ -14,6 +16,7 @@
import { isRouterMode } from '$lib/stores/server.svelte';
import { chatStore } from '$lib/stores/chat.svelte';
import { mcpStore } from '$lib/stores/mcp.svelte';
import { mcpHasResourceAttachments } from '$lib/stores/mcp-resources.svelte';
import { conversationsStore, activeMessages } from '$lib/stores/conversations.svelte';
import type { GetPromptResult, MCPPromptInfo, PromptMessage } from '$lib/types';
import { isIMEComposing, parseClipboardContent } from '$lib/utils';
@ -91,6 +94,10 @@
let isPromptPickerOpen = $state(false);
let promptSearchQuery = $state('');
// Resource Picker State
let isResourcePickerOpen = $state(false);
let preSelectedResourceUri = $state<string | undefined>(undefined);
/**
*
*
@ -489,7 +496,7 @@
onpaste={handlePaste}
>
<ChatFormTextarea
class="px-2 py-1 md:py-0"
class="px-2 py-1.5 md:pt-0"
bind:this={textareaRef}
bind:value
onKeydown={handleKeydown}
@ -501,6 +508,16 @@
{placeholder}
/>
{#if mcpHasResourceAttachments()}
<ChatFormResourceAttachments
class="mb-3"
onResourceClick={(uri) => {
preSelectedResourceUri = uri;
isResourcePickerOpen = true;
}}
/>
{/if}
<ChatFormActions
bind:this={chatFormActionsRef}
canSend={canSubmit}
@ -514,7 +531,18 @@
{onStop}
onSystemPromptClick={() => onSystemPromptClick?.({ message: value, files: uploadedFiles })}
onMcpPromptClick={showMcpPromptButton ? () => (isPromptPickerOpen = true) : undefined}
onMcpResourcesClick={() => (isResourcePickerOpen = true)}
/>
</div>
</div>
</form>
<McpResourcePicker
bind:open={isResourcePickerOpen}
preSelectedUri={preSelectedResourceUri}
onOpenChange={(newOpen) => {
if (!newOpen) {
preSelectedResourceUri = undefined;
}
}}
/>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import { Plus, MessageSquare, Zap } from '@lucide/svelte';
import { Plus, MessageSquare, Zap, FolderOpen } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import * as Tooltip from '$lib/components/ui/tooltip';
@ -14,10 +14,12 @@
hasAudioModality?: boolean;
hasVisionModality?: boolean;
hasMcpPromptsSupport?: boolean;
hasMcpResourcesSupport?: boolean;
onFileUpload?: () => void;
onSystemPromptClick?: () => void;
onMcpPromptClick?: () => void;
onMcpServersClick?: () => void;
onMcpResourcesClick?: () => void;
}
let {
@ -26,10 +28,12 @@
hasAudioModality = false,
hasVisionModality = false,
hasMcpPromptsSupport = false,
hasMcpResourcesSupport = false,
onFileUpload,
onSystemPromptClick,
onMcpPromptClick,
onMcpServersClick
onMcpServersClick,
onMcpResourcesClick
}: Props = $props();
let isNewChat = $derived(!page.params.id);
@ -52,6 +56,11 @@
onMcpServersClick?.();
}
function handleMcpResourcesClick() {
dropdownOpen = false;
onMcpResourcesClick?.();
}
const fileUploadTooltipText = 'Add files, system prompt or MCP Servers';
</script>
@ -195,6 +204,17 @@
<span>MCP Prompt</span>
</DropdownMenu.Item>
{/if}
{#if hasMcpResourcesSupport}
<DropdownMenu.Item
class="flex cursor-pointer items-center gap-2"
onclick={handleMcpResourcesClick}
>
<FolderOpen class="h-4 w-4" />
<span>MCP Resources</span>
</DropdownMenu.Item>
{/if}
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>

View File

@ -31,6 +31,7 @@
onStop?: () => void;
onSystemPromptClick?: () => void;
onMcpPromptClick?: () => void;
onMcpResourcesClick?: () => void;
}
let {
@ -45,7 +46,8 @@
onMicClick,
onStop,
onSystemPromptClick,
onMcpPromptClick
onMcpPromptClick,
onMcpResourcesClick
}: Props = $props();
let currentConfig = $derived(config());
@ -165,6 +167,11 @@
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
return mcpStore.hasEnabledServers(perChatOverrides);
});
let hasMcpResourcesSupport = $derived.by(() => {
const perChatOverrides = conversationsStore.getAllMcpServerOverrides();
return mcpStore.hasEnabledServers(perChatOverrides) && mcpStore.hasResourcesCapability();
});
</script>
<div class="flex w-full items-center gap-3 {className}" style="container-type: inline-size">
@ -174,9 +181,11 @@
{hasAudioModality}
{hasVisionModality}
{hasMcpPromptsSupport}
{hasMcpResourcesSupport}
{onFileUpload}
{onSystemPromptClick}
{onMcpPromptClick}
{onMcpResourcesClick}
onMcpServersClick={() => (showMcpDialog = true)}
/>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { mcpStore } from '$lib/stores/mcp.svelte';
import {
mcpResourceAttachments,
mcpHasResourceAttachments
} from '$lib/stores/mcp-resources.svelte';
import { ChatAttachmentMcpResource, HorizontalScrollCarousel } from '$lib/components/app';
interface Props {
class?: string;
onResourceClick?: (uri: string) => void;
}
let { class: className, onResourceClick }: Props = $props();
const attachments = $derived(mcpResourceAttachments());
const hasAttachments = $derived(mcpHasResourceAttachments());
function handleRemove(attachmentId: string) {
mcpStore.removeResourceAttachment(attachmentId);
}
function handleResourceClick(uri: string) {
onResourceClick?.(uri);
}
</script>
{#if hasAttachments}
<div class={className}>
<HorizontalScrollCarousel gapSize="2">
{#each attachments as attachment (attachment.id)}
<ChatAttachmentMcpResource
{attachment}
onRemove={handleRemove}
onClick={() => handleResourceClick(attachment.resource.uri)}
/>
{/each}
</HorizontalScrollCarousel>
</div>
{/if}