feat(files): comment on selection in read-only markdown previews
Selecting text in a rendered markdown preview shows a Comment pill; attaching stores a file-quote context draft carrying the file path, the selected fragment (not whole lines), the user's comment, and a best-effort source line range resolved by anchoring the fragment's first and last lines in the raw content — a partially located fragment gets no range rather than a misleading one. The fragment stays highlighted while the comment input is open, using the selection overlay rects shared with chat quote comments, and the preview's native selection color now matches chat messages. file-quote flows through the same context contract: composer chip previews, the message context card, and the metadata round-trip.
This commit is contained in:
@@ -68,6 +68,13 @@ describe('model-facing text', () => {
|
||||
.toBe('Comment on this fragment of an earlier message in this conversation:\n> first line\n> second line\n\nwhy so?');
|
||||
});
|
||||
|
||||
test('file quotes carry the fragment with an optional line range', () => {
|
||||
expect(formatContextText(contextPayloadFromDraft(draft({ source: 'file-quote', fileLabel: 'docs/CHANGELOG.md', startLine: 12, endLine: 13, code: 'a\nb', text: 'why?' }))))
|
||||
.toBe('Comment on this fragment of `docs/CHANGELOG.md` lines 12-13:\n> a\n> b\n\nwhy?');
|
||||
expect(formatContextText(contextPayloadFromDraft(draft({ source: 'file-quote', fileLabel: 'docs/CHANGELOG.md', startLine: 0, endLine: 0, code: 'a', text: '' }))))
|
||||
.toBe('Comment on this fragment of `docs/CHANGELOG.md`:\n> a');
|
||||
});
|
||||
|
||||
test('PR comments and checks keep their attachment wording', () => {
|
||||
expect(formatContextText(contextPayloadFromDraft(draft({ source: 'pr-comment', fileLabel: 'octo/repo#7', code: 'the comment', text: '' }))))
|
||||
.toBe('Attached GitHub PR comment (octo/repo#7):\n\nthe comment');
|
||||
@@ -91,6 +98,8 @@ describe('round-trip through part metadata', () => {
|
||||
contextPayloadFromDraft(draft({ source: 'pr-comment' })),
|
||||
contextPayloadFromDraft(draft({ source: 'pr-check' })),
|
||||
contextPayloadFromDraft(draft({ source: 'chat-quote', fileLabel: 'msg_1' })),
|
||||
contextPayloadFromDraft(draft({ source: 'file-quote', startLine: 3, endLine: 5 })),
|
||||
contextPayloadFromDraft(draft({ source: 'file-quote', startLine: 0, endLine: 0 })),
|
||||
];
|
||||
for (const payload of payloads) {
|
||||
expect(readContextPart(asPart(payload))).toEqual(payload);
|
||||
|
||||
@@ -71,6 +71,16 @@ type GitHubIssueContext = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
type FileQuoteContext = {
|
||||
kind: 'file-quote';
|
||||
fileLabel: string;
|
||||
/** Present when the fragment could be located in the file source. */
|
||||
startLine?: number;
|
||||
endLine?: number;
|
||||
quote: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type ChatQuoteContext = {
|
||||
kind: 'chat-quote';
|
||||
/** The message the quote came from, when known. */
|
||||
@@ -92,6 +102,7 @@ export type ContextPartPayload =
|
||||
| BrowserAnnotationContext
|
||||
| PrCommentContext
|
||||
| PrCheckContext
|
||||
| FileQuoteContext
|
||||
| ChatQuoteContext
|
||||
| GitHubIssueContext
|
||||
| GitHubPrContext;
|
||||
@@ -128,6 +139,13 @@ export function formatContextText(payload: ContextPartPayload): string {
|
||||
return payload.text ? `${payload.prompt}\n\n${payload.text}` : payload.prompt;
|
||||
case 'pr-comment':
|
||||
return `Attached GitHub PR comment (${payload.label}):\n\n${payload.body}${payload.text ? `\n\n${payload.text}` : ''}`;
|
||||
case 'file-quote': {
|
||||
const location = payload.startLine != null && payload.endLine != null
|
||||
? ` lines ${payload.startLine}-${payload.endLine}`
|
||||
: '';
|
||||
const quoted = payload.quote.split('\n').map((line) => `> ${line}`).join('\n');
|
||||
return `Comment on this fragment of \`${payload.fileLabel}\`${location}:\n${quoted}${payload.text ? `\n\n${payload.text}` : ''}`;
|
||||
}
|
||||
case 'chat-quote': {
|
||||
const quoted = payload.quote.split('\n').map((line) => `> ${line}`).join('\n');
|
||||
return `Comment on this fragment of an earlier message in this conversation:\n${quoted}${payload.text ? `\n\n${payload.text}` : ''}`;
|
||||
@@ -179,6 +197,14 @@ export function contextPayloadFromDraft(draft: InlineCommentDraft): ContextPartP
|
||||
return { kind: 'pr-comment', label: draft.fileLabel, body: draft.code, text: draft.text };
|
||||
case 'pr-check':
|
||||
return { kind: 'pr-check', label: draft.fileLabel, output: draft.code, text: draft.text };
|
||||
case 'file-quote': {
|
||||
const payload: FileQuoteContext = { kind: 'file-quote', fileLabel: draft.fileLabel, quote: draft.code, text: draft.text };
|
||||
if (draft.startLine > 0 && draft.endLine > 0) {
|
||||
payload.startLine = draft.startLine;
|
||||
payload.endLine = draft.endLine;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
case 'chat-quote': {
|
||||
const payload: ChatQuoteContext = { kind: 'chat-quote', quote: draft.code, text: draft.text };
|
||||
if (draft.fileLabel) payload.messageId = draft.fileLabel;
|
||||
@@ -245,6 +271,14 @@ const contextPayloadSchema = z.discriminatedUnion('kind', [
|
||||
output: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('file-quote'),
|
||||
fileLabel: z.string(),
|
||||
startLine: z.number().optional(),
|
||||
endLine: z.number().optional(),
|
||||
quote: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
kind: z.literal('chat-quote'),
|
||||
messageId: z.string().optional(),
|
||||
|
||||
Reference in New Issue
Block a user