Repository navigation
feat(packs): pack web modules, one message.view registry, Thread.showView - #436
johnnyelwailer merged 11 commits into
Conversation
Thread transfer impact
This comment will update automatically after the next completed run. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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 = { |
There was a problem hiding this comment.
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 👍 / 👎.
| const dir = process.env.T3CODE_DISTRIBUTION?.trim(); | ||
| const entries = dir ? readDistributionWebEntries(canonical(dir)) : []; | ||
| const packDirs = [...new Set(entries.map((entry) => canonical(entry.packDir)))]; |
There was a problem hiding this comment.
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 👍 / 👎.
| for (const activation of activations) { | ||
| try { | ||
| activatePackWebModule(registry, activation); |
There was a problem hiding this comment.
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 👍 / 👎.
| if (!id.startsWith(`${packId}.`) || id.length <= packId.length + 1) { | ||
| throw new Error(`Pack ${packId}: view id "${id}" must be "${packId}.<name>"`); |
There was a problem hiding this comment.
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 👍 / 👎.
| <RenderErrorBoundary | ||
| resetKeys={[props.props]} |
There was a problem hiding this comment.
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 👍 / 👎.
| } else if (verb === "showView" && !declared.has("ui.render")) { | ||
| into.push(missing(ts, sf, callee, "ui.render", "`showView(…)`")); |
There was a problem hiding this comment.
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 👍 / 👎.
…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>
6a0e6e4 to
02a39f9
Compare
…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>
Slice T3 of the explainer seam plan (seams S1 + the
message.viewpart 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 againstPackUiHostKit).@t3code/distribution-web:apps/web/scripts/t3team-distributionWebPlugin.tscompiles in the web entry of every pack indistribution.jsonpacks[](default: the distribution dir) that declarescontents.viewswithview:v1. It aliases@t3team/pack-uito 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 stubsrc/t3team-distributionWeb.ts. The shared reader is@t3team/packs/distribution-web.t3team-viewRegistry.ts, app instancet3team-messageViewRegistry.ts): one table keyed by(slot, id). The workflow shape, decision and card views are now host registrations (t3team-hostMessageViews.tsx);SystemTimelineRow,GenericRow,fullBleedWidgetRowandgetT3TeamRenderableAttachmentsread 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 onui.renderat 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
t3team-packMessageView.test.tsx(showView artifact → artifact join →MessagesTimeline→ pack component; pack-scoped documents; decode fallback; crash containment),t3team-packWebHost.test.ts; existingSystemTimelineRow,fullBleedWidgetRow,messageDecisionCard(Settled),messageShapeCard(Live),messagesTimelineRecipeCards,MessagesTimeline(101) pass.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 buildwith a temp distribution outside the repo: pack code lands in theChatViewchunk, its Tailwind class is emitted, its bare imports resolve from the app. Without a distribution: builds, no pack code.tscclean for web, server, runbook-threads, pack-ui, packs, sdk. Additive guard passes.Review fixes (round 1)
showViewwith at3team.*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.MarkdownpassesparseRawHtml={false}.packs[]containment; host alias imports from pack code fail the build; the Tailwind@sourceappend matches a symlinked stylesheet.t3team-sdk.capabilityScan.tsback under 200 lines.Deviations from the spec
usePackDocument(s)is stubbed against aPackDocumentSourceseam (setPackDocumentSource) and reportsunavailable. T1's client atom (feat(packs): durable pack document store (T1) #435) is onmainnow, 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 thanunavailable. Follow-up slice.viewIdbelongs to its recipe's pack — the broker does not know the run's pack. The id shape is enforced and the hostt3team.*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.propsschema is required on a registration (spec: optional), so undecoded data never reaches a pack component.message-ext:t3team-wf-view:<threadId>:<key>(via the host's existing message-ext split), notview:<key>: keyed per thread so one key can be shown in two threads.main.tsxchange; avoids an import-cycle ordering hazard).apps/server/scripts/t3team-distributionPackPlugin.tsis unchanged: the server does not readpacks[]yet, so there is nothing to share.PACK_UI_VERSIONbuild 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.jsonpacks[]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"fromnexplore-global).../../x,..and absolute paths fail the build.🤖 Generated with Claude Code