From 4872c48c230728150e8e3406722943450ed58dcb Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:53:10 +0800 Subject: [PATCH] fix(app): complete mentions at cursor (#37941) Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> --- .../src/v2/components/prompt-input/machine.test.ts | 14 ++++++++++++++ .../src/v2/components/prompt-input/machine.ts | 11 ++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/session-ui/src/v2/components/prompt-input/machine.test.ts b/packages/session-ui/src/v2/components/prompt-input/machine.test.ts index 4b75e08bcf..231fca61a7 100644 --- a/packages/session-ui/src/v2/components/prompt-input/machine.test.ts +++ b/packages/session-ui/src/v2/components/prompt-input/machine.test.ts @@ -26,6 +26,20 @@ describe("prompt input v2 interaction machine", () => { expect(closed.state.popover).toEqual({ type: "closed" }) }) + test("opens context completion at the cursor", () => { + const value = "alpha @sr omega" + const input = persisted(value) + input.cursor = 9 + + const result = transitionPromptInputV2( + createPromptInputV2InteractionState(), + { type: "input.changed", value, persist: false }, + input, + ) + + expect(result.state.popover).toEqual({ type: "context", query: "sr" }) + }) + test("enters shell mode from an initial exclamation mark", () => { const result = transitionPromptInputV2( createPromptInputV2InteractionState(), diff --git a/packages/session-ui/src/v2/components/prompt-input/machine.ts b/packages/session-ui/src/v2/components/prompt-input/machine.ts index 7102b79318..fcf5131a17 100644 --- a/packages/session-ui/src/v2/components/prompt-input/machine.ts +++ b/packages/session-ui/src/v2/components/prompt-input/machine.ts @@ -61,7 +61,7 @@ export function transitionPromptInputV2( event: PromptInputV2InteractionEvent, persisted: PromptInputV2PersistedState, ): PromptInputV2Transition { - if (event.type === "input.changed") return inputChanged(state, event.value, event.persist !== false) + if (event.type === "input.changed") return inputChanged(state, event.value, event.persist !== false, persisted.cursor) if (event.type === "commands.open") return openCommands(state, persisted) if (event.type === "context.open") return openContext(state, persisted) if (event.type === "popover.query") return queryChanged(state, event.value) @@ -81,14 +81,19 @@ export function transitionPromptInputV2( return changed({ ...state, focus: "external" }) } -function inputChanged(state: PromptInputV2InteractionState, value: string, persist: boolean): PromptInputV2Transition { +function inputChanged( + state: PromptInputV2InteractionState, + value: string, + persist: boolean, + cursor: number | undefined, +): PromptInputV2Transition { const setText: PromptInputV2InteractionCommand[] = persist ? [{ type: "draft.setText", value }] : [] if (state.mode === "normal" && value === "!") { return changed({ ...state, mode: "shell", popover: { type: "closed" }, focus: "editor" }, [ { type: "draft.setText", value: "" }, ]) } - const context = value.match(/(?:^|\s)@([^\s@]*)$/) + const context = value.slice(0, cursor ?? value.length).match(/(?:^|\s)@([^\s@]*)$/) if (context) { const query = context[1] ?? "" return changed({ ...state, popover: { type: "context", query }, focus: "editor" }, [