name: adom-workspace-control description: Use when the user wants to open, close, add, or remove a panel; split a pane; move or swap tabs between panes; change the workspace layout; resize a split; bring a background tab to the foreground; show or hide a sensor view, camera, log, control panel, or any Adom editor panel; or asks "what panels are available". Examples: "open the 3D viewer", "add a webview tab", "close the robot log", "resize the split to 70/30", "split the pane horizontally", "move that tab to the other pane", "swap the two panes", "close that pane", "show the IMU sensor tab", "bring the webview to the front", "what panels can I open?". user-invocable: true

Adom Workspace Control

🛑 Never wipe the user's full Hydrogen layout — and never remove the VS Code tab 🛑

The danger is leaving the Hydrogen screen empty, or without VS Code. The user panics because "everything is gone" and they can't tell what happened.

What's safe

  • Per-tab CLI operations via adom-cli hydrogen workspace ...: add-tab, remove-tab, move-tab, swap, split, resize, active-tab, close-panel. These are bounded — each mutates one tab/pane at a time. Use them freely.
  • GET on the editor endpoint is fine (read-only).

What's NOT safe

  • Never call curl, gh api, fetch, or any raw HTTP client with PUT, PATCH, or DELETE against https://hydrogen.adom.inc/api/workspaces/editor/<user>/<ws>/current. That endpoint is the WHOLE LAYOUT; any write replaces it wholesale and wipes every panel, tab, and split.
  • Never remove the VS Code tab — even via a safe CLI op — unless the user explicitly asks. VS Code is where the user lives; removing it is indistinguishable from "everything is broken" to them.
  • Never remove the last remaining tab in a pane without the user asking (pane goes blank; if it's the only pane, the workspace looks wiped).

Just-in-time confirmation: ALWAYS use AskUserQuestion before any authorized destructive op

Even when the user has explicitly asked for a destructive action (workspace wipe, screenshot-the-empty-state dance, mass tab close, anything that would leave the screen empty), call the harness-level AskUserQuestion tool as the IMMEDIATE LAST ACTION before firing the destruction. The user clicks "Proceed" and the destructive call fires in the same tool-turn — no drift between click and wipe.

Why: the user is typing to Claude while you work. If you said "going now" three tool calls ago, they've resumed typing; firing destruction now eats whatever they're mid-drafting. AskUserQuestion is blocking and synchronous in the Claude Code chat UI, but non-modal for the rest of the workspace — the user finishes typing / saves their work, then clicks. Tested 2026-04-22: in-flight chat drafts survive the prompt intact, so it's safe to use mid-conversation.

Shape of the call:

AskUserQuestion({
  questions: [{
    question: "About to <specific destructive action>. Save any work you're typing first. Ready?",
    header: "Confirm destruction",
    multiSelect: false,
    options: [
      { label: "Proceed — saved", description: "Fire the destructive operation immediately." },
      { label: "Cancel", description: "Abort; no change." }
    ]
  }]
})

If the user picks "Cancel" or any "Other" text that isn't an unambiguous yes, ABORT. Don't argue, don't retry.

Pair this with:

  • A disk-backed backup of any state you're about to mutate (e.g. /tmp/hydrogen-layout-backup-<ts>.json for a layout wipe) so you can restore deterministically after.
  • An announced restore command printed to the user so they can run it themselves if something goes wrong before you get to restore.

Incidents the rule comes from

2026-04-22 (morning) — AI probed the editor API for debug (curl -X PUT -d '{}', then -X DELETE), trying to understand a 409 state. Each call returned 204 and silently replaced the whole layout with an empty one. User had to re-open VS Code from scratch. Filed as hydrogen#322 for server-side guardrails.

2026-04-22 (midday) — AI was authorized to wipe-screenshot-restore the layout for a Colby bug report but did not interpose a just-in-time confirmation, so the user was typing mid-paragraph when the destructive step was about to fire. AskUserQuestion is the fix for that class of failure; before this rule existed, even an authorized destructive op was unsafe because the destruction timing drifted seconds-to-minutes after the user's verbal "go."

Recovery if it already happened

  1. Tell the user immediately — don't try to "quietly restore" a guess; the guess will differ from what they had.
  2. Reload the Hydrogen page via adom-cli hydrogen reload (broadcasts SSE reload to all connected tabs). The client re-registers the editor session with whatever default/saved layout Hydrogen boots. If no browser is connected, ask the user to open the editor tab first.
  3. If you had a disk-backed backup from step 1 of the destruction preamble, restore from that via a single PUT with the saved JSON — but again, call AskUserQuestion first to confirm the user is ready and has saved anything else in flight.
  4. File a bug capturing exactly what you did (method + URL + body) so server-side guardrails can catch the next one — see hydrogen#322 for the template.
  • adom-inc/hydrogen#322 — "Detect wiped / empty workspace layout and offer a 'Restore default layout' recovery banner" + server-side PUT/DELETE guardrails.
  • adom-inc/hydrogen#323 — Editor session 409 auto-recovery (separate failure mode that misleadingly resembles a wipe).

Architecture: Hydrogen vs VS Code vs Docker

Browser
  └── Hydrogen (workspace shell — panels, tabs, splits)
        ├── 3D Viewer panel
        ├── webview panels (shotlog, AV, etc.)
        ├── Sensor panels, camera feeds, etc.
        └── VS Code panel (iframe → code-server running on Docker)
              └── code-server (its own world, own API, own extensions)

Hydrogen is the outer workspace shell. The adom-cli hydrogen workspace API and the REST endpoints documented below control Hydrogen — adding/removing tabs, splitting panes, resizing, etc. These operate on the panel layout, not on what's inside any panel.

VS Code (code-server) runs inside the Docker container and is embedded as an iframe in one of Hydrogen's panels. Hydrogen cannot control what happens inside VS Code — it can only add/remove the VS Code panel itself. To control VS Code (open files, reveal in explorer, run commands), you must use code-server's own mechanisms:

  • Code-server remote CLI (safe for opening files, NOT folders):

    /usr/lib/code-server/lib/vscode/bin/remote-cli/code-linux.sh --reuse-window /path/to/file.png
    

    This opens a file in a new editor tab without changing the workspace root. NEVER pass a folder path — that changes the workspace root and breaks the user's session.

  • Code-server IPC socket ($VSCODE_IPC_HOOK_CLI): Low-level Unix socket the CLI uses under the hood. POST { type: "open", fileURIs: [...] }.

  • VS Code extensions: A code-server extension can expose an HTTP API on a local port, giving Docker processes full access to the VS Code API (vscode.window.showTextDocument(), vscode.commands.executeCommand('revealInExplorer'), etc.).

Key rule: Hydrogen API = panel layout. Code-server CLI/extensions = VS Code internals. Don't mix them up.


Manipulate the Adom editor workspace: add/remove tabs, split/close panes, move/swap tabs between panes, resize splits, take screenshots, and query the layout.

USE adom-cli — NOT curl

adom-cli auto-detects auth, owner, and repo. Use it instead of raw curl for all workspace operations. The curl examples further below are reference only — prefer the CLI commands.

# These just work — no setup, no API key, no owner/repo needed:
adom-cli hydrogen workspace tabs                                    # list all tabs (flat)
adom-cli hydrogen workspace find-tab "Schematic Editor"             # find by name
adom-cli hydrogen screenshot workspace                              # screenshot (auto-saved)
adom-cli hydrogen screenshot panel --name "Schematic Editor"        # screenshot by name
adom-cli hydrogen workspace add-tab --panel-id <id> --panel-type "adom/..." --display-name "My Tab" --alert
adom-cli hydrogen workspace remove-tab --name "My Tab"              # remove by name
adom-cli hydrogen workspace active-tab --name "Schematic Editor"    # activate by name
adom-cli hydrogen workspace close-panel "My Tab"                    # close by name

Name-based addressing: Most commands accept --name to target tabs by display name instead of UUIDs. workspace tabs returns a flat list with names — no tree parsing needed.

Sharing & Permissions

adom-cli hydrogen screenshot status                                          # check what's available
adom-cli hydrogen sharing request --share tab                                # request tab sharing
adom-cli hydrogen sharing request --share screen --audio --reason "Recording demo"  # screen + mic

Screenshots, Recording, Audio, Captions, Alerts, Nav

Media and overlay commands are covered in a reference file: see media-and-alerts-reference.md for:

  • Screenshots (adom-cli hydrogen screenshot workspace|panel|screen, unified approval flow, html2canvas fallback)
  • Screen recording (4 mandatory flags: --share, --mic, --countdown, --reason; smart-delta approval matrix; WebM output)
  • Audio (mic enable/disable, recording, level/gain)
  • Captions (DOM overlay via adom-cli hydrogen caption; always-on-top adom-desktop desktop_caption for native apps)
  • Tab alerts (blink to get attention)
  • Navigation bar (minimize/expand/toggle)

Panel-specific APIs (e.g. navigating a webview to a URL) are documented in separate skills under .claude/skills/adom-panels/<panel-name>/SKILL.md. See .claude/skills/adom-panels/SKILL.md for the full catalog and links.

Environment (for raw curl — prefer adom-cli above)

All API calls require:

  • Base URL: https://hydrogen.adom.inc/api/workspaces/editor/{owner}/{repo}/current
  • JSON parsing: jq is not available in the container. Use python3 -m json.tool to pretty-print JSON output instead.
  • Auth: X-Api-Key header. Obtain the key by reading /var/run/adom/api-key — this file is always present in the container, is read-only, and contains the token with no leading or trailing whitespace:
    API_KEY=$(cat /var/run/adom/api-key)
    
    Fall back to env vars (ADOM_API_KEY, API_KEY, X-Api-Key) only if that file is missing. Ask the user only as a last resort.

Critical Rules — Read Before Doing Anything

  • NEVER PUT or DELETE the /current root. Both are destructive at the workspace level:

    • PUT /current replaces the entire layout and breaks all node IDs.
    • DELETE /current tears down the editor session, wiping every pane and tab (including the user's VS Code session hosting this chat). Mutate workspaces only through the granular sub-paths: POST /current/tabs, DELETE /current/tabs, POST /current/splits, PATCH /current/splits/{id}, POST /current/moves, POST /current/swaps, DELETE /current/panels/{id}. Build up the desired layout one operation at a time. If an operation you want isn't on that list, it isn't supported for AI use — do not probe with other methods to find out.
  • NEVER point a Hydrogen webview at a http://localhost:<port>/ or http://127.0.0.1:<port>/ URL. The webview runs in the user's BROWSER, not inside the Docker container. From the browser's perspective, localhost is the user's host machine, not the container — so anything served by a container-local service (tsci dev on 3040, adom-tsci on 8850, shotlog on 8820, etc.) is unreachable via localhost. The tab loads, the iframe tries to fetch, and the user sees the tscircuit "loading cloud" placeholder or a blank page forever. Always rewrite the URL to use the container's external proxy:

    # Canonical template is stored in the VSCODE_PROXY_URI env var, with {{port}} placeholder:
    #   VSCODE_PROXY_URI=https://john-workspace-8v0y8o3547h2.adom.cloud/proxy/{{port}}/
    PROXY_URL="${VSCODE_PROXY_URI/\{\{port\}\}/8850}"
    adom-cli hydrogen webview navigate --name "My Tab" "$PROXY_URL"
    

    This matches every existing working tab (Video Post, Wiki publish, shotlog channels, adom-tsci instances) — they all use https://<host>/proxy/<port>/.... If you catch yourself typing localhost: or 127.0.0.1: in a webview navigate, add-tab --url, or any displayName/url pair, you're about to hand the user a broken tab. The same rule applies in reverse for curl from inside the container — curl http://127.0.0.1:<port>/ works from the container's own shell (that's how to self-test an HTTP endpoint), but the URL you PUT IN THE TAB must be the proxy URL. Two different contexts, two different URLs:

    Context Correct URL
    curl or Bash tool (runs in container) http://127.0.0.1:<port>/...
    Hydrogen webview url=, navigate, add-tab $VSCODE_PROXY_URI with {{port}} substituted
    Anything a browser fetches (img src, iframe src on a served page) Proxy URL

    Shotlog's shotlog open already does the right substitution internally. adom-cli hydrogen workspace add-tab --url and hydrogen webview navigate <url> do NOT — you must supply the proxy URL. If you're unsure, echo "${VSCODE_PROXY_URI/\{\{port\}\}/<port>}" prints the correct URL to use.

  • NEVER ask the user to do something you can do yourself. If you tell them "reload the shotlog tab and let me know what you see" or "refresh the webview," you've punted manual busywork onto the person you're supposed to be helping. That's a failure. Claude has tools for almost every workspace action:

    • Refresh a webview: adom-cli hydrogen webview refresh --name "<tab name>" (returns 204 on success)
    • Navigate to a URL: adom-cli hydrogen webview navigate --name "<tab name>" --url "<url>"
    • Bring a tab to the front: adom-cli hydrogen workspace active-tab --name "<tab name>"
    • Screenshot a panel: adom-cli hydrogen screenshot panel --name "<tab name>" (capture IS visible to you via the returned path)
    • Open a tab on a specific pane: adom-cli hydrogen workspace add-tab --panel-id <non-vscode-id> ...
    • Alert a tab: adom-cli hydrogen alert ...
    • Send a desktop notification: adom-cli hydrogen notify send --title "..." "body"
    • Force-reload all browser tabs: adom-cli hydrogen reload (hot-reload after deploy, recovery)
    • Show/hide address bar: adom-cli hydrogen webview set-header ...

    Before saying "check the tab", ask yourself: can I refresh it for them? can I screenshot it to verify? can I bring it to the front? If yes — do it. Only hand off to the user when: (a) the action genuinely requires their eyes (taste judgements, choosing between layouts), (b) their physical keyboard/mouse is needed (desktop app outside Hydrogen), or (c) you've done the automated parts and need them to confirm the result matches intent. If you find yourself about to type "let me know if X works," stop and actually check X. curl, screenshot panel, state endpoints — they exist so you don't have to outsource verification.

  • NEVER echo or display the API key in tool output. Store it in a shell variable — never cat it on its own or log it.

  • Add before removing when swapping tabs in a leaf — the API returns 400 if you try to remove the last tab, so always add the new tab first, then remove old ones.

  • Remove tabs from highest index first when removing multiple tabs from a leaf, to avoid index shifting.

  • Re-fetch the layout before each batch of operations — state can change between calls. Always confirm current node IDs and tab indices are still valid.

  • Be cautious when closing panes or removing VS Code tabs — one VS Code panel instance is hosting this chat session. Closing it terminates the conversation. Since there is no reliable way to identify which specific VS Code instance is running the session, avoid closing panes that contain a VS Code tab or removing VS Code tabs unless the user explicitly requested it.

  • VS Code pane guard is now enforced by the CLI (v0.5.4+). adom-cli hydrogen workspace add-tab and webview open-or-refresh automatically redirect away from any pane that hosts VS Code. If you pass a --panel-id that hosts VS Code, the CLI finds another pane (or auto-splits to create one) and adds a _vscode_guard field to the response explaining the redirect. You no longer need the Python script below to avoid the VS Code pane — the CLI handles it. The script is kept for reference only / for raw-curl callers who bypass the CLI.

    If you're using adom-cli, just pass any --panel-id — even the VS Code one — and the guard will redirect silently. The old rule ("query the layout, find a non-VS-Code pane, target it") is still correct but no longer necessary to do manually.

    # VS Code's panelType is the canonical 'eeee' UUID below.
    VSCODE_PT="adom/a1b2c3d4-eeee-4000-a000-00000000000e"
    
    # Walk every leaf in the layout. Return the first leaf whose tabs
    # do NOT include VS Code. Fall back to the currently-focused pane
    # only if every pane has VS Code (single-pane layout). When there
    # are multiple non-VS-Code panes, prefer the one whose activeTab
    # matches something webview-like, or just pick the first.
    adom-cli hydrogen workspace get | python3 -c '
    import json, sys
    VSCODE_PT = "adom/a1b2c3d4-eeee-4000-a000-00000000000e"
    layout = json.load(sys.stdin)
    
    def leaves(node):
        if "tabs" in node:
            yield node
        else:
            if "first" in node: yield from leaves(node["first"])
            if "second" in node: yield from leaves(node["second"])
    
    focused = layout.get("focusedPanelId")
    non_vscode = [l for l in leaves(layout["root"])
                  if not any(t.get("panelType") == VSCODE_PT for t in l["tabs"])]
    if non_vscode:
        # Prefer the focused pane IF it is itself non-VS-Code, otherwise
        # take the first non-VS-Code pane in tree order.
        picked = next((l for l in non_vscode if l["id"] == focused), non_vscode[0])
        print(picked["id"])
    else:
        print("")  # single-pane layout, caller must split or ask
    '
    

    Every CLI and every webapp that opens a Hydrogen tab MUST do this check. The video-post crate implements it in src/webapp/tab.rs; copy that pattern. Do not trust focusedPanelId for target selection. Do not use adom-cli hydrogen workspace active-tab to "fix" the wrong placement after the fact — by that point the user has already seen their chat get covered.

    Third-party CLIs that open their own tabsshotlog open, adom-tsci start, av_status tools, any webapp written before this rule existed — usually have their own unfiltered leaf-picker and will happily land on the VS Code pane. Do not trust them. Before running such a command, either:

    1. Compute the non-VS-Code panel id yourself (snippet above), then pass it as --panel-id <id> if the CLI supports that flag (adom-cli hydrogen workspace add-tab does; shotlog open as of 2026-04 does NOT).
    2. If the third-party CLI has no --panel-id flag: skip the wrapper entirely. Use adom-cli hydrogen workspace add-tab --panel-id <non-vscode-id> --panel-type "adom/a1b2c3d4-0031-4000-a000-000000000031" --display-name "..." --url "<the underlying URL>" directly — the wrapper's extra polish (icons, titles) is not worth the risk of covering VS Code.
    3. If you already opened a tab on the wrong pane, move it via POST /moves (granular) rather than re-creating; don't leave it landed on VS Code "just this once."

    Known problem URLs you can target directly when skipping wrappers:

    • shotlog: https://<wiki-host>/proxy/8820/log/<channel>/
    • adom-tsci preview: http://127.0.0.1:<port>/
    • AV: /proxy/8770/

Workflow

Always follow this order when making changes:

  1. Setup — resolve credentials, repo, and target user (ask once, remember for the session)
  2. GET the current layout to find leaf node IDs
  3. Identify the correct leaf id for where the user wants the tab placed
  4. Look up the panelType full ID from the catalog below
  5. POST/DELETE/PATCH to make the change
  6. Confirm success to the user

Step 0 — Setup: resolve credentials, repo, and target user

At the start of the session, read the API key and auto-discover the owner and repo via the Carbon API:

API_KEY=$(cat /var/run/adom/api-key)

# Auto-discover owner and repo from the Carbon container registry
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'])")

BASE="https://hydrogen.adom.inc/api/workspaces/editor/$OWNER/$REPO/current"

The slug is extracted from $VSCODE_PROXY_URI (everything after the last -, before .adom.cloud). The Carbon API returns the full container info including repository.owner.name and repository.name. Do not ask the user for owner/repo — always use this endpoint to resolve them automatically.

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, fall back to asking the user:

"What is the Adom owner (username or org) and Adom repository name for this project?"

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.

Remember the answers for the rest of the conversation — do not ask again.

Discover the target user. Workspaces are scoped per-user. You must discover who has an open editor:

curl -s -H "X-Api-Key: $API_KEY" "$BASE/users" | python3 -m json.tool
# → { "users": [{ "username": "kcknox" }] }
  • One user (most common): auto-detected by the server — no extra header needed. Set TARGET_HEADER="".
  • Multiple users: ask the user which person's workspace to modify. Remember the choice and set:
    TARGET_HEADER='-H "X-Target-Username: <chosen-username>"'
    
  • Zero users: the editor is not open in any browser. Tell the user to open the editor first.

Save OWNER, REPO, API_KEY, BASE, and TARGET_HEADER (if needed) in your memory file so you don't repeat this setup.

Step 1 — GET current layout

curl -s -H "X-Api-Key: $API_KEY" $TARGET_HEADER "$BASE" | python3 -m json.tool

Parse the response to find:

  • All leaf nodes and their id values (needed for adding/removing tabs)
  • All split nodes and their id values (needed for resizing)
  • The tabs array on each leaf and activeTabIndex

Step 2 — POST: Add a tab

curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"panelId":"<leaf-id>","panelType":"<full-panel-id>"}' \
  "$BASE/tabs"

Returns { "tabId": "<uuid>" } on success.

Custom tab name and icon: You can optionally set displayName and displayIcon to override the tab header. MDI icons are monochrome and inherit the theme color. For colored icons, use a custom SVG via data: URL.

MDI icon example:

curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"panelId":"<leaf-id>","panelType":"adom/a1b2c3d4-0031-4000-a000-000000000031","displayName":"eBay","displayIcon":"mdi:cart"}' \
  "$BASE/tabs"

Custom SVG icon example (colored):

curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"panelId":"<leaf-id>","panelType":"adom/a1b2c3d4-0031-4000-a000-000000000031","displayName":"RMFG","displayIcon":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIgZmlsbD0iIzRDQUY1MCIvPjwvc3ZnPg=="}' \
  "$BASE/tabs"
  • displayName — custom label shown on the tab (instead of the panel's default name)
  • displayIconMDI icon string (e.g. mdi:github, monochrome) or data: URL for custom colored SVG/image (e.g. data:image/svg+xml;base64,...)
  • Both are optional and stored in panelState. Omit them to use the panel's default name/icon.

If the user doesn't specify which panel (leaf) to add to and there is more than one leaf, ask them where they want it placed, or place it in the largest/most relevant leaf.

Step 3 — DELETE: Remove tab(s) — 5 modes

Target the reference tab by name or panelId + tabIndex. The mode field controls what gets closed.

Mode Effect
close (default) Close just this tab
others Close every other tab, keep this one (becomes active)
all Close the entire pane (fails if it's the last pane)
right Close everything to the right of this tab
left Close everything to the left of this tab

For others / right / left, the referenced tab becomes active. For all, the pane is removed and its sibling is promoted.

# Close a single tab (default)
curl -s -X DELETE \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"name":"Bug Review"}' \
  "$BASE/tabs"

# Close all other tabs in the pane
curl ... -d '{"name":"Bug Review","mode":"others"}' "$BASE/tabs"

# Close the entire pane
curl ... -d '{"name":"Bug Review","mode":"all"}' "$BASE/tabs"

# Close tabs to the right / left
curl ... -d '{"name":"Bug Review","mode":"right"}' "$BASE/tabs"
curl ... -d '{"name":"Bug Review","mode":"left"}' "$BASE/tabs"

# By UUID + index (all modes work the same way)
curl ... -d '{"panelId":"<leaf-id>","tabIndex":<n>,"mode":"others"}' "$BASE/tabs"

Or via CLI: adom-cli hydrogen workspace remove-tab --name "Bug Review" [--mode close|others|all|right|left].

Cannot remove the last tab in a leaf via mode: close — API returns 400. Use mode: all to remove the whole pane instead (the sibling is promoted).

Step 4 — PATCH: Resize a split

curl -s -X PATCH \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"ratio":0.3}' \
  "$BASE/splits/<split-id>"

ratio is the fraction given to the first child (0.1–0.9). So "70/30" → ratio: 0.7.

Step 5 — POST: Split a pane

Split an existing pane into two, creating a new split node.

curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"panelId":"<leaf-id>","direction":"horizontal","panelType":"<full-panel-id>"}' \
  "$BASE/splits"

Returns { "panelId": "<new-leaf-id>", "tabId": "<new-tab-id>" }.

Field Required Description
panelId yes Leaf node to split
direction yes "horizontal" or "vertical"
panelType yes Panel type for the new pane's tab
position no "before" or "after" (default "after")
ratio no Split ratio 0.1–0.9 (default 0.5)
displayName no Custom tab header label
displayIcon no MDI icon string (e.g. mdi:github, monochrome) or custom SVG/image as data: URL — use data: for colored icons

Step 6 — POST: Move a tab between panes

Move a tab from one pane to another without creating a split. The moved tab is auto-activated and scrolled into view in the target pane — no separate activate call needed.

# By name (preferred)
curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"sourceName":"Bug Review","targetName":"Schematic Editor"}' \
  "$BASE/moves"

# By UUID
curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"sourcePanelId":"<leaf-id>","tabId":"<tab-uuid>","targetPanelId":"<other-leaf-id>"}' \
  "$BASE/moves"

# With alert (blink the moved tab until clicked)
curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"sourceName":"Bug Review","targetName":"Schematic Editor","alert":true,"alertDuration":0}' \
  "$BASE/moves"

Or via CLI: adom-cli hydrogen workspace move-tab --source-name "Bug Review" --target-name "Schematic Editor" [--target-index N] [--alert] [--alert-duration SECS].

Returns 204 No Content. If the moved tab was the last one in the source pane, the source pane is automatically removed.

Field Required Description
sourcePanelId + tabId yes* Source tab (by UUID)
targetPanelId yes* Destination pane (by UUID)
sourceName alt* Resolves to sourcePanelId + tabId
targetName alt* Resolves to targetPanelId (pane containing that tab)
targetIndex no Insert position (appends if omitted)
alert no true to blink the moved tab in its new pane
alertDuration no Seconds to blink. 0 = indefinite until user clicks

*Provide either UUIDs or names.

Step 7 — POST: Swap content between panes

Swap all tabs between two panes.

curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"sourcePanelId":"<leaf-id-1>","targetPanelId":"<leaf-id-2>"}' \
  "$BASE/swaps"

Returns 204 No Content.

Step 8 — POST: Bring a tab to the foreground

Switch the visible tab within a pane. Use this when a pane has multiple tabs and you want to show one that is currently in the background.

# By tab index (0-based)
curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"panelId":"<leaf-id>","tabIndex":2}' \
  "$BASE/active-tab"

# By tab UUID (preferred — stable if tabs are reordered)
curl -s -X POST \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  -H "Content-Type: application/json" \
  -d '{"panelId":"<leaf-id>","tabId":"<tab-uuid>"}' \
  "$BASE/active-tab"

Returns 204 No Content.

Field Required Description
panelId yes Leaf node containing the tabs
tabIndex one of 0-based position of the tab to show
tabId one of UUID of the tab to show

When the user says "show the IMU sensor" or "switch to the Robot Log tab", GET the layout, find the leaf and tab by panelType, then call POST /active-tab with that leaf's id and the tab's id.

Step 9 — DELETE: Close a pane

Remove a pane entirely. Its sibling is promoted to replace the parent split.

curl -s -X DELETE \
  -H "X-Api-Key: $API_KEY" $TARGET_HEADER \
  "$BASE/panels/<leaf-id>"

Returns 204 No Content. Cannot close the last pane — the API returns 400.

Step 10 — PUT and DELETE on /current: PROHIBITED

Do not PUT or DELETE the /current root.

  • PUT /current replaces the entire workspace layout — every pane, every tab, every node ID. Past incidents: AI sent PUT {} to probe what the endpoint accepts; server happily 204'd and the user's workspace (VS Code + multiple panes) was reduced to an empty layout.
  • DELETE /current tears down the editor session itself — same blast radius. Past incidents: AI probed with DELETE /current to debug a 409 Editor is not open error; the call succeeded and the user lost their entire session.

Neither has a valid AI use case. Use the granular endpoints above (POST /current/tabs, DELETE /current/tabs, POST /current/splits, etc.) instead. Do not probe for additional methods — if the docs above don't list it, it isn't supported.

Step 11 — GET /events: SSE event stream (optional, for real-time sync)

curl -s -H "X-Api-Key: $API_KEY" "$BASE/events"

Emits { "type": "connected" } once, then { "type": "workspace_updated" } on every mutation. The container is typically the mutator, not the listener — this is mainly useful if you need to wait for another actor's change before proceeding.

Layout Structure Reference

The workspace is a binary tree:

  • SplitNode{ type: "split", id, direction: "horizontal"|"vertical", ratio: 0.1–0.9, first: PanelNode, second: PanelNode }
  • LeafNode{ type: "leaf", id, tabs: PanelTab[], activeTabIndex: number }
  • PanelTab{ id, panelType: "<full-panel-id>", panelState?: { displayName?, displayIcon?, ... } }

Singleton Panels

These may only appear once in the entire workspace. Check the current layout before adding — if already present, tell the user rather than adding a duplicate.

Full ID Name
adom/a1b2c3d4-1111-4000-a000-000000000001 3D Viewer
adom/a1b2c3d4-0012-4000-a000-000000000012 Schematic Editor

Panel Catalog, Tab Customization, and Panel-Specific Control

Full panel catalog (all panelType UUIDs for Development, Control, Visualization, Media, Utility), tab rename/icon patterns (MDI strings and base64 SVG data URLs), webview commands (navigate, refresh, set-header, set-proxy), Sandbox commands (eval, reset, set-header), and the full troubleshooting table: see panels-catalog-reference.md.

Quick links to the most common panelType UUIDs:

  • webview: adom/a1b2c3d4-0031-4000-a000-000000000031
  • VS Code: adom/a1b2c3d4-eeee-4000-a000-00000000000e
  • 3D Viewer (singleton): adom/a1b2c3d4-1111-4000-a000-000000000001
  • Schematic Editor (singleton): adom/a1b2c3d4-0012-4000-a000-000000000012

Window mode: check before opening a window

Before opening a window (webview tab or Pup browser window), check the user's preference:

adom-cli hydrogen probe  # → { ..., "window_mode": "webview" | "pup" }
  • "webview" (default) → open as a Hydrogen webview tab (use open-or-refresh below)
  • "pup" → open via Adom Desktop (pup browser_open_window). If Adom Desktop isn't reachable, fall back to webview and warn.

Don't cache window_mode — read it from probe each time you're about to open a window so user toggles take effect immediately. Get/set via adom-cli hydrogen window-mode get|set.

webviews: use open-or-refresh for long-lived viewers

For any webview tab you create once and re-render many times (tscircuit viewer, log panel, custom dashboard), use the idempotent one-shot command — don't cache tabId/panelId:

# First call: creates the tab in the chosen pane
adom-cli hydrogen webview open-or-refresh \
  --name "tscircuit viewer" \
  --url "https://.../proxy/3000/" \
  --panel-id <target-pane-id>

# Subsequent calls: navigates the existing tab, follows user's pane drags.
# --panel-id only needed on the create path.
adom-cli hydrogen webview open-or-refresh \
  --name "tscircuit viewer" \
  --url "https://.../proxy/3000/?v=$(date +%s)"

Lifecycle contract: The tab name is the stable handle. tabId is stable across pane drags, but the tab's panelId changes. Always re-resolve by --name on each call. If the user closed the tab, find-tab returns 404 loudly — open-or-refresh handles this by falling back to create (requires --panel-id). Response includes "action": "created" or "action": "refreshed".

See panels-catalog-reference.md for the remaining webview commands (navigate, refresh, set-header, set-proxy) and other panel types.