Skip to content

Render rows from presentation; add experimental_timelineRenderer; load provider bundles lazily (WS3 layer 2) - #2206

Closed
SawyerHood wants to merge 1 commit into
ws3-l1-projection-presentationfrom
ws3-l2-web-renderers
Closed

Render rows from presentation; add experimental_timelineRenderer; load provider bundles lazily (WS3 layer 2)#2206
SawyerHood wants to merge 1 commit into
ws3-l1-projection-presentationfrom
ws3-l2-web-renderers

Conversation

@SawyerHood

@SawyerHood SawyerHood commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

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 presentation and the new row kinds on TimelineRow, but the web client still drew every row from its own per-kind tables: glyphs from a workKind switch, 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

  • Declarative base (apps/app/src/components/thread/timeline): presentation-display.ts resolves the bridge glyph against the host icon registry (unknown glyph → per-kind fallback) and the per-theme tint through light-dark() after a colour-grammar check (no url()/var() injection). PresentationWorkRowBodies.tsx renders the presentation detail (Markdown, images off) and the plan-steps list. Tool/workflow/extension bodies show the detail; file-read/search rows stay title-only. @bb/shared-ui Icon gains a style prop (plugin registry r/icon.json regenerated).
  • 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, PluginSlotMount crash containment per row with the declarative base as the fallback, Original renders the base inside the plugin body. Entry in docs/api_to_audit.md; documented in the bb-plugin-authoring skill (its docs test pins the prop fields); CapturedPluginApp in the SDK testing harness mirrors it.
  • Provider ownership on the client: ProviderInfo.pluginId (required; the plugin that called bb.providers.register) and InstalledPlugin.providerIds (defaults to [] for older servers). ThreadProviderContext carries { providerId, pluginId } from the thread view into the timeline.
  • Q30 lazy provider bundles: 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; ThreadDetailView calls requestProviderPluginFrontend(pluginId) once the thread's provider resolves. Test: zero provider bundles imported before a thread opens, exactly one after.
  • Picker + settings consume the declaration: reasoningLevelLabel reads ProviderInfo.reasoningLevels (committed provider in useThreadCreationOptions, previewed provider in ModelReasoningPicker); the fast-mode toggle reads the declared "fast" tier label (<label> mode); ProviderIconMark tints the provider mark from strings.iconTint and keeps the vendored class only when none is declared. provider-icon.ts edit is one additive helper (overlap with the GAPS stack noted; no literal removed here — that is theirs/G1).
  • No daemon wire change: everything here is server → app; HOST_DAEMON_PROTOCOL_VERSION unchanged. No Drizzle change.

How you verified

  • New tests: ThreadTimelineRows.presentation.test.tsx (label/headline/glyph/tint/detail from presentation; unknown glyph + unsafe tint fallback; plan-steps statuses; plugin renderer with Original; "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 test green for @get-bb/plugin-sdk (G10 doc test flipped: presentation + slot landed, payload fold 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/server 1,896 passed with the known local umask internal-skill-trees case.
  • Bundle budget (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.
  • A4/A2 unchanged by this layer (no projection change); re-run at the top of the stack.

Folded in after the first push (found by layer-5 live QA and lint).

  • ThreadDetailView.tsx: the ThreadProviderContext value's useMemo sat 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 read serviceTierFastLabel list it.
  • apps/cli plugin-catalog test fixture carries providerIds: [] (the contract default).

AGENT GENERATED: by Claude Opus 5

…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
SawyerHood force-pushed the ws3-l2-web-renderers branch from 1f90a89 to dfc0e33 Compare August 21, 2026 18:09
@SawyerHood SawyerHood closed this Aug 21, 2026
@SawyerHood
SawyerHood deleted the ws3-l2-web-renderers branch August 21, 2026 21:40
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