Repository navigation
Fix capCodePoints returning nearly the full string when maxCodePoints is zero or negative #5537
Description
Description
capCodePoints in packages/core/src/thread-search.ts:599 promises to "Cap a string to at most maxCodePoints code points" (:594-598), but for maxCodePoints <= 0 it returns nearly the entire input. The truncation at :602 computes codePoints.slice(0, maxCodePoints - 1): with maxCodePoints = 0 the end index is -1, and a negative end counts back from the array end, so only the last code point is dropped and 'hell…' (5 code points) comes back for a cap of 0. Negative caps behave the same way (-2 keeps all but 3 code points). The cap is therefore completely bypassed exactly at the boundary where bounding matters most. The two sibling truncation helpers handle this boundary correctly (sanitizeUnicodeText in packages/core/src/text-sanitize.ts:101 returns just the suffix for a 0 cap; truncateUtf16Safe at :131 returns '' for maxUnits <= 0), so this is also inconsistent within the repo. Current callers pass SNIPPET_MAX_CODE_POINTS = 240 (thread-search.ts:71,268,326), so the production path is unaffected today, but any caller relying on the documented bound with a zero/negative cap (e.g. a disabled-snippet or zero-budget path) gets an unbounded string back, defeating the TOTAL_PAYLOAD_CAP_BYTES accounting the snippet feeds.
Reproduce
- From the repository root, run the following (it reads
capCodePointsverbatim out ofpackages/core/src/thread-search.tsand evaluates it, so no build is needed):
rtk node -e 'const fs=require("node:fs");const src=fs.readFileSync("packages/core/src/thread-search.ts","utf8");const s=src.indexOf("export function capCodePoints");let b=src.indexOf("{",s);let d=0;let e=b;for(;e<src.length;e++){const c=src[e];if(c==="{")d++;if(c==="}"){d--;if(d===0)break;}}const fn=src.slice(s,e+1).replace("export ","").replace("(value: string, maxCodePoints: number): string","(value, maxCodePoints)");const f=new Function(fn+";return capCodePoints;")();const rows=[["hello",0],["0123456789".repeat(4),0],["hello",-2],["hello",1]].map(function(a){const o=f(a[0],a[1]);return JSON.stringify(a[0])+" @"+a[1]+" => "+JSON.stringify(o)+" ("+Array.from(o).length+"pts)"});console.log(rows.join("\n"))'- Observed output:
"hello" @0 => "hell…" (5pts)
"0123456789012345678901234567890123456789" @0 => "012345678901234567890123456789012345678…" (40pts)
"hello" @-2 => "he…" (3pts)
"hello" @1 => "…" (1pts)
The @1 control case is correct (1 code point); the @0 and @-2 cases return far more code points than the cap allows.
Expected
capCodePoints(value, 0) on a non-empty string should return "…" (and "" for an empty input, which already short-circuits at :601); negative caps should behave the same as 0. Smallest fix: clamp the slice end so it can never go negative, e.g. codePoints.slice(0, Math.max(0, maxCodePoints - 1)).join('') + '…' at thread-search.ts:602, matching the 0-cap behavior of sanitizeUnicodeText.
Checklist
- Searched 244 open issues -- no capCodePoints/snippet-cap report
- File:line + repro provided
Distinct from fix(desktop): stop history search after enough matches #5524, which limits how many history-search results are collected, not how a snippet string is capped to a code-point budget -- it does not touch capCodePoints or any truncation boundary.
Activity
ggbdpq commentedon Sep 29, 2026
take
ggbdpq commentedon Sep 29, 2026
The symbol this issue targets no longer exists on main:
capCodePointslived inpackages/core/src/thread-search.ts, which the TypeScript thread-search stack removal (8d82007, 2026-09-28) deleted along with its callers.The boundary bug itself survives in the sibling the issue cites as the correct one:
sanitizeUnicodeText(packages/core/src/text-sanitize.ts) handles a zero cap correctly but treats a negative cap as uncapped - the loop's early break never fires, the whole input is expanded into the points array, andslice(0, negative)counts back from the end, so'hello'@-2 came back as'hel…'; an empty input even came back as the'…'suffix instead of'', contradicting the function's own JSDoc contract.A fix is incoming as a PR: clamp the cap with
Math.max(0, …)(same boundary contract astruncateUtf16Safe'smaxUnits <= 0guard), keeping the expected semantics from this issue - zero and negative caps return suffix-only for non-empty input and''for empty.fix(core): clamp sanitizeUnicodeText's code-point cap to zero (apache…