diff options
| author | Shoubhit Dash <[email protected]> | 2026-04-01 16:11:57 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-04-01 16:11:57 +0530 |
| commit | a3a6cf1c075c40c87980dda181d586a1d06ea304 (patch) | |
| tree | 71b3c3d7144f7c38eb73d7e173852858313fc4bf /packages/app/src | |
| parent | 47a676111a3532aebed01110494742e536b7e5b4 (diff) | |
| download | opencode-a3a6cf1c075c40c87980dda181d586a1d06ea304.tar.gz opencode-a3a6cf1c075c40c87980dda181d586a1d06ea304.zip | |
feat(comments): support file mentions (#20447)
Diffstat (limited to 'packages/app/src')
5 files changed, 60 insertions, 0 deletions
diff --git a/packages/app/src/components/prompt-input/build-request-parts.test.ts b/packages/app/src/components/prompt-input/build-request-parts.test.ts index ce09ae921..06c377331 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.test.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.test.ts @@ -100,6 +100,30 @@ describe("buildRequestParts", () => { expect(synthetic).toHaveLength(1) }) + test("adds file parts for @mentions inside comment text", () => { + const result = buildRequestParts({ + prompt: [{ type: "text", content: "look", start: 0, end: 4 }], + context: [ + { + key: "ctx:comment-mention", + type: "file", + path: "src/review.ts", + comment: "Compare with @src/shared.ts and @src/review.ts.", + }, + ], + images: [], + text: "look", + messageID: "msg_comment_mentions", + sessionID: "ses_comment_mentions", + sessionDirectory: "/repo", + }) + + const files = result.requestParts.filter((part) => part.type === "file") + expect(files).toHaveLength(2) + expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/review.ts")).toBe(true) + expect(files.some((part) => part.type === "file" && part.url === "file:///repo/src/shared.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 }] diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts index 4146fb484..a1076e60c 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -39,6 +39,16 @@ const absolute = (directory: string, path: string) => { const fileQuery = (selection: FileSelection | undefined) => selection ? `?start=${selection.startLine}&end=${selection.endLine}` : "" +const mention = /(^|[\s([{"'])@(\S+)/g + +const parseCommentMentions = (comment: string) => { + return Array.from(comment.matchAll(mention)).flatMap((match) => { + const path = (match[2] ?? "").replace(/[.,!?;:)}\]"']+$/, "") + if (!path) return [] + return [path] + }) +} + const isFileAttachment = (part: Prompt[number]): part is FileAttachmentPart => part.type === "file" const isAgentAttachment = (part: Prompt[number]): part is AgentPart => part.type === "agent" @@ -138,6 +148,21 @@ export function buildRequestParts(input: BuildRequestPartsInput) { if (!comment) return [filePart] + const mentions = parseCommentMentions(comment).flatMap((path) => { + const url = `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 [ { id: Identifier.ascending("part"), @@ -153,6 +178,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { }), } satisfies PromptRequestPart, filePart, + ...mentions, ] }) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 917de35b1..18bae6e2d 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -1046,6 +1046,9 @@ export default function Page() { onLineCommentUpdate={updateCommentInContext} onLineCommentDelete={removeCommentFromContext} lineCommentActions={reviewCommentActions()} + commentMentions={{ + items: file.searchFilesAndDirectories, + }} comments={comments.all()} focusedComment={comments.focus()} onFocusedCommentChange={comments.setFocus} diff --git a/packages/app/src/pages/session/file-tabs.tsx b/packages/app/src/pages/session/file-tabs.tsx index 8208b6c99..9430b7025 100644 --- a/packages/app/src/pages/session/file-tabs.tsx +++ b/packages/app/src/pages/session/file-tabs.tsx @@ -302,6 +302,9 @@ export function FileTabContent(props: { tab: string }) { comments: fileComments, label: language.t("ui.lineComment.submit"), draftKey: () => path() ?? props.tab, + mention: { + items: file.searchFilesAndDirectories, + }, state: { opened: () => note.openedComment, setOpened: (id) => setNote("openedComment", id), diff --git a/packages/app/src/pages/session/review-tab.tsx b/packages/app/src/pages/session/review-tab.tsx index c073e6214..76b65a221 100644 --- a/packages/app/src/pages/session/review-tab.tsx +++ b/packages/app/src/pages/session/review-tab.tsx @@ -30,6 +30,9 @@ export interface SessionReviewTabProps { onFocusedCommentChange?: (focus: { file: string; id: string } | null) => void focusedFile?: string onScrollRef?: (el: HTMLDivElement) => void + commentMentions?: { + items: (query: string) => string[] | Promise<string[]> + } classes?: { root?: string header?: string @@ -162,6 +165,7 @@ export function SessionReviewTab(props: SessionReviewTabProps) { onLineCommentUpdate={props.onLineCommentUpdate} onLineCommentDelete={props.onLineCommentDelete} lineCommentActions={props.lineCommentActions} + lineCommentMention={props.commentMentions} comments={props.comments} focusedComment={props.focusedComment} onFocusedCommentChange={props.onFocusedCommentChange} |
