Skip to content

Widget System & Tag Definitions

Complete reference for Pika's tag definition system that enables AI-driven custom UI components.

The widget system allows LLMs to generate custom UI elements by including special tags in their responses. Each tag is defined by a TagDefinition that specifies:

  • Widget type and rendering
  • When and where it can be used
  • Instructions for the LLM
  • Access control
import type {
TagDefinition,
TagDefinitionLite,
TagDefinitionWidget,
TagInstructionForLlm,
WidgetRenderingContexts
} from 'pika-shared/types/chatbot/chatbot-types';
import type {
WidgetAction,
WidgetMetadata,
WidgetMetadataState,
SpotlightWidgetDefinition,
WidgetCallbackContext
} from 'pika-shared/types/chatbot/webcomp-types';

Complete tag definition with all configuration.

interface TagDefinition<T extends TagDefinitionWidget> {
tag: string;
scope: string;
usageMode: 'global' | 'chat-app';
widget: T;
llmInstructionsMd?: string;
status: 'enabled' | 'disabled' | 'retired';
renderingContexts: WidgetRenderingContexts;
canBeGeneratedByLlm: boolean;
canBeGeneratedByTool: boolean;
description: string;
dontCacheThis?: boolean;
createdBy: string;
lastUpdatedBy: string;
createDate: string;
lastUpdate: string;
}

Fields:

  • tag - Tag name (e.g., 'chart', 'order-status')
  • scope - Namespace to prevent collisions (e.g., 'pika', 'custom')
  • usageMode - 'global' (auto-available) or 'chat-app' (explicit enable)
  • widget - Widget configuration (type and settings)
  • llmInstructionsMd - Markdown instructions injected into prompts
  • status - Lifecycle state
  • renderingContexts - Where widget can be rendered
  • canBeGeneratedByLlm - Whether LLM can use this tag
  • canBeGeneratedByTool - Whether tools can generate this tag
  • description - Admin-facing description

Minimal tag identifier.

interface TagDefinitionLite {
scope: string;
tag: string;
}

Used for referencing tags without full definition.

Four types of widgets are supported:

type TagDefinitionWidget =
| BuiltInWidget
| CustomCompiledInWidget
| WebComponentWidget
| PassThroughWidget;

Pika-provided widgets (chart, image, prompt).

interface BuiltInWidget {
type: 'built-in';
builtInType: 'chart' | 'image' | 'prompt';
}

Svelte components compiled into your app.

interface CustomCompiledInWidget {
type: 'custom-compiled-in';
}

References renderers in customRenderers registry.

Dynamically loaded web components.

interface WebComponentWidget {
type: 'web-component';
webComponent: {
s3Bucket: string;
s3Key: string;
encoding?: 'gzip' | 'none';
};
}

Non-rendering semantic markup.

interface PassThroughWidget {
type: 'pass-through';
}

Automatically available to all chat apps unless explicitly disabled.

const globalTag: TagDefinition<BuiltInWidget> = {
tag: 'chart',
scope: 'pika',
usageMode: 'global', // Available everywhere by default
status: 'enabled',
widget: {
type: 'built-in',
builtInType: 'chart'
},
// ... other fields
};

Disabling global tags per chat app:

// In chat app configuration
features: {
tags: {
enabled: true,
tagsDisabled: [
{ scope: 'pika', tag: 'chart' } // Disable charts in this app
]
}
}

Must be explicitly enabled per chat app.

const chatAppTag: TagDefinition<WebComponentWidget> = {
tag: 'order-status',
scope: 'acme',
usageMode: 'chat-app', // Requires explicit enable
status: 'enabled',
widget: {
type: 'web-component',
webComponent: {
s3Bucket: 'acme-widgets',
s3Key: 'order-status.js',
encoding: 'gzip'
}
},
// ... other fields
};

Enabling chat-app tags:

// In chat app configuration
features: {
tags: {
enabled: true,
tagsEnabled: [
{ scope: 'acme', tag: 'order-status' }
]
}
}

Control where widgets can be rendered.

interface WidgetRenderingContexts {
inline?: InlineContextConfig;
canvas?: CanvasContextConfig;
dialog?: DialogContextConfig;
spotlight?: SpotlightContextConfig;
static?: StaticContextConfig;
hero?: HeroContextConfig;
}

Contexts:

  • inline - Within message text flow
  • canvas - Right-side panel, resizable with chat pane
  • dialog - Modal/popup overlay
  • spotlight - Featured carousel above chat input
  • static - Hidden widget that runs initialization code (no visual UI)
  • hero - Singleton widget displayed prominently below spotlight (v0.17.0+)

Example:

renderingContexts: {
inline: { enabled: true },
canvas: { enabled: true },
dialog: { enabled: false },
spotlight: { enabled: false },
static: { enabled: false },
hero: { enabled: false }
}

Hero is a new rendering context for a dominant, singleton widget that displays above the chat input area:

interface HeroContextConfig {
enabled: boolean;
/** If true, hero is auto-rendered on startup. @default false */
autoCreateInstance?: boolean;
/** If true, hero starts collapsed on startup. @default false */
startCollapsed?: boolean;
/** Sizing configuration for width and height constraints */
sizing?: HeroSizeConfig;
}
/**
* Hero sizing configuration (v0.18.0+)
* Controls the dimensions of the hero widget container.
* Hero is always horizontally centered; sizing controls the max bounds.
*/
interface HeroSizeConfig {
/** Fixed width (e.g., '600px', '80%'). If not set, uses content width. */
width?: string;
/** Fixed height (e.g., '300px', 'auto'). If not set, uses content height. */
height?: string;
/** Minimum width constraint. @default '200px' */
minWidth?: string;
/** Maximum width constraint. @default '90%' */
maxWidth?: string;
/** Minimum height in pixels. @default 100 */
minHeight?: number;
/** Maximum height in pixels. @default 600 */
maxHeight?: number;
}

Key characteristics:

  • Singleton - Only one hero widget can exist at a time
  • API controlled - Shown/hidden via showHero(), hideHero(), closeHero()
  • Collapsible - Users can collapse to a compact header bar
  • Can be destroyed - Unlike spotlight, hero can be completely removed

Example:

// Render a hero widget
await chatAppState.renderTag('acme.dashboard', 'hero', { userId: '123' }, {
title: 'Dashboard',
lucideIconName: 'layout-dashboard'
});
// Collapse to header bar
chatAppState.collapseHero();
// Expand from collapsed state
chatAppState.expandHero();
// Toggle collapsed/expanded
chatAppState.toggleHeroCollapsed();
// Hide the hero completely (programmatic only - no UI)
chatAppState.hideHero();
// Show it again
chatAppState.showHero();
// Destroy completely
chatAppState.closeHero();

Hero state properties:

// Check if hero is visible (false = completely hidden, no UI)
const isVisible = chatAppState.heroVisible;
// Check if hero is collapsed to header bar
const isCollapsed = chatAppState.heroCollapsed;

Hero display states:

  • Hidden (heroVisible=false): No UI shown - widget runs in background (programmatic only)
  • Collapsed (heroVisible=true, heroCollapsed=true): Shows header bar with title and expand caret
  • Expanded (heroVisible=true, heroCollapsed=false): Shows full widget with header and content

User can only toggle between collapsed and expanded states. Widgets can programmatically show/hide/collapse/expand.

Hero Sizing (v0.18.0+):

The hero widget container is always horizontally centered. Use sizing config to control dimensions:

// Tag definition with sizing
renderingContexts: {
hero: {
enabled: true,
sizing: {
// Content-driven width with constraints
minWidth: '400px',
maxWidth: '900px',
// Height constraints
minHeight: 150,
maxHeight: 400
}
}
}

Sizing behavior:

  • If width/height are not set, the hero uses the content's intrinsic size (clamped to min/max)
  • If width/height are set, those exact values are used (still clamped to min/max)
  • Percentage values (e.g., '80%', '100%') are responsive to viewport changes
  • maxWidth: '100%' ensures the hero never overflows on small screens

Example sizing configurations:

// Compact hero (fixed width)
sizing: { width: '500px', minHeight: 100, maxHeight: 300 }
// Full-width hero with height limits
sizing: { maxWidth: '100%', minHeight: 200, maxHeight: 500 }
// Responsive hero with constraints
sizing: { minWidth: '400px', maxWidth: '900px', minHeight: 150, maxHeight: 400 }

Spotlight is a carousel of widgets displayed above the chat input area:

interface SpotlightContextConfig {
enabled: boolean;
/** If true, widget appears first in carousel. @default false */
isDefault?: boolean;
/** Display order in carousel (lower = higher). @default 0 */
displayOrder?: number;
/** If true (default), only one instance can exist. @default true */
singleton?: boolean;
/** If false, widget won't appear in unpinned menu. @default true */
showInUnpinnedMenu?: boolean;
/** If true (default), widget is auto-created on startup. @default true */
autoCreateInstance?: boolean;
/** If true, spotlight starts collapsed on startup. @default false */
startCollapsed?: boolean;
}

Spotlight API:

// Show the spotlight carousel
chatAppState.showSpotlight();
// Hide the spotlight to just header
chatAppState.hideSpotlight();
// Toggle spotlight visibility
chatAppState.toggleSpotlight();
// Check if spotlight is visible
const isVisible = chatAppState.spotlightVisible;

Static widgets run initialization code but don't render visually. Useful for orchestration:

interface StaticContextConfig {
enabled: boolean;
shutDownAfterMs?: number; // Remove from DOM after this many ms
}

Use cases:

  • Register title bar actions
  • Set up event listeners
  • Initialize shared state
  • Coordinate between multiple widgets

Guide the LLM on when and how to use tags.

type TagInstructionForLlm =
| { type: 'line'; text: string }
| {
type: 'block';
title: string;
lines: Array<{ type: 'line'; text: string }>;
};
llmInstructions: [
{
type: 'line',
text: 'Use the chart tag when you need to visualize numerical data'
},
{
type: 'block',
title: 'Chart Types',
lines: [
{ type: 'line', text: '- bar: For comparing categories' },
{ type: 'line', text: '- line: For showing trends over time' },
{ type: 'line', text: '- pie: For showing proportions' }
]
},
{
type: 'line',
text: 'Example: <chart type="bar" data=\'{"labels":["A","B"],"datasets":[{"data":[1,2]}]}\' />'
}
]

Instructions can also be provided as Markdown:

llmInstructionsMd: `
# Chart Tag
Use the chart tag when you need to visualize numerical data.
## Chart Types
- **bar**: For comparing categories
- **line**: For showing trends over time
- **pie**: For showing proportions
## Example
\`\`\`xml
<chart type="bar" data='{"labels":["A","B"],"datasets":[{"data":[1,2]}]}' />
\`\`\`
`
type TagStatus = 'enabled' | 'disabled' | 'retired';
  • enabled - Active and available
  • disabled - Temporarily hidden
  • retired - Permanently archived
const chartTag: TagDefinition<BuiltInWidget> = {
tag: 'chart',
scope: 'pika',
usageMode: 'global',
widget: {
type: 'built-in',
builtInType: 'chart'
},
llmInstructionsMd: `Use for data visualization. Supports bar, line, and pie charts.`,
status: 'enabled',
renderingContexts: {
inline: false,
canvas: true,
dialog: true,
spotlight: true
},
canBeGeneratedByLlm: true,
canBeGeneratedByTool: true,
description: 'Renders interactive charts',
createdBy: 'system',
lastUpdatedBy: 'system',
createDate: '2024-01-01T00:00:00Z',
lastUpdate: '2024-01-01T00:00:00Z'
};
const orderStatusTag: TagDefinition<WebComponentWidget> = {
tag: 'order-status',
scope: 'acme',
usageMode: 'chat-app',
widget: {
type: 'web-component',
webComponent: {
s3Bucket: 'acme-components',
s3Key: 'widgets/order-status-v1.js',
encoding: 'gzip'
}
},
llmInstructionsMd: `
# Order Status Widget
Display real-time order status with tracking information.
## Usage
\`\`\`xml
<order-status orderId="12345" />
\`\`\`
## Attributes
- **orderId** (required): The order ID to display
`,
status: 'enabled',
renderingContexts: {
inline: true,
canvas: true,
dialog: false,
spotlight: false
},
canBeGeneratedByLlm: true,
canBeGeneratedByTool: true,
description: 'Shows order status and tracking',
dontCacheThis: false,
createdBy: 'admin@acme.com',
lastUpdatedBy: 'admin@acme.com',
createDate: '2024-01-15T10:00:00Z',
lastUpdate: '2024-01-15T10:00:00Z'
};
const dataTableTag: TagDefinition<CustomCompiledInWidget> = {
tag: 'data-table',
scope: 'custom',
usageMode: 'chat-app',
widget: {
type: 'custom-compiled-in'
// References customRenderers['data-table']
},
llmInstructionsMd: `
Display tabular data with headers and rows.
Example:
\`\`\`xml
<data-table>
{
"headers": ["Name", "Age", "City"],
"rows": [
["John", "30", "NYC"],
["Jane", "25", "LA"]
]
}
</data-table>
\`\`\`
`,
status: 'enabled',
renderingContexts: {
inline: false,
canvas: true,
dialog: false,
spotlight: false
},
canBeGeneratedByLlm: true,
canBeGeneratedByTool: true,
description: 'Renders data tables',
createdBy: 'admin',
lastUpdatedBy: 'admin',
createDate: '2024-01-15T10:00:00Z',
lastUpdate: '2024-01-15T10:00:00Z'
};
const metadataTag: TagDefinition<PassThroughWidget> = {
tag: 'metadata',
scope: 'system',
usageMode: 'global',
widget: {
type: 'pass-through'
},
llmInstructionsMd: 'Use for semantic markup that should not render visually',
status: 'enabled',
renderingContexts: {
inline: true,
canvas: false,
dialog: false,
spotlight: false
},
canBeGeneratedByLlm: true,
canBeGeneratedByTool: true,
description: 'Semantic markup with no visual rendering',
createdBy: 'system',
lastUpdatedBy: 'system',
createDate: '2024-01-01T00:00:00Z',
lastUpdate: '2024-01-01T00:00:00Z'
};

Full access to tag definitions (requires admin permissions).

Create or Update:

POST /api/chat-admin/tagdef
{
"tagDefinition": { /* TagDefinition object */ },
"userId": "admin-user"
}

Delete:

DELETE /api/chat-admin/tagdef
{
"tagDefinition": { "tag": "my-tag", "scope": "custom" },
"userId": "admin-user"
}

Search (including disabled):

POST /api/chat-admin/tagdef/search
{
"includeInstructions": true,
"paginationToken": null
}

Read-only access to enabled tags.

Search (enabled only):

POST /api/chat/tagdef/search
{
"includeInstructions": false,
"paginationToken": null
}
order-status-widget.js
class OrderStatusWidget extends HTMLElement {
connectedCallback() {
const orderId = this.getAttribute('orderId');
this.render(orderId);
}
async render(orderId) {
const status = await this.fetchOrderStatus(orderId);
this.innerHTML = `
<div class="order-status">
<h3>Order #${orderId}</h3>
<p>Status: ${status.state}</p>
<p>Tracking: ${status.tracking}</p>
</div>
`;
}
async fetchOrderStatus(orderId) {
// Fetch from API
return { state: 'Shipped', tracking: '1Z999...' };
}
}
customElements.define('order-status-widget', OrderStatusWidget);
Terminal window
# Compress and upload
gzip order-status-widget.js
aws s3 cp order-status-widget.js.gz s3://my-bucket/widgets/ \
--content-encoding gzip \
--content-type application/javascript
const tagDef: TagDefinition<WebComponentWidget> = {
tag: 'order-status',
scope: 'custom',
usageMode: 'chat-app',
widget: {
type: 'web-component',
webComponent: {
s3Bucket: 'my-bucket',
s3Key: 'widgets/order-status-widget.js.gz',
encoding: 'gzip'
}
},
// ... other fields
};
// POST to /api/chat-admin/tagdef

Components can register themselves in spotlight programmatically at runtime without creating database tag definitions.

Register a web component as a spotlight widget from code.

manuallyRegisterSpotlightWidget(definition: SpotlightWidgetDefinition): void

Parameters:

interface SpotlightWidgetDefinition {
/** Tag name (e.g., 'my-widget') */
tag: string;
/** Scope/namespace (e.g., 'my-app') */
scope: string;
/** Display title in UI */
tagTitle: string;
/** Custom element name (optional, inferred if not provided) */
customElementName?: string;
/** Widget sizing configuration (optional) */
sizing?: WidgetSizing;
/** Component agent instructions (optional) */
componentAgentInstructionsMd?: Record<string, string>;
/**
* Auto-show widget immediately?
* - true: Widget appears in spotlight automatically
* - false: Widget registered but not shown until renderTag() called
* @default true
*/
autoCreateInstance?: boolean;
/**
* Display order in spotlight (lower = higher)
* @default 0
*/
displayOrder?: number;
/**
* Only allow one instance?
* @default true
*/
singleton?: boolean;
/**
* Show in unpinned widget menu?
* @default true
*/
showInUnpinnedMenu?: boolean;
/**
* Optional initial metadata (title, actions, icon) to apply when rendered.
* Metadata can be updated later via getWidgetMetadataAPI().
* @since 0.11.0
*/
metadata?: WidgetMetadata;
}

Behavior:

  • Registration is ephemeral - does not persist across page refreshes
  • Component must re-register on each page load
  • Merges with database-sourced tag definitions
  • Respects user preferences (unpinning, ordering)
  • Compatible with all spotlight features

Example:

import { getPikaContext } from 'pika-shared/util/wc-utils';
class MyWidget extends HTMLElement {
async connectedCallback() {
const context = await getPikaContext(this);
// Register in spotlight
context.chatAppState.manuallyRegisterSpotlightWidget({
tag: 'my-widget',
scope: 'acme',
tagTitle: 'My Widget',
customElementName: 'acme-my-widget',
displayOrder: 0,
autoCreateInstance: true
});
}
}

Common Use Cases:

// Conditional registration (admin-only)
if (context.appState.identity.isSiteAdmin) {
context.chatAppState.manuallyRegisterSpotlightWidget({
tag: 'admin-tools',
scope: 'internal',
tagTitle: 'Admin Tools'
});
}
// Feature-gated widget
const features = await getEnabledFeatures();
if (features.includes('beta-analytics')) {
context.chatAppState.manuallyRegisterSpotlightWidget({
tag: 'analytics',
scope: 'acme',
tagTitle: 'Analytics Dashboard',
displayOrder: 5
});
}
// Base widget for saved instances
context.chatAppState.manuallyRegisterSpotlightWidget({
tag: 'saved-chart',
scope: 'acme',
tagTitle: 'Saved Chart',
autoCreateInstance: false, // Don't show by default
singleton: false, // Allow multiple instances
showInUnpinnedMenu: false // Hide from menu
});

Best Practices:

Widgets can dynamically update their chrome (title bar, actions, loading state):

interface WidgetMetadata {
/** Widget title shown in chrome */
title: string;
/**
* Optional Lucide icon name (fetched automatically and set as iconSvg).
* Use snake-case: 'arrow-big-down' not 'arrowBigDown'
*/
lucideIconName?: string;
/** Optional icon SVG markup for the widget title */
iconSvg?: string;
/** Optional color for the widget icon (hex, rgb, or CSS color name) */
iconColor?: string;
/** Optional action buttons */
actions?: WidgetAction[];
/** Optional loading status */
loadingStatus?: {
loading: boolean;
loadingMsg?: string;
};
}

Action buttons that appear in widget chrome (spotlight, canvas, dialog):

interface WidgetAction {
/** Unique identifier for this action */
id: string;
/** Tooltip/label for the action (also button text in dialog context) */
title: string;
/** SVG markup string for the icon */
iconSvg: string;
/** Whether action is currently disabled */
disabled?: boolean;
/** If true, renders as default/prominent button (dialog footer) */
primary?: boolean;
/**
* Handler called when action is clicked.
* Automatically receives widget context with element, instanceId, and full Pika context.
* @since 0.11.0 - Callback now receives WidgetCallbackContext parameter
*/
callback: (context: WidgetCallbackContext) => void | Promise<void>;
}

Context automatically provided to action button callbacks:

interface WidgetCallbackContext {
/** The web component element */
element: HTMLElement;
/** The unique instance ID assigned to this component */
instanceId: string;
/** The full Pika context with app state, chat state, and more */
context: PikaWCContext;
}
import { extractIconSvg } from 'pika-shared/util/icon-utils';
// Define an action
const refreshAction: WidgetAction = {
id: 'refresh',
title: 'Refresh Data',
iconSvg: await extractIconSvg('refresh-cw', 'lucide'),
disabled: false,
callback: async ({ element, instanceId, context }) => {
// Access the widget element
const widget = element as MyWidget;
// Show loading state
const metadata = context.chatAppState.getWidgetMetadataAPI(
'my-app',
'my-widget',
instanceId,
context.renderingContext
);
metadata.setLoadingStatus(true, 'Refreshing...');
try {
// Refresh data
await widget.refreshData();
context.appState.showToast('Data refreshed!', { type: 'success' });
} catch (error) {
context.appState.showToast('Refresh failed', { type: 'error' });
} finally {
metadata.setLoadingStatus(false);
}
}
};
// Register metadata with action
const metadata: WidgetMetadata = {
title: 'My Widget',
lucideIconName: 'chart-line',
actions: [refreshAction]
};
// Apply metadata when rendering
await context.chatAppState.renderTag('my-app.my-widget', 'canvas', { data }, metadata);

The renderTag() method now accepts optional metadata:

// Render with initial metadata
await chatAppState.renderTag(
'acme.dashboard',
'canvas',
{ userId: '123' },
{
title: 'Sales Dashboard',
lucideIconName: 'bar-chart',
actions: [
{
id: 'export',
title: 'Export Data',
iconSvg: await extractIconSvg('download', 'lucide'),
callback: async ({ context }) => {
// Export logic here
}
}
]
}
);

Metadata can be:

  • Passed initially via renderTag() (as shown above)
  • Updated dynamically via getWidgetMetadataAPI().setMetadata()
  • Applied when manually registering spotlight widgets via SpotlightWidgetDefinition.metadata

When renderTag() is called with 'canvas' or 'dialog' context for a tag that doesn't exist, the system automatically generates a minimal tag definition:

// This works even if 'acme.custom-widget' isn't registered in the database
await chatAppState.renderTag('acme.custom-widget', 'canvas', { data });

Auto-generated tags include:

  • Minimal required fields (scope, tag, status)
  • The requested rendering context enabled
  • usageMode: 'chat-app'
  • status: 'enabled'

Canvas widgets can be rendered with additional options to enable advanced UI patterns.

interface CanvasWidgetOptions {
/** Enter companion mode - compact chat pane while canvas is primary */
companionMode?: boolean;
/** Start with chat pane minimized (requires companionMode) */
chatPaneMinimized?: boolean;
/** Widget provides its own chrome (hides framework title bar) */
fullControl?: boolean;
/** Configure close behavior */
closeConfig?: CanvasCloseConfig;
}

Companion mode creates an "application-first" experience where the canvas widget is the primary focus and the chat becomes a compact assistant pane:

// Render canvas in companion mode
await chatAppState.renderTag('acme.dashboard', 'canvas', {
companionMode: true,
chatPaneMinimized: false // Start with chat visible
});

When companion mode is active:

  • Chat pane becomes compact with reduced padding
  • Spotlight widgets are hidden
  • Hero widget is hidden
  • Chat history is minimized
  • User can minimize chat to a ~20px strip

Full control mode lets the widget render its own chrome (title bar, close button):

await chatAppState.renderTag('acme.ide', 'canvas', {
fullControl: true,
closeConfig: {
confirmOnClose: true,
confirmTitle: 'Unsaved Changes',
confirmMessage: 'You have unsaved work. Close anyway?'
}
});

When fullControl is enabled:

  • Framework title bar is hidden
  • Widget is responsible for its own UI chrome
  • Use requestCanvasClose() to trigger close with confirmation

Configure how the canvas behaves when closing:

interface CanvasCloseConfig {
/** Show confirmation dialog before closing */
confirmOnClose?: boolean;
/** Title for confirmation dialog */
confirmTitle?: string;
/** Message for confirmation dialog */
confirmMessage?: string;
/** Label for confirm button */
confirmButtonLabel?: string;
/** Label for cancel button */
cancelButtonLabel?: string;
}

Using requestCanvasClose():

For fullControl widgets that need to trigger the framework's close confirmation:

// In a fullControl widget's close button handler
async function handleClose() {
const shouldClose = await context.chatAppState.requestCanvasClose();
// If shouldClose is true, framework will handle the close
// If false, user cancelled
}

Widgets can subscribe to framework events to react to state changes.

interface ChatAppEvents {
// Widget lifecycle events
widgetOpen: { tagId: string; instanceId: string; renderingContext: WidgetRenderingContextType };
widgetClose: { tagId: string; instanceId: string; renderingContext: WidgetRenderingContextType };
widgetReady: { tagId: string; instanceId: string; renderingContext: WidgetRenderingContextType }; // v0.18.0+
// Canvas events
canvasOpen: { tagId: string; instanceId?: string };
canvasClose: { tagId: string; instanceId?: string };
// Chat pane events
chatPaneMinimized: Record<string, never>;
chatPaneExpanded: Record<string, never>;
// Companion mode events
companionModeEnter: Record<string, never>;
companionModeExit: Record<string, never>;
// Hero widget events (v0.18.0+)
heroWillShow: Record<string, never>; // Before hero starts showing
heroDidShow: Record<string, never>; // After hero is shown
heroWillHide: Record<string, never>; // Before hero starts hiding
heroDidHide: Record<string, never>; // After hero is hidden
heroCollapse: Record<string, never>; // When hero is collapsed to header bar
heroExpand: Record<string, never>; // When hero is expanded from collapsed
// Spotlight events (v0.18.0+)
spotlightShow: Record<string, never>;
spotlightHide: Record<string, never>;
}

Widgets can signal when they've finished loading and are ready to receive commands. This is a best practice for widgets that load data asynchronously.

const context = await getPikaContext(this);
const { chatAppState, instanceId, tagId } = context;
console.log(`Widget ${tagId} initializing...`);
// After your widget has finished initializing
await this.loadData();
this.renderUI();
// Signal that widget is ready
chatAppState.signalWidgetReady(instanceId);

Why use widgetReady?

  • Agents or orchestrators can wait for a widget to be ready before issuing commands
  • Prevents race conditions where commands are sent before data is loaded
  • Enables sequential widget interactions (e.g., "show tab 3" after widget loads)

Listening for widget ready:

chatAppState.addEventListener('widgetReady', ({ tagId, instanceId }) => {
if (tagId === 'acme.dashboard') {
// Dashboard is ready, safe to send commands
chatAppState.sendCommandToWidget(instanceId, { action: 'showTab', tab: 3 });
}
}, myInstanceId);

The tagId property is automatically provided in the Pika context when a widget is injected. This eliminates the need to hardcode your tag ID:

const ctx = await getPikaContext(this);
// ctx.tagId contains your widget's scope.tag, e.g., 'myapp.orchestrator'
console.log(`I am: ${ctx.tagId}`);
// Useful for Intent Router handler registration - no hardcoding needed
ctx.chatAppState.registerIntentRouterHandler(
ctx.instanceId,
ctx.tagId, // Automatically matches your tag definition
handleCommand
);

Widgets can pre-fill the chat input to help users ask contextual questions. This is useful for AI assist buttons that suggest follow-up queries:

// Suggest a question - expands chat pane, fills input, and highlights it
chatAppState.suggestQuestion(
'What does this weather pattern mean for outdoor activities?'
);
// With options
chatAppState.suggestQuestion('Help me understand these errors', {
focus: true, // Focus the input (default: true)
highlight: true, // Briefly highlight the input (default: true)
expandChatPane: true // Expand if minimized in companion mode (default: true)
});

Example: AI Assist Button

const aiAssistAction: WidgetAction = {
id: 'ai-assist',
title: 'Ask AI about this',
iconSvg: await getIconSvg('sparkles', 'lucide'),
callback: async () => {
// Build contextual question based on widget state
const question = `Based on the data showing ${this.summary}, what should I do next?`;
context.chatAppState.suggestQuestion(question);
}
};

The hero widget emits detailed lifecycle events for coordinating animations and state. Since hero widgets persist when hidden (v0.18.2+), these events are essential for managing data refresh and resource usage:

// Prepare before hero shows (e.g., pause other animations)
chatAppState.addEventListener('heroWillShow', () => {
this.pauseAnimations();
}, instanceId);
// React after hero is visible - IMPORTANT: refresh data here
chatAppState.addEventListener('heroDidShow', () => {
// Hero persists when hidden, so refresh data when shown again
this.refreshData();
this.startDataSync();
}, instanceId);
// Clean up before hero hides
chatAppState.addEventListener('heroWillHide', () => {
this.saveDraft();
}, instanceId);
// React after hero is hidden - pause expensive operations
chatAppState.addEventListener('heroDidHide', () => {
// Hero is still mounted but hidden - stop unnecessary work
this.stopDataSync();
this.pausePolling();
}, instanceId);
// React to collapse/expand
chatAppState.addEventListener('heroCollapse', () => {
this.pauseExpensiveOperations();
}, instanceId);
chatAppState.addEventListener('heroExpand', () => {
this.resumeOperations();
}, instanceId);
const context = await getPikaContext(this);
const { chatAppState, instanceId } = context;
// Subscribe with instance ID for automatic cleanup
const unsubscribe = chatAppState.addEventListener('canvasOpen', (data) => {
console.log(`Canvas opened: ${data.tagId}`);
}, instanceId);
// Or manually unsubscribe later
unsubscribe();
// React to companion mode changes
chatAppState.addEventListener('companionModeEnter', () => {
// Adjust widget layout for full-width
this.classList.add('expanded');
}, instanceId);
chatAppState.addEventListener('companionModeExit', () => {
this.classList.remove('expanded');
}, instanceId);
// Track canvas lifecycle
chatAppState.addEventListener('canvasClose', ({ tagId }) => {
if (tagId === 'acme.editor') {
// Save draft when editor closes
this.saveDraft();
}
}, instanceId);
  1. Check tag is enabled: status === 'enabled'
  2. Verify usage mode and chat app configuration
  3. Check rendering context matches usage
  4. Ensure web component loaded successfully
  1. Review llmInstructionsMd clarity
  2. Check canBeGeneratedByLlm === true
  3. Verify tag is in chat app's enabled list
  4. Test with explicit examples in prompt
  1. Verify S3 bucket permissions
  2. Check S3 key path is correct
  3. Ensure CORS configured on bucket
  4. Verify encoding matches actual file