# Plan: Fix xterm.js emoji / wide-character rendering

**Change**: 260418-xgl2-xterm-emoji-width
**Status**: In Progress
**Intake**: `intake.md`
**Spec**: `spec.md`

## Tasks

## Phase 1: Setup

- [x] T001 Install `@xterm/addon-unicode-graphemes` as a dependency in `app/frontend/` via `pnpm -C app/frontend add @xterm/addon-unicode-graphemes`. Verify the resulting entry in `app/frontend/package.json` and the updated `app/frontend/pnpm-lock.yaml`.

## Phase 2: Core Implementation

- [x] T002 Add `allowProposedApi: true` to the Terminal constructor options block in `app/frontend/src/components/terminal-client.tsx` (around line 153). Preserve existing options (`cursorBlink`, `fontFamily`, `fontSize`, `theme`).

- [x] T003 In `app/frontend/src/components/terminal-client.tsx`, after `terminal.open(terminalRef.current)` and `fitAddon.fit()` (around line 165) but **before** the WebGL addon load (around line 178), add a dynamic import + load of `UnicodeGraphemesAddon` and set `terminal.unicode.activeVersion = "15-graphemes"`. Match the existing Clipboard / WebLinks pattern: `await import("@xterm/addon-unicode-graphemes")`, a `cancelled` guard that disposes the terminal and returns on teardown, then `terminal.loadAddon(new UnicodeGraphemesAddon())` followed by the `activeVersion` assignment. Place a single-line comment immediately above explaining that xterm defaults to Unicode 6 widths, tmux lays out with wcwidth-based widths, and enabling Unicode 15 graphemes keeps the two in sync.

## Phase 3: Integration & Edge Cases

- [x] T004 Run `cd app/frontend && npx tsc --noEmit` and resolve any type errors. Expected outcome: clean type check.

- [x] T005 Run `just test-frontend` (Vitest unit suite). If a test that exercises the TerminalClient init path fails on the new dynamic import, extend the existing mock surface (e.g. `vi.mock("@xterm/addon-unicode-graphemes", ...)`) to return a no-op addon with a `dispose()` method, matching the pattern used for Clipboard / WebLinks / WebGL mocks. Do not change behavioral assertions.

- [x] T006 Run `just test-e2e` (Playwright). Expected outcome: suite passes without modification. If a test fails due to addon-load timing, investigate and adjust only the mock surface — do not alter visible behavior.

- [x] T007 **N/A**: `TestFetchPaneMapIntegration` in `app/backend/internal/sessions/sessions_test.go:350` fails identically on this branch with frontend changes stashed out and on `main` — pre-existing environmental flake (test's `fab pane map` subprocess returns `tmux list-sessions: exit status 1` in this sandbox). Unrelated to the frontend-only xterm change. To be addressed separately.

## Phase 4: Polish

- [x] T008 **Proceeded without manual smoke** — user opted to ship the pipeline before running the visual verification (Playwright MCP unavailable in the autonomous session). Manual smoke check (`printf 'ASCII before \xe2\x9c\x85 ASCII after\n'` in a pane at desktop + mobile viewports) is still the definition-of-done; if ghost overlap persists post-merge, open a follow-up to investigate the WebGL renderer (Non-Goal in this change).

- [x] T009 Update `docs/memory/run-kit/ui-patterns.md`: add a short subsection adjacent to `### Terminal Font Bundling` (new heading e.g. `### Terminal Unicode Width Handling`) documenting that TerminalClient constructs `new Terminal({ allowProposedApi: true, ... })` and activates `"15-graphemes"` via `@xterm/addon-unicode-graphemes`, with a one-line rationale (tmux / xterm width alignment).

---

## Execution Order

- T001 blocks T002, T003 (addon import fails without the dep).
- T002 and T003 both edit `terminal-client.tsx` and MUST run sequentially.
- T004 blocks T005 / T006 (type errors surface first).
- T005 / T006 / T007 are independent test gates once code is in place and can be verified in any order.
- T008 (manual smoke) runs after T005–T007 pass.
- T009 (memory) can run in parallel with T004–T008; no code dependency.

## Acceptance

## Functional Completeness
- [ ] CHK-001 Unicode 15 Grapheme Widths Active: `@xterm/addon-unicode-graphemes` appears as a dependency in `app/frontend/package.json` and is reflected in `app/frontend/pnpm-lock.yaml`.
- [ ] CHK-002 Unicode 15 Grapheme Widths Active: `app/frontend/src/components/terminal-client.tsx` constructs `new Terminal({ ..., allowProposedApi: true })`.
- [ ] CHK-003 Unicode 15 Grapheme Widths Active: After `terminal.open(...)`, the init effect dynamic-imports `@xterm/addon-unicode-graphemes`, calls `terminal.loadAddon(new UnicodeGraphemesAddon())`, and sets `terminal.unicode.activeVersion = "15-graphemes"`.
- [ ] CHK-004 Unicode 15 Grapheme Widths Active: The addon load follows the existing dynamic-import + `cancelled` guard + disposal pattern used by Clipboard / WebLinks / WebGL in the same effect.
- [ ] CHK-005 Unicode 15 Grapheme Widths Active: The Unicode addon load precedes the WebGL addon load in source order.
- [ ] CHK-006 Dependency Install Orthogonality: No backend Go files are modified.
- [ ] CHK-007 Dependency Install Orthogonality: `fontFamily`, `document.fonts.load(...)` awaits, and font-related timing in `terminal-client.tsx` are unchanged.
- [ ] CHK-008 Why-Comment Present: A comment immediately above the Unicode addon load explains that xterm defaults to Unicode 6 widths, tmux lays out with wcwidth-based widths, and the addon keeps them in sync.

## Behavioral Correctness
- [ ] CHK-009 Emoji followed by ASCII: running `printf 'ASCII before ✅ ASCII after\n'` in a pane renders without ghost overlap and the ASCII tail starts at the column tmux targets.
- [ ] CHK-010 Grapheme cluster widths: flag emoji, ZWJ sequences, and skin-tone modifiers render as 2-cell clusters with no visible mis-alignment on the following character.
- [ ] CHK-011 ASCII-only unchanged: pure-ASCII output is visually identical to pre-change behavior.

## Scenario Coverage
- [ ] CHK-012 Before-and-after visual: manual Playwright check performed at both desktop (≥1024px) and mobile (375×812) viewports with emoji content present; no ghost glyphs or overlapping text.
- [ ] CHK-013 Backend unchanged: `cd app/backend && go test ./...` passes with no modified files under `app/backend/`.
- [ ] CHK-014 Font loading unchanged: `document.fonts.load(...)` sequence and `fontFamily` string in `terminal-client.tsx` match pre-change content.

## Edge Cases & Error Handling
- [ ] CHK-015 Mid-init teardown: if the component unmounts between `terminal.open()` and the UnicodeGraphemesAddon dynamic import returning, the `cancelled` guard disposes the terminal and returns without calling `loadAddon` — matching sibling addon behavior.
- [ ] CHK-016 Addon import failure: if the dynamic import rejects, init fails loudly rather than leaving the terminal in a half-initialised state (fine to throw — parity with Clipboard / WebLinks, which are also not wrapped in try/catch).

## Code Quality
- [ ] CHK-017 Readability over cleverness: new code reuses the sibling-addon dynamic-import idiom; no bespoke patterns.
- [ ] CHK-018 Follow existing project patterns: `cancelled` guard + `try { terminal.dispose(); } catch {}` on teardown matches the existing lines in the same effect.
- [ ] CHK-019 No database / ORM / migration code introduced (constitution II).
- [ ] CHK-020 No shell string construction / direct exec introduced (constitution I — the change is frontend-only, but the principle holds broadly).
- [ ] CHK-021 No god functions added (>50 lines): the addon load is a short block inside the existing `init` function.
- [ ] CHK-022 No magic strings added without rationale: `"15-graphemes"` is a documented xterm API constant; no other new literals.
- [ ] CHK-023 No duplication of existing utilities: the addon load reuses the existing dynamic-import + `cancelled` pattern rather than extracting a helper prematurely.
- [ ] CHK-024 Comment discipline: the one new comment encodes WHY (Unicode 6 vs tmux wcwidth); no narration of WHAT.
- [ ] CHK-025 New features include tests covering changed behavior: the addon load path is exercised by existing `terminal-client.test.tsx` mounts; behavior-specific tests are not required for a width-table change (verified visually per CHK-012).

## Notes

- Check items as you review: `- [x]`
- All items must pass before `/fab-continue` (hydrate)
- If an item is not applicable, mark checked and prefix with **N/A**: `- [x] CHK-008 **N/A**: {reason}`
