feat: support reference file mentions
This commit is contained in:
parent
b2baddcd37
commit
8318b686cb
11 changed files with 370 additions and 109 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 }]
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue