anomalyco / anomalyco/opencode
TUI: Locale.truncate splits emoji surrogates and under-truncates CJK — UTF-16 units instead of display width
@simonklee is already working on this.
Since Aug 23, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
Description
packages/tui/src/util/locale.ts:61-79 — truncate, truncateLeft and truncateMiddle measure str.length (UTF-16 code units) and slice on code-unit indices:
export function truncate(str: string, len: number): string {
if (str.length <= len) return str
return str.slice(0, len - 1) + "…"
}
Two user-visible consequences (both reproduced manually on 1.17.9):
1. Split surrogate pairs → mojibake. The slice can cut an emoji in half, leaving a lone surrogate that renders as a broken glyph. Also affects the terminal title at app.tsx:468 (session.title.slice(0, 37)).
2. Column budgets ignored for wide characters. Callers pass display-column budgets, but UTF-16 length counts a CJK character as 1 while it renders as 2 columns. A 40-character Chinese session title is 80 columns on screen yet length === 40 <= 61, so Locale.truncate(title, 61) (ui/dialog-select.tsx:778) never truncates it and the dialog row overflows. Same for truncateMiddle in component/prompt/autocomplete.tsx:347.
Same width-semantics family as #39462. The codebase already has grapheme-based width helpers in packages/tui/src/prompt/display.ts (promptOffsetWidth, displaySlice).
Suggested fix
Single-file change in util/locale.ts (plus routing the terminal title through Locale.truncate), reusing the existing prompt/display.ts helpers:
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" })
// Longest prefix that fits the column budget; never splits a grapheme (emoji, CJK).
function fitWidth(str: string, budget: number) {
let width = 0
for (const part of graphemes.segment(str)) {
const next = width + (part.segment === "\n" ? 1 : Bun.stringWidth(part.segment))
if (next > budget) return str.slice(0, part.index)
width = next
}
return str
}
export function truncate(str: string, len: number): string {
if (promptOffsetWidth(str) <= len) return str
return fitWidth(str, len - 1) + "…"
}
export function truncateLeft(str: string, len: number): string {
const width = promptOffsetWidth(str)
if (width <= len) return str
return "…" + displaySlice(str, width - (len - 1))
}
export function truncateMiddle(str: string, maxLength: number = 35): string {
const width = promptOffsetWidth(str)
if (width <= maxLength) return str
const keepStart = Math.ceil((maxLength - 1) / 2)
const keepEnd = Math.floor((maxLength - 1) / 2)
return displaySlice(str, 0, keepStart) + "…" + displaySlice(str, width - keepEnd)
}
Verified locally on dev source: after this change the emoji title truncates to a whole 🚀 + … (no split surrogate), a 40-CJK-char title truncates to 30 chars + … (61 cols), and ASCII behavior is byte-identical to before. Known minor edge: truncateLeft/truncateMiddle can exceed the budget by 1 column when a wide character straddles the boundary (ASCII stays exact).
Happy to send a PR with tests if the column-width semantics are the intended ones.
Plugins
none
OpenCode version
1.17.9 (reproduced); fix verified on dev source
Steps to reproduce
- Broken emoji in terminal title (verified): open a session, rename it (
ctrl+r) to exactly 36 ASCII chars + 3 emoji, e.g.abcdefghijklmnopqrstuvwxyz0123456789🚀🚀🚀. The terminal tab title shows the first rocket cut in half before...—slice(0, 37)lands between the high and low surrogate. - Broken emoji in dialog: rename a session to 59 ASCII chars + 2 emoji → the session list (
<leader>l) shows a mangled half-emoji at the truncation point. - CJK overflow: narrow the terminal to ~80 columns, rename a session to 40 Chinese characters → the session-list row renders 80 columns wide and overflows the dialog (40 ≤ 61 → never truncated).
Operating System
Windows 11
Terminal
Windows Terminal
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.