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:
inboxis counts per source;messagesis named senders' previews. Use inbox for "how much is waiting", messages for "who's poking me". - next-meeting vs calendar:
next-meetingis THE next event (big countdown);calendaris 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, pushfocusmiddle +todobottom.
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 aboveslot—top-left | top-right | middle | bottom | fulldata— required, matches the type's schemattl— seconds; 0 = no expiry. Use ~1800 for ephemeral info.stale_after— seconds; widget gets a "stale" badge but stays visibletheme—""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-onboardto 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: "..." # Big name (≤ ~10 chars wide ideal)
tagline: "..." # One short subtitle (≤ 36 chars)
bio_lines: # 2-5 lines, auto-wrap. Empty string = half-gap.
- "..."
tags: # Up to 4 chips
- icon: "Job"
text: "..."
qr_image: "qr.png" # Optional; placeholder if missing
qr_label: "..." # One line under QR
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-statuswithsession_name+task. Once per task, not per action — e-ink doesn't like frequent refreshes. - Long-running operation finishes: push
scratchwith 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.