From decd6975077258fbb9a4e5b23a76f9b2102e273f Mon Sep 17 00:00:00 2001 From: Alex Vallone <39172404+amv146@users.noreply.github.com> Date: Tue, 3 Nov 2026 10:28:46 -0800 Subject: [PATCH 1/3] fix(composer): open PR completion after opening brackets Recognize Unicode opening punctuation before # and share PR trigger matching across web, desktop, and mobile. Preserve surrounding brackets when inserting a completion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit f9430b73ec1f07e66257e1f5a866d543ab7b867d) --- apps/web/src/composer-logic.ts | 17 +++---- packages/shared/src/composerTrigger.test.ts | 49 +++++++++++++++++++++ packages/shared/src/composerTrigger.ts | 22 +++++---- 3 files changed, 70 insertions(+), 18 deletions(-) diff --git a/apps/web/src/composer-logic.ts b/apps/web/src/composer-logic.ts index 00196d63497ea..fea59bc60c164 100644 --- a/apps/web/src/composer-logic.ts +++ b/apps/web/src/composer-logic.ts @@ -9,7 +9,11 @@ import { serializeAssistantCitation, withAssistantCitationComment, } from "@t3tools/shared/assistantCitations"; -import { isCurrencyAmountQuery, isPathWord } from "@t3tools/shared/composerTrigger"; +import { + detectComposerPullRequestTrigger, + isCurrencyAmountQuery, + isPathWord, +} from "@t3tools/shared/composerTrigger"; import { splitPromptIntoComposerSegments, type ComposerPromptSegment, @@ -261,15 +265,8 @@ export function detectComposerTrigger(text: string, cursorInput: number): Compos rangeEnd: cursor, }; } - const pullRequestMatch = /^#([\p{L}\p{N}][\p{L}\p{N}_-]*)?$/u.exec(token); - if (pullRequestMatch) { - return { - kind: "pull-request", - query: pullRequestMatch[1] ?? "", - rangeStart: tokenStart, - rangeEnd: cursor, - }; - } + const pullRequestTrigger = detectComposerPullRequestTrigger(token, cursor); + if (pullRequestTrigger) return pullRequestTrigger; const skillPrefix = /^\p{Sc}/u.exec(token); if (skillPrefix) { if (isCurrencyAmountQuery(token.slice(skillPrefix[0].length))) return null; diff --git a/packages/shared/src/composerTrigger.test.ts b/packages/shared/src/composerTrigger.test.ts index 72ff3db848cf5..4241903a92d1a 100644 --- a/packages/shared/src/composerTrigger.test.ts +++ b/packages/shared/src/composerTrigger.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "vite-plus/test"; import { detectComposerTrigger, parseSideChatCommand, + replaceTextRange, serializeComposerFileLink, } from "./composerTrigger.ts"; @@ -46,6 +47,54 @@ describe("detectComposerTrigger", () => { (text) => expect(detectComposerTrigger(text, text.length)).toBeNull(), ); + it.each([ + ["#", "", "", ""], + ["Compare #8737", "Compare ", "8737", ""], + ["(#", "(", "", ")"], + ["Compare (#8737", "Compare (", "8737", ")"], + ["Compare(#fix-name", "Compare(", "fix-name", ")"], + ["((#fix_name", "((", "fix_name", "))"], + ["(#修正", "(", "修正", ")"], + ["[#", "[", "", "]"], + ["Compare [#8737", "Compare [", "8737", "]"], + ["{#fix-name", "{", "fix-name", "}"], + ["【#修正", "【", "修正", "】"], + ["(#8737", "(", "8737", ")"], + ])( + "detects the pull request in %j without replacing surrounding brackets", + (text, prefix, query, suffix) => { + const prompt = `${text}${suffix}`; + const trigger = detectComposerTrigger(prompt, text.length); + expect(trigger).toEqual({ + kind: "pull-request", + query, + rangeStart: prefix.length, + rangeEnd: text.length, + }); + expect(replaceTextRange(prompt, trigger!.rangeStart, trigger!.rangeEnd, "PR")).toEqual({ + text: `${prefix}PR${suffix}`, + cursor: prefix.length + 2, + }); + }, + ); + + it.each([ + "# Heading", + "issue#123", + "https://example.com/#123", + "(issue#123", + "(##123", + "(#123)", + "(#fix.name", + "[issue#123", + "[#123]", + "[#_fix", + "]#123", + ".#123", + ])("does not detect a pull request in %j", (text) => { + expect(detectComposerTrigger(text, text.length)).toBeNull(); + }); + it.each(["$", "€", "𑿝"])("detects %s skill prefixes and their source range", (prefix) => { const text = `Use ${prefix}review`; expect(detectComposerTrigger(text, text.length)).toEqual({ diff --git a/packages/shared/src/composerTrigger.ts b/packages/shared/src/composerTrigger.ts index 80292492154a8..cc15fb0f27949 100644 --- a/packages/shared/src/composerTrigger.ts +++ b/packages/shared/src/composerTrigger.ts @@ -68,6 +68,18 @@ export function isCurrencyAmountQuery(query: string): boolean { return /^\.?[0-9][0-9,._]*(?:[kKmMbBtT]|[eE][0-9]+)?[.,;:!?)]*$/.test(query); } +export function detectComposerPullRequestTrigger(token: string, cursor: number) { + const match = /(?:^|\p{Ps})#([\p{L}\p{N}][\p{L}\p{N}_-]*)?$/u.exec(token); + if (!match) return null; + const query = match[1] ?? ""; + return { + kind: "pull-request" as const, + query, + rangeStart: cursor - query.length - 1, + rangeEnd: cursor, + }; +} + export function detectComposerTrigger( text: string, cursorInput: number, @@ -128,14 +140,8 @@ export function detectComposerTrigger( rangeEnd: cursor, }; } - const pullRequestMatch = /^#([\p{L}\p{N}][\p{L}\p{N}_-]*)?$/u.exec(token); - if (pullRequestMatch) - return { - kind: "pull-request", - query: pullRequestMatch[1] ?? "", - rangeStart: tokenStart, - rangeEnd: cursor, - }; + const pullRequestTrigger = detectComposerPullRequestTrigger(token, cursor); + if (pullRequestTrigger) return pullRequestTrigger; const skillPrefix = /^\p{Sc}/u.exec(token); if (skillPrefix) { if (isCurrencyAmountQuery(token.slice(skillPrefix[0].length))) return null; From 676d640033d92f18011ec8ee68e99dc5b2e55c37 Mon Sep 17 00:00:00 2001 From: Alex Vallone <39172404+amv146@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:23:32 -0800 Subject: [PATCH 2/3] fix(composer): preserve brackets around inserted PR references Exclude opening square brackets from context-link labels so inserting a PR inside square brackets does not consume the surrounding opening bracket. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit b52b804d8ba579174193a0426ecc05b9bdb90214) --- .../shared/src/composerContextReferences.test.ts | 14 ++++++++++++++ packages/shared/src/composerContextReferences.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/composerContextReferences.test.ts b/packages/shared/src/composerContextReferences.test.ts index 6f27538f3728a..4a414dd2fbca8 100644 --- a/packages/shared/src/composerContextReferences.test.ts +++ b/packages/shared/src/composerContextReferences.test.ts @@ -78,6 +78,20 @@ describe("labels and reference links", () => { for (const occurrence of occurrences) { expect(text.slice(occurrence.start, occurrence.end)).toBe(occurrence.source); } + const reference = "[#30296](t3-context://v1/review-comment/ctx_pr)"; + for (const prefix of ["See [", "See [["]) { + expect(collectComposerContextReferences(`${prefix}${reference} ]`)).toEqual([ + { + kind: "review-comment", + contextId: "ctx_pr", + label: "#30296", + image: false, + source: reference, + start: prefix.length, + end: prefix.length + reference.length, + }, + ]); + } }); it("recognizes a prompt made only of image references", () => { diff --git a/packages/shared/src/composerContextReferences.ts b/packages/shared/src/composerContextReferences.ts index a7aeb1373ae10..a4367ff6402b3 100644 --- a/packages/shared/src/composerContextReferences.ts +++ b/packages/shared/src/composerContextReferences.ts @@ -18,7 +18,7 @@ const CONTEXT_KIND_PATTERN = /^[a-z][a-z0-9-]{0,39}$/; const CONTEXT_ID_PATTERN = /^[a-z0-9_-]{1,128}$/i; const MAX_LINK_LABEL_LENGTH = 512; const CONTEXT_LINK = new RegExp( - String.raw`(!?)\[([^\]\n]{0,${MAX_LINK_LABEL_LENGTH}})\]\((${COMPOSER_CONTEXT_HREF_PREFIX}[^\s)]{1,200})\)`, + String.raw`(!?)\[([^[\]\n]{0,${MAX_LINK_LABEL_LENGTH}})\]\((${COMPOSER_CONTEXT_HREF_PREFIX}[^\s)]{1,200})\)`, "g", ); From 6c6390c5ec2d420399e4a1e216b58a41b566e100 Mon Sep 17 00:00:00 2001 From: Alex Vallone Date: Sun, 8 Nov 2026 11:49:50 -0800 Subject: [PATCH 3/3] fix(ci): defer fork checkout credential cleanup Keep checkout default read-only credentials until post-job cleanup, avoiding fatal traversal of undeclared vendored gitlinks before validation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/fork-pr.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fork-pr.yml b/.github/workflows/fork-pr.yml index 45b3a18d8a54f..47ed5956184b4 100644 --- a/.github/workflows/fork-pr.yml +++ b/.github/workflows/fork-pr.yml @@ -28,7 +28,9 @@ jobs: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - persist-credentials: false + # Immediate auth cleanup traverses vendored gitlinks without .gitmodules. + # Keep the default read-only credentials until checkout's post-job cleanup. + persist-credentials: true sparse-checkout: | /* !/.repos/