Publish follow-up progress live, and let Thread Badges draw any plugin's complications - #17
Merged
Merged
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Thread Badges' follow-up ring could not update live. A plugin hears only its own realtime signals, so Follow Up's
followups-changednever 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 oneglobalThis. 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 surfacewants 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.mjsnow runs first innpm run checkand fails when the copies drift; CONTRIBUTING has the rule.follow-up/progress. It answers every wanted thread with onegetFollowUpCountsV1call, asks again onfollowups-changedfor a wanted thread, and asks for all of them again on reconnect. The README documents it under "For other plugins".experimental_Icon) in its tone.runningpulses unless the system asks for reduced motion.complicationPrefs_listandcomplicationPrefs_set. They can't be bb settings, because providers are only discovered after the backend has declared its settings. Every write tells every window.Breaking, in Thread Badges 0.4.0
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:
provide,want,read,isProvided,subscribe,providers,subscribeProviders{ kind, id }; the id is opaque and may be empty for a singleton.detailandopenpass through like any field, until the first surface that draws them (the floating card) defines their shape. A surface checks them before drawing, anhrefabove all.<pluginId>/<name>, one provider per id; a duplicate replaces the first (hot reload needs this)Symbol.for("bb-community.complications.v1")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
detailandopen, 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:
Follow Up 0.7 wasn't linked for this run, because another thread had Follow Up linked to its own worktree.
npm run checkpasses in full locally.After merge
Tag
follow-up/v0.7.0first, thenthread-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