The user watches the widget live — stream content fast, one section per tool call.
bash
npm install -g @openduo/duoduo-widgets # if not installed
bash
duoduo-widget open --title "Dashboard" --ttl-seconds 300
Send links.feishu_sidebar or links.browser to user. NEVER send control_url/ control_token.
bash
cat > /tmp/w-{wid}.html << 'SKELETON'
<div style="background:#1a1a1a;color:#e0e0e0;padding:20px;font-family:system-ui;min-height:100vh;">
<h1 style="color:#fff;font-size:28px;font-weight:500;margin:0 0 4px;">Title</h1>
<p style="color:#999;font-size:14px;margin:0 0 20px;">Subtitle</p>
<!-- NEXT -->
</div>
SKELETON
cat /tmp/w-{wid}.html | duoduo-widget update --wid "wid_..."
bash
python3 - /tmp/w-{wid}.html << 'PYEOF'
import sys
f = sys.argv[1]
html = open(f).read()
section = """<div style="background:#2a2a2a;padding:16px;border-radius:8px;margin-bottom:12px;">
<h3 style="margin:0 0 8px;color:#fff;font-size:16px;font-weight:500;">Section title</h3>
<p style="margin:0;color:#999;font-size:14px;">Content — $100 safe, no escaping needed</p>
</div>
<!-- NEXT -->"""
html = html.replace('<!-- NEXT -->', section)
open(f, 'w').write(html)
PYEOF
cat /tmp/w-{wid}.html | duoduo-widget update --wid "wid_..."
Quoted heredoc 'PYEOF' — write raw HTML, no shell escaping. Only change content inside """...""".
--patch (preferred for data-heavy widgets)#After the skeleton is pushed, use --patch to update specific parts of the page without re-sending the entire HTML. This is faster, uses less bandwidth, and avoids morphdom re-rendering.
bash
duoduo-widget update --wid "wid_..." --patch '[
{"op":"append","selector":"#rows","html":"<tr><td>New item</td><td>$100</td></tr>"},
{"op":"text","selector":"#count","text":"42"},
{"op":"innerHTML","selector":"#status","html":"<strong style=\"color:#4ade80\">Done</strong>"}
]'
Patch operations:
OpWhat it doesRequiresappendInsert html as last child of selector``html``prependInsert html as first child of selector``html``replaceReplace element matching selector``html``innerHTMLSet innerHTML of selector``html``textSet textContent of selector``text``removeRemove element matching selector—
When to use patch vs full HTML:
Important: Patches update the live viewer instantly but do NOT update the stored HTML on the server. To ensure the finalized artifact includes all changes, use this pattern:
update --html (keep the temp file)--patch for live viewer speed/tmp/w-{wid}.html locallyfinalize, do one last cat /tmp/w-{wid}.html | duoduo-widget update --wid ...finalizeOr simply: pass the final complete HTML via duoduo-widget finalize --wid ... --html "..." if available.
Skeleton design for patch: Give target elements id attributes so patches can address them:
html
<tbody id="rows"></tbody>
<!-- append rows here -->
<span id="count">0</span>
<!-- update text here -->
<div id="status">Loading...</div>
<!-- update status here -->
bash
duoduo-widget finalize --wid "wid_..."
references/html_patterns.md — read it first, pick a section template, change only the data values. Never design HTML from scratch_hints in update output: no_viewers → send link; ttl_low/ ttl_expiring → finalize now; many_updates → wrap upbash
duoduo-widget open --title "Confirm" --ttl-seconds 300 \
--interaction-mode submit --interaction-prompt "Review and confirm"
Button: <button onclick="window.duoduo.submit('action', {key:'val'})" style="background:#4a9;color:#fff;border:none;padding:10px 24px;border-radius:6px;font-size:14px;cursor:pointer;">Label</button>
Read result: duoduo-widget wait --wid "wid_..." --timeout-seconds 120
cdnjs.cloudflare.com, esm.sh, cdn.jsdelivr.net, unpkg.comfetch(), XMLHttpRequest, WebSocket, eval(), new Function()Templates — read references/html_patterns.md first. Copy a template, change data values only.
CommandPurposeKey flagsopenCreate draft--title, --ttl-seconds, --interaction-mode, --interaction-prompt``updatePush HTML or patch--wid, stdin or --html, --patch <json>, --text-fallback``finalizeFreeze--wid``waitBlock for submit--wid, --timeout-seconds``getPoll status--wid
draft → finalized → awaiting_input → submitted (terminal)
Finalized artifacts are permanent. Fork: open --fork <widget_id>.
WIDGET_SERVICE_URL env var (default: https://aidgets.dev).