fix(app): preserve mentions during paste (#37940)

Co-authored-by: Brendan Allan <14191578+Brendonovich@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-07-20 22:52:14 +08:00
committed by GitHub
co-authored by Brendan Allan
parent da312b009c
commit 47fc6f2991
3 changed files with 60 additions and 4 deletions
@@ -82,9 +82,7 @@ export function createPromptInputV2Controller(input: {
draft.addMention(part)
return true
}
const text = draft.state.prompt.map((item) => ("content" in item ? item.content : "")).join("")
const cursor = draft.state.cursor ?? text.length
draft.setText(text.slice(0, cursor) + part.content + text.slice(cursor))
draft.addText(part.content)
return true
}
const attachments = input.attachments
@@ -56,6 +56,28 @@ describe("prompt input v2 store", () => {
expect(prompt.state.cursor).toBe(7)
})
test("inserts text without flattening structured mentions", () => {
const [state, setState] = createStore<PromptInputV2PersistedState>({
prompt: [
{ type: "text", content: "A ", start: 0, end: 2 },
{ type: "file", path: "one", content: "@one", start: 2, end: 6 },
{ type: "text", content: " B", start: 6, end: 8 },
],
cursor: 2,
context: { items: [] },
})
const prompt = createPromptInputV2Store([state, setState])
prompt.addText("X\nY")
expect(prompt.state.prompt).toEqual([
{ type: "text", content: "A X\nY", start: 0, end: 5 },
{ type: "file", path: "one", content: "@one", start: 5, end: 9 },
{ type: "text", content: " B", start: 9, end: 11 },
])
expect(prompt.state.cursor).toBe(5)
})
test("mutates context, attachments, and model through shared actions", () => {
const prompt = createPromptStore()
const context = { key: "file:src/index.ts", type: "file" as const, path: "src/index.ts" }
@@ -47,6 +47,13 @@ export function createPromptInputV2Store(input: PromptInputV2StoreInput) {
setStore()("cursor", content.length)
})
},
addText(content: string) {
const cursor = store().cursor ?? promptLength(store().prompt)
batch(() => {
setStore()("prompt", (prompt) => insertText(prompt, cursor, content))
setStore()("cursor", cursor + content.length)
})
},
reset() {
batch(() => {
setStore()("prompt", [{ type: "text", content: "", start: 0, end: 0 }])
@@ -86,6 +93,27 @@ export function createPromptInputV2Store(input: PromptInputV2StoreInput) {
export type PromptInputV2Store = ReturnType<typeof createPromptInputV2Store>
function insertText(prompt: PromptInputV2Prompt, cursor: number, content: string): PromptInputV2Prompt {
let position = 0
let inserted = false
const parts = prompt.flatMap<PromptInputV2Prompt[number]>((part) => {
if (part.type === "image") return [part]
const start = position
position += part.content.length
if (inserted) return [part]
if (part.type === "text" && cursor >= start && cursor <= position) {
inserted = true
const offset = cursor - start
return [{ ...part, content: part.content.slice(0, offset) + content + part.content.slice(offset) }]
}
if (cursor > start) return [part]
inserted = true
return [{ type: "text", content, start: 0, end: 0 }, part]
})
if (!inserted) parts.push({ type: "text", content, start: 0, end: 0 })
return withOffsets(parts)
}
function insertMention(
prompt: PromptInputV2Prompt,
start: number,
@@ -106,11 +134,19 @@ function insertMention(
{ type: "text" as const, content: ` ${after}`, start: 0, end: 0 },
]
})
return withOffsets(parts)
}
function withOffsets(prompt: PromptInputV2Prompt): PromptInputV2Prompt {
let offset = 0
return parts.map((part) => {
return prompt.map((part) => {
if (part.type === "image") return part
const next = { ...part, start: offset, end: offset + part.content.length }
offset = next.end
return next
})
}
function promptLength(prompt: PromptInputV2Prompt) {
return prompt.reduce((length, part) => length + ("content" in part ? part.content.length : 0), 0)
}