Build on Vyre
Tips for your module
A module teaches people how to use it best with tips: short lines that Vyre shows one at a time, in the place they are working. Vyre's own modules ship theirs the same way, so a module you write or add from outside gets tips on every surface with no UI code.
Declare them
Tips live in module.json under teaches.tips:
{
"name": "bakery",
"version": "1.0.0",
"teaches": {
"tips": [
{ "id": "count", "text": "Run `vyre call bakery.count` for today's Northwind Bakery orders.",
"surfaces": ["cli"], "level": "first-use", "trigger": "on-use", "since": "1.0.0",
"command": "vyre call bakery.count" },
{ "id": "rye", "text": "Rye orders now show in Now, above the rest.",
"surfaces": ["deck", "phone"], "level": "discovery", "trigger": "after-update", "since": "1.1.0" }
]
}
}
| Field | Required | What it holds |
|---|---|---|
id | yes | lowercase letters, digits and dashes, unique in the module. The full id is <module>/<id> |
text | yes | the tip itself, at most 140 characters. Put a key or command in backticks |
surfaces | yes | where it makes sense: capsule, deck, chat, phone, cli, glass, statusline |
level | yes | first-use (the basics), power (shortcuts for regular users), discovery (why to try it) |
trigger | yes | on-use (while the person is in this module), idle (when they pause), never-used (for people who have not tried it), after-update (only as news in a release) |
since | yes | the version that brought it. First-party tips use Vyre's version; yours use your module's |
key | no | a shortcut, drawn as a key chip, like ⌥⏎ |
command | no | a command the surface can offer to copy or run, like vyre call bakery.count |
docs | no | a docs page for Show me, like using/planner.md#reminders |
about | no | what the tip is about, when it isn't the module itself (Vyre's surface tips use deck, chat, phone, cli) |
Keys that start with x- are free for your own notes. Any other key is refused.
Write a good tip
- Lead with the action: "Press", "Type", "Run". One idea per tip.
- Show the key or command. A tip nobody can act on right away is a paragraph.
- Plain words. No em dashes, no hype, no "you can".
- Use the sample world in examples (alex, Juniper Studio, Northwind Bakery, juno, kit), never a real person or client.
- Give a module 6 to 10 tips: two or three first-use, a few power, one or two discovery, and one or two idle.
A tip that breaks a rule is dropped and logged (vyre logs tips); the module's other tips still
show. Vyre's own tips are held to the same rules by test/tips-content.test.js, which also checks
that each docs link lands on a real heading.
How one is picked
A surface calls tips.next {surface, context: {module, idle, busy}} and gets one tip or none. The
rules are in core/tips/pick.js, and every rule has a test.
- Nothing when tips are off, when
busyis set (an ask, a draft waiting, a running turn, typing), within the gap since this surface's last tip (30 minutes by default), within two minutes of a tip on any surface, or once six were shown in 24 hours. - Tips about the module named in the call's
contextwith triggeron-use:first-usewhile the person has used it fewer than three times, thenpowerfirst. - When
idle:never-usedtips about modules they have not touched, starting with the module that went longest without a tip. - When
idle: tips newer than the version the person last saw. - When
idle:idletips about modules they use.
On a surface's very first open (context.first: true), one never-used discovery tip may show
without idle, once per surface, as a welcome. Every rule in step 1 still applies to it.
Within a step, manifest order decides, so put your best tip first. A tip retires after two showings, when the person acts on it, or when they dismiss it.
Show one on a surface
A surface that shows tips does three things:
- asks
tips.nextwith the module in front of the person,first: trueon its very first open,idle: trueafter a pause, andbusy: truewhile anything else wants their attention; - calls
tips.seen {id, surface}when it draws the tip, andtips.seen {id, surface, acted: true}when they press Show me or run the command. The CLI passesmark: truetotips.nextinstead, since it prints at once; - calls
tips.dismiss {id}on ×, ortips.dismiss {module}for "Hide tips about this".
A surface that leaves out module gets the view it last reported to cohesion's context.now, and
anything in waiting.count counts as busy, so a surface that already reports its context need not
say it twice. idle, first and a running turn are always the surface's own to say.
Calling tips.used {module} when the person opens a module's view, even with no tip on screen,
keeps the "used" count right. The tips module never pushes anything; it only answers.
tips.whatsnew {since} lists what came after a version, newest first. vyre update calls it
after the changelog, and tips.whatsnew {ack: true} marks the news as seen. When the running
version moves, the tips module emits tips.updated {module, from, to, count} once.
Where the pieces are
core/tips/check.js: the rules a tip must pass (checkTips).core/tips/pick.js: the selection rules.core/tips/index.js: the tools, the store and the version check.- The loader hands the tips module every running module's tips (
ctx.declaredTips()); the tips module never reads another module's files.