feat: Prompt Picker and Prompt Attachment UI/UX improvements

This commit is contained in:
Aleksander Grygier 2026-02-06 03:35:49 +01:00
parent 5fe03b2bdc
commit 3a64334f6c
4 changed files with 40 additions and 21 deletions

View File

@ -32,7 +32,7 @@
{#if !readonly && onRemove}
<div
class="absolute top-8 right-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
class="absolute top-10 right-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
>
<ActionIconRemove id={prompt.name} onRemove={() => onRemove?.()} />
</div>

View File

@ -41,6 +41,7 @@
let selectedIndex = $state(0);
let internalSearchQuery = $state('');
let promptError = $state<string | null>(null);
let selectedIndexBeforeArgumentForm = $state<number | null>(null);
let suggestions = $state<Record<string, string[]>>({});
let loadingSuggestions = $state<Record<string, boolean>>({});
@ -99,12 +100,20 @@
}
function handlePromptClick(prompt: MCPPromptInfo) {
const requiredArgs = prompt.arguments?.filter((arg) => arg.required) ?? [];
const args = prompt.arguments ?? [];
if (requiredArgs.length > 0) {
if (args.length > 0) {
selectedIndexBeforeArgumentForm = selectedIndex;
selectedPrompt = prompt;
promptArgs = {};
promptError = null;
requestAnimationFrame(() => {
const firstInput = document.querySelector(`#arg-${args[0].name}`) as HTMLInputElement;
if (firstInput) {
firstInput.focus();
}
});
} else {
executePrompt(prompt, {});
}
@ -212,6 +221,14 @@
function handleArgKeydown(event: KeyboardEvent, argName: string) {
const argSuggestions = suggestions[argName] ?? [];
// Handle Escape - return to prompt selection list
if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
event.stopPropagation();
handleCancelArgumentForm();
return;
}
if (argSuggestions.length === 0 || activeAutocomplete !== argName) return;
if (event.key === KeyboardKey.ARROW_DOWN) {
@ -224,10 +241,6 @@
event.preventDefault();
event.stopPropagation();
selectSuggestion(argName, argSuggestions[autocompleteIndex]);
} else if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
suggestions[argName] = [];
activeAutocomplete = null;
}
}
@ -248,6 +261,11 @@
}
function handleCancelArgumentForm() {
// Restore the previously selected prompt index
if (selectedIndexBeforeArgumentForm !== null) {
selectedIndex = selectedIndexBeforeArgumentForm;
selectedIndexBeforeArgumentForm = null;
}
selectedPrompt = null;
promptArgs = {};
promptError = null;
@ -259,8 +277,8 @@
if (event.key === KeyboardKey.ESCAPE) {
event.preventDefault();
if (selectedPrompt) {
selectedPrompt = null;
promptArgs = {};
// Return to prompt selection list, keeping the selected prompt active
handleCancelArgumentForm();
} else {
onClose?.();
}

View File

@ -39,7 +39,7 @@
if (selectedElement) {
selectedElement.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
block: 'center',
inline: 'nearest'
});
}

View File

@ -79,11 +79,10 @@
let showArgBadges = $derived(hasArguments && !isLoading && !loadError);
let isAttachment = $derived(variant === McpPromptVariant.ATTACHMENT);
let textSizeClass = $derived(isAttachment ? 'text-sm' : 'text-md');
let textSizeClass = $derived(isAttachment ? 'text-xs' : 'text-md');
let paddingClass = $derived(isAttachment ? 'px-3 py-2' : 'px-3.75 py-2.5');
let maxHeightStyle = $derived(
isAttachment
? 'max-height: 10rem;'
: 'min-height: var(--min-message-height); max-height: var(--max-message-height);'
isAttachment ? 'max-height: 6rem;' : 'max-height: var(--max-message-height);'
);
const serverFavicon = $derived(mcpStore.getServerFavicon(prompt.serverName));
@ -146,7 +145,7 @@
class="relative overflow-hidden rounded-[1.125rem] border border-destructive/50 bg-destructive/10 backdrop-blur-md"
>
<div
class="overflow-y-auto px-3.75 py-2.5"
class="overflow-y-auto {paddingClass}"
style="{maxHeightStyle} overflow-wrap: anywhere; word-break: break-word;"
>
<span class="{textSizeClass} text-destructive">{loadError}</span>
@ -154,15 +153,17 @@
</Card>
{:else if isLoading}
<Card
class="relative overflow-hidden rounded-[1.125rem] border border-purple-200 bg-purple-500/10 text-foreground backdrop-blur-md dark:border-purple-800 dark:bg-purple-500/20"
class="relative overflow-hidden rounded-[1.125rem] border border-purple-200 bg-purple-500/10 backdrop-blur-md dark:border-purple-800 dark:bg-purple-500/20 py-2 px-1"
>
<div
class="overflow-y-auto px-3.75 py-2.5"
class="overflow-y-auto {paddingClass}"
style="{maxHeightStyle} overflow-wrap: anywhere; word-break: break-word;"
>
<span class="{textSizeClass} text-purple-500 italic dark:text-purple-400">
Loading prompt content...
</span>
<div class="space-y-2">
<div class="h-3 w-3/4 animate-pulse rounded bg-foreground/20"></div>
<div class="h-3 w-full animate-pulse rounded bg-foreground/20"></div>
<div class="h-3 w-5/6 animate-pulse rounded bg-foreground/20"></div>
</div>
</div>
</Card>
{:else if hasContent}
@ -170,7 +171,7 @@
class="relative overflow-hidden rounded-[1.125rem] border border-purple-200 bg-purple-500/10 py-0 text-foreground backdrop-blur-md dark:border-purple-800 dark:bg-purple-500/20"
>
<div
class="overflow-y-auto px-3.75 py-2.5"
class="overflow-y-auto {paddingClass}"
style="{maxHeightStyle} overflow-wrap: anywhere; word-break: break-word;"
>
<span class="{textSizeClass} whitespace-pre-wrap">