Adom Viewer
The Adom Viewer lets you send visual content from the Docker container to the Adom web app front-end so the user can see it in their browser. Content appears in an iframe panel with tabs, dark theme, and auto-reconnecting WebSocket.
What the user asked to display
$ARGUMENTS
Rule: Always Reload AV Yourself
After restarting the AV server or making any change that requires a browser refresh, you MUST reload AV yourself using av_reload. Never ask the user to refresh — that's busywork you handle for them. This is non-negotiable.
- Call
av_reload(uses mgmt relay on port 8772, works even during server restart) - If it reports 0 viewers connected, wait 3 seconds and try once more
- Only if all retries fail, say: "I tried to reload AV for you automatically, but your viewer tab isn't connected right now. As a last resort, could you refresh the AV panel?"
Rule: Always Open AV Panel If Not Connected
Before displaying content, check if any AV viewer is connected (e.g. via av_status). If zero viewers are connected, the AV panel is not open — open it yourself using the workspace-control API. See adom-workspace-control.md for the full workspace API reference (environment setup, auth, layout structure, all endpoints).
Target layout: VS Code on the left half, Adom Viewer on the right half (50/50 horizontal split). Always split the pane rather than adding a tab to the existing leaf — the user should see both VS Code and AV side by side.
- Read the API key:
API_KEY=$(cat /var/run/adom/api-key) - Auto-discover owner/repo by extracting the slug from
$VSCODE_PROXY_URI(everything after the last-, before.adom.cloud) and calling Carbon:
Legacy containers: If the Carbon API returnsSLUG=$(echo "$VSCODE_PROXY_URI" | sed 's|.*-\([^.]*\)\.adom\.cloud.*|\1|') CONTAINER_INFO=$(curl -s -H "X-Api-Key: $API_KEY" "https://carbon.adom.inc/containers/$SLUG") OWNER=$(echo "$CONTAINER_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['repository']['owner']['name'])") REPO=$(echo "$CONTAINER_INFO" | python3 -c "import sys,json; print(json.load(sys.stdin)['repository']['name'])"){"error":"CONTAINER_NOT_FOUND"}, this is likely a legacy container that predates the Carbon registry. In that case, ask the user for their owner and repo name. As a last resort, you can try to parse$VSCODE_PROXY_URI(format{owner}-{repo}-{slug}.adom.cloud), but this is unreliable if the repo name contains hyphens. - GET the current layout to find the leaf node ID containing VS Code
- Split the VS Code pane horizontally to create a new pane on the right with a Web View tab:
This returnscurl -s -X POST -H "X-Api-Key: $API_KEY" -H "Content-Type: application/json" \ -d '{"panelId":"<vscode-leaf-id>","direction":"horizontal","position":"after","ratio":0.5,"panelType":"adom/a1b2c3d4-0031-4000-a000-000000000031","displayName":"Adom Viewer","displayIcon":"mdi:eye"}' \ "$BASE/splits"{ "panelId": "<new-leaf-id>", "tabId": "<new-tab-id>" }. - Navigate the new Web View to the AV URL:
curl -s -X PATCH -H "X-Api-Key: $API_KEY" -H "Content-Type: application/json" \ -d '{"panelId":"<new-leaf-id>","action":"navigate","url":"https://<av-url>.adom.cloud/"}' \ "https://hydrogen.adom.inc/api/panels/webview/$OWNER/$REPO" - Wait 3 seconds for the WebSocket to connect, then proceed with
av_display
For a specific AV instance, append ?instance=<id> to the URL and use the matching displayName/displayIcon from INSTANCE_PRESETS.
Never tell the user "please open the AV panel" — open it yourself.
Supported content types
| Type | Extensions / format | Notes |
|---|---|---|
| SVG | .svg, raw SVG markup |
Vector graphics, diagrams, schematics |
| Images | .png, .jpg, .jpeg, .gif, .webp |
Raster images |
| HTML | .html, .htm |
Interactive HTML with <script> tags runs in a sandboxed iframe |
| Markdown | .md, .markdown |
Rendered client-side in the viewer |
| iframe_url | URL string | Embeds a live web app in a real iframe (not sandboxed srcdoc) |
How to display content
Option A: Display an existing file
Use the av_display_file MCP tool:
mcp__adom-viewer__av_display_file({ file_path: '/absolute/path/to/file.svg', title: 'My Diagram' })
This auto-detects the content type from the file extension.
Option B: Display generated content
Use the av_display MCP tool:
mcp__adom-viewer__av_display({
content_type: 'html_interactive', // or 'svg', 'image', 'html', 'markdown'
content: '<html>...</html>',
title: 'My Visualization'
})
For images, provide a data URI: data:image/png;base64,...
Option C: Internal HTTP API fallback
If MCP tools are not available, POST directly to the Adom Viewer internal API:
const http = require('http');
const crypto = require('crypto');
const payload = JSON.stringify({
action: 'display',
content: {
contentType: 'svg', // 'svg' | 'image' | 'html' | 'html_interactive' | 'markdown'
content: '<svg>...</svg>',
title: 'My Diagram',
id: crypto.randomUUID()
}
});
const req = http.request({
hostname: '127.0.0.1',
port: 8771,
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => console.log(res.statusCode, data));
});
req.write(payload);
req.end();
Embedding Live Web Apps (iframe_url)
Use iframe_url to embed a running web server (e.g., a review dashboard, admin panel, or dev tool) as an interactive iframe in AV. Unlike html_interactive (which uses srcdoc/blob), iframe_url sets the iframe's src to a real URL, so the embedded app can make network requests back to its own server.
# Push a live web app to AV
curl -s http://127.0.0.1:8771 -X POST \
-H "Content-Type: application/json" \
-d '{"action":"display","content":{"contentType":"iframe_url","content":"/proxy/3041/","title":"Review Server","id":"review","source":"my-skill"}}'
CRITICAL: Proxy-Safe URLs
Web apps embedded via iframe_url are loaded through the coder reverse proxy (e.g., /proxy/3041/). All URLs in the embedded page MUST be relative, never absolute.
| Pattern | Result |
|---|---|
fetch('/api/status') |
BREAKS — resolves to coder root, not the app |
fetch('api/status') |
WORKS — resolves relative to /proxy/3041/ |
This applies to all URL types: fetch(), <a href>, <form action>, <img src>, WebSocket URLs, etc. When building or reviewing any web server intended for AV embedding, audit all URLs for this.
Replacing Tabs
To update an embedded app (e.g., after restarting its server), close the old tab first:
# Close old tab, then push fresh one
curl -s http://127.0.0.1:8771 -X POST -H 'Content-Type: application/json' \
-d '{"action":"close_tabs","title":"Review Server","source":"my-skill"}'
curl -s http://127.0.0.1:8771 -X POST -H 'Content-Type: application/json' \
-d '{"action":"display","content":{"contentType":"iframe_url","content":"/proxy/3041/","title":"Review Server","id":"review","source":"my-skill"}}'
Built-in interactive views
The viewer dropdown menu includes built-in interactive views for several skills. These are not MCP tools — they're part of the viewer UI itself.
| View | Description |
|---|---|
| JLCPCB Search | Interactive component search with rich cards showing images, pricing, stock, attributes. Search directly from the AV search bar, or results auto-appear when Claude uses jlcpcb_search/jlcpcb_category_search/jlcpcb_list from any channel. Click a component image to open a lightbox with front/back/blank views. Links to JLCPCB parts page (EDA symbol/footprint/3D), LCSC product page, and datasheet. |
| Google Chat | Architecture overview and capabilities of the Kel Google Chat integration. |
| Service Dashboards | Per-service status pages accessible from the "Standalone Services" and "Local Services" dropdown groups. Each dashboard shows the service's full public URL (click to copy), live health check with details, links to repo/editor/health endpoint, MCP server name, and container info. Services are registered in the SERVICE_REGISTRY object in index.html. |
When adding a new skill to the gallia repo, always add a matching menu item to the Adom Viewer dropdown so users can discover it.
Tab Groups and Tab Management
What are tab groups?
Tab groups let you bundle related tabs (e.g., PCB + Schematic + 3D views of the same component) under a shared label with a single close button. The viewer renders the group name as a blue-bordered label in the tab bar.
Creating tab groups with av_display
Pass the group and source parameters:
av_display(
content_type: "svg",
content: "<svg>...</svg>",
title: "PCB",
group: "connector:1710000000",
source: "tscircuit"
)
av_display(
content_type: "svg",
content: "<svg>...</svg>",
title: "Schematic",
group: "connector:1710000000",
source: "tscircuit"
)
group— Tabs with the same group value are visually bundled. Use"name:timestamp"format for unique groups (e.g.,"JST_PH_K_1x02:1710000000"). The part before:becomes the displayed group label.source— Identifies who created the tab (e.g.,"tscircuit","sym-creator"). Used byav_close_tabsfor filtering. Defaults to"av_display"if omitted.
Closing tabs
av_close_group — Remove all tabs in a named group:
av_close_group(group: "connector")
Matches groups with the given name OR name:* prefix (e.g., "connector" removes "connector:1710000000").
av_close_tabs — Remove tabs matching ALL provided filters (AND logic):
av_close_tabs(title: "PCB", source: "tscircuit")
Provide at least one of: id, title, source. All provided filters must match for a tab to be removed.
Replace-before-push pattern
When updating content, close old tabs first to avoid duplicates:
# Close previous group, then push new tabs
av_close_group(group: "mycomponent")
av_display(content_type: "svg", content: "...", title: "PCB", group: "mycomponent:1710000001", source: "my-skill")
av_display(content_type: "svg", content: "...", title: "Schematic", group: "mycomponent:1710000001", source: "my-skill")
For single-tab replacement:
av_close_tabs(title: "Preview", source: "my-skill")
av_display(content_type: "svg", content: "...", title: "Preview", source: "my-skill")
Other MCP tools
| Tool | Purpose |
|---|---|
av_close_group |
Close all tabs in a named group (matches name or name:* prefix) |
av_close_tabs |
Close tabs matching ALL provided filters: id, title, source (AND logic) |
av_switch_tab |
Switch to a specific tab by index |
av_screenshot_paste |
Open clipboard screenshot paste utility in the viewer |
av_clear |
Clear all displayed content from the viewer |
av_status |
Check if the viewer is connected |
av_reload |
Force browser to reload the AV page (survives server restarts) |
av_capture |
Capture a screenshot of just the AV panel content |
av_tab_capture |
Capture the full browser tab via Screen Capture API (captures everything including nested iframes) |
av_set_camera |
Set the 3D camera position (alpha, beta, radius, target) |
av_set_view |
Set a named camera preset (front, back, top, bottom, etc.) |
av_set_bottom_light |
Toggle/set the bottom light for inspecting underside features |
av_stop_tour |
Stop the cinematic camera tour, freeze camera |
Management relay (port 8772)
A separate lightweight server (mgmt-server.js) runs on port 8772, independent of the main AV server (8770/8771). The browser connects to BOTH WebSockets. This solves two problems:
Forced reload after server restart — when you restart the main AV server (for HTML/JS changes), the main WS breaks. The mgmt relay stays alive, so
av_reloadcan tell the browser to reload and pick up the new code. No more manual reloading.AI-initiated screenshot capture —
av_captureasks the browser to screenshot whatever it's showing and returns the image. Use this to see what the user sees without asking them to screenshot manually. The capture uses a 5-strategy fallback powered by html2canvas: (1) postMessage for 3D/canvas iframes, (1b) html2canvas on cloned iframe DOM forhtml_interactivewidgets, (2) SVG serialization, (3)<img>capture, (4) html2canvas on parent DOM. See theav-creatorandadom-screenshotskills for full details.
Tab capture (Screen Capture API)
av_tab_capture captures the entire browser tab as a pixel-perfect screenshot using the browser's Screen Capture API (getDisplayMedia). This is the same API used by video conferencing apps.
When to use av_tab_capture vs av_capture:
av_capture— captures just the AV panel content. Fast, no setup. Works on all content types includinghtml_interactiveiframes (via html2canvas). Fails on multi-pane layouts (LibView 3-pane) where content spans nested iframes.av_tab_capture— captures the full browser tab (IDE + editor + AV + everything). Works on all content including nested iframes. Requires one-time user setup.
Setup: The user must open the capture companion tab once per session:
- Click the camera button (next to forward/back) in AV → opens the capture tab
- Click "Share This Tab" → select the IDE tab in the browser dialog
- The capture stream persists even when AV reloads — the companion tab stays connected
How it works: The companion tab holds a live getDisplayMedia video stream of the shared tab. When av_tab_capture is called (or the user clicks the AV camera button), the server relays a request to the companion tab, which grabs a single frame from the video stream and sends it back. The stream has near-zero CPU cost when idle — frames are only decoded on demand.
Screenshots are saved to project-content/screenshots/av/.
Usage pattern after editing AV code:
1. Edit viewer HTML/JS
2. Restart AV server: pkill -f 'node.*viewer/server.js' && node ~/gallia/viewer/server.js &
3. Call av_reload (via MCP) → browser reloads with new code
4. Push content as usual (av_display, show_instapcb, etc.)
5. Call av_capture to verify the result visually
Direct API (curl):
# Force reload
curl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' -d '{"action":"reload"}'
# Capture screenshot (returns base64 PNG + saves to screenshots dir)
curl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' -d '{"action":"screenshot"}'
# Broadcast arbitrary message to all viewers (forwarded via WebSocket)
curl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \
-d '{"action":"broadcast","message":{"type":"stop_tour"}}'
# Set camera angle and enable FR4 board (for thumbnail capture)
curl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \
-d '{"action":"broadcast","message":{"type":"set_camera","alpha":3.3,"beta":0.8,"radius":1.3}}'
curl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \
-d '{"action":"broadcast","message":{"type":"set_fr4","visible":true}}'
# Health check
curl http://127.0.0.1:8772/health
The broadcast action sends any JSON message to all connected viewers via WebSocket. The viewer's index.html forwards recognized 3D control messages (stop_tour, start_tour, set_fr4, toggle_fr4, set_camera, show_origin, set_view, toggle_bottom_light, set_bottom_light, toggle_pads, set_pads) to the active 3D iframe — routing to the library-review iframe when in LibView mode, or directly to the standalone 3D iframe otherwise. This enables programmatic thumbnail capture workflows (e.g., for wiki 3D component pages).
Note: Prefer the dedicated MCP tools (av_set_camera, av_set_view, av_set_bottom_light, av_stop_tour) over raw broadcast — they provide better error handling and cleaner API.
3D debug/camera commands:
# Show XYZ origin axes (red=X, green=Y, blue=Z)
curl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \
-d '{"action":"broadcast","message":{"type":"show_origin","visible":true}}'
# Set camera to a named view (front, back, left, right, top, bottom, isometric)
curl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \
-d '{"action":"broadcast","message":{"type":"set_view","view":"front"}}'
The mgmt-server is started automatically by install.mjs and should rarely need restarting.
Screenshot paste
The av_screenshot_paste tool opens an interactive paste utility in the viewer. The user can paste screenshots from their clipboard (Ctrl+V / Cmd+V) and they are automatically saved as PNG files to project-content/screenshots/ inside the Docker container. This is the fastest way to get screenshots from the user's browser into the container — one step instead of saving to a file and drag-dropping.
mcp__adom-viewer__av_screenshot_paste({ title: 'Paste Screenshots' })
Viewer panel setup
The Adom Viewer page is displayed inside a Web View panel in the Adom app. If av_status reports 0 connected viewers, the user needs to set this up:
- Ensure the AV port (8770) has a DNS mapping via
adom-cli carbon containers port-add. This gives it a subdomain likehttps://<av-url>.adom.cloud/. - In the Adom app, add a Web View panel and enter the AV URL:
https://<av-url>.adom.cloud/ - The panel persists across Adom app reloads — this only needs to be done once.
AV Instances — Multiple Independent Viewer Panels
A single AV server (port 8770) supports unlimited independent AV panels. Each panel is an AV instance with its own tabs, nav history, dropdown state, and content — no cross-contamination.
How it works
Each AV panel gets a unique URL via the ?instance=<id> query parameter:
/proxy/8770/?instance=sym → Symbols panel
/proxy/8770/?instance=fp → Footprints panel
/proxy/8770/?instance=3d → 3D Models panel
/proxy/8770/?instance=sch → Schematics panel
/proxy/8770/ → Default (main) panel
The server routes content to specific instances — when you call an MCP tool with instance: "sym", only the Symbols panel receives it.
Instance presets
Known instance IDs auto-configure panel name and default view:
| Instance ID | Panel Name | Default View |
|---|---|---|
sym |
Symbols | SymView |
fp |
Footprints | FpView |
fp3d |
3D Pads | Fp3dView |
3d |
3D Models | 3dView |
sch |
Schematics | SchView |
lib |
Libraries | LibView |
search |
Search | JlcSearch |
Custom instance IDs work too — pass ?instance=myid&name=My Panel for a custom name.
Using instances with MCP tools
All AV MCP tools accept an optional instance parameter:
mcp__adom-viewer__av_display({ html: "...", instance: "sym" }) → sends to Symbols panel only
mcp__adom-viewer__av_display({ html: "..." }) → sends to all panels
AV2 (DEPRECATED)
AV2 (separate ports 8790/8791/8792) is deprecated. Use AV instances instead — just open another Web View panel with ?instance=<id>. The adom-viewer-2 MCP server has been removed.
Adom Theme — styling guide for widgets
All Adom Viewer widgets MUST use the Adom theme for consistent styling that matches the Adom app interface. The theme is defined in ~/gallia/viewer/adom-theme.js.
Using the theme in widget generators (JS modules)
import { THEME, themeStyleTag, toolbarCSS, tooltipCSS, infoPanelCSS } from './adom-theme.js';
const html = `<!DOCTYPE html>
<html><head>
${themeStyleTag()} <!-- injects CSS vars + body reset -->
<style>
${toolbarCSS()} <!-- standard toolbar -->
${tooltipCSS()} <!-- hover tooltips -->
${infoPanelCSS()} <!-- bottom info panel -->
</style>
</head>
<body>...</body></html>`;
Or reference tokens directly in template literals:
`background: ${THEME.bg}; color: ${THEME.text}; border: 1px solid ${THEME.border};`
Color tokens (CRITICAL — always use these, never invent new colors)
| Token | Value | Usage |
|---|---|---|
bg |
#0d1117 |
Page / deepest background |
bgSurface |
#161b22 |
Toolbars, cards, panels |
bgElevated |
#1c2128 |
Hover states, elevated cards |
bgOverlay |
#21262d |
Tooltips, modals |
border |
#30363d |
Standard borders |
borderMuted |
#21262d |
Subtle/inner borders |
text |
#e6edf3 |
Primary text |
textSecondary |
#8b949e |
Labels, secondary text |
textMuted |
#484f58 |
Hints, disabled, placeholders |
accent |
#00b8b0 |
Primary teal accent |
accentBright |
#00e6dc |
Hover/focus highlight |
accentMuted |
rgba(0,184,176,0.12) |
Tinted backgrounds |
success |
#3fb950 |
Success states |
warning |
#d29922 |
Warning states |
danger |
#f85149 |
Error states |
Rules
- Never use the old palette (
#1a1a2e,#16213e,#0f3460,#2a2a40,#e0e0e0,#00b8b1,#e04040,#f0a030,#4caf50). These are deprecated. - All widget HTML must set its own background to
#0d1117(or usethemeStyleTag()). Widgets render in sandboxed iframes — they cannot inherit styles from the parent viewer. - Use the font stack:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif(available asTHEME.fontStack). - Semantic colors only for status: green=success, amber=warning, red=error. Never use red/green for decorative purposes.
- Border radius: 4px (small), 6px (medium), 8px (large) — available as
THEME.radiusSm/Md/Lg.
3D Model Display
Use the av_3d_display MCP tool to display GLB files in the viewer's built-in Babylon.js 3D view:
mcp__adom-viewer__av_3d_display({
glb_path: '/path/to/model.glb',
body_size: { x: 4.4, y: 5.0, z: 1.2 },
part_name: 'PART_NAME',
manufacturer: 'Manufacturer',
package_type: 'TSSOP-14',
pad_count: 14,
title: 'PART 3D Model'
})
3D content appears as a separate tab alongside HTML tabs. The viewer includes orbit camera, ground plane, skybox, and laser-etched chip markings.
Important: The 3D tab only appears when av_3d_display is called — av_display_file with a .glb path does NOT activate the 3D viewer. Always use av_3d_display for GLB files.
Synchronous load + immediate capture
av_3d_display blocks until the model is fully loaded and rendered (up to 15s timeout). This means you can call av_capture immediately after — no sleep needed:
av_3d_display({ glb_path: '/tmp/model.glb', skip_tour: true })
av_capture() // model is already loaded — screenshot is immediate
3D camera and lighting controls
After loading a model, use these tools to control the view:
av_stop_tour() // freeze the cinematic tour
av_set_camera({ beta: 2.8, alpha: 0.5 }) // look from below
av_set_bottom_light({ enabled: true }) // illuminate underside
av_capture() // screenshot
av_set_view({ view: 'isometric' }) // named preset
av_capture() // another screenshot
Camera angles (Babylon.js ArcRotateCamera):
alpha= azimuth (orbit around vertical). 0 = front, π/2 = right sidebeta= elevation. 0 = top-down, π/2 = eye-level, π = bottom-upradius= zoom multiplier on current distanceabsolute_radius= set exact distance
Bottom light: Illuminates the underside of components. Useful for inspecting board pads, vias, heatsink copper, and under-chip features that are normally in shadow.
Widget Design Guidelines
When generating interactive HTML widgets for the viewer, follow these rules:
Hover Highlight Rule (CRITICAL)
When any element has a tooltip, it MUST visually highlight on hover so the user knows what the tooltip refers to. Add a visible change on mouseenter (e.g., brighter border, glow, opacity change) and revert on mouseleave. Never show a tooltip on an element that doesn't change appearance on hover.
Minimum Font Sizes
Bottom panels and info bars must be readable at a glance:
- Info values / legend text: 13px minimum
- Source / provenance text: 12px minimum
- Labels / secondary text: 11px minimum
- Never use 9-10px for any user-facing text in panels
Explain Unusual Features
When a widget shows something unusual or non-obvious (e.g., oval through-hole pads, overlapping pads), add a hover tooltip explaining why. Users who aren't familiar with the design pattern should be able to learn from the viewer.
General Guidelines
- Existing files: If the user provides a file path, use
av_display_filedirectly. - Generated content: If the user asks you to create something (a chart, diagram, dashboard, etc.), generate it as HTML or SVG, write it to a temp file, then display it.
- Interactive HTML: Use
html_interactivecontent type when the content includes<script>tags or needs JavaScript execution. Usehtmlfor static HTML. - Styling: The viewer has a dark background (
#0d1117). All generated HTML widgets MUST use the Adom theme colors fromviewer/adom-theme.js. Import and useTHEMEconstants orthemeStyleTag()— never hardcode colors. - Large content: Files over 5MB may be slow to render. Warn the user if content is very large.
- No viewer connected: If 0 viewers are connected, content is queued and will appear when the user opens the viewer panel. Guide the user through the panel setup steps above.
- After editing viewer HTML/JS and restarting the AV server: Always call
av_reloadto refresh the user's browser panel. Never tell the user to refresh manually — use the tool.