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.ts — SnapshotGrid.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
- Create a session and print a line with a wide glyph, e.g.
printf 'rocket \U0001F680 done\n' (or any CJK text).
agent-tty snapshot <id> --json --include-cells → the cells row has one fewer entry than cols.
- 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.ts — mapNativeCells (drops col/width)
src/protocol/schemas.ts — SnapshotCellSchema / RichSnapshotLineSchema
src/dashboard/liveViewProjection.ts — SnapshotGrid.cellAt (index-as-column assumption)
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; seedocs/prd/session-dashboard/PRD.mdand ADR 0006). It is not specific to the dashboard — it is a property of the sharedSnapshotCellrepresentation, so any consumer that mapsSnapshotCell[]array-index → terminal column is affected.Root cause
mapNativeCellsinsrc/renderer/libghosttyVt/backend.tsbuildsRichSnapshotLine.cellsfrom the native snapshot but discards the native cell'scolandwidthfields, 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. BecauseSnapshotCell(src/protocol/schemas.ts) has nocol/widthkey, the array carries no column information.The Session Dashboard projection then treats array index as the terminal column:
src/dashboard/liveViewProjection.ts—SnapshotGrid.cellAt(row, col)doesthis.cellRows.get(row)?.[col](index = column). After a wide glyph the packed array is one entry short, so every following cell renders attrueColumn - 1.sourceCol === snapshot.cursorCol, wherecursorColis a true terminal column butsourceColis array-derived → the highlight is offset.Array.from(text)[col]indexes by Unicode code point, which also ≠ display column for wide glyphs, so it de-syncs identically.Impact / severity
snapshot --include-cellscommand produces the same packed cells, so the dashboard's Live View still matchesagent-tty snapshotbyte-for-byte. The defect is in the shared cell representation.visibleLinestext values are unaffected (only column→glyph mapping is wrong).Reproduction
printf 'rocket \U0001F680 done\n'(or any CJK text).agent-tty snapshot <id> --json --include-cells→ thecellsrow has one fewer entry thancols.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
coland/orwidthtoSnapshotCell; 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).visibleLinestext, padding a spacer cell after each wide glyph. Avoids a protocol change but duplicates width logic and is approximate.Pointers
src/renderer/libghosttyVt/backend.ts—mapNativeCells(dropscol/width)src/protocol/schemas.ts—SnapshotCellSchema/RichSnapshotLineSchemasrc/dashboard/liveViewProjection.ts—SnapshotGrid.cellAt(index-as-column assumption)