Repository navigation
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
Activity
Tarquinen commentedon Oct 20, 2026
Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (
opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.— from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊