ui: Filesystem @mentions for Chat Form (#26715)

* base : @-mention picker foundation - glob search, picker nav, highlight

* feat : @-mention file/folder picker and mention badges in message bubbles

* fix: Imports

* feat : wire the @-mention picker into the chat form

* fix: Bound the glob-search result cache key and prune stale entries
This commit is contained in:
Aleksander Grygier
2026-08-07 18:45:54 +02:00
committed by GitHub
parent 4cb22cd537
commit 23634783c5
40 changed files with 1839 additions and 399 deletions
@@ -0,0 +1,43 @@
<script lang="ts">
import { ChatFormPickerList, ChatFormPickerListItem } from '$lib/components/app/chat';
interface Item {
id: string;
label: string;
}
const items: Item[] = Array.from({ length: 20 }, (_, i) => ({
id: String(i),
label: `item ${i}`
}));
let open = $state(false);
let scrollTrigger = $state(0);
let selectedIndex = $state(0);
export function openPicker() {
open = true;
}
</script>
<div style="height: 5000px;">conversation</div>
{#if open}
<div data-testid="picker-host">
<ChatFormPickerList
{items}
isLoading={false}
{selectedIndex}
searchQuery=""
showSearchInput={false}
{scrollTrigger}
itemKey={(it) => it.id}
>
{#snippet item(it, index, isSelected)}
<ChatFormPickerListItem dataIndex={index} {isSelected} onclick={() => {}}>
{it.label}
</ChatFormPickerListItem>
{/snippet}
</ChatFormPickerList>
</div>
{/if}
@@ -0,0 +1,29 @@
// Regression test: opening a chat-form picker must not scroll the
// conversation to the top. Root cause: the list's scroll effect fired
// scrollIntoView on the initial mount, before the popover was positioned,
// so the browser scrolled every scrollable ancestor to reveal the row.
import { describe, it, expect } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { tick } from 'svelte';
import PickerListScrollHarness from './components/PickerListScrollHarness.svelte';
describe('ChatFormPickerList mount scroll', () => {
it('does not scroll documentElement when the picker mounts', async () => {
const screen = render(PickerListScrollHarness);
await tick();
document.documentElement.scrollTop = document.documentElement.scrollHeight;
await tick();
const before = document.documentElement.scrollTop;
expect(before).toBeGreaterThan(0);
screen.component.openPicker();
await tick();
await new Promise((r) => setTimeout(r, 100));
await tick();
const after = document.documentElement.scrollTop;
expect(after).toBe(before);
});
});