Skip to content

id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431

Closed

Description

@dezren39

Problem

The browser world presenter refuses to render an entire screen when any cell contains an emoji with a skin-tone modifier (or a subdivision-flag tag sequence) — glyphs the Rust side of the same surface explicitly validates and emits as ordinary 2-column cells. One such glyph anywhere on the screen makes renderWorldSurface return false for the whole surface, so the world view silently stays on its previous/legacy content.

Evidence (verified this run, main 37418e56)

Rust side — pkgs/id/src/world_presentation.rs:219 and :350 validate a cell by UnicodeWidthStr::width(symbol) == columns using unicode-width 0.2.2 (pinned in pkgs/id/Cargo.lock). A probe crate compiled and run on the laptop (unicode-width = "=0.2.2", unicode-segmentation) measures:

  • "👋🏽" (U+1F44B U+1F3FD) — graphemes=1, width=2
  • "🧑🏽" (U+1F9D1 U+1F3FD) — graphemes=1, width=2
  • "🏋🏽" (U+1F3CB U+1F3FD) — graphemes=1, width=2
  • England flag U+1F3F4 U+E0067 U+E0062 U+E0065 U+E006E U+E0067 U+E007F — graphemes=1, width=2

So the server legitimately produces columns: 2 cells carrying these symbols, with a columns: 0 continuation cell.

TS side — pkgs/id/web/src/world-surface.ts:149-156 (graphemeWidth) has no modifier/tag-sequence case: outside the ZWJ (:150-152) and U+FE0F (:153) shortcuts it sums per-code-point widths. The skin-tone modifiers U+1F3FB–U+1F3FF fall inside the wide range [0x1f3f8, 0x1f43e] at :95, so a modified emoji measures 2 + 2 = 4; tag characters U+E0067… each measure 1, so the flag measures 8. At :234-235 the presenter checks graphemeWidth(cell.symbol) !== cell.columns and returns false.

Probe against the verbatim world-surface.ts from main (sha256 e92f95f99844db8553c8a60f2368305a8b4d654d4b03dfc4132c5b26ff62bd91, run with Bun on the laptop, minimal DOM stub):

  • controls render: x/1 → true, 😀/2 → true, 👋/2 → true, ZWJ family/2 → true, keycap 1️⃣/2 → true
  • 👋🏽/2 → false, 🧑🏽/2 → false, 🏋🏽/2 → false, England tag flag/2 → false

Why it matters

Skin-tone emoji are everyday user content in a world/chat surface. The Rust producer and the TS presenter disagree about the same contract (the TS file's own header at :163-166 claims its width measurement "matches Rust's unicode-width 0.2 semantics"), and the failure mode is total: one cell rejects the whole frame, with no fallback rendering of the remaining cells. This is the same presenter/Rust width-parity class as closed #761, which special-cased ZWJ and U+FE0F sequences but missed emoji modifiers and tag sequences.

Acceptance criteria

  • A 2-column cell carrying an emoji + skin-tone modifier grapheme (e.g. 👋🏽, 🧑🏽, 🏋🏽) plus its continuation cell renders (renderWorldSurface returns true and produces the span), matching the Rust validator.
  • A 2-column cell carrying a subdivision-flag tag sequence (e.g. the England flag above) renders the same way.
  • Existing width behaviour is unchanged: plain wide glyphs, ZWJ sequences, U+FE0F sequences, keycaps, flags (regional-indicator pairs), combining marks, and the refusal cases in world-surface.test.ts (mismatched widths, multi-grapheme cells, control characters) all keep their current verdicts.
  • New regression tests in pkgs/id/web/src/world-surface.test.ts cover skin-tone-modified emoji and a tag-sequence flag at columns=2 (accepted) — and, where the Rust width would differ, at columns=1 (refused).
  • The fix is in the TS presenter's grapheme-width measurement; the Rust validator and the wire format do not change.

Activity

  1. developing-today commented on Oct 27, 2026

    @developing-today
    Contributor
  2. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    queue/queuedHas a queued task in the Supabase agent queue
    source/creatorFiled by the new-issue-creator job
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    effort/sSmall: expected under 2 hours of work
    on Oct 27, 2026
  3. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed

    • Agent: new-issue-creator-code-b
    • Session / ID: new-issue-creator-code-b cron run, 2026-10-27 ~10:11 CDT
    • Found via: filed this run
    • Task: Filed after verifying on main 37418e5 — Rust unicode-width 0.2.2 measures skin-tone emoji and the England tag flag as 1 grapheme / width 2 (laptop Rust probe), while the verbatim TS presenter (laptop Bun probe) rejects those exact columns=2 cells (rendered=false; controls true), so one such cell refuses the whole world surface.
    • Expectation: effort/s — triage packet enqueued to repo-health-code-pilot; done = presenter grapheme-width parity for emoji-modifier and tag sequences with regression tests in world-surface.test.ts, existing verdicts unchanged.
    • Links: probes on the laptop at ~/git/fleet/probe-2026-10-27-laneB-world (TS, verbatim file sha256 e92f95f9…) and ~/git/fleet/probe-2026-10-27-laneB-width (Rust); logs in the goal's hidden_files/probe-2026-10-27-laneB-world/
  4. dezren39 commented on Oct 27, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · state: filed (evidence addendum, same run)

    • Agent: new-issue-creator-code-b
    • Session / ID: new-issue-creator-code-b cron run, 2026-10-27 ~10:11 CDT
    • Found via: filed this run — parity sweep after the original filing
    • Task: Same graphemeWidth divergence, two more verified classes from a ~60-grapheme Rust↔TS parity sweep (same laptop probes; logs ts-out2.log + rust-sweep.log in the goal's hidden_files/probe-2026-10-27-laneB-world/):
      • Missing wide code points: ⬛ U+2B1B and ⭕ U+2B55 are width 2 in Rust unicode-width 0.2.2 but absent from the TS WIDE_RANGES table (world-surface.ts:53-133) — a columns: 2 cell is refused (rendered=false) and a columns: 1 cell is wrongly accepted (rendered=true), the reverse error of the modifier case. (〰 U+3030 and ㊗ U+3297 controls render correctly at 2.)
      • Narrow base + modifier: ☝🏻 (U+261D U+1F3FB) is width 2 in Rust and refused at columns: 2 — the modifier rule in the fix should make the cluster total 2 regardless of the base's own width, not just handle wide bases.
      • Minor, same sweep: a lone U+0903 (Devanagari spacing mark, category Mc) is width 1 in Rust but measures 0 in TS (the /^\p{M}$/u combining test at :134 covers spacing marks too); refused at columns: 1. Lower confidence in its real-world reachability — a triager may scope it out.
    • Expectation: unchanged (effort/s triage, queue task 1436) — please fold the U+2B1B/U+2B55 table gap and the narrow-base modifier case into the same fix's acceptance tests.
    • Links: issue id(web): world presenter rejects whole screen when a cell has a skin-tone emoji or tag flag — TS grapheme width sums modifiers instead of measuring the cluster #1431; probes on the laptop at ~/git/fleet/probe-2026-10-27-laneB-world and ~/git/fleet/probe-2026-10-27-laneB-width
  5. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 27, 2026
  6. 29 remaining items

  7. added
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    on Oct 28, 2026
  8. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  9. removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Oct 28, 2026
  10. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

  11. dezren39 commented on Oct 28, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobstatus:donetask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions