Fix inline reference at block start rendering on its own line (#298497)

Copilot-authored message: Fix inline reference at block start rendering on its own line

When an inlineReference is the first item in a chat response (no preceding
markdown), it creates a standalone markdownContent with default MarkdownString
properties. The subsequent markdown from the model has different properties
(e.g. isTrusted: true), causing canMergeMarkdownStrings to return false. This
leaves them as separate content parts, rendering the inline reference link on
its own line.

Fix by detecting when the previous item consists solely of synthesized
content-ref links (via isContentRefOnly check) and merging them by prepending
the reference text while adopting the incoming markdown's properties.

Fixes #278191
This commit is contained in:
Rob Lourens
2026-02-28 15:04:28 -08:00
committed by GitHub
parent 2ea417c9ab
commit b094e2f7dd
2 changed files with 65 additions and 3 deletions
@@ -57,9 +57,25 @@ export function annotateSpecialMarkdownContent(response: Iterable<IChatProgressR
result.push({ content: new MarkdownString(markdownText), inlineReferences: annotationMetadata, kind: 'markdownContent' });
}
}
} else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent' && canMergeMarkdownStrings(previousItem.content, item.content)) {
const merged = appendMarkdownString(previousItem.content, item.content);
result[previousItemIndex] = { ...previousItem, content: merged };
} else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent') {
if (canMergeMarkdownStrings(previousItem.content, item.content)) {
const merged = appendMarkdownString(previousItem.content, item.content);
result[previousItemIndex] = { ...previousItem, content: merged };
} else if (previousItem.inlineReferences && isContentRefOnly(previousItem.content.value)) {
// The previous item is a standalone inline reference whose MarkdownString
// was synthesized with default properties that don't match the incoming
// markdown (e.g., different isTrusted). Prepend the reference text and
// adopt the incoming item's properties so they render together in one block.
result[previousItemIndex] = {
...previousItem,
content: {
...item.content,
value: previousItem.content.value + item.content.value,
},
};
} else {
result.push(item);
}
} else if (item.kind === 'markdownVuln') {
const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities));
const markdownText = `<vscode_annotation details='${vulnText}'>${item.content.value}</vscode_annotation>`;
@@ -88,6 +104,18 @@ export function annotateSpecialMarkdownContent(response: Iterable<IChatProgressR
return result;
}
const contentRefPattern = new RegExp(`^(\\[.*?\\]\\(${contentRefUrl}/\\d+\\))+$`);
/**
* Returns true when the text consists entirely of synthesized content-ref
* links (e.g. `[file.ts](http://_vscodecontentref_/0)`), with no other
* markdown text mixed in. Used to decide whether the MarkdownString
* properties are "synthetic defaults" that can safely be replaced.
*/
function isContentRefOnly(text: string): boolean {
return contentRefPattern.test(text);
}
/**
* Checks whether the end of a markdown string is inside a code context
* (fenced code block or inline code span) where markdown link syntax
@@ -239,5 +239,39 @@ suite('Annotations', function () {
assert.ok(!md.content.value.includes('_vscodecontentref_'));
assert.ok(md.content.value.endsWith('index.ts'));
});
test('inline reference at start of block merges with following markdown', () => {
const result = annotateSpecialMarkdownContent([
{ kind: 'inlineReference', inlineReference: URI.parse('file:///index.ts'), name: 'index.ts' },
{ kind: 'markdownContent', content: new MarkdownString(' is the entry point', { isTrusted: true, supportThemeIcons: true }) },
]);
assert.strictEqual(result.length, 1);
const md = result[0] as IChatMarkdownContent;
assert.ok(md.content.value.includes('[index.ts]'));
assert.ok(md.content.value.includes('_vscodecontentref_'));
assert.ok(md.content.value.endsWith(' is the entry point'));
assert.ok(md.inlineReferences);
assert.strictEqual(md.content.isTrusted, true);
assert.strictEqual(md.content.supportThemeIcons, true);
});
test('inline reference after regular text does not force-merge incompatible markdown', () => {
const result = annotateSpecialMarkdownContent([
content('See '),
{ kind: 'inlineReference', inlineReference: URI.parse('file:///index.ts'), name: 'index.ts' },
{ kind: 'markdownContent', content: new MarkdownString(' more info', { isTrusted: true, supportThemeIcons: true }) },
]);
// The first item has "See [index.ts](...)" with default markdown properties,
// the second item has different properties - they must stay separate.
assert.strictEqual(result.length, 2);
const first = result[0] as IChatMarkdownContent;
assert.ok(first.content.value.startsWith('See '));
assert.ok(first.inlineReferences);
const second = result[1] as IChatMarkdownContent;
assert.strictEqual(second.content.value, ' more info');
assert.strictEqual(second.content.isTrusted, true);
});
});
});