Take bb's public APIs where they exist - #18
Merged
Conversation
ariofrio
force-pushed
the
ariofrio/public-apis-where-they-exist
branch
from
August 22, 2026 19:00
4e7623f to
85f399c
Compare
The sidebar's project icons picked the bubble glyph by comparing a row's id to `proj_personal`, a literal that lives in bb's own domain package and that no public API promises to keep. The same decision is made two files away in ThreadFilter, and there it reads `project.isPersonal` — the flag the SDK puts on every sidebar project. `buildProjectIconMap` now takes the personal project's id and the caller reads it from bb, so the plugin holds no opinion about what that id is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The options menu wrote its two toggles by PUTting `/api/v1/plugins/ thread-stages/settings` from the browser. That route exists, but the plugin contract never mentions it, so the plugin also carried its own status-code handling and a fallback for a body that turns out not to be JSON. `useSettings()` reads and does not write, which puts the write on the server, where `bb.sdk.plugins.updateSettings` is the documented call. The menu now goes through an `updateSettings` RPC method whose input schema turns an unknown key into `invalid_input` before bb ever sees it, and the hand-rolled fetch leaves with `sidebar-settings.ts`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar read its project icons by POSTing the Icons plugin's own rpc route from the browser. The route shape is documented, but reaching into a neighbour with it is not, and it put another plugin's id and method in the frontend along with hand-parsed envelopes. `bb.sdk.plugins.callRpc` is the call bb ships for this, so `listProjectIcons` now makes it on the server and mirrors the neighbour's response in a schema that is deliberately not strict — the Icons plugin may grow fields, and an extra key is not this sidebar's business. `fetchProjectIcons` takes a loader instead of doing its own transport, so what happens when the neighbour is absent is now a test rather than a comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ctrl+Shift+S started a side chat by POSTing the Side chat plugin's own rpc route from the browser, with that plugin's id, method, and input shape spelled out in the frontend — including an `anchorText: ""` whose meaning belongs to the neighbour, not to a keyboard shortcut. bb ships `bb.sdk.plugins.callRpc` for one plugin to call another, so the call moves to the server behind this plugin's own `createSideChat`, which takes the one thing the shortcut actually knows: the thread the user is on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both plugins replay bb's own New thread command by synthesizing the keystroke bb listens for, which means reading bb's keybinding table first. They read it by fetching `/api/v1/system/config` from the browser — a route the plugin contract never points a plugin at, and one whose whole shape they then had to guard against by hand. `bb.sdk.system.config()` is the documented read, so each plugin now asks for it over its own `listAppKeybindings`, whose schema keeps the three fields a delegate uses and leaves bb's `when` rule to bb. The delegates themselves are untouched: they still take a `fetchConfig` returning `unknown` and still parse defensively, because a keybinding table is bb's to change. Synthesizing the keystroke remains private. This halves what that costs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Filing the last Idle thread leaves nowhere to go, so the chord opens a new
one. It did that by writing bb's own `bb.root-compose.project-id`, faking the
storage event that makes bb notice, and then synthesizing bb's New thread
keystroke — three private steps to say "compose here".
`openNewThread({ projectId, focusPrompt })` is one public call that does all
three, and it is what bb's own sidebar calls. It is a hook, and the chords run
in a content script, so the mounted list lends it down: while the sidebar
draws, the chord uses bb's action, and without it the old keystroke still
opens the composer, now without arranging bb's state behind its back.
Routing the other destination needed to know which project is personal,
because that one routes without a project segment and the project path would
land on the composer instead. The server knows this publicly — a project's
`kind` — so it now answers with a null project rather than an id the frontend
had to recognize, and the chord's first behavioral test covers it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The icons the personal project gets — a chat bubble, and no picker at all — were decided by comparing an id to `proj_personal`. That literal lives in bb's own domain package, no public API promises it, and the plugin was using it for an authorization decision: which project the user may not restyle. A project's `kind` is public, so the server reads it once per run and answers `listIcons` with the id bb reports. `setIcon` refuses that project rather than that string, the sidebar's content script — which has no hooks to ask with — takes the id from the same response, and the thread header asks the SDK directly, where `isPersonal` was already sitting on every sidebar project. `defaultIcon` and `isEditable` now take the personal project instead of recognizing it, so a project whose id merely looks personal keeps its icon, which is what the two new tests hold them to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ariofrio
force-pushed
the
ariofrio/public-apis-where-they-exist
branch
from
August 22, 2026 19:12
85f399c to
21c47d5
Compare
The chords run from a content script, so they fire wherever you are, but the composer step goes through bb's own "new thread in this project" action, and this plugin can only reach that while it is drawing the sidebar list. Set Settings → Sidebar to bb's built-in list and the difference shows up in one place: emptying Idle opens the composer on the project you last used instead of on none. That is a small enough gap to live with and a confusing enough one to hit unexplained, so it goes next to the sentence that promises the behavior, and into the release note that ships the change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Moving the side-chat call to the server dropped a step. The old frontend routine made two round trips — fork the thread, then persist its panel tab through this plugin's `ensureSideChatTab` — and the rewrite kept only the first. What was left wrote the tab to this client's storage and nowhere else. bb treats its own tab list as authoritative: `reconcileFixedPanelTabsState` rebuilds a thread's panel from the server's tabs whenever the two differ, and a `plugin-panel` tab is one it persists. Opening a thread at all commits bb's own Info and Git diff tabs to the server, so there is no fresh-thread case where the local list wins: the side chat's tab was dropped on every thread, sometimes before its composer was ever focused, leaving a hidden fork with nothing pointing at it. The next ⇧⌘L would fork another. The persist now happens inside `createSideChat`, next to the call it belongs to, which also drops the second round trip the frontend used to make. `ensureSideChatTab` had no caller left and goes with it, and `validateSideChat` goes back to pruning rows that exist. Four reviewers found this independently, one with a runtime trace; none of the tests did, because nothing covered what the shortcut leaves behind. One does now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three more findings from the same reviews, none of them breakage today, all of them a way this branch could break quietly later. A chord moved the thread before it worked out where to send you, so a failed project lookup left the thread filed and the caller told the move had failed. The routing now resolves first: either the chord happens and the destination is right, or nothing happens at all. Both keybinding passthroughs validated bb's table as one array, so a single row bb changed would have cost the whole table and silently reverted the shortcut to its hardcoded default. They now drop the row they cannot read, the way the delegate reading them always has. Drawing icons used to be a local read that could not fail; asking bb which project is personal made it a call that can. A failed ask now costs the bubble on one row rather than every icon in the sidebar, while writing an icon still refuses rather than guessing. And `updateSettings` accepted the two settings that existed when it was written, of six. A control wired to any of the other four would have failed validation with a toast; a test now holds the RPC's keys to the plugin's own. The README's caveat said "with bb's built-in list selected", which is narrower than the truth — the composer step also falls back in the moment before the sidebar has loaded. Both it and the changeset now say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mirrored Icons schema said it was lenient because it allowed extra keys. That is the wrong axis: the fields it pins are the ones that plugin owns and may change. A third owner kind, or a color that stops being a string, and the whole answer fails to parse — costing every project icon in the sidebar, where before the diff the sidebar simply ignored a row it did not recognize. The envelope is now loose enough to reach a per-row filter, and the rows are read one at a time, which is what the consumer does with them anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The sidebar list lends the chords bb's "new thread here" action through a module-level slot, and cleared it unconditionally on unmount. bb mounts one thread list, so today that is the same instance either way — but a second mount followed by the first one's teardown would clear a slot still being used, and the chord would quietly stop preselecting for the rest of the session. Clearing only what is still ours costs a comparison. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The changeset, the README paragraph, and a run of comments had grown past their jobs: a claimed payoff bb does not produce, a closing line about how much of bb's insides these plugins hold onto, and comments restating the code underneath them. Same facts, fewer words. Two comments went entirely: the code they sat on already said it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment-level pass over the same prose: a docblock that explained whose opinion an argument represents rather than what it is, and two filler words. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Section icons (#17) landed on the same functions this branch moved onto bb's public APIs, so the two had to be reconciled rather than picked between. `fetchIcons` keeps main's three maps and takes this branch's loader, so the sidebar still asks bb to call the Icons plugin. `buildProjectIconMap` keeps the personal project as an argument. The Icons server had grown its own read of bb's projects for the names beside each row. Which project is personal now rides along with that list instead of costing a second call: one read, one cache, and the failure handling main already wrote. A write that arrives before the first read waits for it, so refusing the personal project's icon still needs bb's answer rather than a guess. `sidebar-dom.ts` keeps the literal id. It collects anchors from the DOM before any state exists, so there is nothing there to ask; the comment says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The service that reads bb's projects also subscribes to project changes, so running it in a harness that stubs only the read leaves an unhandled rejection: every test passes and the run still fails. Same stub main's own project tests use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`proj_personal` is reserved in bb's domain package, bb's own `isPersonal` is computed from it, and generated ids cannot collide with it. Asking bb which project is personal bought nothing against that, and cost: a field on the Icons wire, a cache beside bb's project list, a write that waited for a read, a memoized lookup in the chord path, and an argument threaded through six functions in five files. All of it goes back to a comparison against a constant. The Icons plugin's sources are now identical to main; the other three keep a `const` with a line saying what it is. Two things fall out. The composer bridge no longer waits for the sidebar's projects, so it is lent on mount rather than after a load — the cold-start window the README had to describe is gone. And a chord can no longer file a thread and then fail resolving its route, because there is nothing left on that path to fail; that fix and its test go with it. Kept: the first behavioral test of `setWorkflowStage`, now holding it to what it actually answers. Co-Authored-By: Claude Opus 5 (1M context) <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.
Summary
Three plugins reached around bb's documented APIs — browser fetches of bb's and neighbouring plugins' routes, writes into bb's localStorage that a fake
StorageEventthen announced. Each now takes the documented call.Purpose
The private surface is what breaks silently on a bb upgrade, and none of it was load-bearing enough to keep: each call here has a documented equivalent that does the same work.
Changes
PUT /api/v1/plugins/…/settingsfrom the browserbb.sdk.plugins.updateSettingsbb.sdk.plugins.callRpcfetch("/api/v1/system/config")bb.sdk.system.config()StorageEvent+ fake keystrokeopenNewThread({ projectId, focusPrompt })Schemas mirroring bb's keybinding table and Icons' rows drop a row they cannot read rather than failing the whole payload, matching the consumers that were already tolerant.
Evidence
npm run release:checkgreen for all three plugins; CI green on all five plugin jobs.check:screenshotsis red: the lock digests each plugin'ssrc/, so any source change stales its shot. Needsnpm run screenshotson macOS to clear.Caveats
One behavior change.
openNewThreadis a hook and stage chords run in a content script, so the mounted sidebar list lends the action through a module-level slot. Without it — bb's built-in list selected — emptying Idle still files the thread and opens a composer, but on the project you last used rather than on none. Keeping the old private write as a fallback would preserve the behavior and the coupling both; Thread stages' README documents the difference instead.A loopback hop per settings write and per cross-plugin call: both exist only on
bb.sdk, so each gains a thin RPC on our own server. User-initiated exceptlistProjectIcons, which runs whenever the sidebar's project set changes.engines.bbunchanged. Every API used here exists atdesktop-v0.35.1, below both declared floors (>=0.36,>=0.35).proj_personalstays a literal. It is reserved in bb's domain package and bb's ownisPersonalis derived from it, so asking bb for it added state and failure modes without removing a risk.Written by Claude