anomalyco / anomalyco/opencode

TUI: Locale.truncate splits emoji surrogates and under-truncates CJK — UTF-16 units instead of display width

Open
#44,338 0 comments 0 reactions 1 assignee View on GitHub

@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-79truncate, 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
  1. 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.
  2. 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.
  3. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.