From 71b27c4cd8c770136c40622a29aff6ecb697d44c Mon Sep 17 00:00:00 2001 From: Dax Date: Thu, 12 Nov 2026 11:46:46 -0500 Subject: [PATCH] fix(app): reuse mention suggestions while typing (#63572) --- packages/app/src/composer/editor/editor.tsx | 29 +++++++++-------- .../app/src/composer/editor/interaction.ts | 12 +++---- .../test-browser/composer-file-search.test.ts | 32 +++++++++++++++++-- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/packages/app/src/composer/editor/editor.tsx b/packages/app/src/composer/editor/editor.tsx index c1843b8b663..6cbfda96575 100644 --- a/packages/app/src/composer/editor/editor.tsx +++ b/packages/app/src/composer/editor/editor.tsx @@ -2,6 +2,7 @@ import { createEffect, createMemo, createResource, + createSelector, createSignal, createUniqueId, For, @@ -16,6 +17,7 @@ import { createStore } from "solid-js/store" import { Portal } from "solid-js/web" import { createResizeObserver } from "@solid-primitives/resize-observer" import { createMediaQuery } from "@solid-primitives/media" +import { Key } from "@solid-primitives/keyed" import { FileIcon } from "@opencode/ui/file-icon" import { Icon } from "@opencode/ui/icon" import { IconButton } from "@opencode/ui/icon-button" @@ -1254,6 +1256,7 @@ function ComposerEditorPopover(props: { }) { const i18n = useI18n() const listID = props.id ?? createUniqueId() + const active = createSelector(() => props.activeID) let container: HTMLDivElement | undefined const [store, setStore] = createStore({ maxHeight: COMPOSER_SUGGESTION_MAX_HEIGHT }) @@ -1397,34 +1400,34 @@ function ComposerEditorPopover(props: { when={props.items.length > 0} fallback={
{props.emptyLabel}
} > - + {(item) => ( )} - + diff --git a/packages/app/src/composer/editor/interaction.ts b/packages/app/src/composer/editor/interaction.ts index fe6f63f9f1d..41a149a0ded 100644 --- a/packages/app/src/composer/editor/interaction.ts +++ b/packages/app/src/composer/editor/interaction.ts @@ -287,9 +287,12 @@ export function createComposerEditor(input: { }) onCleanup(() => dispatchMentionSearch({ type: "disposed" })) + const contextOpen = createMemo(() => state.popover.type === "context") + const fileRows = createMemo(() => selectMentionFiles(mentionSearch())) + const contextList = useFilteredList({ items: (query) => { - if (state.popover.type !== "context") return [] + if (!contextOpen()) return [] const sessions = input.searchSessions?.(query) ?? [] @@ -301,12 +304,7 @@ export function createComposerEditor(input: { const seen = new Set(recent.map((item) => item.id)) - return [ - ...fixed, - ...sessions, - ...recent, - ...selectMentionFiles(mentionSearch()).filter((item) => !seen.has(item.id)), - ] + return [...fixed, ...sessions, ...recent, ...fileRows().filter((item) => !seen.has(item.id))] }, key: (item) => item.id, filterKeys: ["label"], diff --git a/packages/app/test-browser/composer-file-search.test.ts b/packages/app/test-browser/composer-file-search.test.ts index 788a61fbecb..f4a3d0d0fa3 100644 --- a/packages/app/test-browser/composer-file-search.test.ts +++ b/packages/app/test-browser/composer-file-search.test.ts @@ -88,9 +88,37 @@ test("closing @ aborts its session search, and reopening on the same word waits } }) +test("moving the @ highlight reuses the current query results", async () => { + const searches: string[] = [] + + const value = setup( + { se: ["src/server.ts", "src/session.ts"] }, + { + searchSessions: (query) => { + searches.push(query) + + return [] + }, + }, + ) + + try { + await value.type("@se") + searches.length = 0 + value.editor.onKeyDown(new KeyboardEvent("keydown", { key: "ArrowDown" })) + expect(activeID(value)).toBe("file:src/session.ts") + expect(searches).toEqual([]) + } finally { + value.dispose() + } +}) + function setup( results: Record, - options?: { requestSessionSearch?: (word: string, signal: AbortSignal) => Promise }, + options?: { + searchSessions?: (query: string) => ComposerSuggestion[] + requestSessionSearch?: (word: string, signal: AbortSignal) => Promise + }, ) { jest.useFakeTimers() @@ -112,7 +140,7 @@ function setup( return paths.map(file) }, - searchSessions: () => [], + searchSessions: options?.searchSessions, requestSessionSearch: options?.requestSessionSearch, view: { submit: { stopping: () => false, onSubmit() {}, onStop() {} } }, })