| name | card-widget |
| description | Push live data to the user's ai-desk-card M5Paper e-ink companion display.
Use whenever the user asks to show todos, calendar, weather, AI status,
inbox counts, next meeting, or any glanceable info on their card / 卡片 /
副屏 / 墨水屏 / e-ink display. 16 widget types available; AI picks slot
+ widget type, fills data, POSTs to the local card daemon (default
127.0.0.1:9877). Communicates over loopback, so individual widget writes
don't trigger AI agent approval prompts.
|
| trigger_keywords | ["card widget","卡片","副屏","墨水屏","paper card","显示器副屏","secondary display"] |
| allowed-tools | ["Bash","Read","Write"] |
card-widget — drive the M5Paper 副屏
The card is a 540 × 960 e-ink panel sitting next to the user's monitor.
You push 1-4 widgets and the daemon renders a single frame; transfer
time depends on the transport.
Transport & latency (v0.8)
The daemon auto-picks Wi-Fi > USB > BLE at startup. Each path has
different per-push characteristics — be aware:
| Transport | Single-widget update | Full frame | Power use |
|---|
| Wi-Fi (HTTP) | ~0.2 s | ~2 s | high while connected |
| USB serial @ 115200 | ~1 s region / ~32 s full | 32 s | n/a |
| BLE (small cmds only) | works for commands; frame data hangs | broken | low |
The daemon also runs a dirty-region diff so unchanged pixels don't
re-transmit — typical "one widget updated" push is 5-30 KB instead of
the 250 KB full frame. You don't manage this; the daemon decides per
push.
Push frequency rules (e-ink hardware):
- Per-widget content change: fine, push freely (debounced internally
at 1.5 s)
- Per-keystroke streaming: NO. Each push triggers e-ink refresh which
damages the panel over time.
- ai-status taking off / completion: one push at start, one at end.
- For sub-minute "live" info (timer countdowns, music progress):
push at most every ~30 s.
If the user's on USB (1-32 s) instead of Wi-Fi (0.2 s), strongly suggest
configuring Wi-Fi via /card-wifi-setup — it's a quality-of-life upgrade.
Layout cheat-sheet
Four slots. Narrow slots (top-left, top-right) are 270 px wide; wide
slots (middle, bottom, full) span the full 540 px. Some widgets only
shine when wide — see the catalog below.
┌───────────┬───────────┐ top-left / top-right : 270×280 (narrow)
│ top-left │ top-right │
├───────────┴───────────┤ middle : 540×340 (wide)
│ middle │
├───────────────────────┤ bottom : 540×280 (wide)
│ bottom │
└───────────────────────┘
bar (60 px) status/settings bar — always on
How to pick a layout
Walk this in order:
- Ask user what's most important (or infer from context). The card
only fits 4 widgets — be choosy.
- One headline widget → middle. The biggest, most readable slot.
Use for the day's most important info (calendar, focus, next-meeting).
- Two glance widgets → top-left + top-right. Pick widgets whose
most-important info fits a narrow column (weather, inbox, ai-status,
git-status).
- One detail widget → bottom. Lists, multi-row info (todo, deadlines,
pr-queue, messages).
Default fallback if the user has no opinion:
top-left = weather top-right = ai-status
middle = calendar bottom = todo
If the user is deep in a coding session, swap to:
top-left = git-status top-right = ai-status
middle = focus bottom = ai-tasks
If it's a desk-companion / meeting-heavy day:
top-left = weather top-right = inbox
middle = next-meeting bottom = pr-queue
Widget catalog
Every widget has a JSON schema at schemas/<type>.schema.json. The
"shape" lines below are summaries — read the schema for full constraints.
Work staples
weather — narrow OK
City + big temp + condition + up to 2-day forecast.
{"location":"Beijing",
"current":{"temp_c":22,"condition":"晴"},
"forecast":[{"day":"明","high":26,"low":14,"condition":"多云"}]}
calendar — wide preferred (narrow shows fewer rows)
Today's events. Pass now_iso so the renderer can mark which is current.
{"now_iso":"2026-05-21T13:30:00",
"events":[{"start":"09:30","title":"standup"},
{"start":"14:00","end":"15:00","title":"design review"}]}
next-meeting — wide preferred
The single next event with big countdown. Use when only ONE thing matters.
{"title":"design review","start_in":"in 42m","start_at":"14:00",
"location":"Zoom","attendees":"Alice, Bob, Carol"}
messages — wide preferred
Up to 3 IM previews. Each item: sender + preview + age.
{"items":[{"sender":"Bob","preview":"PR ready for review","age":"2m"}]}
inbox — narrow OK
Total unread + per-source breakdown. Cap 4 sources.
{"total":12,
"sources":[{"name":"gmail","count":4},{"name":"slack","count":8}]}
system — narrow OK
CPU / mem / disk / battery / net / temp. Vertical 4-row in narrow slot.
Note: field is memory_pct (not mem_pct); battery_pct=255 means no battery.
{"cpu_pct":42,"memory_pct":63,"disk_pct":81,"battery_pct":88,
"net_down_kbps":120,"temp_c":56}
git-status — narrow OK
Branch + modified/untracked/staged + ahead/behind + last commit.
{"repo_name":"ai-desk-card","branch":"main",
"modified":3,"untracked":1,"staged":0,"ahead":2,"behind":0,
"last_commit_msg":"add login flow"}
pr-queue — wide preferred
PR counts + up to 4 items. Status: review | yours | approved | blocked.
{"review_count":2,"your_open_count":1,
"items":[{"number":"#42","title":"fix race","author":"alice","status":"review"},
{"number":"#51","title":"feat: cron","author":"you","status":"yours"}]}
now-playing — wide preferred
Track + artist + position/duration (seconds, not float progress).
{"track":"Stairway","artist":"Led Zeppelin",
"source":"Spotify","position_sec":252,"duration_sec":482,"playing":true}
Note-taking & focus
scratch — wide preferred
Free-form sticky note. The most flexible 记事 component. Use when
nothing else fits ("Bob coming at 3pm", "remember to update LinkedIn").
{"text":"3pm 见 Bob — 带上昨天那张设计稿","source":"manual","age":"5m"}
todo — either
Up to 4 tasks. tag: today | tomorrow | this-week | later | overdue.
{"title":"今天",
"items":[{"text":"刷固件","tag":"today"},
{"text":"写文档","tag":"overdue","due":"2026-05-20"}]}
focus — wide preferred
ONE active task + big text + subtitle + Pomodoro dots.
{"task":"finish onboarding doc",
"big_text":"18 min","subtitle":"started 12:18 · 番茄 2/4",
"pomodoros_done":2,"pomodoros_planned":4}
deadlines — either
Multi-day countdown of must-finish-by dates. (Different from todo
which is today-focused.)
{"items":[{"title":"H1 review","due_label":"in 2d","is_urgent":true},
{"title":"tax filing","due_label":"3 weeks"}]}
break-reminder — narrow OK
Health nudge. Last-break + sitting + eye-rest.
{"last_break_min_ago":78,"sitting_min":120,"next_eye_rest_min":-5,
"advice":"stand up + look 20ft away"}
AI monitoring
ai-status — narrow OK
Model + task + context bar. Push at the start of any non-trivial task.
{"session_name":"refactor auth","model":"Sonnet 4.6","task":"writing tests",
"context":{"used":42000,"limit":200000},"elapsed_seconds":480}
ai-tasks — narrow OK
Running / waiting / blocked / done-today counters. Vertical in narrow.
{"running":2,"waiting":1,"blocked":0,"completed_today":7}
Disambiguation — common confusions
- inbox vs messages:
inbox is counts per source; messages is
named senders' previews. Use inbox for "how much is waiting", messages
for "who's poking me".
- next-meeting vs calendar:
next-meeting is THE next event (big
countdown); calendar is today's schedule (list).
- deadlines vs todo vs calendar:
deadlines = multi-day countdown;
todo = today's tasks; calendar = today's schedule.
- focus vs todo:
focus = ONE active task; todo = a list. If user
has one main task and 3 background items, push focus middle + todo
bottom.
Pushing — three ways
A. The helper (recommended; validates schema)
$CLAUDE_PLUGIN_ROOT/scripts/widget.sh push <type> <slot> <<EOF
{ "title":"今天", "items":[...] }
EOF
Or directly:
$CLAUDE_PLUGIN_ROOT/skills/card-widget/scripts/push_widget.py \
todo --slot bottom --data-stdin <<EOF
{ "title":"今天", "items":[{"text":"刷固件","tag":"today"}] }
EOF
B. curl
curl -sf -X POST http://127.0.0.1:9877/widget \
-H 'Content-Type: application/json' \
-d '{"type":"todo","slot":"bottom","data":{...},"ttl":1800}'
C. Preview without device
curl -sf -X POST http://127.0.0.1:9877/widgets/preview -o /tmp/p.png && open /tmp/p.png
Pushing fields
type — required, one of the 16 above
slot — top-left | top-right | middle | bottom | full
data — required, matches the type's schema
ttl — seconds; 0 = no expiry. Use ~1800 for ephemeral info.
stale_after — seconds; widget gets a "stale" badge but stays visible
theme — "" default. Don't set unless user asks.
Failure modes
- Schema mismatch → daemon returns HTTP 400 with the failing field.
Fix and re-push.
- Daemon unreachable → run
/card-onboard to diagnose.
- Widget shows but text is truncated → you sent too much. Fewer items,
shorter strings. The card is glanceable, not a Kindle.
- Pushed but nothing changed on screen → daemon debounces by ~1.5 s.
Wait. If 30 s passes and still no change, check
tail /tmp/ai_desk_card_daemon.log.
Before pushing — check connection
bash $CLAUDE_PLUGIN_ROOT/skills/card-onboard/scripts/probe.sh --quick
If transport.connected is false → run /card-onboard first; don't try
to push.
Sleep-frame (name card)
The device has a "digital business card" mode: when it deep-sleeps the
e-ink panel retains the last frame at 0 W until power-cycled. To push the
card and put the device to sleep:
/card-sleep
The card content comes from ai-desk-card/assets/profile.yaml. When the
user asks to update their card, edit the YAML directly:
name: "..."
tagline: "..."
bio_lines:
- "..."
tags:
- icon: "Job"
text: "..."
qr_image: "qr.png"
qr_label: "..."
avatar_image: "avatar.png"
footer: "..."
Avatar + QR are PNGs in ai-desk-card/assets/. If user wants a custom
image, ask them to provide it; don't try to generate emoji or scannable
QR — Pillow's bundled fonts lack emoji and we use image-based QR
intentionally. Tag icons should be plain short labels (Job / City /
Web), not emoji.
When to push proactively
- Starting a non-trivial task: push
ai-status with session_name +
task. Once per task, not per action — e-ink doesn't like frequent
refreshes.
- Long-running operation finishes: push
scratch with a success
line ("PR opened: #1234"). User glances at card and knows.
- Don't push for trivial state changes or every tool call.
Auto-refresh (every 2 hours)
If the user has set up the cron-driven refresh (see /card-refresh skill
REFRESH.md), the cron job will re-invoke a headless AI CLI every 2
hours to refresh widgets with fresh data. You don't need to manually
poll — just push widgets when relevant during the conversation.