Skip to content

fix(client-runtime): subagent previews no longer end in a broken character - #30391

Open
spoukyii wants to merge 2 commits into
pingdotgg:mainfrom
spoukyii:users/spoukyii/subagent-preview-emoji
Open

spoukyii wants to merge 2 commits into
pingdotgg:mainfrom
spoukyii:users/spoukyii/subagent-preview-emoji

Conversation

@spoukyii

@spoukyii spoukyii commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Problem

subagentDetailPreview in packages/client-runtime/src/state/subagentDisplay.ts builds a one-line preview of a subagent's progress or result. It trims leading whitespace, collapses whitespace runs, and cuts anything longer than 280 UTF-16 code units at 280 code units, then adds …. When an emoji straddles that cut, the slice keeps only the emoji's high surrogate (U+D83D for 😀). The preview then ends in a replacement character (�) right before the …, where it should end on the last whole character before the emoji.

Where the broken end shows:

  • Web, the subagent hover card (SubagentTooltipContent). It shows the preview through MiddleTruncate, which always keeps the last 10 code points in a shrink-0 span, so the � stays visible. The card opens from subagent rows in the chat timeline (V2LifecycleRow), the thread relationships panel (ThreadRelationshipsControl), workflow member rows (ThreadLineageWorkflowRow) and the workflow opening block (WorkflowOpeningBlock).

Where it is clipped, so it looks the same before and after this fix:

  • Mobile, Agents sheet, workflow member cards: the detail line is numberOfLines={1} (ThreadWorkflowGroup.tsx), so its end never shows.
  • Mobile, Agents sheet, subagent rows: the detail line is numberOfLines={3} (SubagentRow.tsx). Its end shows only when the whole preview fits in three lines, which takes over 90 characters per line, so not on a phone.

Expected: the cut never splits a surrogate pair, as in the context chip labels #29158 fixed.

Change

When the 280-code-unit cut would land between a high and a low surrogate, it steps back one code unit, so the emoji is dropped whole. This is the same check #29158 added to context chip labels and #29563 added to t3 thread start titles. The 280-code-unit limit, the whitespace collapsing, the … suffix and every preview that does not split a pair are unchanged.

The cut still works per code point, not per grapheme cluster. A multi-code-point emoji, such as a flag, a skin-tone modifier or a ZWJ family, can still be split between its code points. It never leaves a lone surrogate. That matches the precedents.

The existing test already had a case with an emoji on the cut, added by perf PR #21277, and it expected the lone high surrogate. That was pinned old behaviour, not a choice. #21277's body says: "Selection priority, whitespace collapsing, trailing-space handling, ellipsis, null fallback, and the existing UTF-16 truncation boundary are retained." Its goal was output equal to the old code's, so it pinned the split. The case now expects the emoji dropped whole.

The shared truncate in packages/shared is not used here. #24611 (open) fixes the same split in it, but this function does not call it, and its arithmetic differs: it trims, cuts at the limit and adds ... (three dots), while this preview keeps its own whitespace handling and adds … (one character), so switching would change the preview's text and length.

Scope and approval

This is a very small, focused fix for an obvious bug, so there is no prior issue. It is the defect #29158 ("context chip labels no longer split an emoji at the 200-character cap") and #29563 ("t3 thread start titles no longer end in a broken character") fixed elsewhere, still present in the shared subagent preview. It applies their check and changes no other behaviour, setting or contract.

Overlap check, by subagentDetailPreview and by file. #17420, #21643 and #27034 touch subagentDisplay.ts, but none changes subagentDetailPreview's cut. #29637 changes how the timeline row cleans a subagent preview, in V2LifecycleRow.logic.ts, not this cut. #24611 changes the shared truncate, which this function does not call.

Verification

packages/client-runtime/src/state/subagentDisplay.test.ts already projects a failed subagent whose result is 279 ASCII characters followed by 😀, so the emoji sits on the cut. Its expected value changes from the 279 characters, the lone high surrogate U+D83D and … to the 279 characters and ….

On main with only the test commit (d79b414), from packages/client-runtime, vp test run src/state/subagentDisplay.test.ts:

 FAIL  src/state/subagentDisplay.test.ts > normalizes Unicode whitespace and ignores a large hidden suffix
      Tests  1 failed | 4 passed (5)

In words: on main the preview is the 279 ASCII characters, the lone high surrogate U+D83D, then …. With the fix it is the 279 ASCII characters, then ….

With the fix:

  • vp test run src/state/subagentDisplay.test.ts in packages/client-runtime: 5 passed.
  • vp run check:changed from the repo root: format, lint, knip, typecheck (7 packages) and tests (2 files) all pass.

To see it on web: in a thread whose agent ran a subagent, let the subagent report a result longer than 280 characters whose first 279 characters are ASCII with no whitespace runs, followed by an emoji and more text. The preview trims leading whitespace and collapses whitespace runs before it counts, so count after that. Hover the subagent's row in the chat timeline. On main the hover card's preview line ends in � before the …. With the fix it ends in ….

What I could not check: the hover card in a running app. The test covers the preview function every surface above renders.

Bug: a subagent preview ends in a broken character when an emoji sits at the 280-unit cut

This was written with Claude Opus 5.5 in Claude Code, driven from T3 Code.

…acter

A subagent's progress or result preview is cut at 280 UTF-16 code units
with "…" added. When an emoji straddled the cut, the preview kept only its
high surrogate and showed a replacement character before the "…". The
web subagent hover cards show the end of that preview.

The cut now steps back one unit when it would land between a high and a
low surrogate, the same check pingdotgg#29158 added to context chip labels and
pingdotgg#29563 added to thread titles. The existing test had pinned the split
half; it now expects the emoji dropped whole.

Bug: a subagent preview ends in a broken character when an emoji sits at the 280-unit cut
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:S 10-29 changed lines (additions + deletions). labels Nov 3, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at adf15a9

Macroscope's review found this PR approvable — Subagent previews avoid ending with half an emoji when truncated. The change is limited to display formatting, with no changes to stored data, product defaults, or external systems.

You can add or adjust custom eligibility rules. Learn more.

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Walkthrough

subagentDetailPreview now avoids splitting a UTF-16 surrogate pair at its 280-code-unit cutoff. The test expects the preview to end after 279 x characters, followed by an ellipsis.

Changes: Subagent preview truncation

Layer / File(s) Summary
Surrogate-safe preview cutoff
packages/client-runtime/src/state/subagentDisplay.ts, packages/client-runtime/src/state/subagentDisplay.test.ts
When the cutoff splits a surrogate pair, subagentDetailPreview shortens the slice by one code unit. The test expects the ellipsis to follow 279 x characters.

Estimated code review effort: 2 (Simple) | ~5 minutes

Change: Bug fix

Possibly related PRs

Suggested labels: size:XS

Suggested reviewers: luke2x, macroscopeapp

Merge Risk: Minimal · up to adf15

No merge-blocking issue was identified in the Unicode preview fix.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: preventing broken characters in subagent previews.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request follows the required structure. It clearly describes the problem, expected behavior, implementation, scope rationale, overlap review, focused tests, broader validation, manual verific…
Full details: Description check

Explanation

The pull request follows the required structure. It clearly describes the problem, expected behavior, implementation, scope rationale, overlap review, focused tests, broader validation, manual verification steps, and the unverified running-app check. It also includes the model and harness used.

Finishing Touches
Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between aee76a8 and adf15a9.

Files selected for processing: 2
  • packages/client-runtime/src/state/subagentDisplay.test.ts
  • packages/client-runtime/src/state/subagentDisplay.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

@maria-rcks

maria-rcks commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Written by gpt-6.1-sol on behalf of Maria

Verified in a real dev environment

Verified unchanged head adf15a992b86fc7d9ecd9c2187305986dae0b9aa on a Linux Boat VM using repository Playwright and /usr/bin/google-chrome.

Ran vp i and vp run dev on github/main (2f8de8d7bf) in a separate worktree. Created a project through the web app pointing at a real git repository, then sent real Codex gpt-6.1-sol turns that spawned native subagents. No mocks, replay provider, fixtures, or fabricated database rows were used.

A child returned 279 ASCII x characters followed by 😀 boundary-tail. Main's completed-subagent hover card retained U+D83D before the ellipsis and visibly showed a broken character. Stopped main before starting the exact PR head in isolated state. Copied only the stopped, app-created database for matching before/after data. The same hover card on this head ends cleanly with the ellipsis.

Also ran a fresh Codex parent/child turn on the PR head and checked the rendered preview text:

fresh boundary preview: 279 x characters + …; length 280
lone surrogate: false; replacement character: false
short emoji preview: short result 😀 intact
long ASCII preview: 280 a characters + …; length 281
relationships panel hover: same clean boundary preview
open child: full 279 x characters + 😀 boundary-tail visible
open parent: returned to parent conversation

The local proxy retried during two turns and recovered; all requested results completed. The fresh PR parent turn finished in 2m 20s.

Blacksmith passed the existing subagentDisplay.test.ts tests (5/5) and vp run check:changed against the PR base: format, lint, knip, seven-package typecheck, and focused tests. The shallow worker needed its comparison ancestry restored first. No source changes were needed. The PR currently reports 30 successful checks.

Before, main:

main hover card shows the broken character before the ellipsis

After, same app-created result on the PR:

pr hover card ends cleanly with an ellipsis

Fresh PR turn, opening the hover card:

fresh codex subagent hover interaction on the pr head

Inspected the captures and checked uploaded video playback and seeking in T3's browser.

Not verified

Native iOS/Android and the Electron shell were not exercised on this Linux web run. Other providers, workflow-specific hover entry points, and remote/tunnel connections were not exercised. Grapheme-cluster preservation is outside this change; this verifies surrogate-pair safety at the specified boundary.

This branch has not been deployed

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

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants