Render rows from presentation; add experimental_timelineRenderer; load provider bundles lazily (WS3 layer 2) - #2206
Closed
SawyerHood wants to merge 1 commit into
Closed
Conversation
SawyerHood
force-pushed
the
ws3-l2-web-renderers
branch
from
August 21, 2026 17:38
a4b5151 to
1f90a89
Compare
…d provider bundles lazily (WS3 layer 2)
Web client consumption of the grammar v3 data layer 1 projects.
- Declarative base: a work row's leading glyph is the bridge's
`presentation.icon.glyph` (validated against the host registry, per-kind
fallback otherwise) tinted through `light-dark()` from
`presentation.tint` (colour grammar checked); the expanded body shows the
presentation `detail` (Markdown) on tool/workflow/extension rows, a
plan-steps row lists its steps with status. Shared `Icon` gains `style`.
- `app.slots.experimental_timelineRenderer({ kind, component })`: a plugin
renders the body of its own extension kinds and of its providers' generic
tool rows (Q17). The store drops kinds outside the plugin's namespace; the
resolver scopes "tool" to the plugin owning the thread's provider; crashes
are contained per row and fall back to the declarative base; `Original`
renders the base inside the plugin body. Documented in api_to_audit.md,
the authoring skill, and the SDK testing harness.
- `ProviderInfo.pluginId` (the registering plugin) and
`InstalledPlugin.providerIds`: provider ownership reaches the client.
- Q30: provider plugins' frontend bundles are deferred at boot and load on
the first thread of one of their providers
(`requestProviderPluginFrontend` from the thread view through a
boot-path-safe gate); a test asserts zero provider bundles import before
a thread opens. Bundle budget unchanged (boot 1559 KB / 437 KB brotli).
- Picker + settings read the provider's declaration: reasoning labels from
`reasoningLevels` (fallback table only when undeclared), the fast-mode
toggle label from `serviceTiers`, and provider icons tinted from
`strings.iconTint` (vendored class only when undeclared).
- G10 doc test: `TimelineRow.presentation` and the renderer slot are landed;
the single `payload` fold stays a recorded gap.
SawyerHood
force-pushed
the
ws3-l2-web-renderers
branch
from
August 21, 2026 18:09
1f90a89 to
dfc0e33
Compare
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.
WS3 layer 2 of 5, stacked on #2192 (layer 1: projection consumes presentation). Do not merge — the coordinator reviews, Sawyer merges the stack.
What was wrong
Layer 1 put the bridge's persisted
presentationand the new row kinds onTimelineRow, but the web client still drew every row from its own per-kind tables: glyphs from aworkKindswitch, labels from tool names, no body for extension or plan-steps rows, no way for a provider plugin to render its own kinds (the canary's "extension rows do not render; row labels ignore presentation"). Provider plugins' app bundles were loaded with every other plugin in the boot pass, and the picker/settings read reasoning labels, the fast-mode label and provider icon colours from hardcoded tables instead of the provider's declaration (reasoningLevels,serviceTiers,strings.iconTint— declared by WS2a, consumed by nothing).What changed
apps/app/src/components/thread/timeline):presentation-display.tsresolves the bridge glyph against the host icon registry (unknown glyph → per-kind fallback) and the per-theme tint throughlight-dark()after a colour-grammar check (nourl()/var()injection).PresentationWorkRowBodies.tsxrenders the presentationdetail(Markdown, images off) and the plan-steps list. Tool/workflow/extension bodies show the detail; file-read/search rows stay title-only.@bb/shared-uiIcongains astyleprop (plugin registryr/icon.jsonregenerated).app.slots.experimental_timelineRenderer({ kind, component })(@get-bb/plugin-sdk/app): a plugin renders the expanded body of its own extension kinds ("<pluginId>/<name>") and of its providers' generic"tool"rows; core kinds stay core-rendered (Q17). Validation in the collector, namespace enforcement in the slot store (foreign kinds dropped with a warning),"tool"scoped to the plugin owning the thread's provider,PluginSlotMountcrash containment per row with the declarative base as the fallback,Originalrenders the base inside the plugin body. Entry indocs/api_to_audit.md; documented in the bb-plugin-authoring skill (its docs test pins the prop fields);CapturedPluginAppin the SDK testing harness mirrors it.ProviderInfo.pluginId(required; the plugin that calledbb.providers.register) andInstalledPlugin.providerIds(defaults to[]for older servers).ThreadProviderContextcarries{ providerId, pluginId }from the thread view into the timeline.plugin-frontend-provider-gate.ts(boot-path-safe) records which provider plugins are wanted; the reconcile pass loads every non-provider plugin plus the wanted provider plugins (selectLoadablePluginFrontendCandidates) and never prunes a deferred one;ThreadDetailViewcallsrequestProviderPluginFrontend(pluginId)once the thread's provider resolves. Test: zero provider bundles imported before a thread opens, exactly one after.reasoningLevelLabelreadsProviderInfo.reasoningLevels(committed provider inuseThreadCreationOptions, previewed provider inModelReasoningPicker); the fast-mode toggle reads the declared"fast"tier label (<label> mode);ProviderIconMarktints the provider mark fromstrings.iconTintand keeps the vendored class only when none is declared.provider-icon.tsedit is one additive helper (overlap with the GAPS stack noted; no literal removed here — that is theirs/G1).HOST_DAEMON_PROTOCOL_VERSIONunchanged. No Drizzle change.How you verified
ThreadTimelineRows.presentation.test.tsx(label/headline/glyph/tint/detail from presentation; unknown glyph + unsafe tint fallback; plan-steps statuses; plugin renderer withOriginal;"tool"scoping to the provider's plugin; crash containment),plugin-timeline-renderers.test.ts(namespace rule, resolver),plugin-frontend-provider-lazy.test.ts(deferred provider bundles; no prune),reasoning-labels.test.ts(declared labels/tier/tint, fallbacks).turbo test --filter=@bb/app: 418 files, 3,217 passed.turbo typecheck --filter=...@bb/domain(62 packages) clean.turbo testgreen for@get-bb/plugin-sdk(G10 doc test flipped: presentation + slot landed,payloadfold still a gap),@bb/domain,@bb/server-contract,@bb/client-core,@bb/plugin-build,@bb/plugin-registry,@bb/mobile,@bb/demo-server,bb-plugin-provider-retry;@bb/server1,896 passed with the known local umaskinternal-skill-treescase.turbo build --filter=@bb/app+check-bundle-budget.mjs): boot payload 1559.4 KB raw / 436.6 KB brotli, budget 1683.2 / 467.8 — OK, unchanged by this layer.Folded in after the first push (found by layer-5 live QA and lint).
ThreadDetailView.tsx: theThreadProviderContextvalue'suseMemosat below the loading / not-found early returns, so the hook count changed once a thread loaded and the app crashed with "Rendered more hooks than during the previous render" on every thread open. It now sits beside the lazy-load effect, above the returns.react-hooks/exhaustive-deps: the four composer hooks that readserviceTierFastLabellist it.apps/cliplugin-catalog test fixture carriesproviderIds: [](the contract default).