Skip to content

feat(packs): pack web modules, one message.view registry, Thread.showView - #436

Merged
johnnyelwailer merged 11 commits into
mainfrom
feature/seam-t3-pack-web-module-view-registry-6b5a40c9
Oct 8, 2026
Merged

johnnyelwailer merged 11 commits into
mainfrom
feature/seam-t3-pack-web-module-view-registry-6b5a40c9

Conversation

@johnnyelwailer

@johnnyelwailer johnnyelwailer commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Slice T3 of the explainer seam plan (seams S1 + the message.view part of S2). Generic: nothing here names the explainer.

Problem

A pack had no way to put UI in the web app, and the timeline routed { kind: "view" } attachments through three hard-coded ids. A workflow could post HTML widgets, not a registered view.

What changed

  • @t3team/pack-ui (packages/t3team-pack-ui): the pack-facing contract — defineWebActivate, ViewSlot ("message.view"), MessageViewRegistration, PACK_UI_VERSION = 1 — plus a minimal host kit: Button, Badge, Skeleton, Markdown, usePackDocument, usePackDocuments. Types only; the web app supplies the implementation (t3team-packUiImpl.ts, checked against PackUiHostKit).
  • @t3code/distribution-web: apps/web/scripts/t3team-distributionWebPlugin.ts compiles in the web entry of every pack in distribution.json packs[] (default: the distribution dir) that declares contents.views with view:v1. It aliases @t3team/pack-ui to the host kit, resolves a pack's bare imports from the app, and adds pack dirs to Tailwind @source. Without a distribution the specifier resolves to the empty stub src/t3team-distributionWeb.ts. The shared reader is @t3team/packs/distribution-web.
  • View registry (t3team-viewRegistry.ts, app instance t3team-messageViewRegistry.ts): one table keyed by (slot, id). The workflow shape, decision and card views are now host registrations (t3team-hostMessageViews.tsx); SystemTimelineRow, GenericRow, fullBleedWidgetRow and getT3TeamRenderableAttachments read the registry. Pack views render in their pack scope and an error boundary; props that fail the view's schema fall back to the generic attachment row. Pack ids are namespaced and a pack's activation is all-or-nothing.
  • Thread.showView({ key, viewId, props }) (@runbook/threads): gated on ui.render at runtime and in the static capability scan; input validated at the call site and again in the broker. The broker posts a view attachment under a message id derived from (thread, key), so a re-post updates the same row. t3team.* host views are refused.

Verification

  • web: t3team-packMessageView.test.tsx (showView artifact → artifact join → MessagesTimeline → pack component; pack-scoped documents; decode fallback; crash containment), t3team-packWebHost.test.ts; existing SystemTimelineRow, fullBleedWidgetRow, messageDecisionCard(Settled), messageShapeCard(Live), messagesTimelineRecipeCards, MessagesTimeline (101) pass.
  • server: t3team-workflowEngineBrokerShowView.test.ts, t3team-workflowEngineBroker.test.ts. Packages: threads.test.ts, staticAudit.test.ts, workflowReference.test.ts (regenerated), t3team-packs.distributionWeb.test.ts.
  • vp build with a temp distribution outside the repo: pack code lands in the ChatView chunk, its Tailwind class is emitted, its bare imports resolve from the app. Without a distribution: builds, no pack code.
  • tsc clean for web, server, runbook-threads, pack-ui, packs, sdk. Additive guard passes.

Review fixes (round 1)

  • showView with a t3team.* viewId (or any re-check failure) no longer throws in the one-way broker send (an unhandled rejection that crashed the server). The host namespace is refused at the call site (showViewInputProblem), and the broker logs it and records it as the step detail instead.
  • pack-ui Markdown passes parseRawHtml={false}.
  • A pack view's props are decoded once per attachment object, so a crashed view stays down when its row re-renders.
  • packs[] containment; host alias imports from pack code fail the build; the Tailwind @source append matches a symlinked stylesheet.
  • t3team-sdk.capabilityScan.ts back under 200 lines.

Deviations from the spec

  • usePackDocument(s) is stubbed against a PackDocumentSource seam (setPackDocumentSource) and reports unavailable. T1's client atom (feat(packs): durable pack document store (T1) #435) is on main now, but wiring it is not a drop-in: the pack scope has no environment id yet, and the atom reports an unsupported server as an empty list rather than unavailable. Follow-up slice.
  • Known gap (v1 risk accepted): no server-side check that a run's viewId belongs to its recipe's pack — the broker does not know the run's pack. The id shape is enforced and the host t3team.* namespace is refused at the call site and in the broker. Follow-up: carry the recipe's pack id through launch and resume into the broker deps, and refuse foreign namespaces.
  • props schema is required on a registration (spec: optional), so undecoded data never reaches a pack component.
  • The artifact id is message-ext:t3team-wf-view:<threadId>:<key> (via the host's existing message-ext split), not view:<key>: keyed per thread so one key can be shown in two threads.
  • The registry fills on first lookup, not "before the router mounts" (no main.tsx change; avoids an import-cycle ordering hazard).
  • apps/server/scripts/t3team-distributionPackPlugin.ts is unchanged: the server does not read packs[] yet, so there is nothing to share.
  • PACK_UI_VERSION build check and the full import-boundary lint are left to T7, as planned. Host app aliases (~/…, @/…) imported from pack code already fail the build.
  • distribution.json packs[] entries are contained to the distribution's own folder and its siblings, not to the distribution directory alone: the planned layout lists a sibling pack ("../explainer" from nexplore-global). ../../x, .. and absolute paths fail the build.

🤖 Generated with Claude Code

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for 72a074d.

This comment will update automatically after the next completed run.

@johnnyelwailer
johnnyelwailer marked this pull request as ready for review October 7, 2026 13:46
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T13:51:31.955576Z 6a0e6e4 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a0e6e4186

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* pack's own component through the view registry. This host has no pack store yet, so the view
* shows its no-store state.
*/
export const ShowViewArtifact: Story = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cover every supported pack-view state in Storybook

This story only exercises a valid view while the document source is unavailable, while the new component also exposes loading, ready, crash-fallback, expanded, and collapsed states. Add stories and play interactions for those states; tests alone do not satisfy the repository's required Storybook coverage for reusable UI.

AGENTS.md reference: AGENTS.md:L104-L108

Useful? React with 👍 / 👎.

Comment on lines +60 to +62
const dir = process.env.T3CODE_DISTRIBUTION?.trim();
const entries = dir ? readDistributionWebEntries(canonical(dir)) : [];
const packDirs = [...new Set(entries.map((entry) => canonical(entry.packDir)))];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Permit external pack directories in Vite dev

When T3CODE_DISTRIBUTION points outside the workspace—as the standard scripts/t3team-dev-nexi.ts default does—the generated module imports these absolute entry paths, but vite.config.ts never adds packDirs to server.fs.allow. Vite's unbundled dev server therefore rejects the resulting /@fs/ requests under its default strict filesystem policy, so the distribution works in a production build but its web views fail to load during normal development.

Useful? React with 👍 / 👎.

Comment on lines +53 to +55
for (const activation of activations) {
try {
activatePackWebModule(registry, activation);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make activation atomic across every module in a pack

A manifest may contain multiple contents.views entries, and the distribution reader emits each as a separate activation with the same packId. Committing each activation independently here means that if a later module throws or registers an invalid ID, views from earlier modules remain registered, leaving the pack partially active despite the documented all-or-nothing behavior; group activations by pack and commit their pending entries only after every module succeeds.

Useful? React with 👍 / 👎.

Comment on lines +31 to +32
if (!id.startsWith(`${packId}.`) || id.length <= packId.length + 1) {
throw new Error(`Pack ${packId}: view id "${id}" must be "${packId}.<name>"`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate registered IDs against the showView grammar

This prefix check accepts IDs such as standup.bad id or standup..card, but Thread.showView rejects those IDs with its stricter VIEW_ID grammar. Such a pack activates successfully yet no workflow can post its registered view through the supported API; registration should apply the same shared ID validation, including validating the pack namespace itself.

Useful? React with 👍 / 👎.

Comment on lines +33 to +34
<RenderErrorBoundary
resetKeys={[props.props]}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep error-boundary reset keys stable across renders

For structured schemas, each registry lookup decodes the attachment into a fresh props object, so an unrelated timeline rerender changes this reset key even when the underlying attachment is unchanged. A crashing pack component is consequently retried and throws again on every streaming or parent-state update instead of remaining contained behind the fallback; use a stable raw-props identity or structural/version key and reset only when the attachment actually changes.

Useful? React with 👍 / 👎.

Comment on lines +177 to +178
} else if (verb === "showView" && !declared.has("ui.render")) {
into.push(missing(ts, sf, callee, "ui.render", "`showView(…)`"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict showView capability checks to thread receivers

This check uses only the property name, so any workflow-local object with a method such as preview.showView() is reported as requiring ui.render even though the runtime gate applies only to a Thread. Because the static audit blocks otherwise valid workflow sources and explicitly prefers misses over false positives, resolve the receiver back to getThread()/a known thread binding before emitting this finding.

Useful? React with 👍 / 👎.

Phil J and others added 5 commits October 7, 2026 21:38
…View

A pack can now ship React views that render in the thread timeline, and a
workflow can post one with getThread()?.showView({ key, viewId, props }).

- @t3team/pack-ui: the pack-facing contract (defineWebActivate, ViewSlot,
  MessageViewRegistration, PACK_UI_VERSION) and a minimal host kit (Button,
  Badge, Skeleton, Markdown, usePackDocument(s)). Types only; the web app
  aliases the specifier to its own implementation.
- @t3code/distribution-web: a Vite plugin compiles in the web entry of every
  pack the distribution lists (distribution.json packs[], contents.views,
  view:v1). Without a distribution it resolves to an empty stub.
- One view registry keyed by (slot, id). The three host views (workflow
  shape, decision, workflow card) moved in; the timeline routes view
  attachments through it instead of three hard-coded ids. Pack views get a
  pack scope and an error boundary; undecodable props fall back to the
  generic attachment row.
- Thread.showView (runbook-threads), gated on ui.render (runtime and static
  scan). The broker posts a view attachment keyed by (thread, key), so a
  re-post updates the same row.

usePackDocument(s) report "unavailable" until the pack store (S3) lands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The host namespace check now lives in showViewInputProblem, so a body
gets a catchable error at the call site. The broker re-checks, but logs
and records the refusal as the step detail instead of throwing: the SDK
fires one-way verbs without awaiting, so a throw was an unhandled
rejection that crashed the server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ChatMarkdown parses sanitized raw HTML by default; pack text is untrusted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A fresh decode on every row render handed the view new props and reset
its error boundary, so a crashed view re-threw on every re-render.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@johnnyelwailer
johnnyelwailer force-pushed the feature/seam-t3-pack-web-module-view-registry-6b5a40c9 branch from 6a0e6e4 to 02a39f9 Compare October 7, 2026 19:38
Phil J and others added 6 commits October 7, 2026 21:40
…lings

Entries like "../../x" or an absolute path escaped the distribution;
they now fail the build. A sibling pack ("../explainer") stays allowed:
that is the layout distributions use. Adds packWebImportProblem for the
web build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tylesheet

A pack importing "~/..." or "@/..." resolved into host internals; the
build now fails with a clear error (the full import lint stays T7).
The Tailwind @source append compared a non-realpath stylesheet path with
realpath module ids, so on a symlinked checkout it never fired.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings t3team-sdk.capabilityScan.ts back under the 200-line cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Effect diagnostics directives for the one-way broker's console log and the
test's macrotask wait (same as the other Promise-side workflow modules);
the crash-reset test re-renders through a prop that is still a string.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@johnnyelwailer
johnnyelwailer merged commit f5cc74b into main Oct 8, 2026
20 of 21 checks passed
@johnnyelwailer
johnnyelwailer deleted the feature/seam-t3-pack-web-module-view-registry-6b5a40c9 branch October 8, 2026 05:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant