Run hey watch: one live connection per shell, the Imbox read on every change - #3
Conversation
…toasts hey-cli 0.2.0 grows `hey omarchy poll --limit N [--notify] --json`, which answers in the shape of `hey box imbox --json` and, with --notify, toasts new Imbox mail itself -- one toast per poll at most, replaced rather than stacked, identified as HEY so SUPER+CTRL+comma silences it. The plugin passes --notify when its bar entry says `notify: true`; a switch in the panel header (and N) flips that key through updateEntryInline, the same key `hey setup omarchy --notify` and `omarchy bar set 37signals.hey notify true` write. The manifest declares it, default off. The Screener count comes from `hey screener list --count --json` instead of a token pulled out of the CLI and handed to curl; the plugin never sees a token now. Failures are read from stderr, where the CLI writes its error envelope, and `auth` (the CLI's actual code) takes the sign-in branch. A CLI too old for `omarchy poll` is named in the header. `hey tui` nudges the plugin over IPC after it archives or marks a thread. The refresh handler fans that out to every per-monitor instance, the way the shell's BarWidget.broadcast does, and a refresh that arrives mid-fetch is coalesced into one follow-up rather than dropped. Tests: PR #1's harness (node --test over Model.js, qmltestrunner with Process stubs) ported as files, plus the AUR spellings of the setup plan; the probe stderr test stays with #1's Service.qml change. Requires hey-cli >= 0.2.0.
A build that already has the hidden omarchy group but not poll trips over --limit before it gets to report an unknown command; seen live.
|
@jzimdars @robzolkos — heads-up before this draft surprises you: #3 layers hey-cli’s new Two things to flag up front:
No immediate action requested; I wanted you to have the context before encountering the draft cold. |
|
@jzimdars @robzolkos — correction to my earlier heads-up: the polling architecture described there is being superseded. The proposed direction is now a generic |
… change The plugin polled `hey omarchy poll` every ten minutes, so the bar could show a thread as unread ten minutes after it was read elsewhere, and a toast could land ten minutes after the mail. hey-cli's answer is `hey watch` (basecamp/hey-cli, branch watch-notify): it follows every box over HEY's cable, catches up from its cursor after a disconnect and prints a line per change. The plugin treats any line as a wake-up and re-reads the Imbox with `hey box imbox`, coalesced so a burst of changes costs one read, and starts the watch before the first read so nothing falls between the two. The ten-minute timer stays as a safety net. The plugin is now a service as well as a bar widget: the shell instantiates Service.qml once and every bar — one per monitor — reads that instance, so one `hey watch` runs per shell; panels push their settings into it, and an IPC refresh is one call. A shell without service support gets a widget-local instance per bar, kept inert once a shared one exists. A watch that exits signed out waits for the next probe; any other exit is restarted on a backoff from two seconds to a minute, and the panel header says so in the meantime. Flipping `notify` restarts the watch with or without `--notify`, which now sends the toasts itself, one per batch of changes, with no state file.
…p, a bare screener count `connected` meant the watch process was alive, which says nothing about the cable: a reconnect backoff inside `hey watch` looked live. The watch now reports itself — "ready" once it is listening and again after each catch-up, "disconnected" when the cable drops — so `connected` follows those lines and `watching` is the process. The read on "ready" is also what makes the startup picture gap-free, rather than the order the two processes were started in. A burst of watch lines cost two reads: the first line's and the follow-up the rest queued. Lines are debounced now, so a burst is one read, plus one follow-up only when changes land while a read is in flight, since that read may predate them. The follow-up refresh started inside busy's own change handler and flipped the processes busy is made of while busy was still being notified — a binding loop QML warned about on every coalesced refresh since the coalescing landed. It starts on the next turn of the event loop instead. The test harness lets the refresh timer's start trigger fire first, as it does in the shell, instead of racing it. `hey screener list --count --json` answers a bare number since the CLI's global --count took over from the command's own flag; the parser takes that alongside the older envelope.
|
@jzimdars @robzolkos — the hold is lifted; this is ready for your review (still a draft only for the release gate — it needs the hey-cli with What you are looking at, in one breath: the plugin is now a service as well as a bar widget, so one |
|
@jeremy this is a great add! Agree with deltas and unread counts per account. |
There was a problem hiding this comment.
Pull request overview
Introduces live HEY Imbox updates through a shell-wide service backed by hey watch.
Changes:
- Adds debounced live refreshes, reconnect handling, notifications, and watch backoff.
- Shares one service across monitor widgets and exposes live status over IPC.
- Adds Node/QML coverage and updates documentation and requirements.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
Service.qml |
Implements shared watch and refresh lifecycle. |
Panel.qml |
Connects widgets to the shared service and adds notification controls. |
Model.js |
Adds CLI command and response helpers. |
manifest.json |
Registers the service and notification setting. |
README.md |
Documents live updates, notifications, and CLI requirements. |
tests/run |
Runs Node and QML test suites. |
tests/model.test.js |
Tests model helpers and CLI commands. |
tests/setup-command.test.js |
Tests setup locking and recovery. |
tests/qml/tst_service.qml |
Tests service refresh and watch behavior. |
tests/qml/imports/Quickshell/Quickshell.qml |
Stubs Quickshell environment access. |
tests/qml/imports/Quickshell/qmldir |
Registers the Quickshell test module. |
tests/qml/imports/Quickshell/Io/Process.qml |
Stubs process execution and exits. |
tests/qml/imports/Quickshell/Io/ProcessRegistry.qml |
Tracks test process instances. |
tests/qml/imports/Quickshell/Io/SplitParser.qml |
Stubs streaming line parsing. |
tests/qml/imports/Quickshell/Io/StdioCollector.qml |
Stubs collected process output. |
tests/qml/imports/Quickshell/Io/qmldir |
Registers Quickshell I/O test types. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6affc33793
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Stanko asked on #243 whether --notify could just be --run-async. It couldn't, and the reason it couldn't is the tell: the flag bundled two things. What counts as new mail — unseen, not muted, active_at later than the watch's record of the thread or than its start on HEY's clock, remembered across every box so a move-in isn't new — is HEY's semantics and state across events, and belongs in the CLI, decided once and Go-tested. notify-send, the app-name, -p/-r, the Omarchy glyph and exec hints, `Sender — Subject`, one toast per read and the Imbox-only scope are presentation, a desktop's business, and belonged in the plugin that already owns rendering and settings. So the CLI grows a generic, scriptable notion of new mail and loses the desktop: every added and updated line carries "new": true|false, --events new selects the true ones (a union with added, updated and deleted), a --run-* script sees HEY_NEW=1, and `hey watch --box imbox --events new --exit-on-first` is "block until new mail". The tracker always runs — one cheap serverNow GET at startup, one map — so the line shape is stable, and a read is classified before it is recorded, as before. --notify, --notify-box, desktopNotifier and notifiedBoxes are gone; neither flag was released, so .surface-breaking needs no entry. The Omarchy plugin toasts from the new lines itself (basecamp/omarchy-hey-plugin#3). Docs follow: docs/omarchy.md's "hey watch --notify" becomes "New mail is a watch event" and "The plugin toasts", the decision record tightens to "Generic commands; the face presents", README and the skill lead with --events new, and `hey setup omarchy --notify` says it turns on the plugin's toasts rather than running hey watch --notify.
…notify hey-cli 881aa9e made new mail a watch event: every added and updated line says "new": true|false, decided once by the CLI on HEY's clock, and the flag that sent the toast itself is gone. The toast is presentation, so it moves here, where the rendering and the notify setting already are. Service.qml collects the new lines whose box is the Imbox while notify is on, debounces them for 1.5 s so one read's burst is one toast, and sends one omarchy-notification-send — --app-name HEY so SUPER+CTRL+comma silences it, the bar's envelope as the glyph, --exec focusing the TUI daemon-side, -p to learn the daemon's id and -r to replace the last toast while that id is recent enough to trust. Flipping notify no longer restarts the watch; it only gates the toast path, and off drops what was about to go out. Model.js carries the text and argv logic the Go used to (composeMailToast, notificationText, toastCommand, replaceableToastId) under node tests; tst_service.qml drives the lines end to end: a new Imbox line is one toast, two in the window are one, the next burst replaces, The Feed's new mail and a new:false line do not toast, notify off does not.
|
@jzimdars @robzolkos heads-up from 951118e: the toasts are now the plugin's. hey-cli's |
…re-read after a failed seen Five review threads on #3. The probe now prints `hey version`'s line ahead of the auth status and parseProbe splits them: a release below 0.2.0 — which has a `hey watch`, but one that says neither ready nor which threads are new — sets cliOutdated, so no watch runs and the header says to upgrade, while the Imbox is still read on the timer. A dev build or an unreadable version is not held against the CLI. A stop the service asked for still reports an exit; _watchStopping marks it so watchExited neither records a "live updates stopped" error nor schedules a restart. Authentication lost during the accounts or Imbox read now goes through signedOut, which stops the watch as the probe's path always did. A failed `hey seen` schedules the delayed re-read even while live, since the panel marked the thread seen optimistically and the cable has nothing to report; its error is parsed from the envelope like every other handler's. The handler had been calling root.actionStatusTimer and root.refreshAfterRead — ids, not properties — so it threw before advancing the queue: the status never cleared and a second mark never ran. Both are reached by id now, and a test marks two threads in a row.
Sol's review: plain `hey watch` honoured a persisted `hey accounts use` filter, hiding another account's changes from the panel, and a CLI with a watch but no `new` field ran and looked connected while toasts silently never fired. The watch now runs as `hey --account all watch --events added,updated,deleted,new,resync` — every account, and every event by name, `resync` included now that hey-cli makes it selectable — and a CLI that does not know `new` refuses the command up front, which cliTooOld recognizes alongside an unknown command or flag.
Stanko asked on #243 whether --notify could just be --run-async. It couldn't, and the reason it couldn't is the tell: the flag bundled two things. What counts as new mail — unseen, not muted, active_at later than the watch's record of the thread or than its start on HEY's clock, remembered across every box so a move-in isn't new — is HEY's semantics and state across events, and belongs in the CLI, decided once and Go-tested. notify-send, the app-name, -p/-r, the Omarchy glyph and exec hints, `Sender — Subject`, one toast per read and the Imbox-only scope are presentation, a desktop's business, and belonged in the plugin that already owns rendering and settings. So the CLI grows a generic, scriptable notion of new mail and loses the desktop: every added and updated line carries "new": true|false, --events new selects the true ones (a union with added, updated and deleted), a --run-* script sees HEY_NEW=1, and `hey watch --box imbox --events new --exit-on-first` is "block until new mail". The tracker always runs — one cheap serverNow GET at startup, one map — so the line shape is stable, and a read is classified before it is recorded, as before. --notify, --notify-box, desktopNotifier and notifiedBoxes are gone; neither flag was released, so .surface-breaking needs no entry. The Omarchy plugin toasts from the new lines itself (basecamp/omarchy-hey-plugin#3). Docs follow: docs/omarchy.md's "hey watch --notify" becomes "New mail is a watch event" and "The plugin toasts", the decision record tightens to "Generic commands; the face presents", README and the skill lead with --events new, and `hey setup omarchy --notify` says it turns on the plugin's toasts rather than running hey watch --notify.
Stanko asked on #243 whether --notify could just be --run-async. It couldn't, and the reason it couldn't is the tell: the flag bundled two things. What counts as new mail — unseen, not muted, active_at later than the watch's record of the thread or than its start on HEY's clock, remembered across every box so a move-in isn't new — is HEY's semantics and state across events, and belongs in the CLI, decided once and Go-tested. notify-send, the app-name, -p/-r, the Omarchy glyph and exec hints, `Sender — Subject`, one toast per read and the Imbox-only scope are presentation, a desktop's business, and belonged in the plugin that already owns rendering and settings. So the CLI grows a generic, scriptable notion of new mail and loses the desktop: every added and updated line carries "new": true|false, --events new selects the true ones (a union with added, updated and deleted), a --run-* script sees HEY_NEW=1, and `hey watch --box imbox --events new --exit-on-first` is "block until new mail". The tracker always runs — one cheap serverNow GET at startup, one map — so the line shape is stable, and a read is classified before it is recorded, as before. --notify, --notify-box, desktopNotifier and notifiedBoxes are gone; neither flag was released, so .surface-breaking needs no entry. The Omarchy plugin toasts from the new lines itself (basecamp/omarchy-hey-plugin#3). Docs follow: docs/omarchy.md's "hey watch --notify" becomes "New mail is a watch event" and "The plugin toasts", the decision record tightens to "Generic commands; the face presents", README and the skill lead with --events new, and `hey setup omarchy --notify` says it turns on the plugin's toasts rather than running hey watch --notify.
…a pipe Found on the first live run of ea0cdab: piped, `hey version` prints the JSON envelope, so the probe's first line was `{` and nothing after it parsed — the panel said it could not check the CLI. `hey --version` is the one-line form.
… of hey omarchy (#243) * hey watch --notify: the engine under the Omarchy bar plugin, in place of hey omarchy The 37signals.hey bar plugin needed two things from hey-cli: the Imbox, and to know when it changed. `hey omarchy bar-status` and its planned successor `hey omarchy poll` answered both with one ten-minute poll that also diffed a fingerprint file to toast new mail — a toast could land ten minutes after the mail, the TUI had to nudge the plugin over IPC after its own mutations, and an Omarchy-named command that sounded watch-like sat on the CLI surface. `hey box imbox --json` is the read and `hey watch` is the wake-up: it catches up from its cursor on reconnect and skips ahead on a 409, so the bar is live and self-healing, and the cable reports the TUI's own mutations back within a second. `hey watch --notify` is the notifier — a generic flag on a generic command: notify-send under the app-name HEY so notification silencing applies, at most one toast per batch of changes, replaced rather than stacked, with Omarchy's glyph and focus-the-TUI hints when Omarchy is on PATH. What counts as new is an unseen, unmuted posting that was added, or updated with an active_at later than the watch last recorded (or than its start) — active_at moves on new mail only, so seen flips, mutes and moves never toast. No state file: a box's first read is its catch-up from the server's cursor (the box's last activity, not this moment) and is recorded but never toasted, and what the notifier remembers lives and dies with the watch. The cable now authorizes every dial with the credentials of the moment. actioncable-go reuses the header it was given on each reconnect, so a watch that outlived its access token came back with a stale one and exited with an auth error; a Transport wrapper refreshes the Authorization or Cookie header from the auth manager per dial, which the TUI's live updates get too. The `hey omarchy` group goes, never released: bar-status, the poll state and its lock, the config-degraded signalling only the poll reported, the account-scope and local-config exemptions, and the smoke state dir's reason. `hey setup omarchy` keeps configuring the plugin's notify key and removing the legacy hey-unread module; turning toasts on is now just setting the key. * Address review: notify the Imbox by default, remember every thread, say ready/disconnected/resync The notifier measured a thread's activity against its start only when it had no record, and it recorded only what --events let through and only after a box's first read, which it dropped whole. Three holes: mail that arrived while the watch was starting up was thrown away with the backlog; with --events updated an arrival was never recorded, so its later seen flip read as new; and a first read that ended in a 409 left the next arrival to be dropped as backlog. Now every posting the watch reads is recorded, in every box and whatever --events reports, and a toast needs activity later than the record — or later than the watch's start for a thread without one, which is what keeps a first read's backlog quiet without touching what arrived since. --notify covered every watched box, so the plugin, which watches all of them to see a thread leave the Imbox, would have toasted The Feed and Paper Trail too while the docs promised the Imbox. It toasts the Imbox unless --notify-box names another watched box; a name that isn't watched is a usage error, since that toast would never come. The watch now says so itself when its state changes: "ready" once the cursors are set and the subscription is live, and again after each reconnect's catch-up, so a reader that reads its own state on that line has no gap to reason about; "disconnected" when the cable drops; "resync" when a box changed more than the feed can list and the watch skipped ahead, which used to reach stderr only and left a watcher stale. A resync is a change — scripts run for it and --exit-on-first counts it; ready and disconnected go to stdout alone, since a script runs per change and neither is one. Their box is omitted, so the event's box is a pointer now and the environment leaves the box variables out. Rebased onto main, which meanwhile refreshed the cable credentials per dial itself (WithHeaderFunc); the transport wrapper here is gone in its favour. * Address review: order the connection news, cut over on HEY's clock, leave the plugin's notify alone A drop and the reconnect that followed it reached the loop through two channels, so a reconnect that completed while a slow catch-up held the loop could be announced ready and then, from the other channel, disconnected — a reader following the watch's own news would sit offline until the next reconnect. The cable's callbacks now queue every transition in order behind one wake-up, and the loop drains the queue in that order: a drop is announced, a reconnect catches up and is announced ready. The cutoff between backlog and new mail was the workstation's clock, compared with the server's active_at: a clock behind the server toasts backlog, a clock ahead sits on new mail. The watch now reads HEY's own clock off the Date header of one cheap, uncached request before the cursors are taken, and falls back to the local clock only when the server's can't be read. hey setup omarchy --remove deleted the bar plugin's notify key. The key is the plugin's: the panel and `omarchy bar set` write it as readily as setup does, so removal cannot tell a preference it wrote from one it didn't. --remove leaves it and says so; --no-notify is the explicit off switch. * New mail is a watch event; the toast moves to the face Stanko asked on #243 whether --notify could just be --run-async. It couldn't, and the reason it couldn't is the tell: the flag bundled two things. What counts as new mail — unseen, not muted, active_at later than the watch's record of the thread or than its start on HEY's clock, remembered across every box so a move-in isn't new — is HEY's semantics and state across events, and belongs in the CLI, decided once and Go-tested. notify-send, the app-name, -p/-r, the Omarchy glyph and exec hints, `Sender — Subject`, one toast per read and the Imbox-only scope are presentation, a desktop's business, and belonged in the plugin that already owns rendering and settings. So the CLI grows a generic, scriptable notion of new mail and loses the desktop: every added and updated line carries "new": true|false, --events new selects the true ones (a union with added, updated and deleted), a --run-* script sees HEY_NEW=1, and `hey watch --box imbox --events new --exit-on-first` is "block until new mail". The tracker always runs — one cheap serverNow GET at startup, one map — so the line shape is stable, and a read is classified before it is recorded, as before. --notify, --notify-box, desktopNotifier and notifiedBoxes are gone; neither flag was released, so .surface-breaking needs no entry. The Omarchy plugin toasts from the new lines itself (basecamp/omarchy-hey-plugin#3). Docs follow: docs/omarchy.md's "hey watch --notify" becomes "New mail is a watch event" and "The plugin toasts", the decision record tightens to "Generic commands; the face presents", README and the skill lead with --events new, and `hey setup omarchy --notify` says it turns on the plugin's toasts rather than running hey watch --notify. * Ready waits for a failed catch-up read; a skip-ahead moves the box's new-mail cutoff Two Codex threads on #243. A catch-up read that failed transiently left the box queued for a retry and returned nil, so ready was announced with a box still behind it — a reader then took the gap for a clean picture. catchUp now announces ready only once every box is read (readyOnceCaughtUp); a retry that reads the last box owed announces it (retryUnread), and a drop in between cancels the debt, since the reconnect's catch-up announces its own. A 409 skipped a box ahead without the new-mail record learning anything about the gap, so a thread that arrived in it and was later moved or labelled while still unseen measured its gap activity against the watch's start and read as new. skippedTo moves that box's cutoff up to the cursor it skipped to — HEY's last_posting_activity_at for the box, which bounds every thread in it — and the cutoff is the box's alone, so another box's mail is unaffected. A gap thread moved to another box may still read as new once; the resync line is the reader's cue to re-read either way. * A doorbell read pays the ready a catch-up owed The box a catch-up left behind may be the one that rings next, and its read clears it: announce the owed ready there too, rather than waiting for the retry that may be two minutes off. TestWatchDoorbellReadPaysTheReadyACatchUpOwed. * Ready yields to a queued drop; a script inherits no other event's variables Two Codex threads on a5183a5. A drop queued while a catch-up was reading — noteConnection appended it, listen hadn't drained it — let ready go out with the subscription already down; readyOnceCaughtUp now looks at the queue and withholds it, so the drain says disconnected and the reconnect's catch-up says its own ready. And scriptCommand built its environment on os.Environ, so a watch started by another watch's script handed HEY_NEW=1 or a thread id to events that never set them; the event's variables are stripped from the inherited environment first. * Resync is an event of its own; a skip-ahead floors the box for known threads too; HEY_NEW=0 Sol's advisory on #243. The floor a 409 skip-ahead sets (newMail.skippedTo) now applies to every thread in that box, known or not: a reply the watch missed inside the gap and then a move while still unseen measured its gap activity against an older record and read as new. Activity at or before the floor — the cursor the box skipped to, its last posting activity — is never new there; mail after it is, on a known thread or a new one. resync joins the watchable events. It is reported by default, scripts run for it and --exit-on-first counts it, and --events can leave it out — --events new does, so `--events new --exit-on-first` never exits on a 409 and a notify-send one-liner never toasts on one. The plugin asks for it by name. Scripts see HEY_NEW=0 as well as HEY_NEW=1, on top of the inherited-variable strip, so a test for new mail never reads a parent's value. * Cursors start no later than the watch did; transitions one at a time; no ready on the way out Three Codex threads on 83faa7d. The server bakes a box's last posting activity into its cursor, so mail that landed between the watch reading HEY's clock and reading the box list sat behind the cursor and was never read: noLaterThan moves such a cursor back to the start, and the catch-up reads it as new. followConnection took the whole transition queue at once, so a drop queued behind a reconnect was invisible to dropQueued while the reconnect caught up; it now takes one transition at a time. And readyOnceCaughtUp no longer pays a ready to a watch that --exit-on-first has already finished or an interrupt has cut short. * The watch's start is HEY's clock when it asked, not when HEY answered Sol's finding. serverNow returned the Date header as it stood when the server answered, so mail that landed while the request was in flight — its updated_at no later than Date — made a box cursor no later than the start, which noLaterThan left where it was, and the mail sat behind the initial cursor for good. An SDK retry could widen that window by seconds. The Date is now translated back to the request's start by the monotonic elapsed time, so a slow or retried request only moves the start earlier; the fallback is the local clock at the start for the same reason. Two tests: a delayed response puts the start at least the delay before Date, and the whole window end to end — a posting landing during the clock request, the box's cursor moved back to the start, the catch-up reading it, new: true. * The watch's start is a whole millisecond, strictly before the instant Codex on eabf979: started was nanosecond-precise while every cursor and active_at is milliseconds, the feed answers what is strictly later than its cursor, and isNew asks for strictly later than the start — so mail in the start's own millisecond was neither read nor new. cutoffBefore truncates the start to the millisecond and steps one back, on both the server's clock and the local fallback. * The local-clock fallback test allows for the cutoff's millisecond step * Follow every box and report the --box ones; record a posting as soon as it is classified Two Codex threads on b1e1dba. --box used to pick the boxes the watch read, so activity in the others was never recorded and an unseen reply in The Feed followed by a move into the Imbox read as new under the documented `--box imbox --events new`. Every box is followed now and --box picks the boxes whose changes are reported; a --box that names nothing is still not found. And a read's postings were classified against the pre-read record and recorded afterwards as a batch, so a posting carried twice by one read could be new twice; classify records each posting as soon as it is decided. * Ready is decided and announced under the transition lock; the watch stops with its last reported box Two Codex threads on c2bb571. A drop could be queued between readyOnceCaughtUp looking at the queue and announcing: the look and the announcement are now one critical section with noteConnection's queueing, so a drop lands before the look and withholds the ready, or after the ready is out, in the order things happened. And since every box is followed but only the --box ones reported, stopWatching gives up when the last reported box is gone rather than when the last box is: a watch following the rest for the record alone would sit there for good, reporting nothing. * A resync is reported only when the box was actually skipped ahead Codex on 8c68f04: a 409 on a box the server no longer lists drops the box, and when another reported box remains that path returned nil — so readBox still reported a resync for a box that is gone, inviting a re-read of nothing and ending --exit-on-first on it. skipAhead now says whether it moved the cursor, and a resync is reported only when it did.
Live: three HEY toasts a minute apart got three ids — each earlier popup had expired, and a replaces_id the daemon no longer tracks is a new notification by the freedesktop rules; probing the daemon directly, -r replaced a live popup in place under the same id. So replacement covers a burst that lands while the previous toast is still up; the ten minutes is only how long the id is trusted.
|
Live on the merged CLI (hey-cli |
robzolkos
left a comment
There was a problem hiding this comment.
Tested this end to end with the latest HEY CLI and the PR installed in Omarchy. All 23 Node tests and 42 QML tests pass, plugin validation is clean, and the CLI tests pass.
Live testing also worked as intended: one connected hey watch process, no warnings or errors, and marking all New for you threads seen on my phone updated the panel from 5 to 0 without a manual refresh.
Looks great — approved.
Built on the redesign from #2, which is now on
master. Requires hey-cli ≥ 0.2.0 (basecamp/hey-cli#243 — merged 2026-08-22 as 0f189d7: the watch'snewfield and--events new, and itsready/disconnected/resynclines), so merging and publishing wait until 0.2.0 is on the AUR — users must never receive a plugin that needs a CLI they cannot install — it runs against a localmake installbuild meanwhile (verified live on the merged CLI at010efb3(equivalent to main’s0f189d7): connected watch, an external mail → oneHEYtoast,hey seen→new:false).Why
hey-cli shipped its own inline
hey-unreadbar module inhey setup omarchythe same week this plugin grew its panel — two HEY icons, two Imbox pollers, two product stances in one slot. We settled on: the CLI is the engine, the plugin is the face. An earlier cut of this PR gave the plugin ahey omarchy poll; that was ten minutes stale by construction and an Omarchy-named subcommand on a general CLI, so hey-cli#243 replaced it with its generic commands:hey box imboxis the read,hey watchis the wake-up, and everyadded/updatedline the watch writes says whether the thread is new mail — the CLI decides that once, on HEY's clock; what to do about it (the toast) is the plugin's.What changes
manifest.jsondeclareskinds: ["service", "bar-widget"]withService.qmlas the service entry point. The shell instantiates it once; every bar widget — one per monitor — readsbar.shell.serviceFor("37signals.hey")and pushes its settings into it (the shell injects settings into widgets only). So onehey watchruns per shell, and an IPCrefreshis one call. A shell without service support gets a widget-local instance per bar, kept inert (active: false) once a shared one exists.hey watchis the wake-up. Run assetpriv --pdeathsig TERM hey --account all watch --events added,updated,deleted,new,resyncso it dies with the shell, covers every linked account whateverhey accounts usepersisted, and asks for every event by name — a CLI without thenewevent refuses the command up front (unknown event), which reads as an old CLI, rather than running a watch that never says which threads are new. The probe also readshey --versionahead ofhey auth status, so a release below 0.2.0 is reported before any watch starts. Any change line on its stdout re-reads the Imbox withhey box imbox --limit N --json(plus--account allas before), debounced (300 ms) so a burst costs one read — plus one follow-up when changes land while a read is in flight, since that read may predate them. The watch saysreadyonce its cursors are set and it is listening (again after every reconnect's catch-up), and the read on that line is what makes the picture gap-free: anything before the cursor is in the read, anything after it is an event. It catches up from its own cursor after a disconnect, so a laptop back from suspend is current within seconds. The ten-minute timer stays as a safety net.connectedfollows the watch's ownready/disconnectedlines (a reconnect backoff insidehey watchis not live);watchingsays the process is alive. The logo tooltip says· livewhile connected;statusover IPC reportswatching,connectedandwatchError. On exit only the last stderr line is kept (the CLI's envelope).auth→authenticated = false, no restart until the next probe succeeds.unknown command/unknown flag→ "HEY CLI 0.2.0 or newer is required". Anything else → restart on a backoff from 2 s doubling to 60 s (reset after a run longer than a minute), the panel header saying "Live updates paused — …" meanwhile.notifyis on, a watch line withnew: truewhose box is the Imbox (the watch follows every box; only Imbox mail asks for attention) is collected, debounced 1.5 s so one read's burst is one toast, and sent as oneomarchy-notification-send --glyph --app-name HEY -u low --exec "omarchy-launch-or-focus-tui --app-id=org.omarchy.hey hey tui" <headline> [description] -p [-r <id>]—Sender — Subjectfor one thread,N new in Imboxwith the first senders for more;--app-name HEYis what lets SUPER+CTRL+comma silence it,--execruns daemon-side so it survives shell restarts, and the printed id is trusted for ten minutes for-r; a later burst replaces only while the prior popup is still tracked, while the 1.5-second debounce prevents same-burst stacking. Flippingnotifyno longer restarts the watch; it only gates the toast path (off drops what was about to go out).Model.jscarries the text/argv logic (composeMailToast,notificationText,toastCommand,replaceableToastId) under node tests. The switch in the header,hey setup omarchy --notifyandomarchy bar set 37signals.hey notify true --jsonall write the same key. No state file.hey screener list --count --json(now a bare number from the CLI's global--count; the older envelope still parses), errors read from the CLI's stderr envelope, the coalesced refresh (its follow-up now deferred a tick — thebusybinding loop is gone), theflocked setup flow, the tests harness. A successfulhey seenno longer schedules its own delayed re-read while the watch is connected — the cable reports it back; a failed mutation still re-reads.SplitParserstub,Processstub withemitLineand the two-argexited(and an exit onrunning = false, as a real Process reports);tst_service.qmlcovers the watch argv,readymaking it live and reading,disconnectedturning live off without a read, a burst as one debounced read, events mid-read as one follow-up, auth exit waiting for the probe, other exits on a doubling backoff, the old CLI, a new Imbox line as one toast with the exact argv, two lines in the window as one toast, the next burst replacing via-r, The Feed's new mail / anew:falseline /notifyoff not toasting, the notify flip leaving the watch alone, the bare screener count, and the inactive local instance../tests/run(46 node + 57 QML, no QML warnings) andomarchy plugin validategreen.Open for your call
resyncevent keeps the current full re-read path correct after a 409. Recorded in hey-cli'sdocs/omarchy.md.