diff --git a/Components/ChatInput.svelte b/Components/ChatInput.svelte index f572b0c..630f980 100644 --- a/Components/ChatInput.svelte +++ b/Components/ChatInput.svelte @@ -251,6 +251,10 @@ searchStateStore.resetSearch(); } } + + function handleFocusOut() { + searchStateStore.resetSearch(); + }
@@ -282,6 +286,7 @@ on:drop={handleDrop} on:click={handleCursorPositionChange} on:keyup={handleCursorPositionChange} + on:focusout={handleFocusOut} data-placeholder="Type a message..." role="textbox" aria-multiline="true" diff --git a/Components/UserInstruction.svelte b/Components/UserInstruction.svelte index d972124..b2fd5f3 100644 --- a/Components/UserInstruction.svelte +++ b/Components/UserInstruction.svelte @@ -10,44 +10,68 @@ export let userInstructionActive: boolean; + const plugin: AIAgentPlugin = Resolve(Services.AIAgentPlugin); + const fileSystemService: FileSystemService = Resolve(Services.FileSystemService); + let height = 0; - - let emptyResultsContentDiv: HTMLDivElement; - let resultsContentDiv: HTMLDivElement; + + let instructionsContentDiv: HTMLDivElement; + let emptyInstructionsContentDiv: HTMLDivElement; + let instructionsElements: (HTMLDivElement | null)[] = []; + let resultsContainer: HTMLDivElement; let userInstructions: string[] = []; let selectedInstruction: number = 0; $: userInstructions, updateHeight(); - const plugin: AIAgentPlugin = Resolve(Services.AIAgentPlugin); - const fileSystemService: FileSystemService = Resolve(Services.FileSystemService); + $: if (selectedInstruction !== undefined && instructionsElements.length > 0) { + scrollSelectedIntoView(); + } + + $: if (userInstructionActive) { + selectedInstruction = 0; + loadUserInstructions(); + setTimeout(() => { + if (resultsContainer && userInstructionActive) { + document.addEventListener("click", handleClickOutside); + } + }, 10); + } else { + userInstructions = []; + document.removeEventListener("click", handleClickOutside); + } function updateHeight() { tick().then(() => { - if (resultsContentDiv) { - height = resultsContentDiv.scrollHeight; + if (instructionsContentDiv) { + height = instructionsContentDiv.scrollHeight; } - else if (emptyResultsContentDiv) { - height = emptyResultsContentDiv.scrollHeight; + else if (emptyInstructionsContentDiv) { + height = emptyInstructionsContentDiv.scrollHeight; } else { height = 0; } }); } - $: if (userInstructionActive) { - loadUserInstructions(); - } else { - userInstructions = []; + function handleClickOutside(event: MouseEvent) { + if (resultsContainer && !resultsContainer.contains(event.target as Node)) { + userInstructionActive = false; + } } async function loadUserInstructions() { const files = await fileSystemService.listFilesInDirectory(Path.UserInstructions, true, true); userInstructions = files.map(file => file.path).filter(path => path != Path.ExampleUserInstructions); + + if (userInstructions.length > 0) { + userInstructions = [Copy.NoUserInstruction, ...userInstructions]; + } + tick().then(() => { - if (resultsContentDiv) { - resultsContentDiv.focus(); + if (instructionsContentDiv) { + instructionsContentDiv.focus(); } }); } @@ -55,6 +79,7 @@ function handleInstructionSelect() { if (selectedInstruction < userInstructions.length) { plugin.settings.userInstruction = userInstructions[selectedInstruction]; + plugin.saveSettings(); } userInstructionActive = false; } @@ -84,12 +109,24 @@ return; } } + + function scrollSelectedIntoView() { + tick().then(() => { + if (selectedInstruction < instructionsElements.length) { + instructionsElements[selectedInstruction]?.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'nearest' + }); + } + }); + } -
+
{#if userInstructionActive} {#if userInstructions.length === 0} -
+
@@ -104,9 +141,10 @@
{/if} {#if userInstructions.length > 0} + {@const currentInstruction = plugin.settings.userInstruction}
@@ -115,12 +153,16 @@ role="option" tabindex="-1" aria-selected={selectedInstruction === index} + class:current-instruction={currentInstruction === userInstruction} style:background-color={selectedInstruction === index ? "var(--interactive-accent)" : "transparent"} + bind:this={instructionsElements[index]} on:mouseenter={() => selectedInstruction = index} on:click={handleInstructionSelect} on:keydown={() => {}}>
{basename(userInstruction)}
-
{userInstruction}
+ {#if userInstruction !== basename(userInstruction)} +
{userInstruction}
+ {/if}
{/each}
@@ -165,6 +207,10 @@ cursor: pointer; } + .user-instruction-container.current-instruction { + box-shadow: inset 0px 0px 4px 1px var(--color-accent); + } + .user-instruction-title { grid-row: 1; grid-column: 1; diff --git a/Enums/Copy.ts b/Enums/Copy.ts index e7d57bb..8ba06ec 100644 --- a/Enums/Copy.ts +++ b/Enums/Copy.ts @@ -3,6 +3,7 @@ export enum Copy { UserInstructions1 = "You can create custom ", UserInstructions2 = "instructions", UserInstructions3 = " that the AI will follow.", + NoUserInstruction = "No custom instructions", // Model display names ClaudeSonnet_4_5 = "Claude Sonnet 4.5",