Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]

Expand Down
26 changes: 26 additions & 0 deletions packages/app/src/components/prompt-input/build-request-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"),
Expand All @@ -153,6 +178,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) {
}),
} satisfies PromptRequestPart,
filePart,
...mentions,
]
})

Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/pages/session/file-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/pages/session/review-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/components/line-comment-annotations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { render as renderSolid } from "solid-js/web"
import { useI18n } from "../context/i18n"
import { createHoverCommentUtility } from "../pierre/comment-hover"
import { cloneSelectedLineRange, formatSelectedLineLabel, lineInSelectedRange } from "../pierre/selection-bridge"
import { LineComment, LineCommentEditor } from "./line-comment"
import { LineComment, LineCommentEditor, type LineCommentEditorProps } from "./line-comment"

export type LineCommentAnnotationMeta<T> =
| { kind: "comment"; key: string; comment: T }
Expand Down Expand Up @@ -55,6 +55,7 @@ type LineCommentControllerProps<T extends LineCommentShape> = {
comments: Accessor<T[]>
draftKey: Accessor<string>
label: string
mention?: LineCommentEditorProps["mention"]
state: LineCommentStateProps<string>
onSubmit: (input: { comment: string; selection: SelectedLineRange }) => void
onUpdate?: (input: { id: string; comment: string; selection: SelectedLineRange }) => void
Expand Down Expand Up @@ -85,6 +86,7 @@ type CommentProps = {
type DraftProps = {
value: string
selection: JSX.Element
mention?: LineCommentEditorProps["mention"]
onInput: (value: string) => void
onCancel: VoidFunction
onSubmit: (value: string) => void
Expand Down Expand Up @@ -148,6 +150,7 @@ export function createLineCommentAnnotationRenderer<T>(props: {
onPopoverFocusOut={view().editor!.onPopoverFocusOut}
cancelLabel={view().editor!.cancelLabel}
submitLabel={view().editor!.submitLabel}
mention={view().editor!.mention}
/>
</Show>
)
Expand All @@ -167,6 +170,7 @@ export function createLineCommentAnnotationRenderer<T>(props: {
onCancel={view().onCancel}
onSubmit={view().onSubmit}
onPopoverFocusOut={view().onPopoverFocusOut}
mention={view().mention}
/>
)
}, host)
Expand Down Expand Up @@ -389,6 +393,7 @@ export function createLineCommentController<T extends LineCommentShape>(
return note.draft()
},
selection: formatSelectedLineLabel(comment.selection, i18n.t),
mention: props.mention,
onInput: note.setDraft,
onCancel: note.cancelDraft,
onSubmit: (value: string) => {
Expand All @@ -415,6 +420,7 @@ export function createLineCommentController<T extends LineCommentShape>(
return note.draft()
},
selection: formatSelectedLineLabel(range, i18n.t),
mention: props.mention,
onInput: note.setDraft,
onCancel: note.cancelDraft,
onSubmit: (comment) => {
Expand Down
52 changes: 52 additions & 0 deletions packages/ui/src/components/line-comment-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,58 @@ export const lineCommentStyles = `
box-shadow: var(--shadow-xs-border-select);
}

[data-component="line-comment"] [data-slot="line-comment-mention-list"] {
display: flex;
flex-direction: column;
gap: 4px;
max-height: 180px;
overflow: auto;
padding: 4px;
border: 1px solid var(--border-base);
border-radius: var(--radius-md);
background: var(--surface-base);
}

[data-component="line-comment"] [data-slot="line-comment-mention-item"] {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
min-width: 0;
padding: 6px 8px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text-strong);
text-align: left;
}

[data-component="line-comment"] [data-slot="line-comment-mention-item"][data-active] {
background: var(--surface-raised-base-hover);
}

[data-component="line-comment"] [data-slot="line-comment-mention-path"] {
display: flex;
align-items: center;
min-width: 0;
font-family: var(--font-family-sans);
font-size: var(--font-size-small);
line-height: var(--line-height-large);
}

[data-component="line-comment"] [data-slot="line-comment-mention-dir"] {
min-width: 0;
color: var(--text-weak);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

[data-component="line-comment"] [data-slot="line-comment-mention-file"] {
color: var(--text-strong);
white-space: nowrap;
}

[data-component="line-comment"] [data-slot="line-comment-actions"] {
display: flex;
align-items: center;
Expand Down
Loading
Loading