Intent System
The Intent system is a prototype. 1inch and LiFi have been migrated; stargate, wormhole, and uniswap-v4 are Phase 2. The handler contract and dispatch flow described here are stable, but the migration is not complete.
The Intent system is dappTerminal's confirm-before-execute UX pattern. On-chain commands produce a reviewable Intent object — a pre-resolved execution plan — before any signing occurs. The user inspects the plan, types confirm or cancel, and only then does the terminal execute.
What "Intent" Means Here
An Intent is not the conventional blockchain intent pattern. There are no solvers, no ERC-7683 cross-chain intents, no UniswapX-style declarative goal fulfillment, and no off-chain intent relay.
An Intent here is a pre-resolved, step-by-step execution plan held by the CLI until the user confirms. It contains:
- steps — ordered list of UI steps, each with a label, description, and status (
pending/running/complete/skipped/failed) - expectedBalanceChanges — token amounts in/out displayed in the preview card
- execute closure — the async function that runs the on-chain operations when the user confirms
The analogy is a purchase receipt you review before clicking Pay.
Where It Fits in the Layer Stack
The Intent system spans three layers. See System Layers for the full layer map.
| Layer | Role in Intent flow |
|---|---|
| Layer 1 — CLI / Terminal UI | Owns confirm/cancel UX; renders the preview card; stores pendingIntent on the tab; calls executeIntent on confirm |
| Layer 3 — Plugin Layer | IntentHandler<T> functions in handlers.ts build and return the Intent |
| Layer 2 — Core Runtime | Dispatches handler results; detects non-void returns; routes to preview or direct execution |
The CLI layer owns the UX state machine (pending → confirmed → executing). Plugin handlers are pure: they build an Intent and return it. They do not call back into CLI internals.
Handler Types
Plugin handlers live in src/plugins/*/handlers.ts. See Plugin System: Handlers for the broader handler context.
// Display-only commands (quote, price, gas preview) — always return void
CommandHandler<T> → Promise<Intent | void>
// On-chain execution commands — always return an Intent
IntentHandler<T> → Promise<Intent>
IntentHandler<T> is a stricter subtype of CommandHandler<T>. Because Promise<Intent> is assignable to Promise<Intent | void>, IntentHandlers satisfy the existing handler contract without any changes to the plugin loader or command registry.
Display-only handlers (quote, price, limit order preview) return void and continue to satisfy CommandHandler<T> — Promise<void> is assignable to Promise<Intent | void>.
Dispatch Flow
1. handler(data, ctx) → intentResult
2. if (intentResult is Intent):
a. renderIntentPreview()
→ styled preview card added to history item
b. tab.pendingIntent = { intent: intentResult, historyTimestamp }
c. async: fetch current balances
→ re-render preview with balance projections
3. User types "confirm"
→ executeIntent(pendingIntent.intent)
→ runs execute closure
→ setStepStatus callbacks animate each step in real-time
→ appendLinks adds explorer links on completion
4. User types "cancel"
→ pendingIntent cleared
→ normal prompt restored
If the handler returns void (display-only), step 2 is skipped — the CLI renders output directly and no pending intent is set.
Handler Contract
Handlers receive ExecutionContext & CLIContext. CLIContext provides:
| Property / Method | Purpose |
|---|---|
updateHistory / updateStyledHistory | Display-only output (used by void handlers) |
addHistoryLinks | Append links to a history entry |
fetchTokenBalance | Read a token balance for the connected wallet |
activeTabId | Current tab identifier |
walletAddress | Connected wallet address |
chainId | Active chain ID |
Handlers do not call back into CLI internals to trigger transaction flows. An IntentHandler expresses its entire on-chain work inside the execute closure, which receives wagmi-compatible callbacks:
interface ExecuteCallbacks {
setStepStatus(stepId: string, status: StepStatus): void
appendLinks(links: HistoryLink[]): void
walletAddress: string
}
// Inside the Intent's execute closure:
execute: async (callbacks) => {
callbacks.setStepStatus('approve', 'running')
const hash = await sendTransaction(tx)
callbacks.setStepStatus('approve', 'complete')
callbacks.appendLinks([{ label: 'View on Explorer', href: explorerUrl(hash) }])
}
Migration Status
| Plugin | Status |
|---|---|
| 1inch | ✅ Migrated to IntentHandler<T> |
| LiFi | ✅ Migrated to IntentHandler<T> |
| Stargate | 📋 Phase 2 |
| Wormhole | 📋 Phase 2 |
| Uniswap v4 | 📋 Phase 2 |
To migrate a Phase 2 plugin:
- Type the handler as
IntentHandler<T>instead ofCommandHandler<T> - Build and return an
Intent(steps,expectedBalanceChanges,executeclosure) - Replace
ctx.updateHistory(...)loading states with intent steps
No changes to the dispatch layer or runtime are required — the Promise<Intent | void> contract already accommodates it.
Relationship to Execution Engine
The Intent system directly addresses Blocker #1 from the Execution Engine hardening roadmap:
1. No handler return channel — Handler contract returns
Promise<void>, so post-handler structured chaining data is not available for the next pipeline step.
By changing the handler contract to Promise<Intent | void>, the Intent system opens a structured return channel from handler to CLI. The Intent object is the first concrete instance of the HandlerResult concept described in the Execution Engine Phase 0 plan.
The Intent system is not a full implementation of HandlerResult (it does not yet carry pipelineOutput for DSL chaining), but it resolves the blocker for the confirm-before-execute use case and establishes the pattern for the full HandlerResult contract.
Relationship to Plugin System
IntentHandler<T> slots directly into the existing plugin structure — it lives in handlers.ts alongside existing handlers. The plugin loader, fiber registration, and command registry require no changes.
src/plugins/[protocol]/
├── index.ts # Plugin metadata & initialize()
├── commands.ts # G_p command implementations
├── types.ts # Protocol-specific types
├── handlers.ts # CommandHandler<T> and IntentHandler<T> functions ← here
└── ARCHITECTURE.md
The stricter IntentHandler<T> type is enforced at the handler definition site, not at the dispatch site. This means the constraint is expressed in plugin code (where the protocol author works) rather than in shared runtime infrastructure.