Today Canvas Kit
SDK packages

@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, and defaultState are optional (defaults ['fill-auto'], 'fill-auto', {}). Prefer defineManifest(...) for new source so layout remains 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 — formerly TodayPageDescriptor / TodayPageItem — moved to @todayai-labs/tck-host as CanvasDescriptor / 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 (returns AgentChannel | null — null when the manifest didn't declare agent.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 by TCK_EXTERNAL_SPECIFIERS resolution. (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-host is NOT importable from a widget. Widgets never import '@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, no sessionStorage, no IndexedDB, no document.cookie. Persistence flows through useFieldState / usePersistState — the host's doc-store is the single channel.
  • No prefers-color-scheme reads. Theme is host-injected; see theme-injection contract.
  • No portals to document.body. Use ctx.portalTarget; see Platform ABI § Portals.

Source

On this page