@openduo/duoduo-widgets
v0.4.0
Published
Interactive web widgets that give AI agents a visual voice
Downloads
412
Readme
duoduo-widget
Interactive web widgets that give AI agents a visual voice
The Problem
Today's AI agents communicate through messaging — WhatsApp, Slack, Feishu, Telegram. Text works for conversation, but falls short when the agent needs to present non-linear information: a dashboard with live charts, a form that collects structured parameters, a simulation the user can interact with.
IM channels are inherently linear and text-first. When an agent needs to show a data table, collect a multi-field confirmation, or render a visualization, it's stuck formatting markdown in a chat bubble — losing context, interactivity, and clarity.
The Solution
duoduo-widget gives agents the ability to create durable, shareable, interactive web pages through a simple CLI. The agent generates a widget, sends a URL to the user in any channel, and the user opens it in a browser to see rich, interactive content. When the user interacts — submits a form, confirms parameters, makes a selection — the agent reads the structured result as JSON.
Agent creates widget -> User opens URL -> User interacts -> Agent reads result
(any channel) (any browser) (forms, buttons) (structured JSON)This creates richer shared context between AI and humans — going beyond the limitations of linear chat.
Gallery
Click any screenshot to try it live on aidgets.dev:
Quick Start
# Install the CLI
npm install -g @openduo/duoduo-widgetsThe default service is hosted at https://aidgets.dev — no setup needed.
# Set service URL
export WIDGET_SERVICE_URL=https://aidgets.dev
# Create a widget
duoduo-widget open --title "My Dashboard" --ttl-seconds 300
# Push HTML content (the user sees live updates via SSE)
echo '<h1>Hello World</h1>' | duoduo-widget update --wid "wid_..."
# Finalize — freezes as a permanent, shareable artifact
duoduo-widget finalize --wid "wid_..."Send the viewer_url to the user through any channel. Never share the control_url or control_token.
Incremental Patch Mode
Instead of re-sending the entire HTML on every update, agents can send targeted DOM patches — appending rows, updating numbers, changing status text — with minimal data transfer. The viewer applies patches instantly via CSS selectors, bypassing full-page morphdom diffing.
# 1. Push skeleton with id-tagged elements
echo '<div><h1>Dashboard</h1>
<span id="count">0</span> items
<tbody id="rows"></tbody>
<div id="status">Loading...</div>
</div>' | duoduo-widget update --wid "wid_..."
# 2. Stream incremental patches (viewer updates instantly)
duoduo-widget update --wid "wid_..." --patch '[
{"op":"append","selector":"#rows","html":"<tr><td>Acme</td><td>$120k</td></tr>"},
{"op":"text","selector":"#count","text":"1"},
{"op":"innerHTML","selector":"#status","html":"<strong style=\"color:green\">Done</strong>"}
]'Supported operations:
| Op | What it does | Field |
| ----------- | --------------------------- | ------ |
| append | Insert HTML as last child | html |
| prepend | Insert HTML as first child | html |
| replace | Replace the matched element | html |
| innerHTML | Set innerHTML | html |
| text | Set textContent | text |
| remove | Remove the matched element | — |
Why this matters: For data-heavy widgets (tables, dashboards, reports), patch mode reduces SSE payload from O(full page) to O(delta). In benchmarks, a 4-row dashboard update transferred 46% less data than full-HTML mode — and the gap widens linearly as content grows. Patches also skip morphdom diffing and script re-execution, making viewer updates nearly instantaneous.
CLI Reference
| Command | Purpose | Key Flags |
| ---------- | ----------------------------- | ------------------------------------------------------------------------ |
| open | Create a new widget draft | --title, --ttl-seconds, --interaction-mode, --interaction-prompt |
| update | Push HTML or patches | --wid, --html or stdin, --patch <json>, --text-fallback |
| finalize | Freeze as immutable revision | --wid |
| wait | Block until user submits | --wid, --timeout-seconds |
| get | Non-blocking submission check | --wid |
| inspect | Debug widget state | --wid |
--wid uses a local cache (~/.cache/duoduo-widget/) to resolve control URLs, keeping agent context clean.
Interactive Widgets
Collect structured input from users:
# Create with interaction mode
duoduo-widget open --title "Confirm Deploy" --ttl-seconds 300 \
--interaction-mode submit --interaction-prompt "Review and confirm"
# Push HTML with a submit button
cat <<'HTML' | duoduo-widget update --wid "wid_..."
<button onclick="window.duoduo.submit('deploy', {
env: 'production', confirmed: true
})">Deploy</button>
HTML
# Finalize, then wait for user response
duoduo-widget finalize --wid "wid_..."
duoduo-widget wait --wid "wid_..." --timeout-seconds 300Returns:
{
"submitted": true,
"event": { "action": "deploy", "payload": { "env": "production", "confirmed": true } }
}Self-Hosting
Deploy your own widget service on Cloudflare Workers. You need a Cloudflare account with Workers Paid plan (required for Durable Objects).
Step 1: Login to Cloudflare
npx wrangler loginStep 2: Create an R2 bucket
npx wrangler r2 bucket create duoduo-widgetsStep 3: Configure your account
Create service/.env with your Cloudflare Account ID:
cp service/.env.example service/.env
# Edit service/.env and fill in your Account IDCLOUDFLARE_ACCOUNT_ID=your-account-idWhere to find your Account ID: Cloudflare Dashboard → Workers & Pages → right sidebar → "Account ID".
This file is gitignored — your credentials never get committed.
Step 4: Set the token signing secret
cd service
npx wrangler secret put TOKEN_SECRET
# Enter a random string (e.g.: openssl rand -hex 32)Step 5: Deploy
cd service
npm install
npx wrangler deployYour service is now live at https://duoduo-widget.<your-subdomain>.workers.dev.
Step 6: Custom domain (optional)
If you have a domain managed by Cloudflare DNS:
- Go to Workers & Pages → your worker → Settings → Domains & Routes
- Click Add → Custom Domain
- Enter your domain (e.g.
widgets.yourdomain.com)
Cloudflare handles DNS records and SSL certificates automatically.
Step 7: Point CLI to your instance
export WIDGET_SERVICE_URL=https://widgets.yourdomain.com
# Or use the default workers.dev URL:
export WIDGET_SERVICE_URL=https://duoduo-widget.your-subdomain.workers.devConfiguration files
| File | Committed | Purpose |
| ----------------------- | --------- | ------------------------------------------------------- |
| service/wrangler.toml | Yes | Shared config (DO bindings, migrations, R2 bucket name) |
| service/.env | No | Your CLOUDFLARE_ACCOUNT_ID (gitignored) |
| service/.env.example | Yes | Template for service/.env |
| service/.dev.vars | No | Local dev secrets for wrangler dev (gitignored) |
| .env | No | CLI config: WIDGET_SERVICE_URL (gitignored) |
| .env.example | Yes | Template for .env |
Architecture
Agent -> duoduo-widget CLI -> Widget Service (Cloudflare Workers)
|
+------------+------------+
| | |
Workers Durable R2
(routing) Objects (storage)
| (state) |
| | |
+-----+------+-----+-----+
| |
Viewer Immutable
(SSE) Revisions- CLI: Node.js binary, talks to the service over HTTPS
- Workers: API routing + viewer shell serving
- Durable Objects: Draft coordination, wait/get blocking, submit idempotency
- R2: Immutable revision storage (finalized HTML persists permanently)
- Viewer Shell: Sandboxed HTML with morphdom progressive updates, incremental DOM patching, SSE streaming,
window.duoduobridge
Agent Skill
For Claude Code / AI agent integration, see skill/interactive-widget/SKILL.md. The skill teaches agents when and how to use widgets — progressive generation patterns, interaction handling, HTML component patterns, and security guidelines.
Install as a Claude Code skill:
ln -s /path/to/duoduo-widgets/skill/interactive-widget ~/.claude/skills/interactive-widgetDesign Document
For the full design rationale, architecture decisions, state machine specification, and protocol details, see docs/design/widgets.md.
