Download

name: adom-tsci description: > Trigger words: tscircuit preview, adom-tsci, tsci dev webview, preview tscircuit, interactive tscircuit viewer, inspect board, hover chip info, what is this chip, pcb inspect tool, measure pcb distance, measure between pins, tscircuit hot reload, rerun autorouter, re-run autorouter, walkthrough demo, board walkthrough, sharper silkscreen, bump texture resolution, tscircuit 8192, upgrade tscircuit, stale tscircuit deps, hide chip see traces, AI-driven 3D pcb viewer, usb-c, type-c, UsbCReceptacle, place usb-c, rotate usb-c. Interactive tscircuit preview inside a Hydrogen webview panel ("Tscircuit Board Viewer"). First-class 3D / PCB / Schematic tabs rendered from the tscircuit build output, plus: an Inspect tool (hover any chip / pad / hole / via / silkscreen / board and get a labelled info card with refdes, footprint, net routing, stackup — all from circuit.json), a Measure tool (Fusion 360-style smart picking, mm/inches/mils), a Walkthrough Demo (guided 10-step 3D flyover tour), a one-click Re-run autorouter, a background stale-deps check, and a --texture-resolution N flag that re-bakes board-surface texture up to 8192x8192 for readable silkscreen at zoom. The 3D viewer is fully AI-drivable: every toolbar action has a CLI subcommand, HTTP endpoint, JS eval channel, and console forwarder. Use when the user asks to preview a tscircuit project, inspect board features, measure pin-to-pin distances, start a walkthrough demo, re-run the autorouter, bump silkscreen resolution, or upgrade tscircuit packages. Also routes the adom-usbc child guide (adom-tsci/guides/adom-usbc.md) for USB-C placement, rotation math, and the canonical UsbCReceptacle wrapper.

adom-tsci

An Adom app that gives you first-class 3D / PCB / Schematic tabs for a tscircuit project inside a Hydrogen webview panel, plus a one-click Re-run autorouter button, a background stale-deps check against the npm registry, and a secondary tsci live tab (the raw tscircuit RunFrame, served through a slingshot proxy) for features the primary UI hasn't wrapped.

🛡 INVARIANTS — audit BEFORE every change to nearby code

These are user-confirmed behaviours that have already regressed at least once. Before editing the listed files (or any function with the listed keywords in its name) read this list and grep the relevant code path. If your change would alter any of these, ask the user first — don't silently re-trade them for "improvements".

# Invariant What it protects Where the rule lives in the code
1 Right-click hide ghosts at 20% alpha — never setEnabled(false) for any mesh kind, including InstancedMesh. The picker must still be able to hit the ghost so a second right-click can unhide. Hide auto-promotes the component (clone source mesh + clone material) so the alpha write hits ONLY this instance, not its siblings. The user's only "show me what's under this part" workflow. If we full-disable, they cannot ever bring the part back without a CLI command — UX dead-end. Without promotion, hiding R1 also ghosts R2/R3 because tscircuit shares one material across InstancedMesh siblings. setComponentVisibility() in src/assets/shell.html calls _promoteComponent(name) BEFORE the alpha mutation. The "other" branch always goes through material.alpha = 0.20 on the (now-promoted) standalone mesh. Per-instance isolation also belongs in _makeFloatingMarker (HIGHLIGHT path), but via overlay-only architecture — not promotion.
2 Highlight = real Babylon HighlightLayer glow on the actual chip mesh. No bbox tube outlines, no synthetic cone+tail arrows, no emissive material tints, no fake floating squares. ONE consistent visual language for "this is selected": a teal halo around the chip itself. Auto-promote first so the addMesh hits only the target component (not its InstancedMesh siblings). Half-assed approaches (rectangles around bounding boxes, floating arrows that point at things, tinting the material) all read as different "selected" states and confused users. The user explicitly retired those: glow is the only highlight now. _getHighlightLayer() returns the singleton (strong-blur outer-glow). highlightComponents() in shell.html adds the target's meshes via hl.addMesh(mesh, teal) after _promoteComponent(name). drawFeatureOutline() for feature.kind === 'chip' routes to the same glow path; only non-chip features (pads, holes, vias, board-edges) still get the tube outline because they have no mesh to glow.
3 Highlight material uses real PBR via stolen-constructor. new B.PBRMaterial() throws because the viewer ships a minified BABYLON. Find the class via scene.materials.find(m => 'albedoColor' in m && typeof m.metallic === 'number').constructor. Phong/StandardMaterial reads as a flat-shaded cartoon eyeball under the scene's 52,845-watt physical spotlight. Real PBR matches the board's shader family and looks like a glossy teal orb. _makePbrTealMat() in src/assets/shell.html. Always exclude lights with intensity > 100 from the material so the physical spotlight doesn't clamp the channel to white.
4 Walkthrough JSON must match the live project. Mismatched _meta.project_name triggers a loud banner; walkthrough-gen is auto-run on start if the file is stale. Copy-paste-from-another-board class of bug. The user has been burned by this exact thing. walkthrough-gen CLI + auto_regen_if_stale() called in cli/start.rs; banner check in src/assets/shell.html.
5 Every toolbar/HUD action has a CLI subcommand. No "open the preview and click X" instructions. Ralph-loop testability + AI drivability + reproducibility. CLI-FIRST is the whole point of this app. See the CLI table below in this skill. New UI affordance? Add a CLI subcommand at the same time.
6 Babylon classes added to Adom3DViewer.BABYLON belong in our vendored vendor/3d-viewer/standalone.ts, not in shell.html workarounds. Every "is not a constructor" we hit (Quaternion, TransformNode, PBRMaterial, HighlightLayer, GlowLayer, SceneLoader) traced to the upstream adom-inc/3d-viewer only exposing 5 Babylon classes. Workaround = silent breakage; right answer = export the class from our vendored bundle and rebuild. If you see a workaround that says "the bundle doesn't export X, so we manually …", flag it as tech debt and add X to the vendored standalone.ts instead. vendor/3d-viewer/standalone.ts is our fork of Colby's upstream. Add the import + the export under window.Adom3DViewer.BABYLON.X, then bash vendor/build-viewer.sh to rebuild src/assets/js/adom-3d-viewer.min.js, then cargo build --release. Upstream stays untouched.
7 Two render modes: instanced (default, fast) and per-chip (on first per-component mutation). Default rendering keeps tscircuit's InstancedMesh packing — one source mesh + one material per package family — so the board is fast even with hundreds of passives. Any operation that needs per-component isolation (hide, future colour-override, future per-component wireframe) PROMOTES that single instance to a standalone Mesh + cloned material. Highlight uses overlay-only (no promotion). Lets the user manipulate one component without paying instancing-cost-restoration on the other 99. Promotion is sticky for the session — once R1 is standalone, it stays standalone. _promoteComponent(name) in src/assets/shell.html. Auto-runs from setComponentVisibility() on hide. Manual: adom-tsci toggle-component R1 --promote. meta.promoted = true flag on componentMap entry.
8 Trace polygons use earcut + offset polyline + half-disc endcaps + miter joins. EXACT widths from circuit.json — no Math.max(0.15, …) floor, no × 1.05 oversize. Same rule for vias and pcb_plated_holes: via_diameter, outer_diameter, hole_diameter are taken straight from circuit.json. EDA traces are flat copper polygons, not 3D pipes — must look like KiCad/Altium/Eagle output. Floors and oversize multipliers visually lie about real PCB widths and break trace-to-pad-edge alignment. The user has explicitly called out "be exact … don't be lazy" multiple times. _traceBuildOutline() + _traceBuildPolygonMesh() in src/assets/shell.html use window.Adom3DViewer.earcut. _traceRenderNet consumes seg.width and seg.via_diameter raw. _traceRenderPlatedHoles consumes outer_diameter and hole_diameter raw. Earcut is bundled in vendor/3d-viewer/standalone.ts (rebuild via bash vendor/build-viewer.sh).
9 Layer Z is derived from the actual board substrate mesh (Box0_primitive[012] bounding box), NOT window._boardBottomZ. Top-layer traces draw at top + 0.05, bottom-layer at bottom - 0.05, vias and plated-through-holes are full-thickness cylinders spanning top → bottom rotated onto the Z axis. EDA convention: top = red, bottom = blue. window._boardBottomZ is wrongly mirrored to +topZ elsewhere in the app — trusting it caused EVERY bottom-layer trace to render on top, EVERY via to have zero height (invisible discs). Discovered when the user pointed out "blue traces on top? you're missing the whole point." _traceComputeBoardZ() + _traceLayerZ() in src/assets/shell.html. Cached on enable, invalidated on disable (_traceBoardZ = null in _traceDisposeAll).
10 Trace mode is heavy-on-demand: meshes only exist while the Nets panel is open. Closing the panel calls _traceDisposeAll() which dispose every wire polygon, via cylinder, plated-hole barrel, AND every shared / per-net material. Board x-ray is also restored on close (_disableBoardXray re-applies the captured original alpha + transparencyMode + backFaceCulling per substrate mesh). Nets HUD on a 200-net board would otherwise leave thousands of mesh+material objects in memory after the user clicks ×. Same architectural rule as instanced-vs-promoted: pay the heavy cost only when the user is actively in that mode. enableTraceMode() / disableTraceMode() + _traceDisposeAll() + _disableBoardXray() in src/assets/shell.html.

If you're about to ship a change that touches highlight, hide, visibility, walkthrough, or marker material — re-read the row above and confirm it still holds. If you intentionally trade an invariant, call it out in the user-facing message so the user can veto.

Guides (child content routed by this skill)

adom-tsci is a hub for its own workflow. Child guides live as adom-tsci/guides/<name>.md in the repo and deploy to ~/.claude/skills/adom-tsci/guides/<name>.md via adom-tsci install — mirroring the adom/guides/<name>.md pattern Ray uses for his cross-tool skills. This skill's description carries the triggers; when one matches, load the referenced guide.

Guide Load when the user says Path
adom-usbc — placement, rotation, lint for USB-C receptacles on an Adom tscircuit molecule; covers TYPE-C-31-M-12, per-edge rotation math, the UsbCReceptacle wrapper, and the "no SMT pad past the board edge" rule "usb-c", "type-c", "smd usb c", "UsbCReceptacle", "place usb-c", "rotate usb-c", "usb-c east edge", "usb-c west edge" ~/.claude/skills/adom-tsci/guides/adom-usbc.md
demo-recording — making a 30-second demo video of an adom-tsci board: 30/70 workspace split, -s small captions with pre-flight wrap test, mandatory x-ray shot (hide a subset of LEDs/chips), Andrew Neural TTS narration, ffmpeg audio-mix recipe "demo video", "record demo", "30-second demo", "make a demo of this board", "x-ray shot", "show off the 3D viewer", "demo for the wiki" ~/.claude/skills/adom-tsci/guides/demo-recording.md
export-wiki — publishing a tscircuit project to the Adom Wiki as a molecule with the full interactive 3D viewer (auto-play walkthrough on load, Components / Nets HUDs, Inspect, Measure), the source bundle (lib/, package.json, walkthrough.json, plan.md), and a comprehensive AI-handoff body. Required ingredients: hero image, plan.md, body markdown that captures every design decision so a future agent can pick up "publish to wiki", "export to wiki", "wiki page for this board", "share the molecule", "make this a molecule on the wiki", "publish molecule", "ship it to the wiki" ~/.claude/skills/adom-tsci/guides/export-wiki.md

No standalone adom-usbc skill exists — USB-C placement is meaningless outside adom-tsci's workflow, so it lives under this parent, not as a sibling. If older installs left ~/.claude/skills/adom-usbc/ behind, adom-tsci install 1.3.4+ removes it automatically.

🧩 Use Adom Basic Parts — and load the wiki skills before designing

When building ANY board with adom-tsci, prefer Adom Basic Parts — the curated wiki catalog of Adom's most-used components with verified schematic symbols, PCB footprints, and 3D models (machine pins, contacts, 0402/0603/0805 passives, MLCCs, LEDs, USB-C receptacles):

LED nameplates are a signature Adom build and the catalog's LEDs are excellent — if the project smells like a nameplate (a name in an LED grid), start from Adom Basic Parts, not from random JLCPCB searches.

USB-C gets its own guide. Placement is subtle (mouth must slightly overhang the board edge or the cable can't seat) and popular enough that it's a bundled sub-skill of this app: ~/.claude/skills/adom-tsci/guides/adom-usbc.md — the CLI also nudges you (Hint: lines on start and lint) whenever it detects a USB-C part in the project. Trust those hints; they exist because USB-C was mis-placed ~20 times before the rules were written down.

🧹 Process hygiene — don't be the thread that leaks tsci devs (v1.4.23)

Every preview holds a bun tsci dev that idles at ~200 MB and grows. Historically abandoned previews accumulated for months (17 stale PID locks on one container, April to July). The rules:

  • Status before start. adom-tsci status --json; if an instance is already serving your project, REUSE it (adom-tsci open). Never start a second port for the same project.
  • Stop when the task ends. adom-tsci stop at the end of any task that started a preview. Stop now verifies death via /health and falls back to a PID-lock kill + orphan sweep if the HTTP path fails.
  • Idle reaper is on by default. Servers auto-shutdown cleanly after 30 min with no UI polls or CLI calls (--idle-timeout-mins N, 0 disables). An abandoned preview no longer outlives your attention span by more than half an hour.
  • adom-tsci ps lists every server + tsci dev on the box with RSS, age, and orphan state. adom-tsci reap [--dry-run] kills orphan tsci devs (parent server dead) and removes stale PID locks. start runs the orphan reap automatically, and all kills are per-PID (never pkill).
  • adom-tsci upgrade is scoped to tscircuit packages by default. A blanket bun update --latest pulled incompatible rollup/typescript majors and broke tsci entirely (benchmarked 2026-07-20). --all restores the old whole-project behavior when you really want it.
  • Watch the footer RSS readout (tsci dev: N MB, live from /state.tsci_dev_rss_mb). If it climbs into the GB range, recycle with adom-tsci stop && adom-tsci start.

⚡ CLI-FIRST — non-negotiable

If an adom-tsci preview is running (or you're about to start one), every interaction goes through the adom-tsci CLI. Do not reach for adom-cli hydrogen workspace add-tab, raw curl to the slingshot, fetch('/api/...') inside a shell, or any other workaround. The CLI has a subcommand for every runtime concern — including multi-instance tab management, JS eval inside the webview, console tailing, camera control, per-component visibility, and autorouter rerun.

The reflex: when you think "I need to touch the running preview", run adom-tsci --help first and find the subcommand. If the CLI doesn't expose it, that's a bug in the tool, not a reason to hand-roll bash — stop and extend the CLI instead. (If you need to ship a temporary workaround, leave a TODO: CLI gap — add <flag> breadcrumb in the skill so the gap gets closed.)

Quick-reference table — the subcommand you want is probably here:

I want to… Command
Spin up a preview (1st instance) adom-tsci start <dir>
Spin up a 2nd / Nth preview (no tab collision) adom-tsci start <dir> --port 8851 --tsci-port 3041 --tab-name "adom-tsci <ProjectName>"
Open / re-attach the webview tab on an existing slingshot adom-tsci open --port <N> [--tab-name "..."] [--display-icon mdi:led-on]
Stop the preview (clean: kills tsci dev group + releases PID lock) adom-tsci stop [--port <N>]
Re-run the autorouter on the running project adom-tsci rerun [--clean] [--fast]
Kick the 3D / PCB / Schematic tabs to re-poll the build adom-tsci reload [--port <N>]
Move the 3D camera adom-tsci view <home|front|back|left|right|top|bottom|iso> (home = ViewCube Home) or adom-tsci camera --alpha <A> --beta <B> --radius <R>
Cinematic orbit adom-tsci tour start | stop
Run the guided walkthrough adom-tsci walkthrough <start|next|prev|pause|resume|close|status>
Flip a toolbar flag adom-tsci toggle <ground|wireframe|axes>
Hide a chip to see traces under it adom-tsci toggle-component U1 --hide
List components in the loaded GLB adom-tsci list-chips
Run JS inside the webview adom-tsci eval "<js>"
Read the browser console adom-tsci console [--follow] [--tail N] [--level log,warn,error]
Health / status adom-tsci health [--port <N>], adom-tsci status [--port <N>] [--json]
Upgrade tscircuit packages in the target project adom-tsci upgrade [<dir>]
Export fab/EDA files (KiCad zip, .kicad_pcb, gerbers, STEP, netlist) adom-tsci export --format <kicad-zip|kicad-pcb|gerbers|step|readable-netlist> [--project DIR] [--output PATH]

Every write-mutation targeting the preview has --port <N> if you need to target a specific instance; defaults to 8850.

🚨 Iframe is opaque-origin — CORS is mandatory on every route

The Hydrogen webview mounts shell.html inside an iframe whose origin is opaque / null even when the URL is on your own *.adom.cloud subdomain. That means every fetch() from shell.html back to the slingshot is cross-origin from the browser's perspective. Without Access-Control-Allow-Origin: * on the slingshot response, the fetch promise rejects with a CORS error — which shell.html's old catch {} handlers silently ate. Symptom: 3D tab stuck on "Loading Adom 3D viewer…" forever, /eval/pending never consumed, /console?since=0 empty. Cost an hour to diagnose once; no more.

Enforcement already shipped:

  • cors!() and no_cache!() macros in src/server/mod.rs both emit Access-Control-Allow-Origin: * plus Methods + Headers.
  • Top-of-route() OPTIONS preflight handler returns 204 with CORS.
  • Every existing request.respond(...) call is wrapped in one of those two macros.
  • tests/cors_enforcement.rs greps the source and fails the build if you add a bare request.respond(Response::...) without the wrapper. Run with cargo test --test cors_enforcement.
  • shell.html wraps fetch calls in bridgeFetch(); any failure paints a red sticky banner at the top naming the URL + error + detected origin. If CORS regresses, you see it in 1 second.

Rules for future edits:

  1. New HTTP route on the slingshot: use cors!(resp) or no_cache!(resp). Never call request.respond on a bare Response.
  2. New fetch in shell.html: use bridgeFetch(url, opts), not plain fetch. Failures will surface in the banner automatically.
  3. If the banner appears in the tab, read it first. It names the offending URL. Don't guess or reset — look at the URL, check its route for cors!/no_cache! wrapping, add whichever fits, rebuild.

🌩 Cloudflare caching — read cloudflare-networking.md BEFORE touching the server

All Adom container traffic routes through Cloudflare, which caches assets at the edge for up to an hour regardless of what the browser thinks it's doing. When you edit shell.html, the embedded viewer bundle, or any static asset, the webview may still serve the previous version unless the response carries the Cloudflare-specific anti-cache triple:

Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate
Pragma: no-cache
Surrogate-Control: no-store     ← the header Cloudflare actually obeys

These three are set by the no_cache! macro in src/server/mod.rs. Every static-asset response goes through it. When you add a new asset route, use the macro — do not hand-roll a Cache-Control header.

Defense-in-depth: adom-tsci start / adom-tsci open both append a ?_cb={ms-since-epoch} timestamp to the webview URL on every tab creation. A URL Cloudflare has never seen before cannot hit a cached entry — this is the belt to the headers' suspenders.

Full rule: ~/.claude/skills/adom/guides/cloudflare-networking.md. Read it before writing any HTTP response on this binary. Skipping it previously cost ~2 hours of "why is the bundle not updating" thrash.

👁 Always show your latest work — non-negotiable

If you're working on anything adom-tsci is designed to visualize — a tscircuit molecule, a PCB change, a walkthrough edit, a feature of the viewer itself — the user must be looking at it live in the webview before you claim progress. The user is human; they verify with their eyes, not by reading a diff.

The reflex after any of these changes:

  1. Ensure an adom-tsci preview is running on the project you just touched (adom-tsci status --json → check project_dir matches, otherwise adom-tsci stop && adom-tsci start <dir>).
  2. Ensure the adom-tsci preview tab (or the named instance) is activated in a non-VS-Code pane: adom-cli hydrogen workspace active-tab --panel-id <P> --tab-id <T>.
  3. Ensure the webview is pointed at the slingshot URL for this instance's port — don't assume it didn't drift; adom-cli hydrogen webview navigate --tab-id <T> "$HOST/proxy/<port>/".
  4. Confirm the 3D scene is actually rendered (not stuck on "Loading Adom 3D viewer…"): adom-tsci list-chips must succeed, and adom-cli hydrogen screenshot panel --name "adom-tsci preview" should show the board — then Read the screenshot to verify with your own eyes before telling the user it's working.
  5. Only then report the change is done.

Applies to skill-structure changes too. When you change how adom-tsci install deploys skills, when you bump the binary, when you edit SKILL.md — don't stop at "the file is on disk." Run the thing against a real project and show the user the webview. Filesystem checks (ls ~/.claude/skills/...) are not proof the feature works; the webview rendering the board is.

Pitfalls that mean you're not actually showing it:

  • Webview drifted to https://adom.inc or a cached page — always screenshot-verify the URL bar shows …/proxy/<port>/.
  • Tab still says "Loading Adom 3D viewer…" — the GLB hasn't been produced yet. Run bunx tsci build lib/index.tsx --glbs --svgs --3d-png --pcb-png in the project dir and adom-tsci reload.
  • The tab is inactive / buried behind another tab in the same pane — active-tab brings it forward.
  • A second adom-tsci is running on another port and the webview is pointed at the wrong one — adom-tsci status --port <N> each instance you started.

Don't ask the user "want me to show it?" — just show it. The user told you once; don't make them ask again.

🎯 Walkthrough highlight rules — DETERMINISTIC, don't "improve" casually

The highlightComponents(names) function in src/assets/shell.html paints the "this is what the walkthrough step is pointing at" affordance. It has four design rules baked in as comments. If you change any of them, update THIS section in the same PR so the next Claude doesn't re-break the same things.

The four rules

  1. Colour is Adom TEAL — rgb(0.35, 0.75, 0.70) — NEVER amber / yellow. Amber is reserved for the Inspect tool ("what you're probing"). Teal is the Walkthrough tool ("what we're pointing at"). Mixing the two destroys the colour language of the app. There is no "more visible" colour — keep it teal, keep it consistent with the teal app accent.

  2. Always clear everything before applying a new highlight. Prior material clones, HighlightLayer meshes, AND synthesised testpoint discs all get disposed at the top of every highlightComponents call. Without this, walkthrough steps visually bleed into each other — the "capacitors" step ends up glowing the resistors that a previous step highlighted. If you add a new highlight-drawing mechanism (anything that creates geometry or mutates materials), also add its cleanup path to the dispose loop at the top of the function.

  3. Testpoints have no 3D mesh of their own. Tscircuit renders every testpoint as a flat copper pad baked into the board surface texture; the "ghost cuboid" mesh that enumerateComponents creates above each testpoint is setEnabled(false) on load (the cuboid is visually noisy and hides the pad). So:

    • HighlightLayer.addMesh(ghost) paints nothing — the mesh is hidden.
    • material.emissiveColor = ... paints nothing — same reason.
    • The ONLY way to make a testpoint highlight visible is to synthesise a small cyan disc (1.6 mm × 0.05 mm cylinder, TESTPOINT_CYAN) at the testpoint's XY position on the board's top surface (window._boardTopZ) and track it in _highlightMarkers so the next call can dispose it.

    If a user says "the testpoints aren't highlighting any more", it's almost always because someone removed the disc-synthesis path or switched it to a material/mesh approach without realising testpoints don't have one. Restore it.

  4. Empty / missing names clears everything. Nothing pins between steps. If you need a "pin this forever" primitive, build it as a separate call (e.g. the Inspect tool's pinned card) — don't leak it through highlightComponents.

  5. Classification is source_component.ftype — NOT regex-on-refdes. componentMap.kind for every component is derived from tscircuit's emitted source_component.ftype (which reflects the JSX tag the board author wrote: <capacitor/>simple_capacitor → kind capacitor). The old regex-based classify(name) (which guessed capacitor from anything starting with C) is kept as a FALLBACK for refdes prefixes tscircuit doesn't yet distinguish (testpoint / contact / mounting), and only those. Any new kind must be added to _FTYPE_TO_KIND in shell.html — NOT expressed as a regex.

    Why it matters. A board author who writes <capacitor name="CB_FILTER"> used to get misclassified as a resistor because of /^R[_\d]/. Now the ftype says simple_capacitor and the kind is capacitor regardless of what the refdes looks like. Classification agrees with the JSX source by construction.

    If you see a misclassification: the fix lives in the board's lib/index.tsx (wrong JSX tag), not in shell.html. Only touch _FTYPE_TO_KIND if tscircuit ships a genuinely new ftype.

Ralph loop = visual proof in shotlog (pup-driven). State JSON is NOT a ralph loop.

The user has been burned by this twice. A "JSON-level state test" (eg querying mesh.material.alpha for every mesh after a hide) tells YOU that the code works but it doesn't show the USER anything. The user wants to see the actual rendered result, per-component, side-by-side in shotlog so they can scroll the channel and confirm with their eyes.

Every ralph loop in this skill MUST end with shotlog-injected side-by-side BEFORE/AFTER pairs, one per component, zoomed in tight on that component. No exceptions, no "I'll check the JSON instead." If you find yourself reaching for setComponentVisibility(name, false, true) in eval and parsing the result as JSON, stop — that's a sanity check, not a ralph loop.

Drive the page through pup, NOT the Hydrogen webview

Hydrogen workspace is shared with whatever else the user is doing (aci, code editor, other panels). Touching it for a 16-component test loop is intrusive. Run the test in a pup browser window instead:

  • One pup session per app: sessionId=adom-tsci, profile=adom-tsci, url=https://<slug>.adom.cloud/proxy/<port>/. Survives sleep/wake.
  • Shotlog goes in the SAME pup window. Do NOT call shotlog open --channel X — that adds a Hydrogen tab the user has to manually close every time. The shotlog channel page is hosted at https://<slug>.adom.cloud/proxy/8820/log/<channel>/ and is reachable directly. Open it as a pup tab via: adom-desktop browser_eval '{"sessionId":"adom-tsci","expr":"window.open(\"<url>\",\"shotlog-<channel>\")"}'
  • Drive setComponentVisibility(name, visible, true) directly via browser_eval — DO NOT go through /api/toggle-component because that's a 500ms-poll path and the screenshot races it. Direct eval is synchronous in page context.

Mandatory recipe (pup + shotlog tab in pup)

SESSION=adom-tsci
PORT=8889
CHAN=my-test
DIR=/tmp/tsci-ralph; mkdir -p $DIR

# Open shotlog channel as a TAB in the pup window. NEVER `shotlog
# open` (that adds a Hydrogen tab; user has corrected this 10+ times).
SHOTLOG_URL="https://<slug>.adom.cloud/proxy/8820/log/$CHAN/"
adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"window.open('$SHOTLOG_URL','shotlog-$CHAN')\"}"

# Lock the camera + suppress UI overlays for clean diffs
adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{ document.getElementById('comp-overlay').style.display='none'; const tb=document.getElementById('toolbar'); if(tb)tb.style.display='none'; const t=document.getElementById('toast'); if(t)t.style.display='none'; const c=window.viewer.getCamera(); c.alpha=0; c.beta=0.001; c.radius=35; window.viewer.getScene().render(); return 'ok' })()\"}"

cap_shot() {
  local out=$1
  local resp=$(adom-desktop browser_screenshot "{\"sessionId\":\"$SESSION\",\"maxWidth\":1600}")
  local src=$(echo "$resp" | python3 -c "import sys,json; print(json.load(sys.stdin).get('savedTo',''))")
  cp "$src" "$out"
}

mutate() { # name, visible (true/false)
  adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{ setComponentVisibility('$1', $2, true); window.viewer.getScene().render(); return 'ok' })()\"}"
}

i=0
for COMP in $(adom-tsci list-chips --port $PORT | tail -n +2 | awk -F': ' '{print $2}' | tr ',' '\n' | tr -d ' '); do
  i=$((i+1)); num=$(printf '%02d' $i)
  cap_shot "$DIR/${num}-${COMP}-A.png"
  mutate "$COMP" "false"
  cap_shot "$DIR/${num}-${COMP}-B.png"
  convert "$DIR/${num}-${COMP}-A.png" "$DIR/${num}-${COMP}-B.png" \
    -compose difference -composite -colorspace gray -threshold 5% \
    "$DIR/${num}-${COMP}-mask.png"
  PIXELS=$(convert "$DIR/${num}-${COMP}-mask.png" -format "%[fx:w*h*mean]" info: | awk '{printf "%d", $1}')
  convert "$DIR/${num}-${COMP}-A.png" -modulate 70 \
    \( "$DIR/${num}-${COMP}-mask.png" -fill '#ff3030' -opaque white -transparent black \) \
    -composite "$DIR/${num}-${COMP}-overlay.png"
  montage -label "BEFORE" "$DIR/${num}-${COMP}-A.png" \
          -label "HIDE $COMP ($PIXELS px)" "$DIR/${num}-${COMP}-B.png" \
          -label "DIFF" "$DIR/${num}-${COMP}-overlay.png" \
    -tile 3x1 -geometry '+4+4' -background '#111' -bordercolor '#111' \
    -fill white -font DejaVu-Sans -pointsize 18 \
    "$DIR/${num}-${COMP}-trio.png"
  shotlog inject -c $CHAN -d "${num} ${COMP} ${PIXELS}px" -s pup "$DIR/${num}-${COMP}-trio.png"
  mutate "$COMP" "true"
done

Then link the shotlog channel URL in your message back to the user. The user expects to scroll the channel in the pup tab they already have open. Don't paste a single composite when the loop covered 17 components.

fit-to-component exists as a CLI for per-component zoom; if a sibling-bleed test needs zoomed shots instead of top-down full-board diffs, use it before each cap_shot call.

Ralph-loop-test before claiming the highlight works

Eyeball-verification is a wasted turn. Use the adom-tsci highlight CLI to deterministically test highlighting for each kind — flip on, screenshot, flip off, screenshot, compare.

# 1. List the kinds the current board actually has (reads componentMap,
#    reflects real ftype classification, not a guess).
adom-tsci highlight --list-kinds --port 8889

# 2. Baseline OFF screenshot — camera wherever it is now.
adom-tsci highlight --off --port 8889
adom-cli hydrogen screenshot panel --name '<board tab>' --reason 'ralph: off'

# 3. Per-kind ON screenshot — camera auto-fits to the kind.
for kind in capacitor resistor inductor chip testpoint contact mounting; do
  adom-tsci highlight --kind $kind --port 8889 || continue
  adom-cli hydrogen screenshot panel --name '<board tab>' --reason "ralph: $kind"
done

# 4. Read each pair back and compare. If the ON shot doesn't have teal
#    strokes (or cyan discs for testpoints) on every named component,
#    the highlight path is broken — fix it, rebuild, re-run.

--no-fit skips the camera zoom when you want to verify ON/OFF from the same viewpoint. --list-kinds prints counts so you know what to iterate.

Fix workflow if the diff shows the highlight is wrong:

  1. Read the SKILL.md four rules (above) first — you probably broke one.
  2. Fix the code in highlightComponents / _makeTestpointDisc / the classifier.
  3. cargo build --release && cp target/release/adom-tsci ~/.local/bin/.
  4. adom-tsci stop && adom-tsci start … so the new shell.html is served.
  5. Re-run the ralph loop. If still wrong, goto 1.

Shape of the function today

const HIGHLIGHT_TEAL = { r: 0.35, g: 0.75, b: 0.70 };  // stroke + emissive
const TESTPOINT_CYAN = { r: 0.40, g: 0.90, b: 0.95 };  // disc colour

// highlightComponents(names):
//   1. Dispose _compHighlightBackup (material clones), clear
//      _compHighlightLayer (Babylon HighlightLayer), dispose
//      _highlightMarkers (synthesised cyan markers).
//   2. For each refdes:
//      - kind === 'testpoint' → _makeTestpointDisc() at the ghost cuboid's
//        centroid XY, clamped to window._boardTopZ. Push to _highlightMarkers.
//      - otherwise → addMesh() to the HighlightLayer (teal stroke) + clone
//        the material with an emissive teal tint (60% luminance of the
//        stroke) + push to _compHighlightBackup.

🎯 Rotation-center sphere is a USER affordance — suppress it on programmatic camera moves

The little sphere that pops up at the camera target during rotation is the adom-3d-viewer's center-of-rotation indicator. It exists to tell a human user dragging with the mouse "here's what you're orbiting around." It is not a general "camera is moving" badge.

Rule: the sphere must only appear when the user is directly driving the camera with pointer input (drag to orbit, middle-click to pan, wheel to zoom). CLI-driven moves — adom-tsci view <preset>, adom-tsci camera --alpha/--beta/--radius, adom-tsci tour, /api/camera-command polling, walkthrough auto-fly, viewer.frameModel(), anything that runs through the HTTP API — must not pop the sphere. It's distracting in recorded demos and misleads the user into thinking they did something.

Implementation: shell.html wraps programmatic entry points in a _programmaticRotation flag while the move is in flight and patches the viewer's detectAndShowRotationSphere to early-return whenever the flag is set. User pointer-down on the canvas clears the flag. Any new programmatic driver added to shell.html must set this flag around its camera mutation — otherwise the sphere will leak into demo recordings.

Upstream intent: the correct long-term fix lives in adom-3d-viewer: only call detectAndShowRotationSphere from the pointer handlers, not from onBeforeRenderObservable. The shell.html patch is the workaround until that ships.

💸 Do NOT rebuild example projects unless you actually need to

Every examples/<name>/ directory in this repo ships with a pre-built dist/lib/index/ on disk3d.glb, 3d.png, pcb.svg, pcb.png, schematic.svg, circuit.json. That pre-build is the whole point of example projects. A new user (or a new Claude reading the wiki) should be able to look at the examples, screenshot them, stitch them into a demo, paste them into docs — without ever running bunx tsci build.

A rebuild is:

  • 60–180 seconds of wall time per example. Eight examples = 15+ minutes of a user's session you just ate.
  • ~$0.10–$0.50 of Anthropic token cost — log tailing, polling, error re-reads, retries. Invisible to you, very visible to the user.
  • Non-deterministic — tscircuit router output can drift between versions; a "fresh" rebuild may produce different traces than the checked-in dist/, so the example's wiki screenshots stop matching.

The rule: when the user asks you to show, screenshot, stitch, or demo an example, use the pre-built dist/ on disk. ls examples/<name>/dist/lib/index/ first. Only rebuild when:

  • dist/ is genuinely missing AND you need something it would produce.
  • The user just edited lib/index.tsx and wants to see the change.
  • You're investigating a build-time bug and the failure is the goal.

Never rebuild "to make sure it's fresh" or "to match current CLI version." The checked-in dist/ is authoritative unless proven stale by a source-tree edit. If a specific artifact is missing (e.g. 3d.png but pcb.svg exists), fall back to the sibling artifact for that scene — a rasterized pcb.svg makes a perfectly fine demo still image.

This rule applies to any other Claude helping a new Adom user too: the first contact with adom-tsci is "point me at an example" and the experience must be instant, not a 2-minute pause while eight builds run. Update the wiki example page if you change this.

No Adom Viewer dependency (v0.3.0+)

As of v0.3.0, the 3D tab is a self-contained three.js canvas — no external service, no port 8770 probe, no "AV not reachable" states. The old Basic3dView iframe and the av_bridge module were removed. The 3D view is fully AI-drivable from the CLI: every toolbar button has a subcommand, plus there's a JS eval channel for hot-patching and a console forwarder so Claude can read UI-side errors. See the "Remote-control from the CLI" section below.

📐 Schematic layout — place parts, never let them pile (v1.4.x)

tscircuit AUTO-lays-out the schematic when components have no schX/schY. For a board with many repeated parts (LED grids, resistor arrays) that means one giant unreadable vertical column. Fix: give parts explicit schematic coords.

  • Place repeated parts on a grid. Mirror the physical/pixel grid into schematic space via schX / schY (+ schRotation to orient a cell). On the OLIVER nameplate this makes the schematic literally spell the name in LED+resistor cells instead of a 90-tall column.
  • Wrapper components MUST forward schX/schY/schRotation to their inner <chip>. A wrapper that doesn't (the old UsbCReceptacle) silently defaults to the schematic ORIGIN (0,0) and piles on top of other parts. Always pass them through. (Canonical USB-C wrapper is in the adom-usbc guide.)
  • <Molecule> machine pins pile at the origin and can't be placed from the board source (they're emitted internally). Until the molecule lib places them at the schematic corners, LIFT the rest of your layout (schY offset) so the name/parts sit clear of the origin pile.

adom-tsci lint — run BEFORE shipping a board

adom-tsci lint [--project DIR] reads dist/lib/index/circuit.json and catches mistakes the tscircuit build does NOT fail on. Run it after every start / rerun. Checks:

  1. tscircuit build errors hidden in circuit.json
  2. unrouted traces (autorouter silently produced 0)
  3. ghost chips (a simple_chip source with no cad_component)
  4. USB-C / directional connector geometry — body forward of pads, mouth actually exits the board edge within a connector depth (place the receptacle at pcbX ≈ W/2 − 4 for an east edge, per the adom-usbc guide)
  5. schematic overlaps — any components whose symbols stack on each other (the classic "forgot schX/schY → piled at origin" bug). Reports the parts + worst pairs with fix hints.

A non-zero exit = board not ready to ship. The schematic-overlap check is the linter half of "smart layout that never overlaps."

🎬 Walkthrough is GENERATED, never hand-written

The Walkthrough is built deterministically by adom-tsci walkthrough-gen from circuit.json (intro → connectors → ICs → grouped passives → one nets x-ray step → outro). Rules:

  • Never hand-edit walkthrough.json — it carries a _meta.do_not_edit stamp and is OVERWRITTEN on regen.
  • It auto-regenerates on adom-tsci start whenever circuit.json drifts (SHA changes) — so editing the board + restarting the preview is enough; you don't author it.
  • Per-board facts are computed (connector edge from PCB position, counts, nets) — not guessed; long refdes lists are capped (…+N more).
  • It finishes on the ViewCube Home view with the Nets / x-ray panel OFF — a tour must never leave the board mid-orbit or the x-ray meshes loaded.
  • Want a custom intro story? Put it in the board's package.json description (it becomes the intro step and survives regen). Don't hand-edit the JSON.

🪟 Ground plane is 65% transparent (HARD invariant)

The 3D ground MUST stay 65% transparent (board bottom shows through). It's the viewer's own shadowGround mesh (groundMat, PBR) — applyGroundTransparency sets alpha 0.35 and an every-frame guard re-asserts it (goHome/frameModel reset it otherwise). The footer shows the live ground: 65% transparent readout. Verify after any viewer change with adom-tsci view bottom + a screenshot — never claim it's fine without seeing the board's underside through the ground.

What's new in v0.4.x (history)

Everything below is in the v0.4.0 / v0.4.1 binary that's on the wiki right now:

  • Inspect tool (🔍 / keybind I). Hover any feature on the board for a labelled info card: chip refdes + footprint + pin count + JLCPCB part #; pad → pin name + net + "Connects to MC1.pin1, TP1.pin1" via N direct traces + solder-mask state; plated hole → drill/outer diameters + role (mechanical-only vs wired); via → drill/pad/bridged layers/net; silkscreen → text + layer + parent; board → dims + thickness + layer count + baked texture resolution. Click to pin, Esc unpins. Fields tscircuit doesn't emit today (MPN, datasheet, surface finish) render as with a tooltip explaining and linking to upstream feature requests filed in TSCIRCUIT_FEATURE_REQUESTS.md.
  • Measure tool (📏 / keybind M). Fusion 360-style smart picking (pad / hole / chip / pin / edge), precision + secondary units (mm / inches / mils), vertex snap. Hover highlights in cyan; Inspect uses amber so you can tell them apart.
  • Walkthrough Demo (🎬). A narrated 10-step tour. Auto-advances based on narration text length; orbit auto-pauses; 3D flyover animations on the contact-ring + testpoint steps (6 waypoints per step, ease-in-out, so the camera visibly scans instead of staring). CLI drivable: adom-tsci walkthrough start | next | prev | pause | resume | close | status.
  • Board-surface texture resolution — tscircuit bakes silkscreen + copper pads + annular rings + solder mask into a single PNG per board face, hardcoded at 1024. On big boards that's unreadable at zoom. adom-tsci start --texture-resolution 8192 re-bakes the GLB via circuit-json-to-gltf and caches by circuit.json content hash (first bake ~75s; cache hit ~190ms). Current resolution is shown on the "Board Surface → Baked" row of the Components HUD.
  • Components HUD group toggles. Master eye per group (●/○/◐). Test points are hidden by default — tscircuit's default cuboid placeholders add noise without detail; toggle them on when needed.
  • Custom monochrome SVG toolbar icons per the Adom brand rule — no emoji anywhere. Ruler with ticks for Measure, magnifier with a data-point for Inspect, clapperboard with play arrow for Walkthrough.
  • Robust shutdown + PID lock (v0.4.1). adom-tsci start scans /proc for stale bun tsci dev processes from prior runs and SIGTERM+SIGKILL them before spawning. Atomic PID lock at /tmp/adom-tsci-<port>.pid refuses to double-start on the same port. Fixes the "15 orphan tsci devs from 15 crashed restarts" situation that used to compound.

Keyboard shortcuts

Key Action
M Toggle Measure tool
I Toggle Inspect tool
G Toggle ground plane
W Toggle wireframe
C Toggle Components panel
Space Pause / resume walkthrough
Esc Unpin Inspect card / close tool

Shortcuts only fire when the 3D tab is active and no input field is focused.

Credit to Ray

This app is an onboarding layer over Ray's authoritative tscircuit workflow guide at ~/.claude/skills/adom/guides/adom-tscircuit-skill.md (author: ray, 22 kB). Everything this tool does with tscircuit — the <Molecule> component, MachineContactMedium placement rules, the sizing grid, the DRC notes, the bunx tsci build --glbs --svgs flags, the project skeleton, the SN65HVD230 reference — traces back to Ray's guide. If you're building more than a single-chip molecule (connector families, molecule generators, review servers, DRC tooling), go read Ray's guide. This app doesn't replace any of it; it just makes the "show me what I'm building" step trivial.

Quick start

Zero-to-preview — cold container, nothing installed

Run these top-to-bottom. The whole thing takes ~2 min on a warm cache.

# 1. bun (the tscircuit runtime — may not be pre-installed)
command -v bun >/dev/null || curl -fsSL https://bun.sh/install | bash
export PATH="$HOME/.bun/bin:$PATH"

# 2. adom-tsci binary + skill
adom-wiki pkg install adom/adom-tsci
#    (no adom-wiki? curl fallback — release assets are versioned, no "latest" alias:
#     V=$(curl -s https://wiki.adom.inc/api/packages/adom/adom-tsci/versions | python3 -c "import sys,json;print(json.load(sys.stdin)['versions'][0]['version'])")
#     curl -fsSL "https://wiki.adom.inc/download/adom/adom-tsci/$V/adom-tsci" -o ~/.local/bin/adom-tsci
#     chmod +x ~/.local/bin/adom-tsci && adom-tsci install)

# 3. Get a project. If you don't have one yet, pull the pre-built examples
#    bundle from the wiki release (no GitHub access needed; skip if you
#    already have a tscircuit project):
V=$(curl -s https://wiki.adom.inc/api/packages/adom/adom-tsci/versions \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['versions'][0]['version'])")
curl -fsSL "https://wiki.adom.inc/download/adom/adom-tsci/$V/adom-tsci-examples.tgz" | tar xz
cd examples/SN65HVD230-Molecule && bun install

# 4. CRITICAL: adom-tsci start refuses to create its webview tab unless an
#    existing Web View pane is already present ("no existing Web View pane
#    found" error). If the layout is a single VS Code pane, split one:
VSCODE_PANEL_ID=$(adom-cli hydrogen workspace tabs \
  | python3 -c 'import json,sys; [print(t["panelId"]) for t in json.load(sys.stdin)["tabs"] if t["name"]=="Visual Studio Code"]' \
  | head -1)
adom-cli hydrogen workspace split \
  --panel-id "$VSCODE_PANEL_ID" \
  --direction horizontal \
  --panel-type adom/a1b2c3d4-0031-4000-a000-000000000031 \
  --display-name "Web View" --display-icon "mdi:web" --ratio 0.5

# 5. Start the preview — auto-opens a tab in the Web View pane above.
adom-tsci start .

# 6. If the pane ever gets closed (e.g. after an approval-dialog cancel
#    resets the workspace), re-split step 4 and run:
adom-tsci open

Day-to-day

# From any tscircuit project directory (webview pane already present):
adom-tsci start .
# …edit lib/index.tsx, click Re-run autorouter, stop with:
adom-tsci stop

Web View panel UUID (for reference): adom/a1b2c3d4-0031-4000-a000-000000000031. Every split that lands the adom-tsci tab needs this --panel-type.

Moving or renaming a project directory — don't rebuild

When you relocate an existing tscircuit project (e.g. promoting a scratch project in tscircuit-projects/ to adom-tsci/examples/), just mv it. Don't delete node_modules / bun.lock / dist and re-run bun install — that's minutes of wasted time and bandwidth on every move.

  • node_modules has no path-dependent state. Bun / npm symlink binaries inside the folder; nothing points out to the old location. Moving the whole tree keeps every link valid.
  • Changing the name field in package.json does not invalidate installs — deps are keyed by the dep graph, not the root package name. You can rename @tsci/scratch.Foo@tsci/john.Foo without re-installing.
  • bun.lock is relative and portable. Keep it.
  • dist/ is stale after an edit anyway; you'll rebuild the next time you change lib/index.tsx. No reason to pre-emptively wipe it at move time.

Safe sequence:

# stop any live preview watching the old path
adom-tsci stop --port <port>

mv old/path/MyChip-Molecule new/path/MyChip-Molecule

# edit package.json if renaming scope — that's it, no reinstall
adom-tsci start new/path/MyChip-Molecule --port <port> \
  --tab-name "MyChip (new location)" --display-icon "mdi:chip"

Rule of thumb: bun install is for dependency changes (bumping versions, adding/removing packages), not location changes.

The UI

┌──────────────────────────────────────────────────────────────────────────┐
│ [3D*]  [PCB]  [Schematic]   GLB: 8:56:56 AM   ⟳ Re-run autorouter  │
│                                                ─────   tsci live    │
├────┬─────────────────────────────────────────────────┬───────────────────┤
│ ⌂  │                                                 │  COMPONENTS       │
│ T  │                                                 │   U1    CHIP      │
│ F  │               (three.js canvas —                │   U2    CHIP      │
│ R  │            self-contained, no AV)               │   C1    CAPACITOR │
│ I  │                                                 │   Y1    CRYSTAL   │
│ ⎚  │                                                 │   TP1   TESTPOINT │
│ ▭  │                                                 │   ...             │
└────┴─────────────────────────────────────────────────┴───────────────────┘

Primary tabs (first-class, big, prominent):

  • 3D (default). A full-bleed canvas rendered by the real Adom 3D Viewer (adom-3d-viewer.min.js, the same Babylon-based engine the rest of Adom uses for 3D), bundled into the adom-tsci binary so there is no AV service dependency — no port 8770 probe, no "AV not reachable" states. Shadows, IBL environment, transparent ground plane all work out of the box.

    Everything else floats over the canvas so it doesn't steal real estate:

    • Floating toolbar (top-centre, draggable via the ⋮⋮ grip): ⌂ home (iso), ◱ frame, T top, F front, R right, I iso, ▭ ground, ⎚ wireframe, ⊡ ortho/perspective, 📏 measure.
    • Measure tool: click 📏 then click two points on the model to get the distance between them in millimetres, with an on-screen label at the midpoint. Click a third time to clear.
    • Floating component panel (top-right, draggable via header, collapsible with , dismissible with ×, restorable via the button): every chip / passive / testpoint / machine contact from circuit.json, listed with kind. Click a row to hide that component — the "hide U1 to see the traces routed under the FPGA" use case. Click "all on" to restore.

    Mesh → component-name mapping: tscircuit's GLB writer names meshes Box0_primitive0 / OBJBox12_primitive1 (no component names in the scene graph). The UI fetches /circuit.json, auto-detects the GLB unit scale (meters vs mm), probes orientation via mounting-post corner positions, and groups meshes by nearest-neighbour assignment within a per-kind tolerance (chip=10mm, testpoint=1.2mm, contact=2mm, …). Meshes outside tolerance land in a catch-all Board group so the board outline and any unmatched geometry is still toggleable.

  • PCB. Inline dist/lib/index/pcb.svg in a panzoom container. Scroll wheel zooms toward cursor, drag to pan. Lazy-loaded on first click.

  • Schematic. Inline dist/lib/index/schematic.svg, same panzoom. Lazy-loaded on first click.

Secondary tab:

  • tsci live. The raw tscircuit RunFrame served through the slingshot proxy. Escape hatch for features tscircuit ships in the future that we haven't wrapped yet. Dimmed via CSS filter: brightness(0.78) because RunFrame's light theme is hardcoded and would be blinding inside a dark editor.

The Re-run autorouter button

Top-right of the header. Click it to:

  1. Touch lib/index.tsx via the slingshot → tsci dev file server. A no-op POST /api/files/upsert fires a FILE_UPDATED event, tsci dev re-evaluates the circuit (which re-runs the autorouter), and RunFrame (the tsci live tab) refreshes its in-memory view.
  2. Spawn bunx tsci build lib/index.tsx --glbs --svgs in the background. This regenerates dist/lib/index/{3d.glb,pcb.svg, schematic.svg} on disk, which the first-class 3D / PCB / Schematic tabs poll for (/glb/meta every 3 s). Within ~30 s the tabs re-render with the new output.
  3. Shift-click for a clean re-run. Holding Shift while clicking deletes manual-edits.json first, so any interactive trace edits made in RunFrame's PCB editor are discarded and the autorouter runs from scratch.

Same feature from the CLI:

adom-tsci rerun                # re-run autorouter, preserve manual edits
adom-tsci rerun --clean        # also delete manual-edits.json first

Stale-deps check

On adom-tsci start, a background thread queries https://registry.npmjs.org/@tscircuit/cli/latest and compares against the version field in <project>/node_modules/@tscircuit/cli/package.json. If the installed version is behind the latest, you see a HINT: line:

HINT: @tscircuit/cli is out of date (installed 0.1.1226, latest 0.1.1234).
Run `adom-tsci upgrade` to refresh.

The check is non-fatal (3-second timeout, no network → no warning) and never blocks startup. tscircuit does daily releases — this just nudges you when you're behind.

adom-tsci upgrade [project-dir] shells out to bun update --latest in the target directory, upgrading every dep to its latest version (not just tscircuit). Restart adom-tsci start for changes to take effect.

Credit to the slingshot: the 127.0.0.1 problem

tsci dev emits a hardcoded absolute URL in its HTML shell:

<script>
  window.TSCIRCUIT_FILESERVER_API_BASE_URL = "http://127.0.0.1:3040/api";
</script>
<script type="module" src="/standalone.min.js"></script>

Inside a Hydrogen webview iframe, 127.0.0.1 resolves to the user's machine, not the container, so every fetch fails and RunFrame hangs at loading... forever.

adom-tsci's slingshot:

  1. Fetches the shell HTML from 127.0.0.1:3040/.
  2. Rewrites the two hardcoded references to relative paths: TSCIRCUIT_FILESERVER_API_BASE_URL = "api" and <script src="standalone.min.js">.
  3. Proxies /runframe/api/* and /runframe/standalone.min.js to 127.0.0.1:3040, preserving HTTP method, body, and query string. A v0.1 bug dropped query strings on forward, which broke files/get?file_path=...; fixed in v0.2 with a full method-aware proxy.

Tsci dev uses HTTP polling (/api/events/list?since=<id>) for hot reload — no WebSockets — so the slingshot is a plain HTTP forwarder.

3D scene conventions (Z-up, XY = board)

The Babylon scene in shell.html is Z-up: the board's copper / silkscreen surface lies in the XY plane, and +Z is the board normal (above-board direction). Component positions from circuit.json (pcb_component.center.x/y) map directly to world X/Y, and window._boardTopZ holds the Z of the top copper layer.

This matters whenever you add overlay geometry to the scene. Babylon's MeshBuilder.CreateDisc and MeshBuilder.CreatePlane create meshes in the XY plane with normal +Z by default, which — in this scene — already lies flat on the board. Don't rotate them. A rotation.x = π/2 stands the overlay vertical, edge-on to the board, which is wrong for anything that's meant to sit on the copper.

Highlight pattern: pad-surface overlay for baked-texture features

Components whose "body" is baked into the board-surface texture (test points, silkscreen markers, contact pads) have no separate mesh to tint. Their 3D representation is just texels on the PCB surface. Hover-highlight via material clone fails — there's no mesh.

The pattern used for testpoints is:

  1. On enumeration, store each component's (x, y) world position and its pcb_smtpad geometry (shape, radius / width / height) on componentMap.get(name).pad.
  2. In highlightComponents, count how many of a component's meshes are actually isEnabled(). If zero, take the overlay path.
  3. Overlay path: create a CreateDisc (circle pads) or CreatePlane (rect pads) at (x, y, _boardTopZ + 0.3), sized to the exact pad geometry. No rotation — it lies flat. Z offset ≥ 0.3 mm is load-bearing: at 0.1 mm the overlay z-fights the baked pad texture and disappears from top-down.
  4. sideOrientation: 2 (DOUBLESIDE) on CreateDisc / CreatePlane options. Default single-side culls the face whose normal points away from the camera — a top-down view of an XY-plane disc can show the back face, rendering nothing.
  5. Material: disableLighting = true, emissiveColor vivid + opaque (e.g. Color3(0.45, 1.0, 0.95), no alpha). An alpha-blended dim cyan over the green board is invisible — it reads as a shadow, not a highlight. Keep it opaque, let the pad geometry do the visual work. renderingGroupId = 2 draws over silkscreen + pads.
  6. Track overlays in _compHighlightOverlays and dispose every mesh + material on each highlightComponents() call so they clear when the hover leaves.

Effect: the gold pad that's baked into the board-surface texture appears to light up cyan when you hover its row in the Components HUD, giving the user something to visually toggle for a feature that otherwise has no 3D body to tint.

Highlight glow — manual halo (no GlowLayer in bundled Babylon)

The bundled adom-3d-viewer.min.js does not ship BABYLON.GlowLayer, HighlightLayer, or any post-processing pipeline. Cross-wiring CDN-Babylon's GlowLayer onto the bundled scene fails because newer Babylon's GlowLayer calls scene.addObjectRenderer, which the older bundled runtime doesn't expose.

Fake it with a manual halo:

  • Chips / resistors / capacitors (meshes with body): clone the mesh via baseMesh.clone(name, null, true), parent it to the base, scale 1.06x, apply an additive-blended emissive material (alphaMode = 1 = ADD, disableLighting = true, alpha = 0.5). The additive blend makes overlapping pixels brighter than either source — reads as bloom.
  • Testpoints (overlay discs): stack 2 concentric discs underneath the solid cyan core at 1.8x + 2.6x radius, with alpha = 0.45 / 0.22 and additive blend. Place at progressively lower Z (0.005 mm decrement) so the solid core still wins depth but the halo bleeds outward.

Both get disposed by _disposeHalos() on every highlightComponents() call so the scene doesn't accumulate stale halos as the walkthrough or hover state changes.

Walkthrough steps auto-highlight their focus

walkthroughRenderStep derives the highlight set from step.focus whenever step.highlight isn't explicitly provided:

focus.kind auto-highlight
component + name [name]
components + names names
kindAll + componentKind every componentMap entry whose kind matches (e.g. all testpoints)
anything else (view, all, silkscreen) []

This is why a step whose focus is { kind: 'kindAll', componentKind: 'testpoint', flyover: true } lights up every testpoint pad with a cyan overlay disc for the duration of that step — even without an author-provided highlight list. The step is about those components by definition, so they should be tinted.

Explicit step.highlight still wins when set — useful for cross-cutting steps that want to highlight components outside the focus target (e.g. "this chip talks to THAT chip").

Don't confuse "toggle visibility" with highlight

Toggle (setComponentVisibility) and highlight (highlightComponents) touch the same meshes but differently:

  • Toggle uses mesh.visibility = 0.05 | 1.0 (per-mesh, works with shared materials). Always highlightComponents([]) first to restore any clone materials — otherwise the visibility change lands on a short-lived clone that gets disposed when the row re-renders.
  • Highlight clones the mesh's material and tints the clone's emissiveColor. For baked-texture components (no enabled mesh), falls back to the overlay-disc path above.

The two mechanisms share state through componentMap.get(name).meshes but one-way: toggle never reads highlight state, highlight restores any clones it created before doing its work.

CLI commands

Every command follows the Adom CLI conventions (OK: / ERROR: / Hint: prefixes, both human and --json output).

Command Purpose
adom-tsci start <dir> [--port 8850] [--tsci-port 3040] [--no-open] [--texture-resolution N] Spawn tsci dev, start the slingshot + shell server, open the webview tab. --texture-resolution N re-bakes the board-surface texture at N×N (default 1024; recommend 2048 for 60-100 mm boards, 4096 for 100-200 mm, 8192 for >200 mm)
adom-tsci stop Clean shutdown of the preview server + the tsci dev process group (and the descendant sweep + PID lock release — no more orphan bun processes)
adom-tsci walkthrough <start|next|prev|pause|resume|close|status> Drive the Walkthrough Demo from the CLI / AI
adom-tsci status [--json] GET /state from the running instance
adom-tsci open Re-add or navigate to the webview tab
adom-tsci reload Force the 3D / PCB / Schematic tabs to re-poll the build outputs
adom-tsci rerun [--clean] Re-run the tscircuit autorouter (optionally deleting manual-edits.json)
adom-tsci upgrade [dir] bun update --latest in the target tscircuit project
adom-tsci export --format <F> Fab/EDA export via tsci with the --output path quirk handled: kicad-zip (KiCad 7/8 project), kicad-pcb, gerbers, step, readable-netlist. Writes <project>/<name>.<ext> by default
adom-tsci health Probe tsci_reachable
adom-tsci install Drop SKILL.md into ~/.claude/skills/adom-tsci/
adom-tsci --version Print version

Remote-control the 3D viewer from the CLI (no clicking):

Command Purpose
adom-tsci view <preset> Camera preset: home (ViewCube Home — default angle + fit) / front / back / left / right / top / bottom / iso / isometric
adom-tsci camera [--alpha A] [--beta B] [--radius R] Raw alpha/beta (radians) + radius multiplier
adom-tsci tour start | stop Cinematic slow-orbit camera
adom-tsci toggle <flag> Flip a toolbar flag: ground, wireframe, axes
adom-tsci toggle-component <NAME> [--hide | --show] Show/hide a single component — e.g. adom-tsci toggle-component U1 --hide to see traces under the FPGA
adom-tsci list-chips List the components the 3D viewer discovered in the current GLB
adom-tsci eval "<js>" Run a JS snippet inside the running webview, print the result
adom-tsci console [--follow] [--tail N] [--level log,warn,error] Print the JS console log forwarded from the webview (all console.* + uncaught errors)

HTTP API

Every CLI verb is a thin wrapper over an HTTP endpoint. Curl them directly if you want to script without the CLI. All on 127.0.0.1:8850 by default.

Route Method Purpose
/ GET The tabbed shell HTML
/runframe/ GET Slingshot root: rewrites tsci dev's HTML shell to relative paths
/runframe/api/* any Full HTTP proxy to 127.0.0.1:<tsci-port>/api/* (method, body, query string preserved)
/runframe/standalone.min.js GET Streamed proxy of the 8.6 MB RunFrame bundle
/adom.css, /favicon.svg GET Static brand assets
/glb GET Streams <project>/dist/lib/index/3d.glb
/glb/meta GET {mtime, size} for polling
/pcb.svg GET Streams <project>/dist/lib/index/pcb.svg
/schematic.svg GET Streams <project>/dist/lib/index/schematic.svg
/circuit.json GET Streams <project>/dist/lib/index/circuit.json (UI uses it to map GLB meshes to component names)
/rerun[?clean=1] POST Touch lib/index.tsx via upsert + spawn bunx tsci build --glbs --svgs
/reload POST Bump GLB mtime to force 3D tab reload
/health GET {ok, tsci_reachable, project_dir}
/state GET Server state + toolbar flags + component visibility + component list
/shutdown POST Kill tsci dev process group, return 204, exit
/api/set-view POST Body {"view":"top"} — camera preset
/api/set-camera POST Body {"alpha":…,"beta":…,"radius":…} — raw camera
/api/tour POST Body {"action":"start"|"stop"} — slow orbit
/api/toggle POST Body {"name":"wireframe"} — toggle a toolbar flag
/api/toggle-component POST Body {"name":"U1"[,"visible":false]} — show/hide a component
/api/components GET/POST GET returns the UI-discovered component list; POST is how the UI publishes it after GLB load
/api/camera-command GET UI polls this every 500 ms; returns the latest queued command and atomically clears it
/console GET/POST POST from UI appends a console message; GET returns {messages, next_since} for the CLI adom-tsci console
/eval POST Body {"code":"…"} — queue a JS snippet; returns {"id":…}
/eval/pending GET UI polls this; returns the next pending snippet or 204
/eval/:id/result POST UI posts the snippet's result
/eval/:id GET AI polls for the result

curl examples:

# Health check
curl -sS http://127.0.0.1:8850/health

# Re-run autorouter, clean mode
curl -sS -X POST "http://127.0.0.1:8850/rerun?clean=1"

# Fetch the PCB SVG
curl -sS http://127.0.0.1:8850/pcb.svg > pcb.svg

# Stop the server
curl -sS -X POST http://127.0.0.1:8850/shutdown

Architecture

+---------------------------+        +---------------------------+
|  Hydrogen webview tab     |        |  adom-tsci (Rust)         |
|  /proxy/8850/             |<------>|  tiny_http on 127.0.0.1:8850
|                           |        |                           |
|  +----------+             |        |  /              shell.html
|  |   3D     |<------------+------->|  /glb           file stream
|  +----------+             |        |  /pcb.svg       file stream
|  |  PCB     |             |        |  /schematic.svg file stream
|  +----------+             |        |                           |
|  | Schematic|             |        |  /runframe/     slingshot
|  +----------+             |        |  /runframe/api/*  (proxy)
|                           |        |  /runframe/standalone.min.js
|  +----------+             |        |                           |
|  | tsci live|<------------+------->|  /compare       compare.html
|  +----------+             |        |  /av/basic3d    AV bridge
|  | 3d cmp   |             |        |  /av/upload-glb AV bridge
|  +----------+             |        |                           |
|                           |        |  /rerun         [-> upsert + spawn build]
|  ⟳ Re-run autorouter -----+------->|  /reload  /shutdown  /health
+---------------------------+        +---------------------------+
                                              |           |
                                              v           v
                          +--------------------+   +----------------+
                          | bun tsci dev       |   | AV (8770/8771) |
                          |   -p 3040          |   | Basic3dView +  |
                          | (process group)    |   | serve_glb      |
                          +--------------------+   +----------------+

Example session

User: preview my SN65HVD230 molecule

Claude: (runs adom-tsci start ~/project/adom-tsci-projects/SN65HVD230-Molecule)

Output:

OK: spawning tsci dev -p 3040
HINT: @tscircuit/cli is out of date (installed 0.1.1226, latest 0.1.1234).
      Run `adom-tsci upgrade` to refresh.
OK: tsci dev is ready
OK: webview tab created
OK: adom-tsci running — project=... port=8850 tsci_port=3040

The adom-tsci webview tab opens in the editor (with a brand-compliant #e6edf3 monochrome chip icon in the tab bar). The 3D tab is active by default, showing the molecule in a clean dark-themed three.js viewer. User rotates with the mouse, clicks through to PCB, then Schematic, sees the tscircuit SVGs in panzoom containers.

User: move R_TERM to pcbX=8

Claude: (edits lib/index.tsx) Done. Click ⟳ Re-run autorouter in the header to regenerate the build outputs, then the 3D / PCB / Schematic tabs will auto-refresh.

User: (clicks the button)

Shell flashes ⟳ Running…⟳ Re-ran. ~30 s later the 3D tab shows R_TERM at its new position; PCB and Schematic re-fetch on next click.

User: ship it

Claude: (runs bunx tsci push)

Troubleshooting

Error: no existing Web View pane found adom-tsci start will refuse to create its tab over a non-webview pane. See step 4 of the Zero-to-preview section above for the exact workspace split command — it needs --panel-type adom/a1b2c3d4-0031-4000-a000-000000000031. Once a Web View pane exists, re-run adom-tsci start, or adom-tsci open if the server is already running.

Webview tab vanished mid-session (workspace reset) Dismissing certain approval dialogs (screen-share, AI events access) can reset the Hydrogen layout back to a single VS Code pane and drop the adom-tsci tab. The server keeps running; just re-split the pane (step 4 above) and run adom-tsci open to re-attach.

HINT: @tscircuit/cli is out of date Not an error. tscircuit does daily releases; adom-tsci upgrade in your project directory pulls the latest. Or ignore it — the hint is fire-and-forget.

Re-run autorouter button does nothing visible The rebuild is async and takes ~30 s. Watch the GLB: <time> pill in the header; when it updates, the 3D tab has re-rendered. PCB and Schematic lazy-reload on next click.

tsci dev crashed with MethodNotAllowedError: only POST accepted That's tsci dev's own internal fileserver spitting non-fatal noise during its node_modules upload on startup. Doesn't break anything — the server recovers. Check /health to confirm tsci_reachable: true.

Webview stuck at Loading files... Known tscircuit v0.1.1226+ issue. RunFrame's isLoadingFiles flag only clears when files/list + files/get?file_path=... both succeed. In v0.1 of this app, the slingshot dropped query strings on forward, which broke files/get; v0.2 forwards the query string. If you're hitting this on v0.2, curl http://127.0.0.1:8850/runframe/api/files/get?file_path=lib/index.tsx and see if the response has the real file content.

3D tab — flat-lit, no shadows, no transparent ground The same-origin bridge didn't run. Symptoms:

  • ground plane is opaque (not 50% transparent)
  • no board shadow on the ground
  • iframe.contentWindow.viewer is undefined

Causes to check:

  1. Different origin. If the shell is on /proxy/8850/ but Basic3dView is on a different host (not /proxy/8770/ on the same subdomain), the contentWindow.viewer access will throw SecurityError: Blocked a frame with origin. Check that VSCODE_PROXY_URI is set to the same subdomain as AV's 8770.
  2. Viewer not yet constructed. The bridge polls for up to 10 s waiting for scene.meshes to have at least one non-ground mesh. On a cold cache where Basic3dView has to download its Babylon bundle + the GLB, this may not be enough.
  3. basic-3d.html changed module structure. The bridge relies on var viewer being top-level scope in a non-module <script>. If upstream Adom wraps basic-3d.html in type="module", the bridge breaks. Confirm with curl .../proxy/8770/basic-3d.html | grep 'var viewer'.

3D compare pane is still flat-lit Known v0.2 limitation. The 3d compare secondary tab lives in a separate compare.html where both Basic3dView and three.js use default lighting. The compare view is for material-fixup regression, not for aesthetic rendering. Tracked for v0.3.

"Some sites block embedding" banner at the top of the webview That banner is a persistent affordance shown on every webview tab regardless of whether the page loaded. It is not an error signal. If the panel below it is actually blank, check /health and the tsci-preview.log file for subprocess errors.

tsci dev subprocess doesn't die after stop Shouldn't happen in v0.1.0+. tsci dev is spawned in a new process group (setsid) and the whole group is killed on shutdown. If you see an orphan bun ... tsci dev after adom-tsci stop, file an issue with ps auxf | grep tsci output.

Exporting fab files (gerbers / BOM / CPL)

adom-tsci is a preview tool, but the underlying tsci CLI can emit manufacture-ready files. Four gotchas to know before the obvious command actually ships a manufacturable package.

Gotcha 0 — tsci export does NOT write dist/. Only tsci build does. If you're inspecting dist/lib/index/circuit.json to check route counts, autorouter errors, or component sizes, make sure the last thing you ran was tsci build (not tsci export). If you only ran export and then grep dist/, you're reading a stale circuit.json from the previous build — which can make it look like routing failed when the gerber zip it just wrote actually has full routing. Always:

bunx tsci build   lib/index.tsx --glbs --svgs   # writes dist/, regenerates circuit.json
bunx tsci export  lib/index.tsx --format gerbers --output ../gerbers.zip   # reads current dist/, writes zip

If you need the latest numbers after an export, run build again or extract + parse a .gbr file directly (e.g. grep -c '^X.*D0[12]' on F_Cu.gbr counts draw operations).

Gotcha 0.5 — BOM + CPL inside gerbers.zip are EMPTY. tsci export --format gerbers writes bom.csv and pick_and_place.csv into the zip, next to the gerber files. JLCPCB reads those (not any sibling file on disk) when you upload the zip. But tsci's versions are stub CSVs with only designators + values — no LCSC, no MFR part number, no package — so the JLCPCB assembly tool will show "no parts matched" on every row regardless of what you put in fab/bom.csv alongside.

Fix: after tsci export, splice your populated BOM/CPL into the zip over tsci's empty ones. Quick Python:

import zipfile, shutil
src = 'fab/gerbers.zip'; tmp = 'fab/_tmp.zip'
with zipfile.ZipFile(src,'r') as zin, zipfile.ZipFile(tmp,'w',zipfile.ZIP_DEFLATED) as zout:
    for n in zin.namelist():
        if n in ('bom.csv','pick_and_place.csv'): continue
        zout.writestr(n, zin.read(n))
    zout.writestr('bom.csv',           open('fab/bom.csv').read())
    zout.writestr('pick_and_place.csv',open('fab/cpl.csv').read())
shutil.move(tmp, src)

Verify with unzip -p fab/gerbers.zip bom.csv | head -3 — first row after the header should show your real part number, not a blank.

Gotcha 1 — --output path resolution. tsci export <file> --output <path> treats the output path as relative to the entrypoint's directory (lib/), including absolute paths, which fails with

Error writing file: ENOENT ... lib/<your path>/gerbers.zip

Workaround: pass a ../-prefixed relative path and move the file after.

cd ~/project/your-molecule
bunx tsci export lib/index.tsx --format gerbers --output ../gerbers.zip
mv gerbers.zip fab/

Gotcha 2 — inner-layer support. Out of the box, @tscircuit/cli only emits F_Cu and B_Cu gerbers, regardless of whether the board declares layers={4}. Inner-layer traces are routed on inner1 / inner2 in circuit.json but never written out, so a 4-layer board ships as a 2-layer gerber package (silent data loss — fab will build wrong).

Fix (patch node_modules/@tscircuit/cli/dist/cli/main.js until it's upstreamed):

  1. Extend the layer-prefix map to include inner layers:
    var layerRefToGerberPrefix = {
      top: "F_", bottom: "B_",
      inner1: "In1_", inner2: "In2_"   // ← add
    };
    
  2. In convertSoupToGerberCommands, add the two new copper layer headers to the glayers object between F_Paste and B_Cu:
    In1_Cu: getCommandHeaders({ layer: "inner1", layer_type: "copper" }),
    In2_Cu: getCommandHeaders({ layer: "inner2", layer_type: "copper" }),
    
    and include "In1_Cu", "In2_Cu" in the aperture-loop array.
  3. Change the two hardcoded trace/via loops from ["top", "bottom"] / ["top", "bottom", "edgecut"] to include inner layers:
    for (const layer of ["top", "inner1", "inner2", "bottom", "edgecut"]) {
    
  4. Update getAllTraceWidths to return inner1 + inner2 width lists.
  5. File extension fix — most fabs (JLCPCB, PCBWay, Eurocircuits) reject .gbr on inner layers and expect .g1 / .g2 (Protel convention). Change the zip-write loop:
    for (const [fileName, fileContents] of Object.entries(gerberFileContents)) {
      const ext = fileName === "In1_Cu" ? "g1"
                : fileName === "In2_Cu" ? "g2"
                : "gbr";
      zip.file(`${fileName}.${ext}`, fileContents);
    }
    

With all five patches applied, the resulting zip contains

F_Cu.gbr    In1_Cu.g1    In2_Cu.g2    B_Cu.gbr
F_Mask.gbr  B_Mask.gbr   F_Paste.gbr  B_Paste.gbr
F_SilkScreen.gbr   B_SilkScreen.gbr   Edge_Cuts.gbr
drill.drl   drill_npth.drl   bom.csv   pick_and_place.csv

and JLCPCB's upload will accept it as a proper 4-layer board.

Upstream this: the patches above are stopgaps in a local node_modules. When the project is rebuilt from a fresh install they get wiped — the real fix is a PR to tscircuit/tscircuit-cli extending convertSoupToGerberCommands with configurable layer lists

  • the .g1/.g2 extension map.

Other useful formats (all honour the same --output ../ trick):

Flag Output
--format gerbers Zip of gerbers + drill + BOM + CPL
--format kicad_zip KiCad 7/8 project zip (importable in pcbnew)
--format kicad_pcb Single .kicad_pcb file
--format step 3D STEP model of the assembled board
--format readable-netlist Human-readable net list

Publishing your molecule

adom-tsci is a preview tool — it doesn't publish anything. Use tscircuit's own flow:

cd ~/project/adom-tsci-projects/MyChip-Molecule
bunx tsci push

This publishes to @tsci/<your-user>.MyChip-Molecule on the tscircuit registry. Requires bunx tsci login once (browser OAuth, must be an adom-inc org member).

For the full production workflow (connector families, molecule generators, DRC, review servers, snapshot approval), go to Ray's guide at ~/.claude/skills/adom/guides/adom-tscircuit-skill.md.

See also

  • Ray's guide: ~/.claude/skills/adom/guides/adom-tscircuit-skill.md — the authoritative tscircuit-on-Adom workflow (deployed by gallia/install.mjs). Single source of truth for writing tscircuit molecules: connector families, molecule generators, sizing grid, <MachineContactMedium> placement rules, DRC, review servers, snapshot approval, all of it. If the user asks how to build a molecule, route them here — do not summarise Ray's content in this skill.
  • hydrogen-tab-icons skill: documents how the #e6edf3 monochrome chip favicon in src/assets/icon.svg ends up on the tab bar (spoiler: the page's <link rel="icon"> overrides any --display-icon placeholder).
  • Basic3dView: ~/.claude/skills/adom/guides/basic-3d-viewer.md — the Adom Babylon viewer used in the 3D compare tab's left pane.
  • Web View panel API: ~/.claude/skills/adom/webview/SKILL.md.
  • Adom brand guide: ~/.claude/skills/adom/guides/brand/SKILL.md — all icons must be monochrome #e6edf3, the chip favicon follows this rule.
  • tscircuit docs · iconify MDI catalog