@todayai-labs/tck
Widget-author SDK. Types, hooks, primitives, patch helpers. Compiled INTO the widget bundle.
The widget-author SDK. Widget source code (the .tck.tsx file) imports from this package; the bundler externalises it. Every TCK widget compiles against @todayai-labs/tck.
The full surface is split into 5 widget-facing subpaths plus a barrel re-export. Subpath imports keep the bundle's import graph minimal; the barrel is for ergonomics — both resolve to the same physical tck.mjs via the host import map, so module identity is preserved across the boundary (see DEFAULT_GROUPS).
Authoring profiles
defineManifest(...) is the common manifest API for both TCK authoring profiles. The required type discriminates the source shape; it does not select another SDK, bundle format, or renderer.
import { defineManifest } from '@todayai-labs/tck'
export const manifest = defineManifest({
type: 'feed',
layout: 'auto-height',
id: 'com.today.feed.focus',
name: 'Focus',
version: '0.1.0',
summary:
'Focus time is clear this morning, so put the hardest planning task first before meetings fragment the afternoon.',
})The Feed definition accepts type, required layout, id, name, version,
and optional summary. Write type first, layout immediately after it, and
summary last in source. Feed currently supports only auto-height layout, so write
layout: 'auto-height' or { type: 'auto-height' }. Content is written directly
in the React component. The SDK stamps the current ABI, supplies the stateless Feed
schema version, and maps layout to the resolved auto-height fields before the
bundler writes the manifest into .tckb.
The Interactive Widget definition uses the same function with type: 'widget' and exposes
the state, layout, and permission surface. Fixed-grid widgets write
layout: { type: 'grid', supportedSizes, defaultSize }; the helper maps it to the
resolved sizes / defaultSize fields. The author owns state.version and
state.default; the helper maps them to the resolved schemaVersion and defaultState
fields. If state.schema is present, the helper validates state.default and serializes
the schema to the resolved stateSchema JSON field. The SDK still stamps its own ABI.
import { z } from 'zod'
const StateSchema = z.object({
items: z.array(z.object({ title: z.string(), done: z.boolean() })),
})
export const manifest = defineManifest({
type: 'widget',
layout: { type: 'grid', supportedSizes: ['2x2'], defaultSize: '2x2' },
id: 'com.today.todo-list',
name: 'Todo List',
version: '0.1.0',
state: {
version: 1,
schema: StateSchema,
default: { items: [] },
},
})defineManifest(...) is intentionally smaller than the low-level wire manifest:
authors do not write sizes, defaultSize, schemaVersion, defaultState, engines, source, icon, or
description. The helper emits manifestVersion: 3 and type; older bundles without
manifestVersion are treated as the version-1 manifest shape by compatibility readers.
Content locale
Feed and Interactive Widget definitions accept an optional locale. New agent-authored cards
should declare the language of their actual content, including authored controls, empty states,
and slot defaults. Code, comments, proper nouns, and quoted source text do not determine the
declaration. Host-provided slot overrides retain the Host's localization policy.
// Single-language authoring shorthand:
locale: 'en-US'
// Resolved manifest.locale, also accepted directly by defineManifest:
locale: { supported: ['en-US'], default: 'en-US' }
// Multiple languages require corresponding content implementations:
locale: { supported: ['zh-Hans', 'en-US'], default: 'zh-Hans' }defineManifest canonicalizes language tags through Intl.getCanonicalLocales (for example,
en-us becomes en-US) and expands shorthand before bundling. The low-level WidgetManifestInput
and resolved wire format accept only the object with canonical tags. supported must be non-empty
and unique, and default must belong to it. Duplicate tags after canonicalization are errors.
The default is used when no viewer preference is available or no supported language matches;
there is no separate fallback field. Omission remains valid for historical cards and means
undeclared, never an inferred English default.
Tags follow the BCP 47 forms supported by Intl.getCanonicalLocales. Prefer zh-Hans / zh-Hant
when distinguishing Simplified and Traditional Chinese. zh-CN identifies regional Chinese;
zh-Hans-CN additionally specifies the script. zh-CN-Hans is invalid: script precedes region.
The SDK does not infer a script or region, so zh-CN is not rewritten into zh-Hans.
Language-only (en, zh), language-script (zh-Hans), language-region (en-US, zh-TW),
and language-script-region (zh-Hant-TW) declarations are all supported. Short tags are standard
tags, not incomplete input; preserve the author's level of specificity. Explicit combinations
such as zh-Hans-TW are accepted even when uncommon. There is no hard-coded language list or
region-to-script restriction; other tags supported by Intl.getCanonicalLocales, such as ja,
sr-Latn, es-419, and de-CH-1901, use the same contract.
Canonicalization normalizes spelling and aliases, not missing information: en stays en,
zh stays zh, and zh-TW stays zh-TW. A broad tag does not advertise all more-specific
content versions. Language negotiation applies an explicit matching policy without
rewriting the stored declaration. Default membership still uses exact canonical tags:
supported: ['en-US'] requires default: 'en-US', not default: 'en'.
See W3C language tags.
Hosts can read manifest.locale before mounting. A single supported entry declares the card's
only language. Hosts can negotiate the selected language with
resolveCardLocale, which respects
user preference order, compatible scripts, and separate card-default and Host-fallback policies.
This does not translate content, switch card copy at runtime, or provide a useLocale hook.
A future hook should expose the selected supported card language rather than an unmatched viewer
preference.
Upgrade consuming SDK validators before distributing locale-bearing bundles: older strict manifest validators reject the new field. The authoring helper still emits manifest version 3 and ABI v1; those version labels alone do not identify whether a consumer understands this optional metadata.
Host-resolved slots
defineSlots(...) statically declares standard content regions that the Widget renders
but the Host may resolve. Include the declaration in manifest.slots, then pass the same
object to useResolvedSlots(...). Because the declaration is manifest data, a Host can
detect support before first paint. manifest.slots is optional; a Host supplies only
declared keys through its mount surface's slotValues option, and missing values fall back
to the Widget defaults.
import { defineManifest, defineSlots, useResolvedSlots } from '@todayai-labs/tck'
const slots = defineSlots({
actions: {
defaultValue: [
{
id: 'review',
label: 'Review',
action: 'chat.composer.fill',
payload: { text: 'Review this update and recommend the next step.' },
variant: 'primary',
color: 'sky',
},
],
},
updatedAt: { defaultValue: null },
})
export const manifest = defineManifest({
type: 'feed',
layout: 'auto-height',
id: 'com.today.feed.review',
name: 'Review Feed',
version: '0.1.0',
slots,
})
function ReviewFeed() {
const { actions, updatedAt } = useResolvedSlots(slots)
// Render the resolved actions and update time in the Widget's own layout.
}The standard keys are actions (SlotAction[]) and updatedAt
(UpdatedAtSlotValue | null). Each action has a stable id, a typed Today action
descriptor, and a primary | secondary variant. A primary action also requires an explicit
sky | red color; secondary actions always use the neutral treatment and do not
accept a color. The Host may replace labels, variants, and primary colors, reorder the
array, or inject complete additional actions before passing the final value back.
Product-specific policy for deciding those final values remains outside TCK.
UpdatedAtSlotValue is exactly { timestamp: string; label: string }. The Host owns the
clock, user time zone, locale, and final label; the Widget only renders the value in its
declared location, ideally as <time dateTime={updatedAt.timestamp}>{updatedAt.label}</time>.
Hosts can reuse the deterministic formatter and refresh-boundary helpers from
@todayai-labs/tck-host-utils.
The Host always validates the exported manifest and consumes the resolved shape. It does not implement a separate Feed normalization or capability path.
Subpaths
@todayai-labs/tck/manifest
Manifest schema, validators, and the externals whitelist that bundler + host both pull from.
The manifest contract is a single zod schema (packages/tck/src/manifest/schema.ts) — the source of truth that drives the TypeScript types (z.infer), the runtime validator (validateManifest, via .parse()), and the published manifest.schema.json (generated with z.toJSONSchema; regenerate via pnpm --filter @todayai-labs/tck gen:schema). New widget source should use defineManifest(...); the schema still exposes two lower-level manifest types:
-
WidgetManifestInput— the low-level authoring shape accepted by the validator.sizes,defaultSize, anddefaultStateare optional (defaults['fill-auto'],'fill-auto',{}). PreferdefineManifest(...)for new source solayoutremains the only authored layout decision and the Feed / Interactive Widget profiles stay explicit. -
WidgetManifest— the resolved shape (defaults applied), what the host / bundler / runtime consume. The bundler bakes the resolved manifest into the.tckb, so consumers always read populated fields. -
Authoring:
defineManifest,defineSlots. -
Types:
WidgetManifest,WidgetManifestInput,SlotDefinitions,SlotAction,SlotActionColor,UpdatedAtSlotValue,WidgetSize,WidgetPermission,WidgetSource,JsonValue,JsonObject,JsonArray,GridPosition,WidgetInstanceId,HostOnlySpec,ParsedSize,TckExternalSpecifier. (The page-level layout descriptor — formerlyTodayPageDescriptor/TodayPageItem— moved to@todayai-labs/tck-hostasCanvasDescriptor/CanvasItem.) -
Constants:
TCK_EXTERNAL_SPECIFIERS,ALL_WIDGET_SIZES,HOST_ONLY_SPECS,SLOT_ACTION_COLOR_VALUES. -
Predicates / parsers:
validateManifest,isWidgetSize,isAutoHeightSize,isHostOnlySpec,isTckExternalSpecifier,isSizeSupported,parseSize,manifestIdToScopeSlug. -
Errors:
InvalidManifestError,InvalidSizeError.
@todayai-labs/tck/patch
RFC-6902 JSON Patch primitives and the envelope wire shape.
- Operations:
applyPatch,applyOp— pure functions; produce new immutable docs. - Pointer:
parsePointer,formatPointer— JSON Pointer (RFC 6901). - Envelope:
PatchEnvelope,PatchAck,newPatchId. - Errors:
PatchError,PointerError.
@todayai-labs/tck/agent
Wire types for the bidirectional a2ui agent protocol.
- Types:
AgentEnvelope,AgentMessage(discriminated union:state.change/patch.send/host.add-widget/host.move-widget/host.remove-widget/agent.suggestion/agent.pin/error),Suggestion. - Version:
A2UI_PROTOCOL_VERSION.
@todayai-labs/tck/hooks
The widget-facing hooks family. All consume WidgetCtx from @todayai-labs/tck/runtime via React context.
- Doc state:
useFieldState(JSON-pointer sugar),usePersistState(selector + setter). - Host context:
useWidgetCtx(raw ctx),useCurrentWidgetSize,useVisibility,useTheme,useResolvedSlots. - Agent channel:
useAgent(returnsAgentChannel | null— null when the manifest didn't declareagent.read/agent.write),useAgentSubscription(subscribe to inbound envelopes). - Errors:
MissingWidgetCtxError.
Host-side ambient services (focus timer, media controller, weather, calendar, …) are NOT framework concerns. Their read-side state flows through the doc/patch plane (host writes service state into the widget's doc; widgets read via usePersistState) and commands use the generic widget → host RPC primitive ctx.invokeHost(target, action, args?). Slots are limited to the standard Host-resolved presentation regions above; they are not an ambient service transport. Outside the built-in action protocol below, targets and actions are opaque strings the host's product layer routes by — the widget SDK does not know which capabilities a particular host implements. A typed product-side wrapper (e.g. @today/widget-services) lives outside this package.
The one product-level action surface bundled with TCK is the <Action> component. By default it renders a real button and emits the strict today.action protocol through ctx.invokeHost; hosts may resolve the same action into a link, intercept or allow default anchor navigation, or wrap the rendered node for product chrome. Widget styles should be element-agnostic because host rendering may be a button, anchor, custom press target, or wrapper.
import { Action, type TodayActionDescriptor } from '@todayai-labs/tck'
const summarizeAction: TodayActionDescriptor<'chat.composer.fill'> = {
action: 'chat.composer.fill',
label: 'Summarize release status',
payload: {
text: 'Summarize the release-readiness status and recommend the next action. Two required checks are still failing, and the platform team owns both blockers.',
},
}
<Action
actionId='summarize-release-status'
action={summarizeAction.action}
payload={summarizeAction.payload}
>
{summarizeAction.label}
</Action>
<Action action='web.open' payload={{ url: 'https://today.ai', title: 'Today AI' }}>
Open website
</Action>
<Action action='connector.navigate' payload={{ connectorId: 'gmail', route: 'connect' }}>
Reconnect Gmail
</Action>For chat.composer.fill, the typed payload is required and must not be omitted.
payload.text is the complete composer prefill; keep it independent from the short
visible children. Integrate every relevant fact into a direct, natural-language request
that makes sense on its own. Do not refer to a card the model cannot see, append raw
widget JSON, or write protocol-style action metadata.
Pass an optional stable actionId when the Host needs to correlate the rendered control
with analytics or custom press behavior. actionId reaches resolveHostAction and custom
press handlers, but does not alter the action payload or become a DOM attribute.
The generated project runs tsc --noEmit before bundling, so new builds fail when the
payload is omitted. The runtime still keeps historical bundles renderable: if an older
chat.composer.fill action omits a usable payload.text, Action logs a migration
warning and falls back to its visible label. The fallback exists only for runtime
compatibility.
The barrel exports the protocol types so widget code stays type-safe: TodayActionName, TodayActionPayload, TodayActionDescriptor, TodayActionEvent, ChatComposerFillPayload, WebOpenPayload, and ConnectorNavigatePayload.
@todayai-labs/tck/runtime
Types for the host-provided context object.
- Types:
WidgetCtx,AgentChannel,Theme. - Context:
WidgetCtxContext— the React context. Singleton across the host/widget boundary byTCK_EXTERNAL_SPECIFIERSresolution. (See Runtime instance semantics for why this matters.)
@todayai-labs/tck (barrel)
Re-export of the common surface above plus three families that don't live under a dedicated subpath:
- Container offer:
ContainerParams,containerParamsToQuery,parseContainerParams,InvalidContainerParamsError. See container contract. - Bridge (cross-window / cross-realm transport):
BridgeDispatcher,BridgeEnvelope,BridgeRequest,BridgeReply,BridgePush,BridgeNotify,BridgeHandler,BridgeTransport,BridgeReplyError,isBridgeEnvelope,postMessageTransport,TCK_BRIDGE_VERSION. - Host control plane:
HostControlEnvelope,HostControlRequest,HostControlReply,HostControlPush,HostControlInstanceSnapshot,isHostControlEnvelope. - ID minter:
widgetInstanceId.
@todayai-labs/tck/specifiers is a special-case subpath that exports only the TCK_EXTERNAL_SPECIFIERS array as plain JS — for build tools (bundler, host import-map plugin, the scripts/build-widgets.ts parity check) that need the constant without paying for the rest of the SDK's TypeScript surface.
Constraints
tck-hostis NOT importable from a widget. Widgets neverimport '@todayai-labs/tck-host'— it's host-only and is deliberately absent from the externals whitelist. The bundler rejects bundles that import it.- No browser storage APIs. No
localStorage, nosessionStorage, noIndexedDB, nodocument.cookie. Persistence flows throughuseFieldState/usePersistState— the host's doc-store is the single channel. - No
prefers-color-schemereads. Theme is host-injected; see theme-injection contract. - No portals to
document.body. Usectx.portalTarget; see Platform ABI § Portals.
Source
- Package:
packages/tck - Externals list:
packages/tck/src/manifest/externals.js - Types:
packages/tck/src/types.ts - Manifest schema (zod SSOT):
packages/tck/src/manifest/schema.ts - Manifest schema (JSON, generated):
packages/tck/manifest.schema.json— regenerate withpnpm --filter @todayai-labs/tck gen:schema