Skip to content

Add Info tab row and section slots that core uses - #6117

Merged
ymichael merged 5 commits into
mainfrom
bb/info-tab-row-and-section-slots-thr_eexyda8t9s
Oct 22, 2026
Merged

ymichael merged 5 commits into
mainfrom
bb/info-tab-row-and-section-slots-thr_eexyda8t9s

Conversation

@ymichael

@ymichael ymichael commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Human comments

What was wrong

The thread Info tab was a hard-coded list of core rows and sections in ThreadMetadataContent, so plugins had no way to add to it. Automations that target a thread (#5554) could only be seen in the global Automations view. An earlier attempt (local commit 11ec452c81) gave plugins a data hook that only core rendered from, and core did not use it itself.

What changed

Two slots, used by core and plugins alike

  • app.slots.experimental_threadInfoRow adds a row to the details block, and app.slots.experimental_threadInfoSection adds a section below it.
  • A registration is { id, group, order?, component }. The component receives { threadId, projectId } and mounts once per visible thread.
  • ThreadMetadataContent now renders only through ThreadInfoRows / ThreadInfoSections (apps/app/src/components/secondary-panel/info/ThreadInfoSlots.tsx). These merge CORE_THREAD_INFO_ROWS and CORE_THREAD_INFO_SECTIONS with plugin registrations.
  • Placement works like thread actions: sort by group, then order within it (unset last), then registration order, with core first on a tie. The comparator thread actions used moved to apps/app/src/lib/grouped-placement.ts, and both use it.
  • bb's groups are exported as experimental_THREAD_INFO_ROW_GROUPS (thread, environment, git, pullRequest, lifecycle) and experimental_THREAD_INFO_SECTION_GROUPS (related, changes, storage). Core uses orders 10, 20, 30 within each group and keeps today's order.
  • Plugin entries mount in a per-thread crash boundary. A crash removes only that row or section.

Shared building blocks

  • experimental_InfoSection (heading, count pill, collapse), experimental_DetailRow, experimental_InfoList (the "N more" list) and experimental_InfoListRow are the components core draws with.
  • DetailRow ({ label, icon?, align?, children }) is now bb's only label/value row: every core Info row and the commit dialog draw with it, and it is experimental_DetailRow. The old variant with class overrides is deleted. Branch and Archived truncate through an inner span, and the commit dialog's vertical changed-files row draws its own markup.
  • Section collapse now comes from the slot. Core ids are unchanged (commits, forks, …), so saved collapse state carries over. Plugin sections are stored as <pluginId>/<id> in infoPanel.collapsedSections.
  • Section separators moved from divide-y on direct children to each [data-info-section], so plugin wrappers do not break them.

Automations (first plugin caller, #5554)

  • A section in the related group, ahead of Child threads, lists automations whose execution.targetThreadId is the open thread. It shows 3 rows with "N more", status labels from the overview, and refreshes on any automations realtime signal. Selecting a row opens the automation.
  • automations_list and bb automation list --target-thread <id> take the target-thread filter. This is the server, CLI and test part of 11ec452c81. The skill documents it.

Docs and API process

  • docs/api_to_audit.md entry with core callers and audit items.
  • New Plugin Guide card "Thread Info rows and sections", with an Info tab fixture in the side panel wireframe.
  • Authoring references and API indexes updated.
  • Plugin SDK 0.6.69 → 0.6.70.

Not moved through the slots

  • The empty/loading decision (hasAnyThreadMetadata plus the forks query in ThreadDetailSecondaryContent) still decides from core state whether the tab shows details, a skeleton, or "No thread details available." Plugin entries do not count toward it. This is audit item 3.

No server/daemon wire changes.

How you verified

Tests

  • ThreadInfoSlots.test.tsx covers:
    • plugin rows and sections placed among core's by order, with unordered ones last;
    • a crashing plugin row or section dropped while core stays;
    • per-registration collapse, where a plugin section that reuses a core id does not collapse the core section.
  • thread-info-section.test.tsx (automations) covers the target-thread RPC, realtime refresh from another project, status labels, and navigation.
  • Server harness test for automations_list and the CLI --target-thread filter.
  • Guide fixture, order, and mobile tests.
  • turbo run test typecheck lint for @get-bb/plugin-sdk, @bb/app, bb-plugin-plugin-api-docs, bb-plugin-automations and @bb/domain: 20/20 tasks.
  • turbo run typecheck lint --filter='...[origin/main]': 142/142 tasks.
  • @bb/server test/services/plugins: 551 tests passed.
  • check-plugin-sdk-version.mjs passes.

DetailRow consolidation: Info panels at 589 and 339 px wide (long branch names truncating, an archived thread, a PR) and the commit dialog were pixel-compared against main. The dialog and five of eight panels are identical; the rest differ only by an animated glyph, a ticking relative time, and one anti-aliased pixel.

Parity with main in the real app

  • Built apps/app at main (cc292b9) and at this branch, and served each through a proxy in front of the live bb, so both builds showed the same real threads.
  • Captured the Info panel at 1500×2200 for five threads covering parent/child, PR, commits, uncommitted files, a detached checkout and thread storage, then pixel-diffed the panels.
    • Two threads are identical, and so is a third once its slow workspace status (87 ahead / 198 behind) had loaded.
    • The other two differ only in the animated status glyphs of running child threads.
  • Collapsing "Child threads" on the branch build showed it collapsed on the main build, so the preference key is unchanged. It was restored afterwards.

Automations in a dev instance (pnpm start:worktree)

  • Created five automations that target a thread. The Info tab showed "Automations 5" below the details with three rows and "2 more", in the same style as core sections.
  • Expand, collapse and re-expand work.
  • Pausing and deleting from the CLI updated the section without a reload.
  • Selecting a row opened /plugins/automations/automations/<project>/<id>.
  • The Guide card renders with badge 13 over the Info tab.

Fixes #5554

AGENT GENERATED

🤖 Generated with Claude Code

ymichael and others added 5 commits October 22, 2026 10:27
The thread Info tab now renders only through two registries:
app.slots.experimental_threadInfoRow (details block) and
app.slots.experimental_threadInfoSection (below it). Core's eleven rows
and five sections are registrations of the same shape, merged with plugin
registrations by order, so the tab keeps today's order and behaviour.

Info's own building blocks ship to plugins as experimental_InfoSection
(heading, count, collapse), experimental_DetailRow, experimental_InfoList,
and experimental_InfoListRow; core's rows and sections draw with the same
components. Section collapse is keyed per registration (core ids
unchanged, plugin sections as <pluginId>/<id>).

The Automations plugin registers the first plugin section: automations
whose execution.targetThreadId is the open thread. automations_list and
`bb automation list --target-thread` gain the target-thread filter.

Fixes #5554

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Info tab slot section pushed frontend-registration.md to 528 lines,
over the shipped-skill reference cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DetailRow and InfoDetailRow were two row components: the Info tab, plugins
and most rows used InfoDetailRow, while Branch, Archived and the commit
dialog reached past it to style the value cell themselves. DetailRow is now
the single row ({ label, icon?, align?, children }) that core and
experimental_DetailRow share. Branch and Archived truncate through an inner
span, and the commit dialog's vertical changed-files row draws its own
markup with the shared label and value classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Info rows and sections took one flat order number, and every core row's
number was documented, so renumbering core would move plugin entries.
They now take a required group plus an optional order within it, sorted
with the same comparator thread actions use (moved to
lib/grouped-placement.ts). bb's groups ship as
experimental_THREAD_INFO_ROW_GROUPS (thread, environment, git, pullRequest,
lifecycle) and experimental_THREAD_INFO_SECTION_GROUPS (related, changes,
storage). Core keeps today's order; Automations joins related ahead of
Child threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run-only signals never change the automation rows the section shows; every
write that does also publishes automations-changed. Ignoring
automation-runs-changed halves the section's refetches while runs happen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymichael
ymichael merged commit 26de548 into main Oct 22, 2026
89 checks passed
@ymichael
ymichael deleted the bb/info-tab-row-and-section-slots-thr_eexyda8t9s branch October 22, 2026 19:13
alexandre-vl added a commit to alexandre-vl/bb that referenced this pull request Oct 22, 2026
get-bb#6117 renders Info sections through slots, so RelatedThreadsSection no longer takes sectionId.
ymichael added a commit that referenced this pull request Oct 22, 2026
## Human comments

## What was wrong

GitHub pull request status was core. The daemon ran `gh`, the server
assembled and watched the status, and the app, SDK, CLI and Tasks and
GitHub plugins each reached it through core routes and commands. Core's
PR UI couldn't leave core because there was nothing outside core to read
from.

## What changed

**New bundled, default-enabled plugin: `plugins/github-pr-status`
(`bb--github-pr-status`)**
- `host.ts` / `host/git-host.ts`: the in-worktree lookup moved from
`packages/host-workspace/src/git-host.ts`. It covers the current branch,
upstream and fork remotes (`getPullRequestTarget`), the merge-queue
GraphQL query, and `gh pr ready|ready --undo|merge`. It runs on the
environment's own machine with the daemon's shell `PATH`.
- `server.ts`:
  - resolves the environment;
- serves a keyed `pullRequests` read, one row per environment id (up to
200), so a list of rows loads in one request;
- serves `markReady`, `markDraft` and `merge`, each of which re-checks
state before acting;
  - registers `bb pull-request show|ready|draft|merge <environment-id>`.
- `store.ts` is the single place reads are cached (10 s), re-checked
while watched, and published when they change.
- PR snapshots are kept by PR URL, so environments sharing a PR share
one snapshot. Persistence, cross-row reuse and webhooks can feed this
one place later.
- While a row is watched (`bb.rpc.experimental_whileWatched`), it
re-checks every 30 s when checks are pending, auto-merge is on, the PR
is queued or mergeability is unknown. That is the same rule as core's
`pull-request-watch.ts`.
  - Other triggers match main's:
    - `status-changed` and environment create/delete fetch fresh;
    - a completed turn re-checks through the 10-second cache;
    - `git-refs-changed` only marks the cached row stale;
- `metadata-changed` re-checks only when the environment's branch
actually changed (one environment read, no `gh`);
- in the app, a cached row is re-fetched on mount, and again when the
page is shown if it's older than 30 s (1 h once merged or closed). Main
got this from TanStack's `refetchOnMount: "always"` and focus refetch.
Here it uses only the public read hook's `refetch`, in
`pull-request-queries.ts`.
- It invalidates the row's tag only when the row changed, and never for
a result a viewer is already receiving.
- `contract.ts` is the public contract. The read returns the fully
assembled shape the UI renders: state, attention, checks, review,
mergeability, auto-merge and merge queue.

**Watched reads**
- `bb.rpc.experimental_whileWatched` comes from `bb/5939-recut`,
mechanism only: the `plugin-read` subscription target, the server
registry and producer lifecycle, app query hooks holding the
subscription, per-target subscribe validation, and fake-host support.
- Left out: the SDK `experimental_watchRpc` /
`experimental_watchDiscovery`, the CLI `--watch` flags, and
plugin-signal scoping.

**Core reads and acts only through the plugin's public contract**
- The composer banner segment, Info row and thread view use
`experimental_useRpcKeyedQuery` / `experimental_useRpcMutation` with
`pluginId: "bb--github-pr-status"`
(`apps/app/src/hooks/queries/pull-request-queries.ts`). These are the
same calls another plugin makes, so moving the UI later is a cut and
paste.
- The app's RPC hook host now allows those hooks outside a plugin slot
when `pluginId` is explicit.
- `bb thread show`, Tasks (`listTaskPullRequests`, now one batched read)
and the GitHub plugin's `pullForThread` call the same RPC.

**Deleted from core**
- The server PR watcher, route, cache, `environment-pull-request` tag
and subscription target, and the PR action branches.
- The SDK `environments.pullRequest` / `markPullRequestReady` /
`markPullRequestDraft` / `mergePullRequest`, and `bb environment
pull-request …`.
- The `ThreadPullRequest` / `GitHostPullRequest` domain schemas.
- The daemon's `workspace.pull_request` and
`workspace.pull_request_action`. **`HOST_DAEMON_PROTOCOL_VERSION` 250 →
251**, compatible `[250, 251]`. 250 stays compatible because the server
no longer sends either command. 246–249 stay removed, as #6018 intended.

**`experimental_useSidebarThreadPullRequest`**
- Removed from `@get-bb/plugin-sdk/app`, the Plugin Guide and the docs.
- Published bundles keep working: the app installs
`retiredPluginSdkAppMembers` on the plugin runtime, backed by the
plugin's read. It returns `{ isLoading: false, pullRequest: null }` when
the plugin is disabled.
- **Builds never fail on it.** bb builds git-sourced marketplace plugins
at install and update, and rebuilds path and builtin plugins when the
SDK changes. So the `bb-plugin-build` runtime shim keeps the name in its
`@get-bb/plugin-sdk/app` export list. Importing it is a build warning
with the replacement call, which `bb plugin build`/`dev` print and the
server writes to the plugin's log.
- Migration guide:
`bb-plugin-authoring/references/frontend-pull-requests.md`. The runtime
shim is recorded in `docs/api_to_audit.md`.
- Plugin SDK 0.6.72.

**No wake.** Plugin host calls never wake a suspended host. A read on an
asleep or offline machine returns `unavailable`, which the UI renders as
no PR. Ready, Draft and Merge fail with "<machine> isn't available …".
**Behaviour change:** reads no longer wake a suspended host. On main,
`workspace.pull_request` used the `work` wake policy, so opening a
thread whose machine was suspended woke it.

## How you verified

**Request measurement in the real app.** Main and this branch ran as two
`pnpm start:worktree` instances side by side. Each had a thread on a
project checkout of PR #6117's branch while its checks were pending
(`checks_pending`, 0 failed). In each, a headless page opened the thread
and every `/api` request was recorded for 3 minutes, both runs at the
same time:

| | PR requests | when (s) | all `/api` requests |
|---|---|---|---|
| main | 6 × `GET /environments/:id/pull-request` | 1, 32, 64, 95, 127,
158 | 84 |
| branch | 6 × `POST /plugins/bb--github-pr-status/rpc/pullRequests` |
1, 33, 64, 96, 127, 159 | 88 |

The other `/api` differences changed from run to run (hosts, server-move
and execution-options in this run; system config, providers and timeline
in another). They come from background activity in the freshly restarted
branch instance and its extra sidebar threads, not PR traffic. An
earlier run found two bugs that are fixed here with tests:
- a re-check right after open published a result the opening read was
already fetching, costing one extra request;
- a watch that went idle when checks finished never resumed when a new
CI run started.

**Busy thread.** Main and this branch ran side by side again, each with
a thread on its own scratch PR in a private test repo
(`ymichael/test-project` #303 and #304, both closed and their branches
deleted afterwards). Each thread was open in a page. Both received the
same agent turn at the same moment: three commits, a `git fetch`, a
third commit between them, and a `git push`, which also started the
repo's CI. Over 5 minutes, a process monitor counted every `gh` started
by each instance, attributed by process ancestry and confirmed by
working directory. The page counted PR read requests. Two runs gave
identical counts:

| | `gh` calls | PR read requests |
|---|---|---|
| main | 7 (4 `pr view`, 3 merge-queue `api graphql`) | 4 |
| branch | 5 (3 `pr view`, 2 merge-queue `api graphql`) | 2 |

Before this fix the branch also re-checked with `gh` on every
`git-refs-changed` and `metadata-changed`, so a thread that commits and
pushes would have exceeded main.

**Focus refresh.** Main (`f909788625`) and this branch ran side by side
with the same open PR. A page on each hid and showed itself 5 times over
4 minutes, with gaps from 20 s to 60 s, so some shows were under the 30
s stale time and some over. A process monitor counted `gh` per instance:

| | `gh` calls | PR read requests |
|---|---|---|
| main | 5 | 5 |
| branch | 5 | 6 |

The extra read is the realtime connection resuming at the same moment
the page showed. Both reads hit the plugin's 10 s cache, so there's no
extra `gh`.

**Published plugins that use the retired hook.** On the review app, I
installed four git-sourced Community marketplace plugins that call
`experimental_useSidebarThreadPullRequest`. Each went through `bb plugin
install <plugin>@bb-community`, so bb cloned and built it at its
published commit. Each was then used as the sidebar thread list:

| Plugin | Commit | Builds | Warning in `plugin.log` | Renders PR badges
|
|---|---|---|---|---|
| t3sidebar | `8b535729dd` | ✅ | ✅ `src/ThreadCard.tsx:6` | ✅ |
| dockside | `e8736f3858` | ✅ | ✅ `components/inbox/thread-card.tsx:7` |
✅ |
| radar-sidebar | `9a20bfab7e` | ✅ | ✅
`components/radar/RadarThreadRow.tsx:23` | ✅ |
| thread-inbox | `c0981fb6fe` | ✅ | ✅ `src/ThreadCard.tsx:18` | ✅ |

gtd-sidebar fails to install on main too ("Could not resolve
@get-bb/plugin-sdk"), so it's unrelated. This test caught a real bug: T3
Sidebar crashed with a React hook-order error. React Compiler only
treats `use…` names as hooks, so it cached the
`experimental_useRpcKeyedQuery` call inside `useEnvironmentPullRequest`
and skipped it on re-renders with unchanged arguments. Core's own
callers pass a fresh options object on every render, which hid it. The
hooks are now imported under `use`-prefixed local names, and all four
plugins render.

**UI parity.** Screenshots of the same thread on main and on the branch
show the same composer banner segment (PR #6117 with the Merge split
button) and the same Info row (#6117, Open, Checks running). `bb thread
show` printed the PR through the plugin. On this PR, `bb pull-request
draft <env>` converted it to draft. The banner then showed "PR #6125 ·
Draft" with Mark ready, and clicking Mark ready showed the loading
toast, made the PR ready on GitHub, and returned the banner to the open
state with Merge.

**Tests**
- New plugin tests (75):
- host-entry fork, upstream and detached cases using a fake `gh`, moved
from host-workspace;
  - the parse, action-argument and merge-queue table;
  - the assembly table, moved from the server;
  - store polling, publish-on-change and the viewer race;
  - server reads, refusals, the no-wake refusal and the CLI.
- Also added: the `bb-plugin-build` retired-import warning test, the
focus-refresh staleness table and refetch tests, Tasks batched reads and
the disabled-plugin case, CLI `thread show` through RPC, and a hub test
for the `plugin-read` target.
- After merging #6135: `pnpm exec turbo run typecheck lint
--filter='...[origin/main]'` passes 162/162, and `turbo run test
--filter='...[origin/main]'` passes 115/115. That includes #6135's
host-workspace default-branch tests and the host-daemon
`watch-manager-startup` setup-window test. The host-workspace merge kept
#6135's code. This branch only removes the PR lookup from it.

**Not covered**
- `gh` ran only on a local host. Remote hosts use the same plugin
host-artifact path as the other host plugins but weren't exercised.
- Merge wasn't run against a real PR. It uses the same `gh pr merge
--<method>` arguments, covered by the host tests.

> AGENT GENERATED

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ymichael added a commit that referenced this pull request Oct 23, 2026
## Human comments

## What was wrong

Thread action tabs, navigation-page fixed tabs, and core Info/Diff used
different registration and opening paths. That made tab context and
lifecycle inconsistent and prevented plugins from owning core pinned
tabs. Saved layouts also needed to survive the transition, plugin
loading, visibility changes and reloads.

## What changed

- Add one registration: `app.slots.experimental_tab({ id, title, icon,
tooltip?, layout?, context, availability, useVisible?, component, ...
})`. Context is `{ kind: "thread" }` with `{ threadId }` props, or `{
kind: "nav-panel", panelId }` with `{ subPath }` props. Fixed tabs have
one non-closable identity; action tabs launch closable instances with
persisted/deduplicated params. Only actions accept `run` and params.
Navigation-page actions also fit this model.
- Use one registry and destination dispatcher.
`ExperimentalAppPanel.experimental_openTab` is the new opener; existing
fixed-tab, action and thread-panel open calls remain wrappers over the
same implementation. A visibility hook hides a fixed tab or launcher
while retaining saved fixed order and existing action instances. Hooks
execute inside the plugin crash boundary; plugin generations reset
crashed content across every context.
- Register core Info and Diff as thread/fixed tabs. Preserve Diff's
Meta+D toggle, changed-file/commit entry points, Git setting, visibility
and pane-local review controllers. Preserve #6117's ordered Info row and
section registrations. There is no core-only Info/Diff content dispatch.
- Migrate first-party Docs, GitHub, Side Chat, Tasks, Workflows and the
ThreadChat demo. Remove the unshipped `experimental_threadFixedTab` API.
Remove `threadPanelAction`, `navPanel.fixedTabs`, and their old
registration/prop types from public SDK declarations and normal
Guide/authoring documentation. Keep lenient runtime adapters so
already-published plugins keep loading. Git install/update and
path/builtin rebuilds emit migration **warnings**, also recorded in
plugin logs; they never fail builds for these retired names. Runtime SDK
exports remain available. Record the 92/15 known legacy marketplace
callers and removal criteria in `docs/api_to_audit.md`.
- Preserve existing `plugin-page-fixed` and `plugin-panel` IDs/params.
Convert old Info/Diff entries, selected IDs and split placement to core
`plugin-fixed` entries without reordering, including revision-checked
server rewrites. SDK/CLI thread tab inspection/update uses the same
persisted model. No SQL schema or daemon wire change.
- Update Plugin Guide cards/surfaces and authoring migration examples.
Rebase onto main `2884950f17` (#6125); bump SDK with
`bump-plugin-sdk.mjs --patch` to **0.6.73**, above main's 0.6.72.
New-thread-screen actions retain their separate project context and
share the action-opening implementation.

- Resolve #6125 conflicts by retaining the retired PR-hook runtime
export and both tab adapters. Keep named-import and
property-registration warnings together, all non-fatal. Use main's
returned `migrationWarnings` and `logPluginBuildWarnings` pipeline for
all three names so warnings are logged once; preserve both audit entries
and migration docs. First-party callers of the new tab API require SDK
>=0.6.73.

## How you verified

- After rebasing, freshly clone/build/install **Graph** at
`3fd202e9af47` and **T3 Sidebar** at `8b535729dd22` through bb's Git
install flow. Both artifact manifests report SDK 0.6.73. Graph logs the
two tab warnings; T3 logs the retired PR-hook warning at
`src/ThreadCard.tsx:6`. Both have zero plugin-log errors. Graph's
thread/fixed tabs render, and T3's actual sidebar renders PR #6013
alongside the matching core Info row, with no browser errors. Restore
the sidebar preference and disable T3 after QA. Evidence:
`/Users/michael/.bb/thread-storage/thr_fkjwifewbd/pr6013-rebase-6125/verification.md`.

- `pnpm exec turbo run typecheck lint test --filter='...[origin/main]'
--concurrency=1` passes: **243/243 tasks**, including 6,323 app tests
and 3,675 server tests. SDK version guard, formatting and `git diff
--check` pass. The unchanged source-layout builtin watcher test failed
in the initial broad run, then passed in its complete isolated suite
(28/28) and the full rerun. No timeout was weakened.
- One six-case contract table covers new thread fixed/action, new
navigation-page fixed/action and both legacy adapters: stable saved
identity, persisted params, deduplication, close behavior, visibility,
remount and crash/reload. Existing migration and split-readiness tests
remain. Independent mutations prove crash recovery failures in both the
table and the actual navigation-page host; repaired focused tests pass.
A full-suite failure also verified the legacy fixed-target callback
contract before its repair.
- Real same-store upgrade from main `c0013e2aba`: save
first-party/marketplace tabs and a Side Chat instance, upgrade to the
branch, and compare persisted data. Every action entry, parameter and
order remains unchanged; Info/Diff convert in place. Verify Info, Diff,
changed-file navigation, Meta+D, Docs/Tasks navigation, GitHub,
Workflows and restored Side Chat. Prior branch evidence covers
reordered/split Diff restoration and the Git setting.
- Actual fresh Git-source install flow at immutable public commits:
**Agent Graph** `3fd202e9af47`, **SQL** `69593e887aaf`, **Host Monitor**
`1d84e329e69d` (`plugins/host-monitor`) and **JB Flow** `66d4700cb1c3`.
They build, load and log the migration warnings. Graph/SQL/JB Flow
action tabs open, close, reopen and restore once; Graph/SQL/Host Monitor
fixed tabs reopen and persist without close controls. No errors in the
fresh app console or their plugin logs. JB Flow's published commit has
actions but no fixed tabs.
- Install original published **Lazygit 0.2.10** and **Global Workflows
0.1.3** npm bundles; app.js hashes match the scanned published
artifacts. Both legacy action tabs open, close and restore. The Lazygit
terminal renders; Global Workflows shows an empty configured catalog. No
workflows, database queries, voice sessions or external GitHub writes
run.
- **Handsfree 0.1.6** has the same sidebar voice-bar crash on main and
this branch; its fixed Views tab renders the same companion state at
phone width. This is recorded as a pre-existing baseline, not counted
toward the three working fixed-tab plugins.
- Review app runs from the new head with `pnpm start:worktree`:
[prepared
thread](https://ymichael--26550.getbb.app/projects/proj_nyijgxmg6f/threads/thr_whvufeb6m5).
Detailed local evidence:
`/Users/michael/.bb/thread-storage/thr_fkjwifewbd/pr6013-unified/verification.md`.
- Size: **80 files, +3,093 / -999**; production/tooling +1,805/-673,
tests/support +1,095/-195, docs +193/-131. The previous cleanup removed
188 net lines, including the JSX glyph assertion and redundant fixed-tab
setup. Visibility observers remain so plugin hooks run within crash
isolation.

> AGENT GENERATED
ymichael added a commit that referenced this pull request Oct 23, 2026
…PR Status plugin (#6023)

## Human comments

## What was wrong

Git and GitHub pull request status were still core UI. The composer's
context banner hard-wired its parent-thread, changed-files and pull
request rows. The Info tab drew the Pull request row from core, using PR
data owned by the bundled GitHub PR Status plugin (#6125). There was no
slot through which core and plugins could add context beside a thread's
composer.

## What changed

- **`app.slots.experimental_threadContextSegment`** (SDK 0.6.75):
segments share the context row above an existing thread's composer.
- A registration takes separate `summary`, `action?` and `details?`
components, like composer customizations take separate lists. The host
mounts each in its own crash boundary: `summary` in the row, `action` at
the row's end, `details` below the row.
- `group` then `order` place segments, as for Info rows and thread
actions. `experimental_THREAD_CONTEXT_SEGMENT_GROUPS` names only core's
groups, `thread` (`1_thread`) and `git` (`3_git`); the PR plugin uses
its own `2_pullRequest`.
- Props give `threadId`, `projectId`, `environment`, the visible
`segments` as `{ key, pluginId }` (`key` is `<pluginId>/<id>` with core
as `bb--core`, the shape thread action keys use), and a shared
one-open-at-a-time disclosure.
  - `useVisible` runs in plugin scope per thread.
- The banner is drawn above `ComposerBox`, so it shows under every
composer plugin.
- **Core uses the same path.** The parent thread (including read-only
history) and changed files (with the merge-base action) register through
`definePluginApp` and the collector, and render through the same host as
plugins. Core segments render without a plugin crash boundary, as core
Info rows do, so their DOM is unchanged.
- **Active child threads are their own core row**
(`ThreadChildThreadsBanner`) above the context row, like the todo and
workflow cards, with their own open state. They are not a segment, so
the slot has no card placement that would overlap composer `banners`.
- **GitHub PR Status plugin gets an app entry**
(`plugins/github-pr-status/app.tsx`, `ui/`):
- the pull request segment: status pill, label rules, Mark ready and the
merge split button with Convert to draft, the remembered merge method
under the same localStorage key, and the same toast copy;
- the Info tab's Pull request row, through #6117's
`experimental_threadInfoRow`;
- the display helpers, status pill, next-step label, favicon and the
keyed read hook, moved from core with their tests.

Core has no pull request UI or PR imports left. The one exception is the
retired `experimental_useSidebarThreadPullRequest` runtime member for
old marketplace plugins.
- **`environment` on slot props.** It is `{ id } | null` on the new slot
and on `ExperimentalThreadInfoProps`.
- On both, it is null when the thread has no environment, its host is
not active, or the environment is not a usable checkout.
- On the segment slot only, it is also null when "Show Git changes and
Commit button" is off. That keeps main's behaviour, where the PR hides
with changed files and the Info row does not.
- **No extra PR reads after the move.**
- The plugin hook refetches only when an environment starts showing, not
for each extra view, so opening Info adds no read.
- `WebSocketManager` sends its final unsubscribe a microtask later and
cancels it when the same target resubscribes in that task. Without this,
a thread switch on the same environment dropped the plugin read
subscription and restarted the server's watch, which can run `gh`.
- `BannerActionSlot` and the banner action buttons moved to
`@bb/shared-ui/prompt-banner-actions`, so the plugin draws the same
controls.
- **One toast path for core and plugins.** The `toast` plugins import
from `sonner` is now a facade over core's `appToast`
(`apps/app/src/lib/notifications/plugin-toast.ts`), replacing the
recording wrapper.
- `toast()`, `message`, `info`, `success`, `warning`, `error`,
`loading`, `dismiss` and `promise` render and record exactly like core's
toasts.
  - `custom`, `getHistory` and `getToasts` pass through.
- A toast whose `action` or `cancel` is a plain React node (sonner
allows it; no bundled plugin uses it) falls back to sonner.
- Every plugin's toasts change appearance to match core's, which is
intended.
- **The retired `experimental_useSidebarThreadPullRequest` member keeps
main's refresh.** It uses main's hook, kept in core as
`useRetiredSidebarEnvironmentPullRequest` with main's tests: a cached
row refetches on mount, and a stale row on page show.
- Core's copy and the plugin's copy of that hook announce every refetch
they start with a `window` event (`PULL_REQUESTS_REFETCH_EVENT` in the
plugin contract) and mark those rows loaded.
- A stale-row refetch rechecks staleness right before it runs, so a row
that already has a fetch in flight, or was refreshed within its
staleness window, is not read twice. SDK refetch behaviour is unchanged.
- **Docs:**
- a `docs/api_to_audit.md` entry; the Info entry and the live-reads and
retired-member text updated;
- the Banners Guide card's symbols, bullets and fixture, with anatomy
anchors;
  - `frontend-registration.md` and `frontend-api-index.md`;
  - the PR stories moved to the plugin and added to Ladle.

No database, daemon wire or CLI changes.

Rebased onto #6143. Its "hide cached PR status when the plugin is
disabled" check in core's PR hook moved with that hook's last core
caller, the retired sidebar shim (`retired-sidebar-pull-request.ts`),
with #6143's test; the plugin's own hook only renders while the plugin
is enabled. The keyed read stays one PR per environment, so the segment
and Info row need no change.

**Deviations from main**

- While core's commit or push runs, the PR buttons are no longer
disabled; the plugin cannot see core's git actions. The replacement
Michael asked for is to pass the displayed head to `gh pr merge
--match-head-commit`. That needs `headRefOid` on the PR, which #6143
adds, so it lands on top of #6143.
- A crash in the PR segment removes only that segment, or its part.
- The active child threads card keeps its own open state, as Michael
asked. On main it shared one open section with the context row. Now it
can be open while changed files or the parent details are open, and it
opens by itself when a child newly needs input even while another
section is open.

**Known follow-ups:**

- The segment slot's `environment` folds in the "Show Git changes and
Commit button" setting. That setting should become the git plugin's own
when changed files move out of core, and the `git` segment group moves
with it.
- `experimental_THREAD_INFO_ROW_GROUPS.pullRequest` (from #6117) still
names a plugin's group in core. Removing it is a public SDK removal, so
it is left for a separate change.

## How you verified

- Parity checklist: every banner row and segment, the Info PR row,
refresh sources, phone and desktop layout, and each composer. Item IDs:
A composers, B inline row, C child card, D read-only, E disclosure, F
data, G layout, H Info row.

<details><summary>Checklist results</summary>


| Checklist | Result |
|---|---|
| A1, A2, A3, A8 | Same. A4–A7 unchanged code paths (banner only in the
follow-up stack); not driven separately. |
| B1–B5, B11, B13–B15 | Same text and pixels at 1400 and 390 px for: PR
+ changed files, PR alone, parent + PR + changed files, parent + PR. |
| B6–B8, B10 | Same: menu items, Convert to draft, Mark ready, Squash
merge on disposable PRs; toasts identical. |
| B7 persistence | Same key `bb.pullRequest.mergeMethod`; Squash merge
remembered. |
| B9 | Deviation: core commit/push no longer disables the PR buttons. |
| C1–C6 | Same content, auto-open on a new waiting question and 8 px
spacing; the card is now its own row with its own open state (deviation
above). |
| D1–D3 | Same pixels for archived child with parent. |
| E1–E4 | Same: parent disclosure body, focus to Collapse, focus back to
toggle. |
| F1–F6 | Same PR reads (4 per scripted session); fewer WS frames on the
branch. |
| G1–G4 | Same compact rules (390 px and narrowed by the right panel). |
| H1–H4 | Same markup and row order; Show Git changes off keeps the row
on both; plugin disabled hides it on both. |

</details>

Each item was driven in two source dev instances side by side, one on
this branch and one on `origin/main` 2884950, with identical fixtures
and disposable PRs in `ymichael/test-project`.
- Text was identical at 1400 and 390 px for PR + changed files, PR
alone, parent + PR + changed files, and parent + PR.
- Screenshots were pixel-identical apart from the caret and the
scheduled-time text.
- The same held for archived history, Show Git changes off, the plugin
disabled, the child-thread card with a waiting question, the
pending-question stack, and the parent disclosure with its focus
handoff.
- Toasts: the PR plugin's Mark ready toasts match main's pixel for
pixel; the loading toast differs only in the spinner frame. The Drafts
plugin's "Draft saved" and the probe's `success`, `error`, `info` and
`promise` toasts render as core toasts, with the close button, the
outlined icon and an entry in the notification center.
- Actions on disposable PRs: Convert to draft, Mark ready and Squash
merge went through with the same toasts and the segment refreshed. The
merge method was remembered. A CLI `bb pull-request draft` updated the
segment live in the production build (`pnpm start:worktree`).
- Requests, from browser CDP counts of `bb--github-pr-status` HTTP reads
and WebSocket frames. The scripted session: load a thread with a PR and
wait 40 s, open Info, switch to a thread on the same environment and
back, switch to another environment and back, then fire a page show
after 35 s.

  | | PR reads | WS frames |
  |---|---|---|
  | main (dev) | 4 | 12 |
  | branch (dev) | 4 | 8 |
  | branch (production build) | 4 | 8 |

Before the WebSocket fix the branch sent 16 frames, because of
resubscribes on same-environment switches.

With a probe plugin that renders
`experimental_useSidebarThreadPullRequest` in the thread header (an old
marketplace sidebar's call), the same session gave:

  | | load | 4 thread switches | page show |
  |---|---|---|---|
  | main | 1 | 4 | 1 |
  | branch | 1 | 4 | 1 |

  Before the shared announcement, the branch's page show read twice.
- Tests:
- new host contract tests
(`ThreadPromptContextBanner.segments.test.tsx`): group and order
placement, a plugin's own group, `segments` keys, visibility, one-open
disclosure, per-component and `useVisible` crash isolation, read-only,
and the row held while git loads;
- child-threads row tests: auto-open for a newly waiting child while
changed files are open, and the question flow through
`ThreadChildThreadsBanner`;
- plugin tests for label rules, the attention matrix, actions and
visibility;
- query tests for refetch on first show and same-commit remounts, which
fail without the deferral;
  - a WebSocketManager test for same-task resubscribe;
- a plugin `toast` test through the real toaster (error and promise),
which fails on the old shim;
- a test with the plugin's hook and the retired member's hook on one
environment: a stale page show makes one read, and two without the
announcement.
- `pnpm exec turbo run test typecheck lint --filter='...[origin/main]'
--continue`: 239/244 passed on the first run.
- Fixed: the Automations Info fixture (`environment`), the server's
slot-prop docs table, and the 500-line cap on
`frontend-registration.md`.
- Passed when rerun alone: `CommandPalette`, `PluginSettings`,
`builtin-plugins` hot-reload and the MCP CLI timeout (the first run had
heavy parallel load).
- Affected packages rerun green: `bb-plugin-plugin-api-docs`,
`bb-plugin-automations`, `@bb/server`.
- `bb plugin build` passes for `plugins/plugin-api-docs` and
`plugins/github-pr-status`. The Plugin Guide's Banners card and fixture
were checked in the app at desktop and phone layouts.
- After the groups and component split: `pnpm exec turbo run test
typecheck lint --filter='...[origin/main]' --continue` gave 242/244. A
test-harness mock of the banner and the old auto-open test were updated
and `@bb/app` reran green; `bb-plugin-bb--storage-retention` (untouched)
passed when rerun alone.
- In the production build (`pnpm start:worktree`) on a checkout of this
branch: parent, PR and changed files in that order with Merge at the
end; the child-threads row above the context row, open together with
changed files; only the child row beside a waiting question.
- CI: see the checks on this PR.

> AGENT GENERATED

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
VKirill pushed a commit to VKirill/bb that referenced this pull request Oct 25, 2026
… bb's Info section slot

desktop-v0.47 ships experimental_threadInfoSection (get-bb#6117). The VK slot now
registers the section there: group changes, order 30 (after commits and
uncommitted changes, before storage), inside the labelled group it always had.
bb renders it: per-pane mounts, ordering, crash isolation. The app-side hook
points are gone (VkPluginThreadInfoSections, the plugin-slots kind, the
ThreadMetadataContent registration, the harness field). Lane Pilot keeps
working unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VKirill pushed a commit to VKirill/bb that referenced this pull request Oct 29, 2026
… bb's Info section slot

desktop-v0.47 ships experimental_threadInfoSection (get-bb#6117). The VK slot now
registers the section there: group changes, order 30 (after commits and
uncommitted changes, before storage), inside the labelled group it always had.
bb renders it: per-pane mounts, ordering, crash isolation. The app-side hook
points are gone (VkPluginThreadInfoSections, the plugin-slots kind, the
ThreadMetadataContent registration, the harness field). Lane Pilot keeps
working unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VKirill pushed a commit to VKirill/bb that referenced this pull request Oct 30, 2026
… bb's Info section slot

desktop-v0.47 ships experimental_threadInfoSection (get-bb#6117). The VK slot now
registers the section there: group changes, order 30 (after commits and
uncommitted changes, before storage), inside the labelled group it always had.
bb renders it: per-pane mounts, ordering, crash isolation. The app-side hook
points are gone (VkPluginThreadInfoSections, the plugin-slots kind, the
ThreadMetadataContent registration, the harness field). Lane Pilot keeps
working unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Show automations targeting a thread in its Info panel

1 participant