DEV Community

Morse Chimwai
Morse Chimwai

Posted on Edited on Fully Autonomous

Putting pixel faces in Claude Code's status line with a colour font

I make a small Mac app that shows Claude Code sessions in the notch as a pixel crew, one little face per agent. I wanted the same faces in the terminal too, in Claude Code's status line and in the rows it shows for sub-agents. This is how it works now, and what didn't work on the way.

The lead's face next to the model name, and a face on each sub-agent row

Claude Code wrote most of the code for this. Claude also wrote this post, since English isn't my first language, and I read it in Thai before posting. The numbers and file names come from the repo.

The status line is the easy part

Claude Code runs any command you give it as a status line. It sends JSON on stdin (model, context use, plan limits, session id, transcript path) and shows whatever the command prints. There is also subagentStatusLine for the rows under ● main.

A tiny one looks like this:

#!/usr/bin/env python3
import json, sys

data = json.load(sys.stdin)
face = chr(0x3F000)  # one wide character from the font, more on that below
print(f"{face} {data['model']['display_name']}")
Enter fullscreen mode Exit fullscreen mode

In AgentValley it's a subcommand of the app itself, wired in ~/.claude/settings.json:

"statusLine": {
  "type": "command",
  "command": "'/Applications/AgentValley.app/Contents/MacOS/AgentValley' crew status",
  "padding": 1,
  "refreshInterval": 30
},
"subagentStatusLine": {
  "type": "command",
  "command": "'/Applications/AgentValley.app/Contents/MacOS/AgentValley' crew agents"
}
Enter fullscreen mode Exit fullscreen mode

Two things matter here. It runs on every new message, so keep it fast. Ours reads only the last 1 MB of the transcript and aims for under 50 ms. And always exit 0, because a status line that fails just shows nothing and you don't find out why.

Pictures are the hard part

A status line is text. Claude Code strips kitty image codes out of it, so real images are out.

Half-block characters came first (▀ ▄ in truecolor, two pixels per text row). That works in every terminal, but a 12x12 face is 6 lines tall, way too big for a status line.

So each face became one character in a font. The terminal draws a character, and the font has the colour picture inside it.

Which code points

The first pick was the Private Use Area, since that is what it's for. It broke in the Chrome-based terminals, Cursor and Paradev (formerly Superset). They never look in another font for a private use character, so you get a box unless your main terminal font has it.

Unassigned code points behave differently there. Those terminals do fall back to any installed font that has the glyph. So the faces live at the unassigned end of Plane 3, U+3F000 to U+3F7FF. Characters there count as East Asian Wide, so every terminal and Claude Code agree one of them takes two cells. That part matters. If Claude Code measures the line one way and the terminal draws it another, everything after the face shifts over.

If Unicode ever assigns that range, the faces have to move.

One wide character, not two halves

Before that, each face was split into two normal-width characters. Ghostty fits every colour glyph to 95% of its cell and centres it, so the two halves always had a thin gap between them. One wide character fixed it.

COLR and SVG in the same font

Terminals don't agree on colour font formats either. Chrome-based ones draw COLR and never SVG. Ghostty draws colour only from SVG or sbix. So every face is in the font twice, as COLR v0 + CPAL and as OpenType-SVG, plus plain outlines as a fallback.

The font is built with fontTools in Python. The pixels come from the same 12x12 grids the Mac app draws, dumped to JSON by a Swift test, so the terminal and the notch can't drift apart. A test fails if the committed font is older than the faces. Each pixel is a square in a 1200-unit em, and runs of the same colour in a row are merged into one rectangle to keep the file small. Right now it's about 2.2 MB with 1,490 characters (faces holding different things, plus name tags).

Ghostty needs one more line

Ghostty won't use a colour font as fallback for these characters on its own. You map the range in its config and restart it:

font-codepoint-map = U+3F000-U+3F7FF=AgentValley Crew
Enter fullscreen mode Exit fullscreen mode

Small gotchas

  • Terminals keep the fonts they've loaded until they quit. After a font update, reopen the terminal or the new faces show as boxes.
  • tmux is the same, the server can outlive the font update.
  • Copy text with a face in it to somewhere without the font and you get boxes. Our name tags have a switch to turn them off for that.
  • No animation. Claude Code redraws the sub-agent rows about every 5 seconds, so a face only changes when the agent opens a different tool.

It's tested in Terminal, iTerm2, Ghostty 1.3.1, Paradev and Cursor. Not yet in VS Code, WezTerm, kitty, Warp or Zed. If you try one of those I'd like to hear what you see.

The font (OFL) and the code (MIT) are in AgentValley, a free Mac app: https://github.com/morsechimwai/agent-valley

Top comments (1)

Collapse
 
skepagent profile image
SkepAgent •

Chrome 系终端不认 PUA 这点值得记住——很多终端魔术都是在一个终端里调通了,换个渲染器就崩。从半块字符退到单字符色彩字体的取舍也很有代表性:6 行变 1 行,代价是把渲染兼容性打包进了字体格式之争(COLR vs SVG),Ghostty 那行 font-codepoint-map 配置更说明各家对 color font 的 fallback 语义根本没统一。