Skip to content

fix(bridge): truncate derived session titles on grapheme boundaries - #1982

Merged
kevincodex1 merged 3 commits into
Gitlawb:mainfrom
0xfandom:fix/bridge-title-surrogate-split
Jul 22, 2026
Merged

fix(bridge): truncate derived session titles on grapheme boundaries#1982
kevincodex1 merged 3 commits into
Gitlawb:mainfrom
0xfandom:fix/bridge-title-surrogate-split

Conversation

@0xfandom

@0xfandom 0xfandom commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Problem

The bridge derives a session title from the user's first REPL message and PATCHes it to the claude.ai backend, where it's JSON-serialized and UTF-8-encoded for the remote/mobile session list.

deriveTitle truncates with flat.slice(0, TITLE_MAX_LEN - 1), a UTF-16 code-unit slice. When an emoji or astral-plane character straddles the cut, the slice keeps its high surrogate and drops the low one, leaving a lone surrogate. Over the UTF-8 wire that becomes the U+FFFD replacement character — the session title shows mojibake.

Reproduced against the exact slice logic with a first message of 'a'.repeat(48) + '😀😀😀 fix the login bug' (the 😀 lands on the 50-char boundary):

title.isWellFormed() = false           // lone high surrogate U+D83D
wire body            = {"title":"aaa…aaa\ud83d…"}
UTF-8 roundtrip      = "aaa…aaa�…"      // U+FFFD

Fix

Route through truncateToWidth, the grapheme-safe helper that the parallel derivation deriveSessionTitle in bridgeMain.ts already uses for the identical purpose. It segments on grapheme boundaries, so it never splits a surrogate pair, and appends on truncation — matching the original intent.

One-line source change plus a regression test covering the emoji-boundary case, long-title ellipsis, short-title passthrough, empty message, and whitespace collapse.

Same class as the recently merged byte/UTF-8 truncation fixes (#1918, #1960).

Summary by CodeRabbit

  • Bug Fixes

    • Improved title truncation to be grapheme/emoji-safe, avoiding broken characters and lone-surrogate artifacts.
    • Ensures truncated output uses an ellipsis when needed, stays within the defined character-length limit, and avoids introducing replacement symbols.
    • Normalizes multiline titles into a single-line result and omits titles when input is empty.
  • Tests

    • Added coverage for emoji boundaries, surrogate safety, UTF-8 round-trip validity, character-count caps (including CJK), ellipsis behavior, and multiline/empty cases.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 857aae18-52a3-4495-8b89-ed05c9e66fc6

📥 Commits

Reviewing files that changed from the base of the PR and between 723d510 and a3993c4.

📒 Files selected for processing (2)
  • src/bridge/initReplBridge.titleTruncation.test.ts
  • src/bridge/initReplBridge.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: smoke-and-tests (24.11.x)
  • GitHub Check: smoke-and-tests (22)
  • GitHub Check: typecheck
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

TypeScript code in this repository must use strict mode and ESM imports.

**/*.{ts,tsx}: Provider changes must follow the documented integration patterns and avoid inconsistent behavior across provider paths.
When changing provider behavior, avoid breaking third-party providers and test the exact provider/model path changed when possible.
Review AI-generated code for correctness, style consistency, unnecessary changes, and adherence to project architecture before submission.
Run multiple rounds of self-review on AI-generated code; compilation alone is insufficient to establish correctness.

Files:

  • src/bridge/initReplBridge.ts
  • src/bridge/initReplBridge.titleTruncation.test.ts
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Keep pull requests focused on one issue or one clearly scoped improvement; avoid unrelated cleanup, fixes, features, or refactors in the same change.
Preserve existing repository patterns unless intentionally refactoring them, and stay within the project's existing language, runtime, dependency, and architectural direction.
Add or update tests when a change affects behavior.
Update documentation when setup, commands, or user-facing behavior changes.
Follow the existing code style in touched files.
Prefer small, readable changes over broad rewrites.
Do not reformat unrelated files merely because they are nearby.
Keep comments useful and concise.
Run the narrowest meaningful validation command for the touched area before opening a pull request, and ensure relevant CI checks pass before merge.
Provider-change pull requests must identify affected providers, state the tested provider/model path, and document limitations or follow-up work.
Do not assign or use provider tags; provider tags are controlled and applied by maintainers.
Security reports must follow the instructions in SECURITY.md.
PR descriptions must explain what changed and why, user or developer impact, exact checks run, and include relevant issue links; UI, terminal presentation, or VS Code extension changes require screenshots.
PR authors must address CodeRabbit findings before maintainer review proceeds.

Files:

  • src/bridge/initReplBridge.ts
  • src/bridge/initReplBridge.titleTruncation.test.ts

⚙️ CodeRabbit configuration file

**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.

Files:

  • src/bridge/initReplBridge.ts
  • src/bridge/initReplBridge.titleTruncation.test.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}

⚙️ CodeRabbit configuration file

{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.

Files:

  • src/bridge/initReplBridge.titleTruncation.test.ts
🔇 Additional comments (2)
src/bridge/initReplBridge.ts (1)

39-39: LGTM!

Also applies to: 563-601

src/bridge/initReplBridge.titleTruncation.test.ts (1)

1-63: LGTM!


📝 Walkthrough

Walkthrough

deriveTitle now uses grapheme-safe, UTF-16-length-bounded truncation and is exported for testing. Tests cover Unicode boundaries, length limits, display-width edge cases, empty and short inputs, and multiline whitespace normalization.

Changes

REPL title truncation

Layer / File(s) Summary
Update title derivation
src/bridge/initReplBridge.ts
Exports deriveTitle and adds truncateTitleToLength, which truncates normalized titles by grapheme boundaries while enforcing a UTF-16 length cap.
Validate title behavior
src/bridge/initReplBridge.titleTruncation.test.ts
Tests surrogate safety, UTF-8 round-tripping, character-based limits, display-width edge cases, unchanged short titles, empty input, and normalized multiline whitespace.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: chioarub, kevincodex1

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the problem and fix, but it does not follow the required template sections for Summary, Impact, Testing, and Notes. Rewrite it using the repository template with Summary, Impact, Testing, and Notes sections, including the required bullets for user-facing, maintainer, and test details.
✅ Passed checks (6 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, scoped to the bridge, and accurately reflects the grapheme-safe truncation change and regression coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Risk Surface Disclosed ✅ Passed N/A: the PR only changes bridge title truncation and tests; it doesn’t alter auth, routing, permissions, or other listed risk surfaces.
No Hidden Policy Change ✅ Passed HEAD^..HEAD touches only initReplBridge title truncation and its test; no product, trust, routing, telemetry, or permission-policy code changed.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bridge/initReplBridge.titleTruncation.test.ts`:
- Around line 13-15: Update the LONE_SURROGATE regex in the test to remove the
negative lookbehind, matching an unpaired low surrogate when it is at the start
of the string or preceded by a non-high-surrogate character. Preserve the
existing high-surrogate matching and ensure the pattern remains compatible with
YARR JIT constraints.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7cd528b7-f36d-4ac6-a2d8-3d798c5dd341

📥 Commits

Reviewing files that changed from the base of the PR and between 7ad96e9 and ec4e4f3.

📒 Files selected for processing (2)
  • src/bridge/initReplBridge.titleTruncation.test.ts
  • src/bridge/initReplBridge.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: smoke-and-tests (24.11.x)
  • GitHub Check: smoke-and-tests (22)
  • GitHub Check: typecheck
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

TypeScript code in this repository must use strict mode and ESM imports.

**/*.{ts,tsx}: When changing provider behavior, avoid breaking third-party providers and test the exact provider/model path changed when possible.
Run bun run typecheck and bun run typecheck:type-tests for TypeScript changes.
Run provider tests and provider recommendation tests when changing provider behavior: bun run test:provider and bun run test:provider-recommendation.

Files:

  • src/bridge/initReplBridge.titleTruncation.test.ts
  • src/bridge/initReplBridge.ts
**/*

📄 CodeRabbit inference engine (CONTRIBUTING.md)

**/*: Keep changes focused on one problem or feature and avoid mixing unrelated cleanup into the same change.
Preserve existing repository patterns unless intentionally refactoring them.
Update documentation when setup, commands, or user-facing behavior changes.
Review AI-generated changes for correctness, style consistency, unnecessary changes, and adherence to project architecture before submitting them.
Follow the existing code style in touched files.
Prefer small, readable changes over broad rewrites.
Do not reformat unrelated files.
Keep comments useful and concise.
Provider changes must explicitly identify affected providers, limitations, and follow-up work in the pull request description.
Do not assign or use provider tags; provider tags are controlled by maintainers.
Run the relevant validation checks locally before submitting changes; pull requests must pass CI checks.
Run bun run security:pr-scan before submitting a pull request.
Dependency changes require a concrete project benefit, such as fixing a bug, addressing a security issue, or supporting an approved feature.
Do not change the project's language, core runtime, or dependency stack without prior maintainer agreement.

Files:

  • src/bridge/initReplBridge.titleTruncation.test.ts
  • src/bridge/initReplBridge.ts

⚙️ CodeRabbit configuration file

**/*: Apply the OpenClaude maintainer review rubric from AGENTS.md. Review the current diff, not stale discussion context. Separate real blockers from suggestions. Do not request changes for vague style churn. Treat approval as merge-ready from CodeRabbit's side, pending required human review and GitHub Checks. If checks are failing or unavailable, say so clearly instead of implying the PR is fully ready.

Files:

  • src/bridge/initReplBridge.titleTruncation.test.ts
  • src/bridge/initReplBridge.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Add or update tests when a code change affects behavior.

Files:

  • src/bridge/initReplBridge.titleTruncation.test.ts
  • src/bridge/initReplBridge.ts
{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}

⚙️ CodeRabbit configuration file

{src/**/*.test.ts,src/**/*.test.tsx,tests/**,scripts/**/*.test.ts,vscode-extension/**/*.test.js}: Review tests for meaningful coverage of the changed behavior, isolation of global/env/config state, async cleanup, fake timers, provider profile leaks, and Windows-compatible assumptions. Block when risky runtime changes lack focused regression coverage or tests assert implementation details while missing the user-visible behavior.

Files:

  • src/bridge/initReplBridge.titleTruncation.test.ts
🔇 Additional comments (1)
src/bridge/initReplBridge.ts (1)

38-38: 🎯 Functional Correctness

No import change needed truncateToWidth is exported from src/utils/format.ts, so the current ../utils/format.js import is correct.

			> Likely an incorrect or invalid review comment.

Comment thread src/bridge/initReplBridge.titleTruncation.test.ts
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 17, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Preserve a character/transport bound instead of using terminal display width
    src/bridge/initReplBridge.ts:580
    TITLE_MAX_LEN is the 50-character limit for the session-title API field, but truncateToWidth measures terminal columns. This both truncates valid wide-script titles early (for example, 30 characters become 24 plus ) and removes the effective cap for zero-width graphemes: deriveTitle('\u200B'.repeat(100000)) returns all 100,000 code units because its display width is zero. The result is logged and PATCHed as the remote session title, so ordinary CJK titles lose content while a pasted or injected combining/zero-width-heavy prompt can produce an arbitrarily large request. Please use a grapheme-safe truncator that also enforces this field's character or API-size limit, and cover both wide and zero-width inputs.

0xfandom added 3 commits July 20, 2026 18:29
deriveTitle cut the title with flat.slice(0, TITLE_MAX_LEN - 1), a
UTF-16 code-unit slice. When an emoji or astral-plane character in the
user's first message straddles the cut, the slice keeps its high
surrogate and drops the low one, leaving a lone surrogate. The title is
PATCHed to the claude.ai backend and UTF-8-serialized, so that lone
surrogate is transmitted as the U+FFFD replacement character and the
remote/mobile session list shows mojibake.

Route through truncateToWidth, the grapheme-safe helper deriveSessionTitle
in bridgeMain.ts already uses for the identical purpose.
The source regex in initReplBridge.ts avoids lookbehinds to stay within
YARR/JSC (the engine Bun uses); mirror that in the test by matching an
unpaired low surrogate with a leading non-high-surrogate alternation
instead of a negative lookbehind.
TITLE_MAX_LEN caps the session-title API field in characters, but
truncateToWidth measures terminal columns. That charged 2 columns per wide
glyph, so 30 CJK characters — well inside the 50-char field — were cut to
24 plus an ellipsis, while zero-width graphemes cost 0 columns and removed
the cap entirely (100,000 code units passed through as a title).

Walk graphemes and accumulate against the code-unit length instead. That
keeps the surrogate pair and any combining marks intact, which is what the
original raw slice broke, while still enforcing the documented character
bound.
@0xfandom

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. Good catch — truncateToWidth measures terminal columns, but TITLE_MAX_LEN bounds the API field in characters, so I traded one bug for two: 30 CJK characters truncated to 24, and 100k zero-width code units passed through uncapped (confirmed both). Now walks graphemes and accumulates against code-unit length, which keeps the surrogate pair intact — the original defect — while enforcing the character bound. Regressions added for both directions.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@kevincodex1
kevincodex1 merged commit a6b3d7a into Gitlawb:main Jul 22, 2026
5 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.

3 participants