# 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.

1. Call `av_reload` (uses mgmt relay on port 8772, works even during server restart)
2. If it reports 0 viewers connected, wait 3 seconds and try once more
3. 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.

1. Read the API key: `API_KEY=$(cat /var/run/adom/api-key)`
2. Auto-discover owner/repo by extracting the slug from `$VSCODE_PROXY_URI` (everything after the last `-`, before `.adom.cloud`) and calling Carbon:
   ```bash
   SLUG=$(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'])")
   ```
   **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.
3. GET the current layout to find the leaf node ID containing VS Code
4. **Split the VS Code pane horizontally** to create a new pane on the right with a Web View tab:
   ```bash
   curl -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"
   ```
   This returns `{ "panelId": "<new-leaf-id>", "tabId": "<new-tab-id>" }`.
5. Navigate the new Web View to the AV URL:
   ```bash
   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"
   ```
6. 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:

```javascript
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.

```bash
# 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:

```bash
# 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 by `av_close_tabs` for 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:

1. **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.

2. **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.

## 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 including `html_interactive` iframes (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:
1. Click the **camera button** (next to forward/back) in AV → opens the capture tab
2. Click **"Share This Tab"** → select the IDE tab in the browser dialog
3. 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):**
```bash
# 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:**
```bash
# 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:

1. 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/`.
2. In the Adom app, add a **Web View** panel and enter the AV URL:
   `https://<av-url>.adom.cloud/`
3. 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)

```javascript
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:

```javascript
`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

1. **Never use the old palette** (`#1a1a2e`, `#16213e`, `#0f3460`, `#2a2a40`, `#e0e0e0`, `#00b8b1`, `#e04040`, `#f0a030`, `#4caf50`). These are deprecated.
2. **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.
3. **Use the font stack**: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif` (available as `THEME.fontStack`).
4. **Semantic colors only for status**: green=success, amber=warning, red=error. Never use red/green for decorative purposes.
5. **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 side
- `beta` = elevation. 0 = top-down, π/2 = eye-level, π = bottom-up
- `radius` = zoom multiplier on current distance
- `absolute_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_file` directly.
- **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_interactive` content type when the content includes `<script>` tags or needs JavaScript execution. Use `html` for static HTML.
- **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.
- **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_reload` to refresh the user's browser panel. Never tell the user to refresh manually — use the tool.
