feat: support reference file mentions

This commit is contained in:
Shoubhit Dash 2026-05-09 13:58:13 +05:30
commit 8318b686cb
11 changed files with 370 additions and 109 deletions

View file

@ -564,6 +564,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
.filter((agent) => !agent.hidden && agent.mode !== "primary")
.map((agent): AtOption => ({ type: "agent", name: agent.name, display: agent.name })),
)
const referenceAgentNames = createMemo(() =>
sync.data.agent.filter((agent) => agent.options.reference !== undefined).map((agent) => agent.name),
)
const agentNames = createMemo(() => local.agent.list().map((agent) => agent.name))
const handleAtSelect = (option: AtOption | undefined) => {
@ -589,6 +592,9 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
} = useFilteredList<AtOption>({
items: async (query) => {
const agents = agentList()
const reference = referenceAgentNames().find(
(name) => query.startsWith(`${name}:/`) || query.startsWith(`${name}/`),
)
const open = recent()
const seen = new Set(open)
const pinned: AtOption[] = open.map((path) => ({ type: "file", path, display: path, recent: true }))
@ -597,6 +603,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
const fileOptions: AtOption[] = paths
.filter((path) => !seen.has(path))
.map((path) => ({ type: "file", path, display: path }))
if (reference) return fileOptions
return [...agents, ...pinned, ...fileOptions]
},
key: atKey,

View file

@ -124,6 +124,33 @@ describe("buildRequestParts", () => {
expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/shared.ts")).toBe(true)
})
test("builds reference file URLs for configured repo paths", () => {
const result = buildRequestParts({
prompt: [{ type: "file", path: "effect:/src/Effect.ts", content: "@effect:/src/Effect.ts", start: 0, end: 22 }],
context: [
{
key: "ctx:reference-comment",
type: "file",
path: "src/review.ts",
comment: "Compare with @effect:/src/Context.ts.",
},
],
images: [],
text: "@effect:/src/Effect.ts",
messageID: "msg_reference",
sessionID: "ses_reference",
sessionDirectory: "/repo",
})
const files = result.requestParts.filter((part) => part.type === "file")
expect(files.some((part) => part.type === "file" && part.url === "opencode-reference://effect/src/Effect.ts")).toBe(
true,
)
expect(
files.some((part) => part.type === "file" && part.url === "opencode-reference://effect/src/Context.ts"),
).toBe(true)
})
test("handles Windows paths correctly (simulated on macOS)", () => {
const prompt: Prompt = [{ type: "file", path: "src\\foo.ts", content: "@src\\foo.ts", start: 0, end: 11 }]

View file

@ -41,6 +41,22 @@ const fileQuery = (selection: FileSelection | undefined) =>
const mention = /(^|[\s([{"'])@(\S+)/g
const referencePath = (value: string) => {
const match = value.match(/^([^:/\\]+):\/(.*)$/)
if (!match) return
if (/^[A-Za-z]$/.test(match[1]!)) return
return { name: match[1]!, path: match[2] ?? "" }
}
const referenceUrl = (value: string, selection?: FileSelection) => {
const reference = referencePath(value)
if (!reference) return
return `opencode-reference://${encodeURIComponent(reference.name)}/${reference.path
.split("/")
.map(encodeURIComponent)
.join("/")}${fileQuery(selection)}`
}
const parseCommentMentions = (comment: string) => {
return Array.from(comment.matchAll(mention)).flatMap((match) => {
const path = (match[2] ?? "").replace(/[.,!?;:)}\]"']+$/, "")
@ -97,25 +113,34 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
},
]
const files = input.prompt.filter(isFileAttachment).map((attachment) => {
const path = absolute(input.sessionDirectory, attachment.path)
const filePart = (file: string, selection?: FileSelection, source?: FileAttachmentPart) => {
const url = referenceUrl(file, selection)
const filepath = url ? file : absolute(input.sessionDirectory, file)
return {
id: Identifier.ascending("part"),
type: "file",
mime: "text/plain",
url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`,
filename: getFilename(attachment.path),
source: {
type: "file",
text: {
value: attachment.content,
start: attachment.start,
end: attachment.end,
},
path,
},
url: url ?? `file://${encodeFilePath(filepath)}${fileQuery(selection)}`,
filename: getFilename(file),
...(source
? {
source: {
type: "file" as const,
text: {
value: source.content,
start: source.start,
end: source.end,
},
path: filepath,
},
}
: {}),
} satisfies PromptRequestPart
})
}
const files = input.prompt
.filter(isFileAttachment)
.map((attachment) => filePart(attachment.path, attachment.selection, attachment))
const agents = input.prompt.filter(isAgentAttachment).map((attachment) => {
return {
@ -133,34 +158,20 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
const used = new Set(files.map((part) => part.url))
const context = input.context.flatMap((item) => {
const path = absolute(input.sessionDirectory, item.path)
const url = `file://${encodeFilePath(path)}${fileQuery(item.selection)}`
const url = referenceUrl(item.path, item.selection) ?? `file://${encodeFilePath(path)}${fileQuery(item.selection)}`
const comment = item.comment?.trim()
if (!comment && used.has(url)) return []
used.add(url)
const filePart = {
id: Identifier.ascending("part"),
type: "file",
mime: "text/plain",
url,
filename: getFilename(item.path),
} satisfies PromptRequestPart
const contextFilePart = filePart(item.path, item.selection)
if (!comment) return [filePart]
if (!comment) return [contextFilePart]
const mentions = parseCommentMentions(comment).flatMap((path) => {
const url = `file://${encodeFilePath(absolute(input.sessionDirectory, path))}`
const url = referenceUrl(path) ?? `file://${encodeFilePath(absolute(input.sessionDirectory, path))}`
if (used.has(url)) return []
used.add(url)
return [
{
id: Identifier.ascending("part"),
type: "file",
mime: "text/plain",
url,
filename: getFilename(path),
} satisfies PromptRequestPart,
]
return [filePart(path)]
})
return [
@ -177,7 +188,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
origin: item.commentOrigin,
}),
} satisfies PromptRequestPart,
filePart,
contextFilePart,
...mentions,
]
})