{
  "schema_version": 1,
  "type": "skill",
  "slug": "adom-viewer",
  "title": "Adom Viewer",
  "brief": "Display visual content in the Adom Viewer.",
  "version": "1.0.0",
  "tags": [],
  "license": "MIT",
  "source_path": "SKILL.md",
  "readme": "# Adom Viewer\n\nThe 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.\n\n## What the user asked to display\n\n$ARGUMENTS\n\n## Rule: Always Reload AV Yourself\n\n**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.\n\n1. Call `av_reload` (uses mgmt relay on port 8772, works even during server restart)\n2. If it reports 0 viewers connected, wait 3 seconds and try once more\n3. 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?\"*\n\n## Rule: Always Open AV Panel If Not Connected\n\nBefore 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).\n\n**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.\n\n1. Read the API key: `API_KEY=$(cat /var/run/adom/api-key)`\n2. Auto-discover owner/repo by extracting the slug from `$VSCODE_PROXY_URI` (everything after the last `-`, before `.adom.cloud`) and calling Carbon:\n   ```bash\n   SLUG=$(echo \"$VSCODE_PROXY_URI\" | sed 's|.*-\\([^.]*\\)\\.adom\\.cloud.*|\\1|')\n   CONTAINER_INFO=$(curl -s -H \"X-Api-Key: $API_KEY\" \"https://carbon.adom.inc/containers/$SLUG\")\n   OWNER=$(echo \"$CONTAINER_INFO\" | python3 -c \"import sys,json; print(json.load(sys.stdin)['repository']['owner']['name'])\")\n   REPO=$(echo \"$CONTAINER_INFO\" | python3 -c \"import sys,json; print(json.load(sys.stdin)['repository']['name'])\")\n   ```\n   **Legacy containers:** If the Carbon API returns `{\"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.\n3. GET the current layout to find the leaf node ID containing VS Code\n4. **Split the VS Code pane horizontally** to create a new pane on the right with a Web View tab:\n   ```bash\n   curl -s -X POST -H \"X-Api-Key: $API_KEY\" -H \"Content-Type: application/json\" \\\n     -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\"}' \\\n     \"$BASE/splits\"\n   ```\n   This returns `{ \"panelId\": \"<new-leaf-id>\", \"tabId\": \"<new-tab-id>\" }`.\n5. Navigate the new Web View to the AV URL:\n   ```bash\n   curl -s -X PATCH -H \"X-Api-Key: $API_KEY\" -H \"Content-Type: application/json\" \\\n     -d '{\"panelId\":\"<new-leaf-id>\",\"action\":\"navigate\",\"url\":\"https://<av-url>.adom.cloud/\"}' \\\n     \"https://hydrogen.adom.inc/api/panels/webview/$OWNER/$REPO\"\n   ```\n6. Wait 3 seconds for the WebSocket to connect, then proceed with `av_display`\n\nFor a specific AV instance, append `?instance=<id>` to the URL and use the matching `displayName`/`displayIcon` from INSTANCE_PRESETS.\n\n**Never tell the user \"please open the AV panel\"** — open it yourself.\n\n## Supported content types\n\n| Type | Extensions / format | Notes |\n|------|-------------------|-------|\n| SVG | `.svg`, raw SVG markup | Vector graphics, diagrams, schematics |\n| Images | `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp` | Raster images |\n| HTML | `.html`, `.htm` | Interactive HTML with `<script>` tags runs in a sandboxed iframe |\n| Markdown | `.md`, `.markdown` | Rendered client-side in the viewer |\n| iframe_url | URL string | Embeds a live web app in a real iframe (not sandboxed srcdoc) |\n\n## How to display content\n\n### Option A: Display an existing file\n\nUse the `av_display_file` MCP tool:\n\n```\nmcp__adom-viewer__av_display_file({ file_path: '/absolute/path/to/file.svg', title: 'My Diagram' })\n```\n\nThis auto-detects the content type from the file extension.\n\n### Option B: Display generated content\n\nUse the `av_display` MCP tool:\n\n```\nmcp__adom-viewer__av_display({\n  content_type: 'html_interactive',  // or 'svg', 'image', 'html', 'markdown'\n  content: '<html>...</html>',\n  title: 'My Visualization'\n})\n```\n\nFor images, provide a data URI: `data:image/png;base64,...`\n\n### Option C: Internal HTTP API fallback\n\nIf MCP tools are not available, POST directly to the Adom Viewer internal API:\n\n```javascript\nconst http = require('http');\nconst crypto = require('crypto');\n\nconst payload = JSON.stringify({\n  action: 'display',\n  content: {\n    contentType: 'svg',  // 'svg' | 'image' | 'html' | 'html_interactive' | 'markdown'\n    content: '<svg>...</svg>',\n    title: 'My Diagram',\n    id: crypto.randomUUID()\n  }\n});\n\nconst req = http.request({\n  hostname: '127.0.0.1',\n  port: 8771,\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' }\n}, (res) => {\n  let data = '';\n  res.on('data', c => data += c);\n  res.on('end', () => console.log(res.statusCode, data));\n});\nreq.write(payload);\nreq.end();\n```\n\n## Embedding Live Web Apps (`iframe_url`)\n\nUse `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.\n\n```bash\n# Push a live web app to AV\ncurl -s http://127.0.0.1:8771 -X POST \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"action\":\"display\",\"content\":{\"contentType\":\"iframe_url\",\"content\":\"/proxy/3041/\",\"title\":\"Review Server\",\"id\":\"review\",\"source\":\"my-skill\"}}'\n```\n\n### CRITICAL: Proxy-Safe URLs\n\nWeb 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.**\n\n| Pattern | Result |\n|---------|--------|\n| `fetch('/api/status')` | **BREAKS** — resolves to coder root, not the app |\n| `fetch('api/status')` | **WORKS** — resolves relative to `/proxy/3041/` |\n\nThis 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.\n\n### Replacing Tabs\n\nTo update an embedded app (e.g., after restarting its server), close the old tab first:\n\n```bash\n# Close old tab, then push fresh one\ncurl -s http://127.0.0.1:8771 -X POST -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"close_tabs\",\"title\":\"Review Server\",\"source\":\"my-skill\"}'\ncurl -s http://127.0.0.1:8771 -X POST -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"display\",\"content\":{\"contentType\":\"iframe_url\",\"content\":\"/proxy/3041/\",\"title\":\"Review Server\",\"id\":\"review\",\"source\":\"my-skill\"}}'\n```\n\n## Built-in interactive views\n\nThe viewer dropdown menu includes built-in interactive views for several skills. These are not MCP tools — they're part of the viewer UI itself.\n\n| View | Description |\n|------|-------------|\n| **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. |\n| **Google Chat** | Architecture overview and capabilities of the Kel Google Chat integration. |\n| **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`. |\n\nWhen adding a new skill to the gallia repo, always add a matching menu item to the Adom Viewer dropdown so users can discover it.\n\n## Tab Groups and Tab Management\n\n### What are tab groups?\n\nTab 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.\n\n### Creating tab groups with `av_display`\n\nPass the `group` and `source` parameters:\n\n```\nav_display(\n  content_type: \"svg\",\n  content: \"<svg>...</svg>\",\n  title: \"PCB\",\n  group: \"connector:1710000000\",\n  source: \"tscircuit\"\n)\nav_display(\n  content_type: \"svg\",\n  content: \"<svg>...</svg>\",\n  title: \"Schematic\",\n  group: \"connector:1710000000\",\n  source: \"tscircuit\"\n)\n```\n\n- **`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.\n- **`source`** — Identifies who created the tab (e.g., `\"tscircuit\"`, `\"sym-creator\"`). Used by `av_close_tabs` for filtering. Defaults to `\"av_display\"` if omitted.\n\n### Closing tabs\n\n**`av_close_group`** — Remove all tabs in a named group:\n```\nav_close_group(group: \"connector\")\n```\nMatches groups with the given name OR `name:*` prefix (e.g., `\"connector\"` removes `\"connector:1710000000\"`).\n\n**`av_close_tabs`** — Remove tabs matching ALL provided filters (AND logic):\n```\nav_close_tabs(title: \"PCB\", source: \"tscircuit\")\n```\nProvide at least one of: `id`, `title`, `source`. All provided filters must match for a tab to be removed.\n\n### Replace-before-push pattern\n\nWhen updating content, close old tabs first to avoid duplicates:\n\n```\n# Close previous group, then push new tabs\nav_close_group(group: \"mycomponent\")\nav_display(content_type: \"svg\", content: \"...\", title: \"PCB\", group: \"mycomponent:1710000001\", source: \"my-skill\")\nav_display(content_type: \"svg\", content: \"...\", title: \"Schematic\", group: \"mycomponent:1710000001\", source: \"my-skill\")\n```\n\nFor single-tab replacement:\n```\nav_close_tabs(title: \"Preview\", source: \"my-skill\")\nav_display(content_type: \"svg\", content: \"...\", title: \"Preview\", source: \"my-skill\")\n```\n\n## Other MCP tools\n\n| Tool | Purpose |\n|------|---------|\n| `av_close_group` | Close all tabs in a named group (matches `name` or `name:*` prefix) |\n| `av_close_tabs` | Close tabs matching ALL provided filters: `id`, `title`, `source` (AND logic) |\n| `av_switch_tab` | Switch to a specific tab by index |\n| `av_screenshot_paste` | Open clipboard screenshot paste utility in the viewer |\n| `av_clear` | Clear all displayed content from the viewer |\n| `av_status` | Check if the viewer is connected |\n| `av_reload` | Force browser to reload the AV page (survives server restarts) |\n| `av_capture` | Capture a screenshot of just the AV panel content |\n| `av_tab_capture` | Capture the full browser tab via Screen Capture API (captures everything including nested iframes) |\n| `av_set_camera` | Set the 3D camera position (alpha, beta, radius, target) |\n| `av_set_view` | Set a named camera preset (front, back, top, bottom, etc.) |\n| `av_set_bottom_light` | Toggle/set the bottom light for inspecting underside features |\n| `av_stop_tour` | Stop the cinematic camera tour, freeze camera |\n\n## Management relay (port 8772)\n\nA 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:\n\n1. **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_reload` can tell the browser to reload and pick up the new code. No more manual reloading.\n\n2. **AI-initiated screenshot capture** — `av_capture` asks 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 for `html_interactive` widgets, (2) SVG serialization, (3) `<img>` capture, (4) html2canvas on parent DOM. See the `av-creator` and `adom-screenshot` skills for full details.\n\n## Tab capture (Screen Capture API)\n\n`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.\n\n**When to use `av_tab_capture` vs `av_capture`:**\n- `av_capture` — captures just the AV panel content. Fast, no setup. Works on all content types including `html_interactive` iframes (via html2canvas). Fails on multi-pane layouts (LibView 3-pane) where content spans nested iframes.\n- `av_tab_capture` — captures the full browser tab (IDE + editor + AV + everything). Works on all content including nested iframes. Requires one-time user setup.\n\n**Setup:** The user must open the capture companion tab once per session:\n1. Click the **camera button** (next to forward/back) in AV → opens the capture tab\n2. Click **\"Share This Tab\"** → select the IDE tab in the browser dialog\n3. The capture stream persists even when AV reloads — the companion tab stays connected\n\n**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.\n\nScreenshots are saved to `project-content/screenshots/av/`.\n\n**Usage pattern after editing AV code:**\n```\n1. Edit viewer HTML/JS\n2. Restart AV server: pkill -f 'node.*viewer/server.js' && node ~/gallia/viewer/server.js &\n3. Call av_reload (via MCP) → browser reloads with new code\n4. Push content as usual (av_display, show_instapcb, etc.)\n5. Call av_capture to verify the result visually\n```\n\n**Direct API (curl):**\n```bash\n# Force reload\ncurl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' -d '{\"action\":\"reload\"}'\n\n# Capture screenshot (returns base64 PNG + saves to screenshots dir)\ncurl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' -d '{\"action\":\"screenshot\"}'\n\n# Broadcast arbitrary message to all viewers (forwarded via WebSocket)\ncurl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"broadcast\",\"message\":{\"type\":\"stop_tour\"}}'\n\n# Set camera angle and enable FR4 board (for thumbnail capture)\ncurl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"broadcast\",\"message\":{\"type\":\"set_camera\",\"alpha\":3.3,\"beta\":0.8,\"radius\":1.3}}'\ncurl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"broadcast\",\"message\":{\"type\":\"set_fr4\",\"visible\":true}}'\n\n# Health check\ncurl http://127.0.0.1:8772/health\n```\n\nThe `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).\n\n**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.\n\n**3D debug/camera commands:**\n```bash\n# Show XYZ origin axes (red=X, green=Y, blue=Z)\ncurl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"broadcast\",\"message\":{\"type\":\"show_origin\",\"visible\":true}}'\n\n# Set camera to a named view (front, back, left, right, top, bottom, isometric)\ncurl -X POST http://127.0.0.1:8772/command -H 'Content-Type: application/json' \\\n  -d '{\"action\":\"broadcast\",\"message\":{\"type\":\"set_view\",\"view\":\"front\"}}'\n```\n\nThe mgmt-server is started automatically by `install.mjs` and should rarely need restarting.\n\n## Screenshot paste\n\nThe `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.\n\n```\nmcp__adom-viewer__av_screenshot_paste({ title: 'Paste Screenshots' })\n```\n\n## Viewer panel setup\n\nThe 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:\n\n1. Ensure the AV port (8770) has a DNS mapping via `adom-cli carbon containers port-add`. This gives it a subdomain like `https://<av-url>.adom.cloud/`.\n2. In the Adom app, add a **Web View** panel and enter the AV URL:\n   `https://<av-url>.adom.cloud/`\n3. The panel persists across Adom app reloads — this only needs to be done once.\n\n## AV Instances — Multiple Independent Viewer Panels\n\nA 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.\n\n### How it works\n\nEach AV panel gets a unique URL via the `?instance=<id>` query parameter:\n```\n/proxy/8770/?instance=sym     → Symbols panel\n/proxy/8770/?instance=fp      → Footprints panel\n/proxy/8770/?instance=3d      → 3D Models panel\n/proxy/8770/?instance=sch     → Schematics panel\n/proxy/8770/                  → Default (main) panel\n```\n\nThe server routes content to specific instances — when you call an MCP tool with `instance: \"sym\"`, only the Symbols panel receives it.\n\n### Instance presets\n\nKnown instance IDs auto-configure panel name and default view:\n\n| Instance ID | Panel Name | Default View |\n|-------------|-----------|--------------|\n| `sym` | Symbols | SymView |\n| `fp` | Footprints | FpView |\n| `fp3d` | 3D Pads | Fp3dView |\n| `3d` | 3D Models | 3dView |\n| `sch` | Schematics | SchView |\n| `lib` | Libraries | LibView |\n| `search` | Search | JlcSearch |\n\nCustom instance IDs work too — pass `?instance=myid&name=My Panel` for a custom name.\n\n### Using instances with MCP tools\n\nAll AV MCP tools accept an optional `instance` parameter:\n\n```\nmcp__adom-viewer__av_display({ html: \"...\", instance: \"sym\" })   → sends to Symbols panel only\nmcp__adom-viewer__av_display({ html: \"...\" })                    → sends to all panels\n```\n\n### AV2 (DEPRECATED)\n\nAV2 (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.\n\n## Adom Theme — styling guide for widgets\n\nAll 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`.\n\n### Using the theme in widget generators (JS modules)\n\n```javascript\nimport { THEME, themeStyleTag, toolbarCSS, tooltipCSS, infoPanelCSS } from './adom-theme.js';\n\nconst html = `<!DOCTYPE html>\n<html><head>\n${themeStyleTag()}  <!-- injects CSS vars + body reset -->\n<style>\n  ${toolbarCSS()}   <!-- standard toolbar -->\n  ${tooltipCSS()}   <!-- hover tooltips -->\n  ${infoPanelCSS()} <!-- bottom info panel -->\n</style>\n</head>\n<body>...</body></html>`;\n```\n\nOr reference tokens directly in template literals:\n\n```javascript\n`background: ${THEME.bg}; color: ${THEME.text}; border: 1px solid ${THEME.border};`\n```\n\n### Color tokens (CRITICAL — always use these, never invent new colors)\n\n| Token | Value | Usage |\n|-------|-------|-------|\n| `bg` | `#0d1117` | Page / deepest background |\n| `bgSurface` | `#161b22` | Toolbars, cards, panels |\n| `bgElevated` | `#1c2128` | Hover states, elevated cards |\n| `bgOverlay` | `#21262d` | Tooltips, modals |\n| `border` | `#30363d` | Standard borders |\n| `borderMuted` | `#21262d` | Subtle/inner borders |\n| `text` | `#e6edf3` | Primary text |\n| `textSecondary` | `#8b949e` | Labels, secondary text |\n| `textMuted` | `#484f58` | Hints, disabled, placeholders |\n| `accent` | `#00b8b0` | Primary teal accent |\n| `accentBright` | `#00e6dc` | Hover/focus highlight |\n| `accentMuted` | `rgba(0,184,176,0.12)` | Tinted backgrounds |\n| `success` | `#3fb950` | Success states |\n| `warning` | `#d29922` | Warning states |\n| `danger` | `#f85149` | Error states |\n\n### Rules\n\n1. **Never use the old palette** (`#1a1a2e`, `#16213e`, `#0f3460`, `#2a2a40`, `#e0e0e0`, `#00b8b1`, `#e04040`, `#f0a030`, `#4caf50`). These are deprecated.\n2. **All widget HTML must set its own background** to `#0d1117` (or use `themeStyleTag()`). Widgets render in sandboxed iframes — they cannot inherit styles from the parent viewer.\n3. **Use the font stack**: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` (available as `THEME.fontStack`).\n4. **Semantic colors only for status**: green=success, amber=warning, red=error. Never use red/green for decorative purposes.\n5. **Border radius**: 4px (small), 6px (medium), 8px (large) — available as `THEME.radiusSm/Md/Lg`.\n\n## 3D Model Display\n\nUse the `av_3d_display` MCP tool to display GLB files in the viewer's built-in Babylon.js 3D view:\n\n```\nmcp__adom-viewer__av_3d_display({\n  glb_path: '/path/to/model.glb',\n  body_size: { x: 4.4, y: 5.0, z: 1.2 },\n  part_name: 'PART_NAME',\n  manufacturer: 'Manufacturer',\n  package_type: 'TSSOP-14',\n  pad_count: 14,\n  title: 'PART 3D Model'\n})\n```\n\n3D content appears as a separate tab alongside HTML tabs. The viewer includes orbit camera, ground plane, skybox, and laser-etched chip markings.\n\n**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.\n\n### Synchronous load + immediate capture\n\n`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:\n\n```\nav_3d_display({ glb_path: '/tmp/model.glb', skip_tour: true })\nav_capture()  // model is already loaded — screenshot is immediate\n```\n\n### 3D camera and lighting controls\n\nAfter loading a model, use these tools to control the view:\n\n```\nav_stop_tour()                           // freeze the cinematic tour\nav_set_camera({ beta: 2.8, alpha: 0.5 }) // look from below\nav_set_bottom_light({ enabled: true })   // illuminate underside\nav_capture()                             // screenshot\nav_set_view({ view: 'isometric' })       // named preset\nav_capture()                             // another screenshot\n```\n\n**Camera angles** (Babylon.js ArcRotateCamera):\n- `alpha` = azimuth (orbit around vertical). 0 = front, π/2 = right side\n- `beta` = elevation. 0 = top-down, π/2 = eye-level, π = bottom-up\n- `radius` = zoom multiplier on current distance\n- `absolute_radius` = set exact distance\n\n**Bottom light**: Illuminates the underside of components. Useful for inspecting board pads, vias, heatsink copper, and under-chip features that are normally in shadow.\n\n## Widget Design Guidelines\n\nWhen generating interactive HTML widgets for the viewer, follow these rules:\n\n### Hover Highlight Rule (CRITICAL)\nWhen 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.\n\n### Minimum Font Sizes\nBottom panels and info bars must be readable at a glance:\n- Info values / legend text: **13px** minimum\n- Source / provenance text: **12px** minimum\n- Labels / secondary text: **11px** minimum\n- Never use 9-10px for any user-facing text in panels\n\n### Explain Unusual Features\nWhen 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.\n\n## General Guidelines\n\n- **Existing files**: If the user provides a file path, use `av_display_file` directly.\n- **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.\n- **Interactive HTML**: Use `html_interactive` content type when the content includes `<script>` tags or needs JavaScript execution. Use `html` for static HTML.\n- **Styling**: The viewer has a dark background (`#0d1117`). All generated HTML widgets MUST use the Adom theme colors from `viewer/adom-theme.js`. Import and use `THEME` constants or `themeStyleTag()` — never hardcode colors.\n- **Large content**: Files over 5MB may be slow to render. Warn the user if content is very large.\n- **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.\n- **After editing viewer HTML/JS and restarting the AV server**: Always call `av_reload` to refresh the user's browser panel. Never tell the user to refresh manually — use the tool.",
  "author": {
    "id": "695820315b5f1e4db2fcf602",
    "name": "Kyle Bergstedt",
    "email": "kyle@adom.inc"
  },
  "visibility": {
    "public": true
  },
  "hero": null,
  "sample_prompts": [],
  "discovery_triggers": [],
  "discovery_pitch": null,
  "metadata": {},
  "created_at": "2026-05-28T05:30:03.456Z",
  "updated_at": "2026-05-28T05:30:03.456Z",
  "sub_skills": [],
  "parent_app": null
}