Skip to content

Fix capCodePoints returning nearly the full string when maxCodePoints is zero or negative #5537

Description

@pamod-madubashana

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

  1. From the repository root, run the following (it reads capCodePoints verbatim out of packages/core/src/thread-search.ts and 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"))'
  1. 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions