This document describes FRAME's UI architecture, including the UI planner dApp, widget schemas, capability introspection, and deterministic layout engine.
FRAME generates UI dynamically from intents through a deterministic pipeline:
Intent
↓
Application dApp Execution
↓
UI Planner dApp (generates widget schema)
↓
Widget Schema Canonicalization
↓
Deterministic Layout Engine
↓
Widget Rendering
The UI planner (ui.planner) is a deterministic dApp that generates widget schemas from intents and execution results.
Location: ui/dapps/ui.planner/index.js
- Widget Schema Generation: Creates widget schemas from intents
- Capability Introspection: Discovers capabilities from dApp manifests
- Deterministic Caching: Caches schemas for instant replay
- Canonical JSON Output: Returns only canonical JSON data
Triggered: After application dApp execution, before receipt building
Input:
{
intent: { action, payload, timestamp },
executionResult: { /* dApp execution result */ },
dappManifest: { /* dApp manifest */ }
}Output:
{
type: "ui.plan",
widgets: [
{
type: "card",
title: "Wallet Balance",
source: "wallet.balance",
region: "workspace",
params: {}
}
]
}The planner introspects dApp manifests to discover available data sources:
Process:
- Reads
capabilitySchemasfrom manifest - Sorts capabilities deterministically
- Infers widget types from capability metadata:
type: "payment"→tablewidgettype: "data"with timeseries →chartwidgettype: "data"with list output →listwidget- Default →
cardwidget
- Extracts widget parameters from capability output
Example Manifest:
{
"capabilitySchemas": [
{
"id": "wallet.balance",
"type": "data",
"description": "Check wallet balance",
"output": { "balance": "number" }
},
{
"id": "wallet.send",
"type": "payment",
"description": "Send frame tokens",
"output": { "message": "string" }
}
]
}Planner Output:
{
widgets: [
{
type: "card",
title: "Check wallet balance",
source: "wallet.balance",
region: "workspace",
params: {}
},
{
type: "table",
title: "Send frame tokens",
source: "wallet.send",
region: "workspace",
params: {}
}
]
}The planner caches widget schemas for instant replay:
Cache Key: ui_plan_cache:<intent_hash>:<manifest_hash>
Cache Entry:
{
widgetSchema: { /* widget schema */ },
intentAction: "wallet.balance",
cachedAt: intent.timestamp // Deterministic timestamp
}Cache Behavior:
- Cache lookup before generation
- Cache write after generation
- Uses intent timestamp (not
Date.now()) - Deterministic across replay
Widget schemas are canonical JSON structures:
{
type: "ui.plan",
widgets: [
{
type: "card" | "table" | "chart" | "grid" | "tabs" | "map" | "timeline" | "kanban" | "editor" | "panel" | "list" | "form",
title: "string",
source: "string", // Data source ID (e.g., "wallet.balance")
region: "workspace" | "sidebar" | "dock" | "statusbar",
params: {
// Widget-specific parameters
chartType: "line" | "bar" | "pie", // For chart widgets
// ... other params
}
}
]
}FRAME supports 12 widget primitive types:
- card: Simple data display card
- table: Tabular data display
- chart: Data visualization (line, bar, pie, etc.)
- grid: Grid layout for multiple items
- tabs: Tabbed interface
- map: Geographic map display
- timeline: Timeline visualization
- kanban: Kanban board
- editor: Text/code editor
- panel: Panel container
- list: List display
- form: Form input interface
Widget schemas are validated for determinism:
Checks:
- No functions or executable code
- No floating point values (only safe integers)
- No undefined values
- All keys sorted (canonicalized)
- Recursive validation of nested objects
Location: ui/runtime/engine.js → verifyWidgetSchemaDeterminism()
The layout engine computes widget placement deterministically:
Location: ui/runtime/widgets/layout.js
Layout is computed solely from widget count:
- 1 widget →
singlelayout (1 column, workspace) - 2-3 widgets →
verticallayout (1 column stack, workspace) - 4 widgets →
gridlayout (2x2 grid, workspace) - 5+ widgets →
dashboardlayout (3 column grid, workspace)
Process:
- Count widgets in schema
- Determine layout type from count
- Assign widgets to regions (sorted by source)
- Apply layout to FRAME layout manager
Determinism Guarantees:
- Same widget count → same layout type
- Same widget order → same region assignments
- Widgets sorted by source before assignment
- No system state dependencies
Widgets are assigned to regions deterministically:
Regions:
workspace: Main content areasidebar: Sidebar areadock: Dock areastatusbar: Status bar area
Assignment:
- Use
widget.regionfrom schema if specified - Otherwise, use computed layout
- Sort widgets by source before assignment
- Assign to regions in sorted order
Widgets are rendered by the widget manager:
Location: ui/system/widgetManager.js
Process:
- Read widget schema from execution result
- Create widgets from schema
- Connect data sources
- Apply layout
- Render to UI
Widget Creation:
FRAME_WIDGETS.createWidget({
id: widgetDef.id || genId(),
title: widgetDef.title,
type: widgetDef.type,
dataSource: widgetDef.source,
region: widgetDef.region,
config: widgetDef.params || {}
});Widgets connect to data sources:
Data Sources:
- dApp capabilities (e.g.,
wallet.balance) - Storage keys (e.g.,
storage:notes:recent) - System state (e.g.,
system.status)
Connection:
widget.dataSource = function() {
// Fetch data from source
return data;
};The complete UI generation flow:
// User intent
{ action: "wallet.balance", payload: {} }
↓
// Application dApp executes
{ balance: 100 }
↓
// UI planner called
{ widgets: [{ type: "card", source: "wallet.balance" }] }
↓
// Widget schema canonicalized
{ widgets: [...] } // Canonicalized
↓
// Added to execution result
execResult.widgetSchema = widgetSchema
↓
// Included in receipt hash
resultHash = hash(execResult) // Includes widget schema// Widget schema
{ widgets: [{ type: "card", source: "wallet.balance" }] }
↓
// Layout engine computes layout
{ type: "single", regions: { workspace: { widgets: [...] } } }
↓
// Layout applied
FRAME_LAYOUT.setLayout(layoutConfig)// Widgets created
FRAME_WIDGETS.createWidget({ type: "card", source: "wallet.balance" })
↓
// Data source connected
widget.dataSource = () => fetchBalance()
↓
// Widgets rendered
FRAME_WIDGETS.renderAll()FRAME guarantees identical UI during replay:
Property:
same receipt chain
→ same execution result (via resultHash verification)
→ same widget schema (included in execResult)
→ same layout (deterministic from widget count)
→ same UI
Verification:
- Widget schema included in
execResult.widgetSchema resultHash = hash(execResult)includes widget schema- Replay verifies
resultHashmatches - Planner regenerates identical schema (deterministic)
- Layout engine computes identical layout (deterministic)
The planner is fully deterministic:
- Sorted key iteration (
Object.keys().sort()) - Canonical JSON hashing
- Intent timestamp (not
Date.now()) - Safe integer arithmetic only
Date.now()(uses intent timestamp)Math.random()(not used)- Floating point arithmetic (only safe integers)
- Unordered object iteration (must sort keys)
Planner determinism verified through:
- Schema canonicalization
- Cache key determinism
- Replay consistency
FRAME.verifyDeterminism()utility
Widget schemas are integrated into receipts:
Process:
- Planner generates widget schema
- Schema canonicalized
- Schema added to
execResult.widgetSchema resultHash = hash(execResult)includes schema- Receipt includes
resultHash - Replay verifies
resultHashmatches
Receipt Field:
{
resultHash: "sha256(canonicalize(execResult))" // Includes widgetSchema
}This ensures that widget schemas are cryptographically committed in receipts, enabling verifiable UI reconstruction during replay.