fix(annotate): support @ markdown file references (#488)

OpenCode can pass annotate targets as @file references, so resolve markdown paths with an @ fallback only after the primary lookup misses. This preserves real @-prefixed filenames while making @README.md and quoted @ inputs resolve reliably.
This commit is contained in:
Esteban Sanchez
2026-04-05 19:46:10 -05:00
committed by GitHub
parent 03b3bdd011
commit 70b0cfb34e
2 changed files with 102 additions and 3 deletions
+46
View File
@@ -106,6 +106,33 @@ describe("resolveMarkdownFile", () => {
});
});
test("resolves @ filename via fallback when @ file does not exist", async () => {
const root = createTempProject({ "README.md": "# Hello" });
const result = resolveMarkdownFile("@README.md", root);
expect(result).toEqual({
kind: "found",
path: resolve(root, "README.md"),
});
});
test("prioritizes real @ filename before fallback", async () => {
const root = createTempProject({ "@README.md": "# At" });
const result = resolveMarkdownFile("@README.md", root);
expect(result).toEqual({
kind: "found",
path: resolve(root, "@README.md"),
});
});
test("resolves quoted @ filename", async () => {
const root = createTempProject({ "README.md": "# Hello" });
const result = resolveMarkdownFile('"@README.md"', root);
expect(result).toEqual({
kind: "found",
path: resolve(root, "README.md"),
});
});
test("resolves relative paths with Windows separators", async () => {
const root = createTempProject({ "docs/test-plan.md": "# Test plan\n" });
const result = resolveMarkdownFile("docs\\test-plan.md", root);
@@ -147,6 +174,19 @@ describe("resolveMarkdownFile", () => {
}
});
test("returns ambiguous in @ fallback when target exists multiple times", async () => {
const root = createTempProject({
"docs/plan.md": "# Plan 1",
"api/plan.md": "# Plan 2",
});
const result = resolveMarkdownFile("@plan.md", root);
expect(result.kind).toBe("ambiguous");
if (result.kind === "ambiguous") {
expect(result.input).toBe("@plan.md");
expect(result.matches).toHaveLength(2);
}
});
// Ignored directories
test("skips node_modules", async () => {
@@ -190,6 +230,12 @@ describe("resolveMarkdownFile", () => {
expect(result.kind).toBe("not_found");
});
test("returns not_found for @ path that cannot be resolved", async () => {
const root = createTempProject();
const result = resolveMarkdownFile("@nope.md", root);
expect(result).toEqual({ kind: "not_found", input: "@nope.md" });
});
test("handles deeply nested files", async () => {
const root = createTempProject({
"a/b/c/d/deep.md": "# Deep",
+56 -3
View File
@@ -78,6 +78,20 @@ function isSearchableMarkdownPath(input: string): boolean {
return MARKDOWN_PATH_REGEX.test(input.trim());
}
function stripWrappingQuotes(input: string): string {
if (input.length < 2) {
return input;
}
const first = input[0];
const last = input[input.length - 1];
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
return input.slice(1, -1);
}
return input;
}
/** Check if a path looks like a Windows absolute path (e.g. C:\ or C:/) */
function hasWindowsDriveLetter(input: string): boolean {
return /^[a-zA-Z]:[/\\]/.test(input);
@@ -155,12 +169,10 @@ export function isAbsoluteMarkdownPath(
* @param input - User-provided path (absolute, relative, or bare filename)
* @param projectRoot - Project root directory to search within
*/
export function resolveMarkdownFile(
function resolveMarkdownFileCore(
input: string,
projectRoot: string,
): ResolveResult {
// Trim whitespace/CR that may leak from Windows shell pipelines
input = input.trim();
const normalizedInput = normalizeMarkdownPathInput(input);
const searchInput = normalizeSeparators(normalizedInput);
const isBareFilename = !searchInput.includes("/");
@@ -218,6 +230,47 @@ export function resolveMarkdownFile(
return { kind: "not_found", input };
}
/**
* Resolve a markdown file path within a project root.
*
* @param input - User-provided path (absolute, relative, or bare filename)
* @param projectRoot - Project root directory to search within
*/
export function resolveMarkdownFile(
input: string,
projectRoot: string,
): ResolveResult {
const originalInput = input.trim();
const unquotedInput = stripWrappingQuotes(originalInput);
const primary = resolveMarkdownFileCore(unquotedInput, projectRoot);
if (primary.kind === "found") {
return primary;
}
if (primary.kind === "ambiguous") {
return { ...primary, input: originalInput };
}
if (!unquotedInput.startsWith("@")) {
return { kind: "not_found", input: originalInput };
}
const normalizedInput = unquotedInput.replace(/^@+/, "");
if (!normalizedInput) {
return { kind: "not_found", input: originalInput };
}
const fallback = resolveMarkdownFileCore(normalizedInput, projectRoot);
if (fallback.kind === "found") {
return fallback;
}
if (fallback.kind === "ambiguous") {
return { ...fallback, input: originalInput };
}
return { kind: "not_found", input: originalInput };
}
/**
* Check if a directory contains at least one markdown file.
* Used to validate folder annotation targets.