From f39fe4a6a2e420b62932087f578123c49dbfafce Mon Sep 17 00:00:00 2001 From: Sawyer Date: Fri, 18 Sep 2026 01:57:58 -0700 Subject: [PATCH] fix(web): fenced package blocks labelled by a comment line or path are recognized (CL-8517) --- apps/web/src/chat/deployable-package.test.ts | 9 +++++++ apps/web/src/chat/deployable-package.ts | 28 +++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/apps/web/src/chat/deployable-package.test.ts b/apps/web/src/chat/deployable-package.test.ts index a16ab655d..b995608e7 100644 --- a/apps/web/src/chat/deployable-package.test.ts +++ b/apps/web/src/chat/deployable-package.test.ts @@ -21,6 +21,15 @@ describe("deployablePackageFromBody", () => { expect(result?.pkg.name).toBe("Echo"); }); + test("reads a comment label with a path prefix on the fence's first line", () => { + const body = + `Here is the package:\n\n\`\`\`json\n// Scribe/definition.json\n${DEFINITION_JSON}\n\`\`\`\n\n` + + `\`\`\`json\n# Scribe/package.json\n${PACKAGE_JSON}\n\`\`\`\n\nPress Deploy.`; + const result = deployablePackageFromBody(body); + expect(result?.pkg.name).toBe("Echo"); + expect(result?.strippedBody).toBe("Here is the package:\n\nPress Deploy."); + }); + test("is null when only one of the two files is present", () => { const body = `package.json\n\`\`\`\n${PACKAGE_JSON}\n\`\`\``; expect(deployablePackageFromBody(body)).toBeNull(); diff --git a/apps/web/src/chat/deployable-package.ts b/apps/web/src/chat/deployable-package.ts index e56a20fee..bdbf1167b 100644 --- a/apps/web/src/chat/deployable-package.ts +++ b/apps/web/src/chat/deployable-package.ts @@ -55,9 +55,18 @@ export function deployablePackage( /** Strips wrapping backticks/colon and returns the canonical file name the * text names, or null when it names neither of the two contract files. */ function namedFile(text: string): string | null { - const stripped = text.trim().replace(/^`+/, "").replace(/`+$/, "").replace(/:$/, "").trim(); - if (/^package\.json$/i.test(stripped)) return PACKAGE_MANIFEST_NAME; - if (/^definition\.json$/i.test(stripped)) return AGENT_DEFINITION_NAME; + // A label may be a comment (`// x`, `# x`) and a path (`Scribe/x`); only + // the final segment names the file. + const stripped = text + .trim() + .replace(/^(\/\/|#)\s*/, "") + .replace(/^`+/, "") + .replace(/`+$/, "") + .replace(/:$/, "") + .trim(); + const base = stripped.split("/").pop() ?? ""; + if (/^package\.json$/i.test(base)) return PACKAGE_MANIFEST_NAME; + if (/^definition\.json$/i.test(base)) return AGENT_DEFINITION_NAME; return null; } @@ -110,6 +119,19 @@ function findNamedFencedBlocks(lines: readonly string[]): Map content.trim() !== ""); + const first = contentLines[firstContent] ?? ""; + if (/^\s*(\/\/|#)/.test(first)) { + const fromComment = namedFile(first); + if (fromComment !== null) { + name = fromComment; + contentLines.splice(0, firstContent + 1); + } + } + } if (name !== null && !found.has(name)) { found.set(name, { content: contentLines.join("\n"),