docs GitHub

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" }
    ]
  }
}
FieldRequiredWhat it holds
idyeslowercase letters, digits and dashes, unique in the module. The full id is <module>/<id>
textyesthe tip itself, at most 140 characters. Put a key or command in backticks
surfacesyeswhere it makes sense: capsule, deck, chat, phone, cli, glass, statusline
levelyesfirst-use (the basics), power (shortcuts for regular users), discovery (why to try it)
triggeryeson-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)
sinceyesthe version that brought it. First-party tips use Vyre's version; yours use your module's
keynoa shortcut, drawn as a key chip, like ⌥⏎
commandnoa command the surface can offer to copy or run, like vyre call bakery.count
docsnoa docs page for Show me, like using/planner.md#reminders
aboutnowhat 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.

  1. Nothing when tips are off, when busy is 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.
  2. Tips about the module named in the call's context with trigger on-use: first-use while the person has used it fewer than three times, then power first.
  3. When idle: never-used tips about modules they have not touched, starting with the module that went longest without a tip.
  4. When idle: tips newer than the version the person last saw.
  5. When idle: idle tips 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.next with the module in front of the person, first: true on its very first open, idle: true after a pause, and busy: true while anything else wants their attention;
  • calls tips.seen {id, surface} when it draws the tip, and tips.seen {id, surface, acted: true} when they press Show me or run the command. The CLI passes mark: true to tips.next instead, since it prints at once;
  • calls tips.dismiss {id} on ×, or tips.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.