@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: 2, type, and the legacy cardType
field for compatibility; older bundles without manifestVersion are treated as the
version-1 manifest shape by compatibility readers.
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. -
Types:
WidgetManifest,WidgetManifestInput,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. -
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. - 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 and have no typed slots on WidgetCtx. They flow through the doc/patch plane (host writes service state into the widget's doc; widgets read via usePersistState) and the generic widget → host RPC primitive ctx.invokeHost(target, action, args?). 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
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.
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