v0.1 pinger is a fixed six-button control surface, hand-coded and Rocky-bound. v0.2 generalizes it: the rack is a frame, the buttons are data, the functions are a registry. The same widget becomes a media remote, a build runner, a localMesh dispatcher, a fast-launcher — by swapping its registry, not its code.
"Detachable" means the widget isn't bound to its launch node. Started on Rocky, it can be picked up and continued on aqua or quartz; the registry travels with it. Functions that are local to a host (e.g. start mini-mac on Rocky) get proxied back through localMesh; functions that are intrinsically portable (open URL, send mesh message) run wherever pinger is.
One button entry, one registry record. Stored as JSON, edited via GUI (see proposals below), persisted at ~/.pinger/registry.json (or wherever pinger currently is).
{
"id": "ping",
"label": "Ping!",
"row": 1,
"color": "primary", // primary | secondary | danger | success | purple | custom
"icon": "📡", // optional emoji or path to png
"hotkey": "F1", // optional
"scope": "remote@rocky", // local | remote@<node> | mesh (any node)
"command": {
"type": "shell", // shell | http | mesh-message | python | url
"run": "python ~/forge6/b33p/b33p.py identify vert"
},
"confirm": false, // pop a "are you sure?" before firing
"feedback": "chirp" // chirp | toast | none
}
scope is the load-bearing field. local = run wherever pinger is. remote@rocky = proxy via localMesh to that node. mesh = broadcast or pick the right node from a hint. The frame doesn't care; only scope + command.type determine routing.
The frame is generic; the registry is the product. Three shapes for editing it without dropping to JSON.
Press the gear once to enter edit mode; buttons get tiny ✎ corners + a "+ add" tile appears at the end. Right-click on any button for the full menu. Drag to reorder. Press gear again to commit.
Gear icon slides out a side drawer with the full button list. Drag handles for reorder, ✎ opens a per-button form (label, scope, command, hotkey, color, icon), ✕ removes. The pinger frame stays untouched; edits commit live.
Separate pinger.edit window — full table view with sortable columns, multi-select, import/export of JSON registries, live preview of pinger as you edit. Heaviest option but best for managing several profiles.
B (edit drawer) on Rocky/desktop, A (inline) on aqua/Pebble. Same underlying registry, different surfacing per form factor. C (companion) earns its keep when registries grow past ~12 buttons or when sharing/importing button packs becomes a thing — defer until then.
All three proposals share the same form when you actually create or edit a button. The form is the only place a registry record gets shaped — proposals A/B/C just decide where it floats. Two modes: guided (default — dropdowns assemble the command for you) and raw (paste/edit the JSON directly).
┌─ new button ────────────────── ✕ ┐
│ │
│ LABEL │
│ ┌────────────────────────────┐ │
│ │ Ping! │ │
│ └────────────────────────────┘ │
│ │
│ COLOR ROW │
│ [primary▾] [1 ▾] │
│ │
│ ICON HOTKEY │
│ [📡] [F1 ] │
│ │
│ ─── what does it do ────────── │
│ │
│ TYPE │
│ ( ) shell ( ) http │
│ (•) mesh-msg ( ) python │
│ ( ) url ( ) chirp │
│ │
│ RUNS ON │
│ ( ) local (wherever I am) │
│ (•) remote@ [Rocky ▾] │
│ ( ) mesh (any node) │
│ │
│ TO MESSAGE │
│ [vert ▾] [hey from pinger ] │
│ │
│ ─── on press ───────────────── │
│ │
│ ☐ confirm before firing │
│ FEEDBACK [chirp ▾] │
│ │
│ [test it] [cancel] [ save ] │
└──────────────────────────────────┘
┌─ new button ────── [guided|raw] ✕┐
│ │
│ ┌────────────────────────────┐ │
│ │ { │ │
│ │ "id": "ping", │ │
│ │ "label": "Ping!", │ │
│ │ "row": 1, │ │
│ │ "color": "primary", │ │
│ │ "icon": "📡", │ │
│ │ "hotkey": "F1", │ │
│ │ "scope": "remote@rocky", │ │
│ │ "command": { │ │
│ │ "type": "mesh-msg", │ │
│ │ "to": "vert", │ │
│ │ "message": "hey from │ │
│ │ pinger" │ │
│ │ }, │ │
│ │ "confirm": false, │ │
│ │ "feedback": "chirp" │ │
│ │ } │ │
│ └────────────────────────────┘ │
│ ✓ schema valid │
│ │
│ [test it] [cancel] [ save ] │
└──────────────────────────────────┘
shell reveals a command-line input; http reveals method/url/headers/body; mesh-msg reveals to/message; python reveals a path-or-snippet input; url reveals the URL field; chirp reveals the b33p picker. Form is dynamic — only the fields the chosen type needs.scope. local = wherever pinger is right now. remote@<node> = always proxy to that node, regardless of where pinger lives. mesh = broadcast or pick from a hint.The [test it] button before save is load-bearing. It fires the command exactly as configured (respecting scope/proxy) and shows the result inline — exit code, stdout, error — without committing the button to the registry. Lets you iterate the command shape before paying the registry cost.
Above the type radio, an optional "discover" chip:
┌────────────────────────────────────────────┐
│ ⌕ discover "ping the agent vert" ↵ │
└────────────────────────────────────────────┘
suggests: type=mesh-msg, to=vert
message="ping" (edit)
[accept] [skip]
Free-text intent → pinger queries known mesh capabilities (innate set + onMesh service registry) and pre-fills the form. Skip = fall through to manual. Tied to q3 — exists when discovery is wired, doesn't block the form's manual path.
Not every capability needs to be a registered button. Pinger ships with two layers of functions that exist below the registry, always available:
Pinger-level operations that don't belong in the user's button registry. Reachable from the gear menu in the titlebar regardless of what's been configured. Survive a wiped registry.
about — version, registry path, mesh stateedit — open the active edit surface (proposal A/B/C)profiles — switch / new / duplicate registry profileimport · export — JSON registry in/outrelocate — detach + travel to another mesh node (q2)onMesh — toggle live mesh awareness (default: on if localMesh reachable)ping self — fires the pinger's own chirp + statusbar flash; sanity checkquitWhen onMesh is on, pinger introspects the localMesh service registry and exposes whatever's currently registered as ad-hoc fireable functions — without making them user-buttons. Two surfaces:
┌─ pinger ──────────── ⚙ ─┐
│ [Ping!] [Mac] [Stop ] │ ← user registry
│ [stack] [snap] [HALT ] │
│ ─── services ───────── │ ← onMesh layer
│ • pinger /ping ⚡ │
│ • pingle /ping ⚡ │
│ • mailbox /ping ⚡ │
│ • registry /ping ⚡ │
│ • messageRtr /ping ⚡ │
│ ⌕ filter… │
│ ready · localMesh:8801 │
└─────────────────────────┘
Live list of registered services with one-tap probe. Auto-updates as services start/stop. Click ⚡ to fire /ping against that service; long-press to "promote to button" (opens the configure form pre-filled).
┌─ pinger ──────────────────────┐
│ ┌───────────────────────────┐ │
│ │ ⌕ msg vert hey │ │
│ └───────────────────────────┘ │
│ → mesh-msg · to=vert │
│ → mesh-msg · to=vert.* (3) │
│ → registry · ping vert │
│ │
│ recent: │
│ · ping pingle │
│ · halt processFlow │
│ · msg gourmand "ack" │
│ │
│ [↵ fire] [⌘↵ promote] │
└───────────────────────────────┘
⌘K opens a Spotlight-style command palette over pinger. Free-text intent matches against innate functions + mesh services + agents in registry. Enter fires it ad-hoc; ⌘+Enter opens the configure form to make it a permanent button.
Important separation: the registry is your buttons — curated, reordered, themed. The mesh service list is the world's services — discovered, ephemeral, changes as nodes come and go. Pinger surfaces both but doesn't conflate them. Promotion is an explicit user action: "this mesh function is something I want a permanent button for."
This means onMesh = on with zero user-buttons is still a useful pinger — it's a mesh probe. And onMesh = off falls back to a pure user-registry remote (offline / privacy mode / when traveling outside the tailnet).
One global ~/.pinger/registry.json, or per-profile (~/.pinger/profiles/{daily,build,media}.json) with a profile picker in the titlebar?
Does pinger physically run on aqua (separate process there, mesh-aware), or does it run on Rocky and project a UI to aqua via web/QR/screen-share? First is cleaner architecturally; second is easier to ship.
Can buttons be auto-suggested from things already on the mesh? e.g. "I see localMesh.kill, b33p.identify, questboard.add — want any of these as buttons?" — or is the registry strictly hand-built?
Buttons can run arbitrary shell. Is that fine (operator-only widget) or should there be a confirmation prompt for any command.type=shell entry? Pre-approved registries vs ad-hoc edits?
Pinger ships empty. The frame, the gear, the statusbar — those are the chassis. Every visible action (chirp, halt, ping, mini-mac, scrollstack, questboard, mesh-msg) is either a registry entry the user added or an innate capability dispatched via the gear menu / command palette. There is no hardcoded button row, no hardcoded "Pingle integration," no hardcoded HALT semantics. v0.1's six buttons are example registry contents, not pinger features.
A button bound to a mesh service only appears, and is only configurable, when that service is online. Concretely:
RUNS ON dropdown is populated from live mesh state. If Rocky isn't reachable, remote@rocky is not an option — period. You can't author a button for a service that isn't there.TO dropdown for mesh-msg is filled from currently-registered agents. Offline agents drop out. No way to author a button addressed to nobody./registry/online. A service that's not running is not a function pinger knows about, full stop.This makes "is this button safe to fire right now?" a property of the live mesh, not of the registry. The registry is intent; availability is reality.