| author | |
| committer | |
| log | 062b4618c229f3e2f13e44efd8dab8c71ad33dae |
| tree | dcb30a3a57e1db4af496fe06783f3b709c5a799b |
| parent | deb901d6381e0c86ef326946c8fb45ee323db708 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: tsouth89 <tsouth89@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>2 files changed, 36 insertions(+), 1 deletions(-)
packages/shared/src/composerInlineTokens.test.ts+21| ... | ... | @@ -129,4 +129,25 @@ describe("collectComposerInlineTokens", () => { |
| 129 | 129 | }, |
| 130 | 130 | ]); |
| 131 | 131 | }); |
| 132 | ||
| 133 | it("still collects a file link whose label is at the length cap", () => { | |
| 134 | const label = `${"a".repeat(508)}.tsx`; | |
| 135 | const tokens = collectComposerInlineTokens(`see [${label}](src/${label}) ok`); | |
| 136 | ||
| 137 | expect(tokens).toHaveLength(1); | |
| 138 | expect(tokens[0]?.value).toBe(`src/${label}`); | |
| 139 | }); | |
| 140 | ||
| 141 | it("leaves a file link past the label cap as plain text", () => { | |
| 142 | const label = `${"a".repeat(509)}.tsx`; | |
| 143 | expect(collectComposerInlineTokens(`see [${label}](src/${label}) ok`)).toEqual([]); | |
| 144 | }); | |
| 145 | ||
| 146 | it("stays fast on unterminated bracket runs", () => { | |
| 147 | // Unbounded, the label body rescanned the rest of the text from every | |
| 148 | // whitespace: this input took seconds. | |
| 149 | const started = performance.now(); | |
| 150 | expect(collectComposerInlineTokens(" [[".repeat(40_000))).toEqual([]); | |
| 151 | expect(performance.now() - started).toBeLessThan(1_000); | |
| 152 | }); | |
| 132 | 153 | }); |
packages/shared/src/composerInlineTokens.ts+15-1| ... | ... | @@ -20,7 +20,21 @@ export interface CollectComposerInlineTokensOptions { |
| 20 | 20 | |
| 21 | 21 | const SKILL_TOKEN_REGEX = /(^|\s)\$([a-zA-Z][a-zA-Z0-9:_-]*)(?=\s)/g; |
| 22 | 22 | const MENTION_TOKEN_REGEX = /(^|\s)@(?:"((?:\\.|[^"\\])*)"|([^\s@"]+))(?=\s)/g; |
| 23 | const FILE_LINK_TOKEN_REGEX = /(^|\s)\[((?:\\.|[^\]\\])*)\]\(([^)\s]+)\)(?=\s)/g; | |
| 23 | /** | |
| 24 | * The label body is bounded rather than `*`. Unbounded, every whitespace in | |
| 25 | * the composer is a candidate start: the engine scans the rest of the text for | |
| 26 | * a closing `]`, fails, and rescans from the next whitespace — quadratic on | |
| 27 | * input like " [[[[[…". A cap makes each attempt constant-bounded. | |
| 28 | * | |
| 29 | * Only a basename ever survives the `label !== basename` check below, so this | |
| 30 | * cannot reject a link a user could meaningfully write; the longest filename | |
| 31 | * any common filesystem allows is 255. | |
| 32 | */ | |
| 33 | const MAX_FILE_LINK_LABEL_LENGTH = 512; | |
| 34 | const FILE_LINK_TOKEN_REGEX = new RegExp( | |
| 35 | `(^|\\s)\\[((?:\\\\.|[^\\]\\\\]){0,${MAX_FILE_LINK_LABEL_LENGTH}})\\]\\(([^)\\s]+)\\)(?=\\s)`, | |
| 36 | "g", | |
| 37 | ); | |
| 24 | 38 | const URI_SCHEME_REGEX = /^[A-Za-z][A-Za-z0-9+.-]*:/; |
| 25 | 39 | const WINDOWS_DRIVE_PATH_REGEX = /^[A-Za-z]:[\\/]/; |
| 26 | 40 | // Autocomplete emits canonical file links, so ambiguous bare @scope/package text stays a package. |