Skip to content

Publish follow-up progress live, and let Thread Badges draw any plugin's complications - #17

Merged
matthewdias merged 6 commits into
mainfrom
complications-v1
Oct 5, 2026
Merged

matthewdias merged 6 commits into
mainfrom
complications-v1

Conversation

@matthewdias

@matthewdias matthewdias commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Why

Thread Badges' follow-up ring could not update live. A plugin hears only its own realtime signals, so Follow Up's followups-changed never reached Thread Badges. The ring moved on a 30-second poll or when the window regained focus.

bb imports every plugin's frontend bundle into one page (a plain import(url), no iframe or worker), so plugins share one globalThis. This PR puts a small registry there. Follow Up hears its own change and republishes, and the ring redraws within a second.

Once the ring reads a published value, there's no reason for Thread Badges to know about follow-ups at all. So it now draws any complication any plugin publishes about a thread, and Follow Up's progress is just the first one.

What

  • lib/complications.ts: the registry, protocol v1. A provider publishes complications (small values about a subject), and a surface wants them and draws them. The module is copied into both plugins rather than published to npm. bb bundles each plugin's dependencies into that plugin, so a package would run the same way; it only adds versioning, which nothing outside this repository needs yet. scripts/check-vendored.mjs now runs first in npm run check and fails when the copies drift; CONTRIBUTING has the rule.
  • Follow Up 0.7.0 registers an app overlay that provides follow-up/progress. It answers every wanted thread with one getFollowUpCountsV1 call, asks again on followups-changed for a wanted thread, and asks for all of them again on reconnect. The README documents it under "For other plugins".
  • Thread Badges 0.4.0 draws any thread complication that's live in the registry:
    • Each provider appears under Badges from other plugins in its settings, with its description and a preview. It's off until you turn it on.
    • Turned on, it shares one priority scale with the built-in badges, sorting after them by default. Each has two options: show its text, and hide once complete.
    • A fraction draws as the old ring; anything else draws as the provider's own icon (experimental_Icon) in its tone. running pulses unless the system asks for reduced motion.
    • These settings live in the plugin's storage behind complicationPrefs_list and complicationPrefs_set. They can't be bb settings, because providers are only discovered after the backend has declared its settings. Every write tells every window.
    • The pull-request, checks and ports badges are still drawn by Thread Badges alone. Publishing them as complications waits for a second surface that would draw them.

Breaking, in Thread Badges 0.4.0

  • The follow-ups badge type and its four settings are gone. The ring is off until you turn it on under Badges from other plugins.
  • The polling fallback is gone, so the ring needs Follow Up 0.7.
  • It moves to plugin SDK 0.6.15 for experimental_Icon, so it needs bb 0.45 or later. On older bb, stay on 0.3.

Protocol v1: what is frozen

Whichever bundle loads first creates the registry that every plugin in the window uses, methods included. So v1 freezes behaviour, not schema, and a fix in one copy only runs when that copy loads first. The implementation stamp makes it visible which copy is in charge:

v1
Frozen methods provide, want, read, isProvided, subscribe, providers, subscribeProviders
Values Named fields are validated. Other JSON-safe fields pass through, deep-frozen, capped at 4096 characters.
Subjects Open { kind, id }; the id is opaque and may be empty for a singleton.
Tones Open string: default · info · success · warning · error · running
Detail / tap-through Reserved names, not validated: detail and open pass through like any field, until the first surface that draws them (the floating card) defines their shape. A surface checks them before drawing, an href above all.
Ids <pluginId>/<name>, one provider per id; a duplicate replaces the first (hot reload needs this)
Symbol Symbol.for("bb-community.complications.v1")
Implementation stamp Each copy carries COMPLICATIONS_IMPLEMENTATION, bumped with any change in behaviour. The registry reports the creating copy's number, and a newer copy that finds an older one in charge warns once in the console.

Verification

  • Follow Up: 38 registry and progress tests (node --test) and 5 publisher UI tests through the SDK harness (emitRealtime, setRealtimeConnectionState, faked RPC).

  • Thread Badges gets a vitest suite: 37 tests across ordering, drawing, settings defaults, the prefs RPC on the fake host, and the badge and settings section through the app harness. All 27 mutants are killed, each by the test written for it.

  • Mutation-tested. Every mutant of the registry's guards, the implementation stamp, the pass-through of detail and open, and the publisher's behaviour is killed, except one equivalent: a function in the known-field set never survives the JSON copy anyway.

  • Live, Follow Up, on the first commit (eb1a2c0), linked into a running bb: the provider shows up in providers(), and 10 sidebar rings drew from the registry. Recording a follow-up and then dismissing it moved this thread's ring 6 → 7 → 6, with no reload. The stamp commit (4cb089e) only changes how an existing registry is looked up; it is covered by unit tests and wasn't re-run live.

  • Live, Thread Badges, on 0b891fd, with a provider registered from the page:

    • turned off, it drew nothing and wasn't asked about any thread;
    • turned on through the RPC, 17 rows drew it, from one batched ask;
    • a new value redrew the row in under 300 ms, green for a success tone;
    • in the real settings page, the section listed it with its preview, and its checkboxes stored their changes;
    • withdrawing the provider switched the section to its empty state.

    Follow Up 0.7 wasn't linked for this run, because another thread had Follow Up linked to its own worktree.

  • npm run check passes in full locally.

After merge

Tag follow-up/v0.7.0 first, then thread-badges/v0.4.0. In that order the ring never goes missing: Thread Badges 0.3 keeps polling Follow Up's counts call, which 0.7 still serves. Thread Badges 0.4 against Follow Up 0.6 draws no ring until Follow Up updates. After upgrading Thread Badges, turn the ring on under Badges from other plugins.

🤖 Generated with Claude Code

matthewdias and others added 3 commits October 4, 2026 15:26
Thread Badges' follow-up ring could not update live. A plugin hears only
its own realtime signals, so Follow Up's `followups-changed` never reached
it, and the ring moved on a 30-second poll or when the window regained
focus. Recording a follow-up and watching nothing happen was the
documented behaviour.

bb imports every plugin bundle into one page, so plugins share one
`globalThis`. This adds a registry there, under
`Symbol.for("bb-community.complications.v1")`: a provider publishes small
values ("complications") about a subject, and a surface wants and draws
them. Follow Up hears its own change and republishes; Thread Badges' ring
redraws within a second.

- `lib/complications.ts` is the registry and protocol v1. It is copied
  into both plugins because each installs alone, and
  `scripts/check-vendored.mjs`, now the first step of `npm run check`,
  fails when the copies drift.
- v1 freezes behaviour, not schema. Whichever bundle loads first owns the
  registry for the window, so its methods (`provide`, `want`, `read`,
  `isProvided`, `subscribe`, `providers`, `subscribeProviders`) cannot
  change under this symbol. Values pass through: named fields are
  validated, other JSON-safe fields are copied, deep-frozen, and capped at
  4096 characters. Subjects are an open `{ kind, id }`; tones are an open
  string; `detail` carries up to 8 rows; `open` allows only http(s) and app
  paths. Ids are `<pluginId>/<name>`, one provider per id, and a duplicate
  replaces the first, which hot reload needs.
- Follow Up registers an app overlay that provides `follow-up/progress`.
  It answers the threads surfaces want with one `getFollowUpCountsV1` call,
  asks again on `followups-changed` for a wanted thread, and asks for all
  of them again on reconnect.
- Thread Badges' ring draws the published value when a provider is
  present, and falls back to the counts poll for an older Follow Up.

Tested with 35 registry and progress tests and 5 publisher UI tests
through the SDK harness. Every mutant of the registry's guards and of the
publisher's behaviour is killed except one equivalent (a function in the
known-field set never survives the JSON copy). Verified live on this build:
the provider is listed, and recording then dismissing a follow-up moved
the sidebar ring 6 -> 7 -> 6 without a reload.

Releases Follow Up 0.7.0 and Thread Badges 0.4.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whichever bundle loads first creates the registry, so its code is what
every plugin in the window runs. A fix shipped in one plugin only takes
effect when that plugin happens to load before every unfixed copy, and
nothing showed which copy was in charge.

Every copy now carries COMPLICATIONS_IMPLEMENTATION, bumped with any
change in behaviour. The registry reports the number of the copy that
created it, and a newer copy that finds an older one in charge says so
once in the console. It still uses that registry: the behaviour is frozen,
so the older copy keeps working.

Also corrects the reasoning for copying the module. A published npm
package would install; only a workspace package would not. Either way bb
bundles each plugin's dependencies into that plugin, so copying is a
distribution choice, not a runtime one. Fixed in the module header,
CONTRIBUTING and scripts/check-vendored.mjs, along with the header's list
of frozen methods, which was missing isProvided.

Four tests, and all six mutants of the stamp are killed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The follow-ups ring was a badge written for one provider. Thread Badges
now draws whatever thread complications are live in the window's
registry, and Follow Up's progress is the first.

- Every provider that answers threads appears under "Badges from other
  plugins" in Thread Badges' settings, with its description and a
  preview drawn from its sample. Each is off until turned on, so
  installing a plugin never changes your rows by itself.
- Turned on, it joins the built-in badges on one priority scale. It
  sorts after them by default, and ties go to the built-ins. It has two
  options: show its text beside it, and hide it once complete.
- A fraction draws as the old ring; anything else draws as the
  provider's own icon through experimental_Icon. Tones use the built-in
  palette, and `running` pulses unless the system asks for reduced
  motion.
- Those settings can't be bb settings, because bb declares settings when
  the backend loads and providers are found later, in the app. They live
  in the plugin's storage behind complicationPrefs_list and
  complicationPrefs_set, and every write tells every window.

Breaking, for Thread Badges 0.4.0:

- The follow-ups badge type and its four settings are gone, and the
  ring is off until you turn it on in the new section.
- The polling fallback for Follow Up 0.6 is gone; the ring needs Follow
  Up 0.7.
- Moves to plugin SDK 0.6.15, for experimental_Icon, so this needs bb
  0.45 or later.

The pull-request, checks and ports badges are still drawn only by
Thread Badges. Publishing them as complications waits for a second
surface that would draw them.

Thread Badges gets a vitest suite: 37 tests across ordering, drawing,
settings, the server RPC on the fake host, and the badge and settings
section through the app harness. All 27 mutants are killed. Verified
live with a provider registered from the page: off, nothing was drawn
and nothing was asked; turned on, 17 rows drew it from one batched ask;
a new value redrew in under 300 ms; the section's controls stored their
changes; and withdrawing the provider emptied the section.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthewdias matthewdias changed the title Publish follow-up progress live through a shared complications registry Publish follow-up progress live, and let Thread Badges draw any plugin's complications Oct 5, 2026
matthewdias and others added 2 commits October 4, 2026 22:45
The registry checked `detail` (the first 8 rows, each needing a label
and a value) and `open` (an http(s) or app-path href, nothing else).
No surface draws either yet; the floating card would be the first.
Released copies freeze what they validate, so a card that turned out to
need 12 rows, a row without a value, or a click that runs a command
would have had those stripped by every older copy, for good.

Both are now reserved names that pass through like any field: copied as
JSON, deep-frozen, inside the size cap. The first surface that draws
them defines their shape, and it validates them before drawing, an href
above all. What v1 still checks is exactly what a real surface has
drawn: icon, label, tone, text and fraction.

Nothing uses either field today, and nothing has been released, so
COMPLICATIONS_IMPLEMENTATION stays at 1.

Replaces the three validation tests with two that pin the pass-through,
including an href arriving exactly as sent. Making either name
validated again turns them red.

Also moves the heavy imports in two new UI test files to the top of the
file, as #18 did for Follow Up's older ones: the first load of the
hugeicons package outlasted a test's timeout on a busy machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthewdias
matthewdias merged commit 78a3f37 into main Oct 5, 2026
1 check passed
@matthewdias
matthewdias deleted the complications-v1 branch October 5, 2026 04:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant