Skip to content

Cut a long page's text between characters too, not only a control's name - #578

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
Hotragn:cut-the-page-text-between-characters-too
Sep 16, 2026
Merged

davidmckayv merged 2 commits into
CopilotKit:mainfrom
Hotragn:cut-the-page-text-between-characters-too

Conversation

@Hotragn

@Hotragn Hotragn commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

What this changes

#539 fixed the emoji-safe cut for a control's accessible name and value in a page snapshot. It left the third site — the readable page text a navigation hands back — and noted it as not covered because agent-computer/src/index.ts imports Playwright at load, so there was no test to run against it.

That site is the same bug at thirty times the length. Measured at the real limit, on main:

main   : last unit d83d  JSON tail "aaa\ud83d"  UTF-8 tail efbfbd  length 6000
branch : last unit 61    JSON tail "aaaa"       UTF-8 tail 61      length 5999

efbfbd is U+FFFD. So the Bot reads a broken character that is not on the page, in the text it is about to answer a question from.

More likely to bite here than in a control's name, for the reason the limit is bigger. A 200-unit accessible name rarely reaches an emoji at all. 6000 units of somebody's page usually passes through several, and whether the cut lands mid-character is decided by whatever happened to be above it.

Where it runs

  • New state that outlives a request? None. One call swapped for another over a string already in hand.
  • What happens on the second replica? Nothing changes; no state.
  • Anything serialised? The page text, as before — one code unit shorter in the case that was broken.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

Untouched. A navigation is read-only, nothing acting changes, and only the text of the result differs.

Changelog

  • A line under Unreleased, saying it now matches what a control's name and value already do.

Proof

The fix is one word and one call, because the rule was already written and already correct — it just was not being called. cutAtCodeUnits becomes export, and index.ts:3 already imported from ./aria-snapshot, so there is no new coupling:

-function cutAtCodeUnits(text: string, limit: number): string {
+export function cutAtCodeUnits(text: string, limit: number): string {

-import { parseAriaSnapshot, type SnapshotElement } from "./aria-snapshot";
+import { cutAtCodeUnits, parseAriaSnapshot, type SnapshotElement } from "./aria-snapshot";

-    text: collapsed.slice(0, TEXT_EXTRACT_LIMIT),
+    text: cutAtCodeUnits(collapsed, TEXT_EXTRACT_LIMIT),

It stays declared in the parser module rather than moving beside its caller, and that is deliberate: aria-snapshot.ts imports no Playwright, index.ts does at load, so a helper declared beside the caller could not be tested at all — which is exactly why this site was left out of #539.

Five tests in agent-computer/tests/aria-snapshot.test.ts, at 6000 rather than 200, pinning the rule where it is declared:

cd agent-computer && bun test tests/aria-snapshot.test.ts
 35 pass, 0 fail          (30 before, 5 added)

cd agent-computer && bunx tsc --noEmit       # exit 0, no output
bunx @biomejs/biome check <the four changed files>   # no fixes

Being straight about what those tests do and do not prove. They do not fail on main, and they are not regression tests for this bug — the rule was already right, so exporting it on main would make them pass there too. What they pin is the rule at the limit this caller uses, including the two edges worth having: a character that ends exactly at the limit is kept whole (so the guard cannot cost a whole emoji every time), and a string ending on a low surrogate is kept (so the guard looks only for an unpaired high half). The evidence for the defect itself is the measurement above, which is the same form #539 used, because readablePageText cannot be reached from a test in this package.

What is not covered

  • readablePageText itself still has no test, and this does not add one. The Page it takes comes from Playwright, imported at module load. Reaching it means either a browser in CI or splitting the pure tail of that function out of index.ts — a structural change, and not one to smuggle into a two-line fix.
  • I have not swept for a fourth site. grep -rn "slice(0, [A-Z_0-9]" agent-computer/src returns only the two Cut a long control name or value in a page snapshot between characters, not through an emoji #539 fixed and this one, and nothing else in that package cuts a string at a constant.

🤖 Generated with Claude Code

CopilotKit#539 fixed this for a control's accessible name and value in a page snapshot and left the readable
page text a navigation returns, which is the same bug at thirty times the length. `slice` counts
UTF-16 code units, an emoji is two, and a limit landing between the halves leaves a lone high
surrogate as the last thing the Bot is handed. Measured at the real limit on main:

  main   : last unit d83d  JSON tail "aaa\ud83d"  UTF-8 tail efbfbd  length 6000
  branch : last unit 61    JSON tail "aaaa"       UTF-8 tail 61      length 5999

`efbfbd` is U+FFFD, so the Bot reads a broken character that is not on the page, in text it is about
to answer a question from.

More likely to bite here than in a control's name, for the reason the limit is bigger. A 200-unit
name rarely reaches an emoji; 6000 units of somebody's page usually passes through several, and
whether the cut lands mid-character is decided by whatever happened to be above it.

The rule was already written and already correct — it just was not being called. `cutAtCodeUnits` is
exported rather than duplicated, and `index.ts` already imported from `aria-snapshot.ts`, so this is
one word and one call. It stays declared in the parser module because that module imports no
Playwright: `index.ts` does, at load, so a helper declared beside its caller could not be tested at
all, which is why the caller was left out of CopilotKit#539 in the first place.

`truncated` is unchanged and needs no thought: dropping one more code unit cannot make an over-limit
string fit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@davidmckayv davidmckayv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code-verified clean; CI green on this sha.

@davidmckayv
davidmckayv merged commit cdd1b9d into CopilotKit:main Sep 16, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants