Skip to content

harness: ✎ (U+270E) tofus in rich markdown, code fences, and the queue-row button #678

Description

@btipling

Summary

U+270E (LOWER RIGHT PENCIL) paints as tofu in the Wasm harness. It must render on all three product surfaces — not just the queue-row button:

Surface Path today Why it tofus
Queue-row ✎ button queue_band.zig dvui.button + composerIconFont() → DejaVu-only Widget may not honor .font; if it falls through to mixed/faceFor, emoji wins
Rich markdown (paragraph / emphasis / headings) paint_text.zigaddTextMixed(…, body) faceFor(0x270E) == .emoji via isEmojiRelated 0x2600…0x2712. OpenMoji subset does not ship U+270E (it has ✏ U+270F). .notdef
Code / fence / inline code paint_code.zig / fence runs → addTextMixed(…, .theme(.mono)) Same faceFor → OpenMoji. Vera does not have U+270E either. A mono-only path (no mixed) would also tofu

dvui has no per-glyph fallback. One wrong faceFor hits body and mono because both go through addTextMixed.

Shipped as the queue edit mark in #666. Operators also paste into prompts / model output / fenced snippets.

Coverage (locked)

  • in a normal assistant/user paragraph is a pencil, not tofu
  • inside a fenced code block / inline code is a pencil, not tofu (DejaVu at mono size, same as arrows in fences)
  • Queue-row 40 px ✎ button is a pencil, not tofu
  • Copy / ring bytes stay U+270E (paint-only face switch; no lookalike rewrite)

Grounded facts

Face U+270E
DejaVu symbols subset yes
Noto Sans Regular no
OpenMoji subset no (has U+270F ✏)
Vera Mono no

unicode_face.zig: isEmojiRelated includes 0x2600…0x2712. That range already had ✓/✗ (U+2713 / U+2717) carved out for the same reason — OpenMoji claimed the block and did not ship the glyph. U+270E is still inside the emoji span.

addTextMixed (.symbols): paints fontSymbols() at base.size — so base = mono already sizes DejaVu to the fence. No new paint helper. Routing is the bug.

Suggested fix (not locked — direction)

Carve U+270E out of isEmojiRelated (same pattern as 0x2713 / 0x2717 in the 0x2600…0x2712 hole) so faceFor(0x270E) == .symbols. That one change is what rich markdown and code blocks need.

Queue button: keep pinning composerIconFont() (DejaVu). After the carve-out, a mixed fallthrough also works.

Do not switch the product glyph to ✏ U+270F unless every path is the emoji face — DejaVu has both, OpenMoji only 270F, and we are not rewriting operator/model text.

Tests

# Case Layer
1 faceFor(0x270E) == .symbols and !isEmojiRelated(0x270E) unicode_face / test-rich
2 Neighbor dingbats that OpenMoji actually ships stay emoji (e.g. U+270F if still claimed, U+2764) unit
3 Operator: paragraph containing harness preview
4 Operator: fenced block + inline `✎` harness preview
5 Operator: queue-row ✎ button harness preview

Related (same change, not a substitute)

Cancel "×" U+00D7 on the queue row also uses composerIconFont(). U+00D7 is in Noto, so the DejaVu subset excludes it → symbols-only button can tofu. Prefer U+2715 (in DejaVu) or the body face. Does not replace the U+270E contract above.

Non-goals

Docs

If the carve-out lands: one timeless sentence in docs/harness-limits.md / fonts/README.md (U+270E → DejaVu, including fences). No issue numbers.

Refs

  • #666
  • native/harness/src/rich/unicode_face.zigisEmojiRelated / faceFor
  • native/harness/src/rich/mixed_text.zigaddTextMixed symbols @ base.size
  • native/harness/src/rich/paint_code.zig — fences / inline
  • native/harness/src/ui/queue_band.zig — queue-row button
  • native/harness/src/fonts/README.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingharnessHarness / agent UIui

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions