mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 09:17:09 +08:00
Models frequently emit bare codepoints like U+26A0 (⚠), U+2139 (ℹ),
U+2764 (❤), U+2714 (✔), U+2600 (☀), U+263A (☺) which, per Unicode, have
Emoji_Presentation=No and render as monochrome text-style glyphs in
terminals unless followed by VS16 (U+FE0F). Agent output leaked through
the TUI like `⚠ careful` instead of `⚠️ careful`.
Added `ensureEmojiPresentation` (lib/emoji.ts): scans for the curated
set of text-default codepoints and appends VS16 when the next char is
not already VS16, ZWJ, or a keycap-enclosing mark. Idempotent and
fast-pathed by a Unicode-range regex so ASCII-heavy text is untouched.
Applied once at the top of `Md`'s line parse. Hermes-ink's stringWidth
already accounts for VS16, so cursor/layout stays correct.
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
|
||
import { ensureEmojiPresentation } from '../lib/emoji.js'
|
||
|
||
const VS16 = '\uFE0F'
|
||
|
||
describe('ensureEmojiPresentation', () => {
|
||
it('passes through ASCII unchanged', () => {
|
||
expect(ensureEmojiPresentation('hello world')).toBe('hello world')
|
||
expect(ensureEmojiPresentation('')).toBe('')
|
||
})
|
||
|
||
it('passes through emoji that already defaults to emoji presentation', () => {
|
||
expect(ensureEmojiPresentation('🚀 rocket')).toBe('🚀 rocket')
|
||
expect(ensureEmojiPresentation('😀')).toBe('😀')
|
||
})
|
||
|
||
it('injects VS16 after text-default emoji codepoints', () => {
|
||
expect(ensureEmojiPresentation('⚠ careful')).toBe(`⚠${VS16} careful`)
|
||
expect(ensureEmojiPresentation('ℹ info')).toBe(`ℹ${VS16} info`)
|
||
expect(ensureEmojiPresentation('love ❤ you')).toBe(`love ❤${VS16} you`)
|
||
expect(ensureEmojiPresentation('✔ done')).toBe(`✔${VS16} done`)
|
||
})
|
||
|
||
it('is idempotent when VS16 is already present', () => {
|
||
const already = `⚠${VS16} ℹ${VS16} ❤${VS16}`
|
||
|
||
expect(ensureEmojiPresentation(already)).toBe(already)
|
||
expect(ensureEmojiPresentation(ensureEmojiPresentation('⚠'))).toBe(`⚠${VS16}`)
|
||
})
|
||
|
||
it('leaves keycap sequences alone when the base is not a text-default emoji', () => {
|
||
expect(ensureEmojiPresentation('1\u20e3')).toBe('1\u20e3')
|
||
})
|
||
|
||
it('injects VS16 before ZWJ so text-default bases participate in emoji sequences', () => {
|
||
// ❤ + ZWJ + 🔥 → ❤️🔥 (heart on fire). Without VS16 between the heart
|
||
// and the ZWJ, terminals render the heart in text/monochrome form and
|
||
// the ZWJ ligature can fail to form.
|
||
const heartFire = '\u2764\u200d\ud83d\udd25'
|
||
|
||
expect(ensureEmojiPresentation(heartFire)).toBe(`\u2764\uFE0F\u200d\ud83d\udd25`)
|
||
})
|
||
|
||
it('leaves explicit text-presentation selector (VS15) alone', () => {
|
||
// `❤︎` (U+2764 + U+FE0E) asks for text presentation — injecting VS16
|
||
// would create an invalid double-variation sequence.
|
||
const explicitText = '\u2764\ufe0e'
|
||
|
||
expect(ensureEmojiPresentation(explicitText)).toBe(explicitText)
|
||
})
|
||
|
||
it('returns the original reference when no change is needed', () => {
|
||
const already = `⚠${VS16} ℹ${VS16} ❤${VS16}`
|
||
|
||
// Reference equality — the lazy allocator should short-circuit to the
|
||
// input when nothing needed injection.
|
||
expect(ensureEmojiPresentation(already)).toBe(already)
|
||
})
|
||
|
||
it('handles mixed content', () => {
|
||
expect(ensureEmojiPresentation('⚠ path: /tmp/x ❤ done')).toBe(`⚠${VS16} path: /tmp/x ❤${VS16} done`)
|
||
})
|
||
})
|