From 5223a408538706172e70cfdcb12de8fd4dc70a0a Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:03:35 -0800 Subject: [PATCH 1/9] fix(server): PR watch wakes quote a bot comment's text, not its HTML CodeRabbit review comments open with a badge link, so the 200-character excerpt in a PR watch wake was all markup. snippet() now drops HTML element tags before it collapses whitespace. It only matches real element names, so prose like Promise stays. --- .../orchestration-v2/pullRequestWatch.test.ts | 23 +++++++++++++++++++ .../src/orchestration-v2/pullRequestWatch.ts | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts index 6ab0191649490..a263f70ff1890 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts @@ -352,6 +352,29 @@ describe("evaluatePullRequestWatch", () => { assert.include(text, `"${"x".repeat(196)}..."`); }); + it("quotes a remark's text instead of its leading HTML, and keeps generics in prose", () => { + const badge = + 'Review'; + const report = evaluatePullRequestWatch(watch(), detail(), [ + remark( + "coderabbitai", + "2026-10-02T12:06:00Z", + `${badge}\n\n_⚠️ Potential issue_ | _🟠 Major_\n\n**Guard the empty list.**`, + ), + remark("reviewer", "2026-10-02T12:07:00Z", "Return Promise here, not Promise."), + ]); + const { text } = pullRequestWatchMessage({ + number: 1, + url: "https://github.com/o/r/pull/1", + baseBranch: "main", + headSha: null, + report, + }); + assert.include(text, `"_⚠️ Potential issue_ | _🟠 Major_ **Guard the empty list.**"`); + assert.include(text, `"Return Promise here, not Promise."`); + assert.notInclude(text, " { const conflicting = detail({ mergeability: "conflicting" }); const first = evaluatePullRequestWatch(watch(), conflicting, noRemarks); diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.ts b/apps/server/src/orchestration-v2/pullRequestWatch.ts index f4c0000847236..04a23e73b6376 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.ts @@ -13,6 +13,9 @@ export const PULL_REQUEST_WATCH_WAKE_LIMIT = 10; export const PULL_REQUEST_WATCH_NO_CHECKS_MS = 15 * 60_000; const LISTED_ITEMS = 10; const SNIPPET_LENGTH = 200; +// Only real element names, so prose like Promise survives. Review bots open with badge markup. +const HTML_TAG = + /<\/?(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])[^>]*>/gi; export type PullRequestWatchChange = | { readonly kind: "checks-failed"; readonly failed: ReadonlyArray } @@ -161,6 +164,7 @@ export function evaluatePullRequestWatch( function snippet(body: string): string { const text = body .replaceAll(//g, " ") + .replaceAll(HTML_TAG, " ") .replaceAll(/\s+/g, " ") .trim(); if (text.length <= SNIPPET_LENGTH) return text; From 4c717d64152fbe27f07180efb6ca07ee1e28786c Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:10:07 -0800 Subject: [PATCH 2/9] fix(server): PR watch excerpts keep tags inside Markdown code A quoted `
` in a code span named the element the reviewer meant. Strip tags only outside fenced blocks and inline code. --- .../src/orchestration-v2/pullRequestWatch.test.ts | 4 +++- apps/server/src/orchestration-v2/pullRequestWatch.ts | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts index a263f70ff1890..18b586c572dea 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts @@ -352,7 +352,7 @@ describe("evaluatePullRequestWatch", () => { assert.include(text, `"${"x".repeat(196)}..."`); }); - it("quotes a remark's text instead of its leading HTML, and keeps generics in prose", () => { + it("quotes a remark's text instead of its leading HTML, and keeps tags in prose and code", () => { const badge = 'Review'; const report = evaluatePullRequestWatch(watch(), detail(), [ @@ -362,6 +362,7 @@ describe("evaluatePullRequestWatch", () => { `${badge}\n\n_⚠️ Potential issue_ | _🟠 Major_\n\n**Guard the empty list.**`, ), remark("reviewer", "2026-10-02T12:07:00Z", "Return Promise here, not Promise."), + remark("designer", "2026-10-02T12:08:00Z", "Replace `
` with ``."), ]); const { text } = pullRequestWatchMessage({ number: 1, @@ -372,6 +373,7 @@ describe("evaluatePullRequestWatch", () => { }); assert.include(text, `"_⚠️ Potential issue_ | _🟠 Major_ **Guard the empty list.**"`); assert.include(text, `"Return Promise here, not Promise."`); + assert.include(text, '"Replace `
` with ``."'); assert.notInclude(text, " survives. Review bots open with badge markup. const HTML_TAG = /<\/?(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])[^>]*>/gi; +const MARKDOWN_CODE = /(```[\s\S]*?```|`[^`]*`)/; + +// Split keeps the captured code regions at odd indexes, and those keep their tags. +const withoutHtmlTags = (text: string) => + text + .split(MARKDOWN_CODE) + .map((part, index) => (index % 2 === 1 ? part : part.replaceAll(HTML_TAG, " "))) + .join(""); export type PullRequestWatchChange = | { readonly kind: "checks-failed"; readonly failed: ReadonlyArray } @@ -162,9 +170,7 @@ export function evaluatePullRequestWatch( } function snippet(body: string): string { - const text = body - .replaceAll(//g, " ") - .replaceAll(HTML_TAG, " ") + const text = withoutHtmlTags(body.replaceAll(//g, " ")) .replaceAll(/\s+/g, " ") .trim(); if (text.length <= SNIPPET_LENGTH) return text; From dbd5bded390fe5bf1a4239aa54915b22aa058f49 Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:12:54 -0800 Subject: [PATCH 3/9] fix(server): PR watch excerpts keep type parameters like Promise Case-insensitive matching treated , , and

as HTML elements. Bot markup is lowercase, so match element names case-sensitively. --- apps/server/src/orchestration-v2/pullRequestWatch.test.ts | 4 ++-- apps/server/src/orchestration-v2/pullRequestWatch.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts index 18b586c572dea..925ccf503d0f3 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts @@ -361,7 +361,7 @@ describe("evaluatePullRequestWatch", () => { "2026-10-02T12:06:00Z", `${badge}\n\n_⚠️ Potential issue_ | _🟠 Major_\n\n**Guard the empty list.**`, ), - remark("reviewer", "2026-10-02T12:07:00Z", "Return Promise here, not Promise."), + remark("reviewer", "2026-10-02T12:07:00Z", "Return Promise here, not Promise."), remark("designer", "2026-10-02T12:08:00Z", "Replace `

` with ``."), ]); const { text } = pullRequestWatchMessage({ @@ -372,7 +372,7 @@ describe("evaluatePullRequestWatch", () => { report, }); assert.include(text, `"_⚠️ Potential issue_ | _🟠 Major_ **Guard the empty list.**"`); - assert.include(text, `"Return Promise here, not Promise."`); + assert.include(text, `"Return Promise here, not Promise."`); assert.include(text, '"Replace `
` with ``."'); assert.notInclude(text, " survives. Review bots open with badge markup. +// Review bots open with badge markup. Only lowercase element names match, so Promise +// and type parameters like Promise survive. const HTML_TAG = - /<\/?(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])[^>]*>/gi; + /<\/?(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])[^>]*>/g; const MARKDOWN_CODE = /(```[\s\S]*?```|`[^`]*`)/; // Split keeps the captured code regions at odd indexes, and those keep their tags. From b0541d9c80fe4ad74f379cfef72c477df73fde84 Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:14:10 -0800 Subject: [PATCH 4/9] fix(server): PR watch excerpts strip tags with > in a quoted attribute A > inside a quoted attribute ended the tag match early and left the rest of the tag in the excerpt. --- apps/server/src/orchestration-v2/pullRequestWatch.test.ts | 2 +- apps/server/src/orchestration-v2/pullRequestWatch.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts index 925ccf503d0f3..e4c165cccecad 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts @@ -354,7 +354,7 @@ describe("evaluatePullRequestWatch", () => { it("quotes a remark's text instead of its leading HTML, and keeps tags in prose and code", () => { const badge = - 'Review'; + 'Severity > Minor'; const report = evaluatePullRequestWatch(watch(), detail(), [ remark( "coderabbitai", diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.ts b/apps/server/src/orchestration-v2/pullRequestWatch.ts index a586ce629c35a..88c2198313d84 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.ts @@ -16,7 +16,7 @@ const SNIPPET_LENGTH = 200; // Review bots open with badge markup. Only lowercase element names match, so Promise // and type parameters like Promise survive. const HTML_TAG = - /<\/?(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])[^>]*>/g; + /<\/?(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>/g; const MARKDOWN_CODE = /(```[\s\S]*?```|`[^`]*`)/; // Split keeps the captured code regions at odd indexes, and those keep their tags. From 4781e189368b683c44a1047162d163409bba381b Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:26:03 -0800 Subject: [PATCH 5/9] fix(server): PR watch excerpts scan a bounded prefix in one pass Remark bodies are untrusted and can reach 64 KB. Unclosed openers made the markup regexes backtrack, about 1.9 s on 64 KB of ' stays. --- .../orchestration-v2/pullRequestWatch.test.ts | 18 +++++++++---- .../src/orchestration-v2/pullRequestWatch.ts | 25 +++++++++---------- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts index e4c165cccecad..d5ac2c318aff3 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts @@ -354,15 +354,23 @@ describe("evaluatePullRequestWatch", () => { it("quotes a remark's text instead of its leading HTML, and keeps tags in prose and code", () => { const badge = - 'Severity > Minor'; + 'Severity > `Minor`'; const report = evaluatePullRequestWatch(watch(), detail(), [ remark( "coderabbitai", "2026-10-02T12:06:00Z", `${badge}\n\n_⚠️ Potential issue_ | _🟠 Major_\n\n**Guard the empty list.**`, ), - remark("reviewer", "2026-10-02T12:07:00Z", "Return Promise here, not Promise."), - remark("designer", "2026-10-02T12:08:00Z", "Replace `
` with ``."), + remark( + "reviewer", + "2026-10-02T12:07:00Z", + "Return Promise, not Promise or Promise.", + ), + remark( + "designer", + "2026-10-02T12:08:00Z", + "Replace `
` with ````:\n~~~html\n

Done

\n~~~", + ), ]); const { text } = pullRequestWatchMessage({ number: 1, @@ -372,8 +380,8 @@ describe("evaluatePullRequestWatch", () => { report, }); assert.include(text, `"_⚠️ Potential issue_ | _🟠 Major_ **Guard the empty list.**"`); - assert.include(text, `"Return Promise here, not Promise
."`); - assert.include(text, '"Replace `
` with ``."'); + assert.include(text, `"Return Promise, not Promise or Promise."`); + assert.include(text, '"Replace `
` with ````: ~~~html

Done

~~~"'); assert.notInclude(text, " -// and type parameters like Promise
survive. -const HTML_TAG = - /<\/?(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>/g; -const MARKDOWN_CODE = /(```[\s\S]*?```|`[^`]*`)/; - -// Split keeps the captured code regions at odd indexes, and those keep their tags. -const withoutHtmlTags = (text: string) => - text - .split(MARKDOWN_CODE) - .map((part, index) => (index % 2 === 1 ? part : part.replaceAll(HTML_TAG, " "))) - .join(""); +// Remark bodies are untrusted and run to 64 KB, and unclosed openers make the markup pass +// backtrack, so it only reads far enough to fill an excerpt. +const SNIPPET_SCAN_LENGTH = 2_000; +// One left-to-right pass: code spans keep their tags, and backticks inside a tag's attributes are +// not code. Element names are lowercase, and one-letter ones need attributes, so Promise, +// Promise and Promise stay. +const MARKUP = + /(?(?`+)[\s\S]*?(?(?!`)|(?~{3,})[\s\S]*?\k)||$)|<\/(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)\s*>|<(?:blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|img|li|ol|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>|<(?:a|b|i|p)\s(?:[^>"']|"[^"]*"|'[^']*')*>/g; export type PullRequestWatchChange = | { readonly kind: "checks-failed"; readonly failed: ReadonlyArray } @@ -171,7 +167,10 @@ export function evaluatePullRequestWatch( } function snippet(body: string): string { - const text = withoutHtmlTags(body.replaceAll(//g, " ")) + const text = body + .slice(0, SNIPPET_SCAN_LENGTH) + .toWellFormed() + .replaceAll(MARKUP, (match, code: string | undefined) => (code === undefined ? " " : match)) .replaceAll(/\s+/g, " ") .trim(); if (text.length <= SNIPPET_LENGTH) return text; From 1300dffe6caa90c6fe0ce41a3e92baf9d54e69ad Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:29:02 -0800 Subject: [PATCH 6/9] fix(server): PR watch code spans match each delimiter run once A run of backticks could backtrack to every shorter length from every start, which is cubic: about 320 ms for 800 backticks. A lookahead now pins each delimiter run at full length, and a match can't start inside a run. --- apps/server/src/orchestration-v2/pullRequestWatch.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.ts b/apps/server/src/orchestration-v2/pullRequestWatch.ts index 6691180c7dd70..86febe77d3890 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.ts @@ -16,11 +16,11 @@ const SNIPPET_LENGTH = 200; // Remark bodies are untrusted and run to 64 KB, and unclosed openers make the markup pass // backtrack, so it only reads far enough to fill an excerpt. const SNIPPET_SCAN_LENGTH = 2_000; -// One left-to-right pass: code spans keep their tags, and backticks inside a tag's attributes are -// not code. Element names are lowercase, and one-letter ones need attributes, so Promise, -// Promise and Promise stay. +// One left-to-right pass keeps code spans' tags and reads attribute backticks as part of the tag. +// A lookahead pins each delimiter run at full length, so a long run can't backtrack cubically. +// Element names are lowercase, and one-letter ones need attributes, so Promise and stay. const MARKUP = - /(?(?`+)[\s\S]*?(?(?!`)|(?~{3,})[\s\S]*?\k)||$)|<\/(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)\s*>|<(?:blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|img|li|ol|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>|<(?:a|b|i|p)\s(?:[^>"']|"[^"]*"|'[^']*')*>/g; + /(?(?`+))\k[\s\S]*?(?(?!`)|(?~{3,}))\k[\s\S]*?\k)||$)|<\/(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)\s*>|<(?:blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|img|li|ol|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>|<(?:a|b|i|p)\s(?:[^>"']|"[^"]*"|'[^']*')*>/g; export type PullRequestWatchChange = | { readonly kind: "checks-failed"; readonly failed: ReadonlyArray } From bcaeabbb8fd442ebf71bd28efdaffb8b5f5185b8 Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:35:19 -0800 Subject: [PATCH 7/9] fix(server): PR watch excerpts survive long HTML comments and bare

is removed when its closing tag follows, so

text

reads as text and Promise stays. --- .../orchestration-v2/pullRequestWatch.test.ts | 8 +++++++ .../src/orchestration-v2/pullRequestWatch.ts | 22 +++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts index d5ac2c318aff3..905abdefafea0 100644 --- a/apps/server/src/orchestration-v2/pullRequestWatch.test.ts +++ b/apps/server/src/orchestration-v2/pullRequestWatch.test.ts @@ -371,6 +371,12 @@ describe("evaluatePullRequestWatch", () => { "2026-10-02T12:08:00Z", "Replace `
` with ````:\n~~~html\n

Done

\n~~~", ), + remark("maintainer", "2026-10-02T12:09:00Z", "

Guard this

"), + remark( + "summary-bot", + "2026-10-02T12:10:00Z", + `\nCheck the retry.`, + ), ]); const { text } = pullRequestWatchMessage({ number: 1, @@ -382,6 +388,8 @@ describe("evaluatePullRequestWatch", () => { assert.include(text, `"_⚠️ Potential issue_ | _🟠 Major_ **Guard the empty list.**"`); assert.include(text, `"Return Promise, not Promise
or Promise."`); assert.include(text, '"Replace `
` with ````: ~~~html

Done

~~~"'); + assert.include(text, 'maintainer on src/index.ts: "Guard this"'); + assert.include(text, '"Check the retry."'); assert.notInclude(text, " and
stay. +// Element names are lowercase. A one-letter element needs attributes or a later closing tag, +// so Promise stays. const MARKUP = - /(?(?`+))\k[\s\S]*?(?(?!`)|(?~{3,}))\k[\s\S]*?\k)||$)|<\/(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)\s*>|<(?:blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|img|li|ol|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>|<(?:a|b|i|p)\s(?:[^>"']|"[^"]*"|'[^']*')*>/g; + /(?(?`+))\k[\s\S]*?(?(?!`)|(?~{3,}))\k[\s\S]*?\k)|<(?[abip])>(?=[\s\S]*?<\/\k>)|<\/(?:a|b|blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|i|img|li|ol|p|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)\s*>|<(?:blockquote|br|code|dd|details|div|dl|dt|em|h[1-6]|hr|img|li|ol|picture|pre|source|span|strong|sub|summary|sup|table|tbody|td|th|thead|tr|ul)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>|<(?:a|b|i|p)\s(?:[^>"']|"[^"]*"|'[^']*')*>/g; export type PullRequestWatchChange = | { readonly kind: "checks-failed"; readonly failed: ReadonlyArray } @@ -166,8 +167,21 @@ export function evaluatePullRequestWatch( }; } +// indexOf keeps this linear on unclosed openers, so it can read the whole body. +function withoutHtmlComments(body: string): string { + let text = ""; + let from = 0; + for (let open = body.indexOf("", open + 4); + if (close === -1) return text; + from = close + 3; + } + return text + body.slice(from); +} + function snippet(body: string): string { - const text = body + const text = withoutHtmlComments(body) .slice(0, SNIPPET_SCAN_LENGTH) .toWellFormed() .replaceAll(MARKUP, (match, code: string | undefined) => (code === undefined ? " " : match)) From 3c21386c67ceaa9f61603d4ce4c45242b47435f4 Mon Sep 17 00:00:00 2001 From: Taylor Dolezal <117500+onlydole@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:51:30 -0800 Subject: [PATCH 8/9] fix(server): PR watch excerpts keep text after an unclosed is usually prose such as `", open + 4); - if (close === -1) return text; + // An unclosed opener is usually prose about `\nCheck the retry.`, + `\nCheck the retry.\n ", open + 4); - // An unclosed opener is usually prose about `

tags HTML comments are now removed from the whole body with a linear indexOf scan before the 2,000-character markup pass, so a long comment no longer hides the text after it. A bare , , or