Skip to content

Wide characters (CJK/emoji) misalign per-cell rendering and cursor highlight in SnapshotCell consumers #112

@ThomasK33

Description

@ThomasK33

Summary

Terminal wide characters (East-Asian/CJK glyphs and emoji, native cell width: 2) misalign the per-cell rendering: in a line containing a wide glyph, every cell after the glyph is shifted one column to the left, and the cursor-cell highlight lands on the wrong cell.

Surfaced during the Session Dashboard review (feat/session-dashboard; see docs/prd/session-dashboard/PRD.md and ADR 0006). It is not specific to the dashboard — it is a property of the shared SnapshotCell representation, so any consumer that maps SnapshotCell[] array-index → terminal column is affected.

Root cause

mapNativeCells in src/renderer/libghosttyVt/backend.ts builds RichSnapshotLine.cells from the native snapshot but discards the native cell's col and width fields, emitting one packed array entry per cell record (not per column). A width-2 glyph at column N produces a single entry, and column N+1 has no entry. Because SnapshotCell (src/protocol/schemas.ts) has no col/width key, the array carries no column information.

The Session Dashboard projection then treats array index as the terminal column:

  • src/dashboard/liveViewProjection.tsSnapshotGrid.cellAt(row, col) does this.cellRows.get(row)?.[col] (index = column). After a wide glyph the packed array is one entry short, so every following cell renders at trueColumn - 1.
  • The cursor flag compares sourceCol === snapshot.cursorCol, where cursorCol is a true terminal column but sourceCol is array-derived → the highlight is offset.
  • The text fallback Array.from(text)[col] indexes by Unicode code point, which also ≠ display column for wide glyphs, so it de-syncs identically.

Impact / severity

  • Visual fidelity only, and only for sessions whose screen contains CJK/emoji glyphs. Content left of the first wide glyph on a line is correct; content to its right shifts left; the cursor highlight can be off by the number of preceding wide glyphs on that row.
  • Not a dashboard regression: the snapshot --include-cells command produces the same packed cells, so the dashboard's Live View still matches agent-tty snapshot byte-for-byte. The defect is in the shared cell representation.
  • visibleLines text values are unaffected (only column→glyph mapping is wrong).

Reproduction

  1. Create a session and print a line with a wide glyph, e.g. printf 'rocket \U0001F680 done\n' (or any CJK text).
  2. agent-tty snapshot <id> --json --include-cells → the cells row has one fewer entry than cols.
  3. In agent-tty dashboard, watch that session: characters after the 🚀 render one column too far left; the cursor highlight is offset if the cursor is on that row past the glyph.

Why this was deferred

A correct fix requires column information on SnapshotCell, which is a change to the public protocol schema — explicitly listed as out of scope for the Session Dashboard v1 (PRD “Out of Scope”: “Any change to public CLI JSON contracts, protocol schemas, or artifact formats.”). The dashboard intentionally mirrors the existing cell behavior rather than introducing a divergent fix.

Suggested fix options

  • (a) Schema-level (correct, broader): add optional col and/or width to SnapshotCell; have producers (mapNativeCells, and the ghostty-web backend) populate them, and consumers (dashboard projection, any cell consumer) index by true column, accounting for width-2 cells. This is a public-schema addition (additive, optional fields).
  • (b) Projection-level (no schema change, approximate): reconstruct column positions in the dashboard projection from grapheme/East-Asian-width measurement of the visibleLines text, padding a spacer cell after each wide glyph. Avoids a protocol change but duplicates width logic and is approximate.

Pointers

  • src/renderer/libghosttyVt/backend.tsmapNativeCells (drops col/width)
  • src/protocol/schemas.tsSnapshotCellSchema / RichSnapshotLineSchema
  • src/dashboard/liveViewProjection.tsSnapshotGrid.cellAt (index-as-column assumption)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageMaintainer needs to evaluate this issue

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions