← Commit history
CHANGELOG.md+5
@@ -1,5 +1,10 @@ # Changelog +## 0.2.3 — 2026-09-02+- **The punch_in preset stops flying the view into blank canvas.** Altium zooms TOWARD THE CURSOR, so the 4-step zoom dive compounded drift toward wherever the user's mouse sat and scrolled the content off screen (John, watching take three: "you are like scrolling the content off the screen ... the screen going blank sometimes"). Per his fix: one gentle zoom-in/zoom-out breath, reframed with zoom_all — single-step drift is invisible.+- **The demo/live hints now teach the adom-tts cue lane** (1.9.47's `load`/`cue --confirm`/`cue-status`, built from this thread's spec on adom/adom-tts#4): pre-cache the narration as markers, cue each stop in the background as the stage call starts, advance when both finish. Prefetched cues start instantly, which closes the last sync gap the say --play lane had.++ ## 0.2.2 — 2026-09-02 - **The live show is code now, not a hand-rolled AI script** (John: "for all the stuff you need for the demo, you better make verbs" — and his diagnosis of the sync jitter was exactly right: "maybe cuz u were doing it via manual ai instead of via code?"). Driven from a container, every camera step paid the full CLI→ab→bridge→agent round trip (~1.4 s, jittery); in-bridge it is one agent file-drop (~40 ms), so choreography holds its timing.   - **`altium_animate_view`** — camera/zoom choreography on the current document from NATIVE Altium camera commands (SetCamera/ResetCamera/FlipBoard/Zoom; Altium animates the transitions itself — no synthetic mouse). Steps (orbit_left/right/up/down, isometric, flip, zoom_in/out, pans, rolls…) or presets (punch_in, showcase3d, orbit, flip, finale), each with a pauseMs.
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.2.2",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.2/adom-bridge-altium-v0.2.2.zip",-  "sha256": "93d9e9284609693a29f1d53dc6635778b59ad5e5cd559832e2b07cedfeca6b60",-  "size": 1420778,+  "version": "0.2.3",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.2.3/adom-bridge-altium-v0.2.3.zip",+  "sha256": "4abfafd77765b7ff1e4604be1f33644146e7cff581066fdbea91b037dfab8bd6",+  "size": 1421053,   "verbPrefixes": [     "altium_"   ],
bridge-rs/Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "altium-bridge"-version = "0.2.1"+version = "0.2.3" edition = "2021" description = "Adom altium-bridge — the bridge process, in Rust. Calls ab's verbs for the desktop; speaks file IPC to the resident DelphiScript agent inside Altium." license = "MIT"
bridge-rs/src/main.rs+1−1
@@ -24,7 +24,7 @@ mod verbs; use std::time::Instant; use tiny_http::{Header, Method, Response, Server}; -pub const BRIDGE_VERSION: &str = "0.2.1";+pub const BRIDGE_VERSION: &str = "0.2.3";  fn json_response(status: u16, body: &serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {     let data = serde_json::to_vec(body).unwrap_or_else(|_| b"{}".to_vec());
bridge-rs/src/surfaces.rs+257
@@ -332,3 +332,260 @@ pub fn demo(args: &Value) -> Value {         "_hint": format!("Beat {}/{} ({label}). Narrate, show the screenshot, then call altium_demo {{stage:{}}}.",                          stage + 1, DEMO_BEATS.len(), stage + 1)}) }++// ---- the live show lanes (John, 2026-09-02: "for all the stuff you need for the demo, you+// better make verbs") ----------------------------------------------------------+//+// The hand-driven show proved the model and exposed why it must live IN the bridge: driven from+// a container, every camera step pays the full CLI→ab→bridge→agent round trip (~1.4 s, jittery);+// driven here, a step is one agent file-drop (~40 ms), so choreography holds its timing and the+// narration a caller plays over it stays in sync.++/// Bring Altium to the FOREGROUND — the sanctioned exception to the never-foreground rule.+/// Requires a human-readable `reason` (>=10 chars), mirroring ab's caption rule: the user can+/// always see who is taking their screen and why.+pub fn foreground(args: &Value) -> Value {+    let reason = s_or(args, "reason", "");+    if reason.trim().len() < 10 {+        return json!({"success": false, "errorCode": "reason_required",+            "error": "foregrounding takes the USER'S screen — pass reason (>=10 chars) saying why, written for the human (e.g. 'John asked to watch the import live')",+            "_hint": "The default etiquette is background-only (captures restore-into-background and hand focus straight back). Foreground is for a human who asked to WATCH."});+    }+    let mw = match instance::main_window() {+        Some(w) => w,+        None => return json!({"success": false, "errorCode": "altium_not_running", "_hint": "altium_launch first."}),+    };+    let hwnd = mw.get("hwnd").cloned().unwrap_or(Value::Null);+    let r = crate::ab::call("desktop_raise_window", json!({"hwnd": hwnd, "reason": reason}), &[], 15);+    json!({"success": r.get("success").and_then(|b| b.as_bool()).unwrap_or(true),+        "hwnd": hwnd, "windowEtiquette": "foreground",+        "_hint": "Altium is now in front of the user. Hand their screen back when the show ends (their own window regains focus when they click; the bridge never needs to force it)."})+}++/// One choreography step -> (process, params) for the given lane, or None for an unknown step.+fn view_step(lane: &str, step: &str) -> Option<(&'static str, &'static str)> {+    if lane == "sch" {+        return Some(match step {+            "zoom_all" => ("Sch:Zoom", "Action=All"),+            // Sch's token is ZoomIn/ZoomOut — Action=In is silently IGNORED (John watched a+            // whole stop where "pushing in" never moved, 2026-09-02)+            "zoom_in" => ("Sch:Zoom", "Action=ZoomIn"),+            "zoom_out" => ("Sch:Zoom", "Action=ZoomOut"),+            "zoom_level_2" => ("Sch:Zoom", "ZoomLevel=2"),+            "zoom_level_4" => ("Sch:Zoom", "ZoomLevel=4"),+            _ => return None,+        });+    }+    Some(match step {+        "zoom_all" => ("PCB:Zoom", "Action=All"),+        "zoom_in" => ("PCB:Zoom", "Action=In"),+        "zoom_out" => ("PCB:Zoom", "Action=Out"),+        "micro_in" => ("PCB:Zoom", "Action=MicroIn"),+        "micro_out" => ("PCB:Zoom", "Action=MicroOut"),+        "view_2d" => ("PCB:SwitchTo2D", ""),+        "view_3d" => ("PCB:SwitchTo3D", ""),+        "isometric" => ("PCB:ResetCamera", "To=ISOMETRIC"),+        "top" => ("PCB:ResetCamera", "To=Top"),+        "bottom" => ("PCB:ResetCamera", "To=Bottom"),+        "front" => ("PCB:ResetCamera", "To=Front"),+        "back" => ("PCB:ResetCamera", "To=Back"),+        "left" => ("PCB:ResetCamera", "To=Left"),+        "right" => ("PCB:ResetCamera", "To=Right"),+        "rotate90" => ("PCB:ResetCamera", "To=Rotate90"),+        "ortho" => ("PCB:ResetCamera", "To=RotateOrtho"),+        "orbit_left" => ("PCB:SetCamera", "To=30DegreeLeft"),+        "orbit_right" => ("PCB:SetCamera", "To=30DegreeRight"),+        "orbit_up" => ("PCB:SetCamera", "To=30DegreeUp"),+        "orbit_down" => ("PCB:SetCamera", "To=30DegreeDown"),+        "rotate_fit_left" => ("PCB:SetCamera", "To=RotateFit_Left"),+        "rotate_fit_right" => ("PCB:SetCamera", "To=RotateFit_Right"),+        "rotate_fit_up" => ("PCB:SetCamera", "To=RotateFit_Up"),+        "rotate_fit_down" => ("PCB:SetCamera", "To=RotateFit_Down"),+        "roll_left" => ("PCB:SetCamera", "To=RollFit_Left"),+        "roll_right" => ("PCB:SetCamera", "To=RollFit_Right"),+        "pan_left" => ("PCB:SetCamera", "To=500MilLeft"),+        "pan_right" => ("PCB:SetCamera", "To=500MilRight"),+        "pan_up" => ("PCB:SetCamera", "To=500MilUp"),+        "pan_down" => ("PCB:SetCamera", "To=500MilDown"),+        "flip" => ("PCB:FlipBoard", ""),+        _ => return None,+    })+}++fn preset_steps(preset: &str) -> Option<Vec<(&'static str, u64)>> {+    Some(match preset {+        // ONE gentle breath, not a dive: Altium zooms TOWARD THE CURSOR, so repeated zoom_in+        // steps compound the drift and fly the view into blank canvas (John, watching take+        // three: "you are like scrolling the content off the screen"). A single in/out keeps+        // the drift invisible, and zoom_all reframes at the end regardless.+        "punch_in" => vec![("zoom_all", 1000), ("zoom_in", 1800), ("zoom_out", 1400), ("zoom_all", 800)],+        "showcase3d" => vec![("view_3d", 1400), ("zoom_all", 900), ("isometric", 1500),+                             ("orbit_left", 1000), ("orbit_left", 1000), ("orbit_left", 1000),+                             ("orbit_up", 1100), ("isometric", 1000)],+        "orbit" => vec![("isometric", 1400), ("orbit_left", 1000), ("orbit_left", 1000),+                        ("orbit_left", 1000), ("orbit_left", 1000)],+        "flip" => vec![("flip", 2000), ("flip", 1800)],+        "finale" => vec![("view_3d", 1400), ("zoom_all", 900), ("isometric", 1500),+                         ("orbit_left", 1000), ("orbit_left", 1000), ("orbit_left", 1000), ("orbit_left", 1000),+                         ("orbit_down", 1100), ("flip", 2000), ("flip", 1800), ("isometric", 1200), ("zoom_all", 600)],+        _ => return None,+    })+}++/// Camera/zoom choreography on the CURRENT document, executed in-bridge (~40 ms per step to the+/// agent) so the timing is tight enough to narrate over. Every move is a NATIVE Altium camera+/// command — Altium animates the transitions itself; no synthetic mouse, no cursor movement.+pub fn animate_view(args: &Value) -> Value {+    if let Some(g) = support::need_licensed() {+        return g;+    }+    let lane = s_or(args, "lane", "pcb");+    let default_pause = args.get("pauseMs").and_then(|v| v.as_u64()).unwrap_or(1000);+    let mut plan: Vec<(String, u64)> = Vec::new();+    if let Some(p) = s(args, "preset").filter(|p| !p.is_empty()) {+        match preset_steps(&p) {+            Some(steps) => plan.extend(steps.into_iter().map(|(st, ms)| (st.to_string(), ms))),+            None => return json!({"success": false, "error": format!("unknown preset {p:?}"),+                "presets": ["punch_in", "showcase3d", "orbit", "flip", "finale"]}),+        }+    }+    if let Some(arr) = args.get("steps").and_then(|st| st.as_array()) {+        for x in arr {+            match x {+                Value::String(st) => plan.push((st.clone(), default_pause)),+                Value::Object(o) => plan.push((+                    o.get("step").and_then(|v| v.as_str()).unwrap_or("").to_string(),+                    o.get("pauseMs").and_then(|v| v.as_u64()).unwrap_or(default_pause),+                )),+                _ => {}+            }+        }+    }+    if plan.is_empty() {+        return json!({"success": false, "error": "pass preset and/or steps[]",+            "presets": ["punch_in", "showcase3d", "orbit", "flip", "finale"],+            "steps": ["zoom_all", "zoom_in", "zoom_out", "view_2d", "view_3d", "isometric", "top", "bottom",+                       "front", "back", "left", "right", "rotate90", "ortho", "orbit_left", "orbit_right",+                       "orbit_up", "orbit_down", "roll_left", "roll_right", "pan_left", "pan_right",+                       "pan_up", "pan_down", "flip"],+            "_hint": "lane:'sch' for schematic docs (zoom steps only). Steps are strings or {step, pauseMs}. Altium animates each camera transition itself."});+    }+    let t0 = support::now();+    let mut ran = 0usize;+    let mut unknown: Vec<String> = Vec::new();+    for (st, pause) in &plan {+        match view_step(&lane, st) {+            Some((proc_, params)) => {+                agent::call("run_process", &[("process", proc_.to_string()), ("params", params.to_string())], None, Duration::from_secs(30));+                ran += 1;+                std::thread::sleep(Duration::from_millis(*pause));+            }+            None => unknown.push(st.clone()),+        }+    }+    let mut out = json!({"success": unknown.is_empty(), "lane": lane, "stepsRun": ran,+        "elapsedSec": support::elapsed_round(t0), "windowEtiquette": "unchanged",+        "_hint": "Choreography ran on the CURRENT document. For a show the user watches: altium_foreground first, and play narration over the motion (the whole call takes ~elapsedSec)."});+    if !unknown.is_empty() {+        out["unknownSteps"] = json!(unknown);+        out["error"] = json!(format!("unknown steps for lane {lane:?}: {unknown:?} — every OTHER step still ran"));+    }+    out+}++// ---- the live, narrated demo (demo {live:true}) ------------------------------++static LAUNCHING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);++fn demo_caption(text: &str, dur_ms: u64) {+    crate::ab::call("desktop_caption", json!({"text": text, "id": "altium-demo", "position": "bottom",+        "duration": dur_ms,+        "reason": "Narrating the live Altium tour the user asked to watch"}), &[], 15);+}++struct LiveBeat {+    title: &'static str,+    doc: fn() -> String,+    lane: &'static str,+    setup: &'static [(&'static str, &'static str)],+    preset: &'static str,+    narrate: &'static str,+}++fn live_beats() -> [LiveBeat; 6] {+    [+        LiveBeat { title: "Stop 1/6 — The symbol", doc: support::demo_schlib, lane: "sch",+            setup: &[("Sch:Zoom", "Action=All")], preset: "punch_in",+            narrate: "Stop one, the symbol, in the Symbol editor. Pushing in for a closer look at the pins, then back out." },+        LiveBeat { title: "Stop 2/6 — The footprint", doc: support::demo_pcblib, lane: "pcb",+            setup: &[("PCB:SwitchTo2D", ""), ("PCB:Zoom", "Action=All")], preset: "punch_in",+            narrate: "Stop two, the land pattern — pads, silkscreen, courtyard. Zooming down to the pads." },+        LiveBeat { title: "Stop 3/6 — The part in 3D", doc: support::demo_pcblib, lane: "pcb",+            setup: &[], preset: "showcase3d",+            narrate: "Stop three: the same part in three D. Watch it swing to isometric and orbit around." },+        LiveBeat { title: "Stop 4/6 — The schematic", doc: support::demo_sch, lane: "sch",+            setup: &[("Sch:Zoom", "Action=All")], preset: "punch_in",+            narrate: "Stop four, the Bluetooth Sentinel top level schematic — six sheets wired together by ports." },+        LiveBeat { title: "Stop 5/6 — The board in 2D", doc: support::demo_pcb, lane: "pcb",+            setup: &[("PCB:SwitchTo2D", ""), ("PCB:Zoom", "Action=All")], preset: "punch_in",+            narrate: "Stop five, the layout in two D — a two board rigid flex, four copper layers." },+        LiveBeat { title: "Stop 6/6 — The 3D finale", doc: support::demo_pcb, lane: "pcb",+            setup: &[], preset: "finale",+            narrate: "The finale: the populated board in three D — isometric, a full orbit, a flip to the bottom side, and back." },+    ]+}++/// The narrated, animated, FOREGROUND demo — one beat per call, choreography in-bridge.+/// The caller owns the VOICE (adom-tts): speak `nextNarrate` (detached) right before making+/// `nextCall`, and the words play over exactly the motion they describe.+pub fn demo_live(args: &Value) -> Value {+    let lic = instance::licence_state();+    if lic.get("running").and_then(|b| b.as_bool()) != Some(true) {+        // Narrate the wait instead of dying: paint the caption, start the launch in the+        // background, and hand the caller the line to SPEAK (John: "if you have to launch+        // altium, play another tts clip and ab caption explaining you have to open it").+        demo_caption("Altium isn't running — launching it first (about a minute)…", 90000);+        if !LAUNCHING.swap(true, std::sync::atomic::Ordering::SeqCst) {+            std::thread::spawn(|| {+                let _ = crate::misc::launch(&json!({"waitSec": 240}));+                LAUNCHING.store(false, std::sync::atomic::Ordering::SeqCst);+            });+        }+        return json!({"success": true, "status": "launching", "stage": args.get("stage").cloned().unwrap_or(json!(0)),+            "narrate": "One thing before the tour: Altium isn't running, so I'm launching it now. Cold start takes about a minute — watch the progress bar on the taskbar icon.",+            "nextCall": {"verb": "altium_demo", "args": {"live": true, "stage": args.get("stage").cloned().unwrap_or(json!(0))}},+            "_hint": "SPEAK narrate now (adom-tts say --play, or cue a pre-loaded marker), wait ~20s, then repeat the SAME call — it proceeds once Altium is up."});+    }+    let stage = args.get("stage").and_then(|v| v.as_u64()).unwrap_or(0) as usize;+    let beats = live_beats();+    if stage >= beats.len() {+        demo_caption("Tour complete.", 8000);+        crate::ab::call("desktop_caption", json!({"action": "force-clear",+            "reason": "the live Altium tour is over — clearing the tour captions"}), &[], 15);+        return json!({"success": true, "done": true, "stage": stage,+            "narrate": "That is the whole tour — every zoom, orbit and flip was a native Altium camera command through the bridge.",+            "_hint": "SPEAK narrate, then hand the user's document back (altium_activate_document their file) if you changed it."});+    }+    let b = &beats[stage];+    demo_caption(b.title, 60000);+    if let Some(g) = support::need_licensed() {+        return g;+    }+    let t0 = support::now();+    let doc = (b.doc)();+    support::agent_open(&doc, 90);+    for (proc_, params) in b.setup {+        agent::call("run_process", &[("process", proc_.to_string()), ("params", params.to_string())], None, Duration::from_secs(60));+    }+    foreground(&json!({"reason": "The user asked to WATCH this tour live — keeping Altium in front"}));+    let anim = animate_view(&json!({"lane": b.lane, "preset": b.preset}));+    let next_stage = stage + 1;+    let next_narrate = beats.get(next_stage).map(|nb| nb.narrate);+    json!({"success": anim.get("success").cloned().unwrap_or(json!(true)), "done": false,+        "stage": stage, "label": b.title, "narrate": b.narrate,+        "nextNarrate": next_narrate,+        "animation": {"preset": b.preset, "stepsRun": anim.get("stepsRun"), "elapsedSec": anim.get("elapsedSec")},+        "elapsedSec": support::elapsed_round(t0), "windowEtiquette": "foreground",+        "nextCall": {"verb": "altium_demo", "args": {"live": true, "stage": next_stage}},+        "_hint": "SYNC MODEL (best): pre-cache the whole narration once with `adom-tts load` (one marker per stop), then per stop fire `adom-tts cue <id> <n> --confirm` in the background and IMMEDIATELY make nextCall — the prefetched cue starts instantly and this verb's in-bridge choreography (~5-17s) runs under the voice; advance when BOTH finish. Fallback without 1.9.47: say --play detached per stop. On the last call (done:true) restore the user's own document."})+}
bridge-rs/src/verbs.rs+33−1
@@ -24,6 +24,7 @@ pub const KNOWN: &[&str] = &[     // surfaces     "show_project", "show_schematic", "show_2d_board", "show_3d_board", "show_library",     "show_symbol", "show_footprint", "show_3d_chip", "list_library", "demo",+    "animate_view", "foreground",     // outputs / project     "compile", "run_erc", "run_drc", "run_outjob", "export_gerber", "export_bom", "export_step",     "save", "test_board", "board_shape", "pcb_report", "import_kicad",@@ -94,6 +95,29 @@ fn describe() -> Value {         verb_doc("show_symbol", "Open a SchLib member in the Symbol editor and capture a background proof.", json!({"libraryPath": "path", "symbolName": "string"}), shot.clone(), 60,             "Never foregrounds: a minimized Altium is restored INTO THE BACKGROUND and the user's window handed straight back.",             &["show_footprint", "show_3d_chip", "show_library"], &["A capture under 400px wide is refused as proof_unavailable - a title-bar sliver is not evidence."], json!({})),+        verb_doc("animate_view", "Camera/zoom choreography on the current document - native Altium camera commands, animated by Altium itself.",+            json!({"preset": "punch_in|showcase3d|orbit|flip|finale", "steps": "[string | {step, pauseMs}]", "lane": "pcb (default) | sch", "pauseMs": "int default per-step pause"}),+            json!({"stepsRun": "int", "elapsedSec": "float"}), 120,+            "Runs IN-BRIDGE (~40ms per step to the agent) so timing is tight enough to narrate over. Steps: zoom_all/in/out, view_2d/3d, isometric, top/bottom/front/back/left/right, rotate90, ortho, orbit_left/right/up/down, rolls, pans, flip.",+            &["demo", "foreground", "run_command"],+            &["Driving camera steps one-by-one from a container adds ~1.4s of jitter per step - that is what this verb exists to avoid.",+              "sch lane supports zoom steps only; everything else needs a PCB document current."],+            json!({"preset": "showcase3d"})),+        verb_doc("foreground", "Bring Altium in front of the user - the sanctioned exception to the never-foreground rule.",+            json!({"reason": "string >=10 chars, written for the HUMAN (required)"}),+            json!({"windowEtiquette": "foreground"}), 15,+            "For a human who asked to WATCH (a live demo, a walkthrough). Everything else stays background.",+            &["demo", "animate_view"],+            &["Refused without a real reason - same rule as ab's desktop_caption."],+            json!({"reason": "John asked to watch the import live"})),+        verb_doc("demo", "The tour. Default: one background beat per call with a screenshot. live:true = the narrated FOREGROUND show: ab caption + document + in-bridge camera choreography per stop, returning the narration text for YOUR TTS.",+            json!({"stage": "int 0-5", "live": "bool"}),+            json!({"narrate": "string - speak this", "nextNarrate": "string", "nextCall": "{verb, args}", "done": "bool"}), 180,+            "live SYNC MODEL: pre-cache narration with `adom-tts load` (one marker per stop), then per stop fire `adom-tts cue <id> <n> --confirm` in the background and IMMEDIATELY make nextCall - the prefetched cue starts instantly and the ~5-17s of on-screen motion runs under the voice; advance when BOTH finish. If Altium is cold, the first call answers status:launching with a narrate line - speak it, poll again in ~20s.",+            &["animate_view", "foreground", "show_symbol"],+            &["The caller owns the VOICE (adom-tts) - the bridge never synthesizes speech.",+              "After done:true, restore the user's own document (altium_activate_document)."],+            json!({"live": true, "stage": 0})),         verb_doc("license", "Drive the License Management page (UIA first - no cursor, no foreground).", json!({"action": "show|refresh|use_license|start_trial|sign_out"}), shot, 60,             "One On-Demand seat serves EVERY Altium app on this machine. use_license claims a free seat.",             &["ensure_licensed", "license_status", "close"], &["NEVER kill an Altium holding the seat: the lease strands, the page shows Seats 0/1 while every claim fails. altium_close releases it."], json!({"action": "use_license"})),@@ -348,7 +372,15 @@ pub fn dispatch(command: &str, args: &Value, _forwarded: &[(String, String)]) ->         "show_footprint" => surfaces::show_footprint(args),         "show_3d_chip" => surfaces::show_3d_chip(args),         "list_library" => surfaces::list_library(args),-        "demo" => surfaces::demo(args),+        "demo" => {+            if args.get("live").and_then(|b| b.as_bool()) == Some(true) {+                surfaces::demo_live(args)+            } else {+                surfaces::demo(args)+            }+        }+        "animate_view" => surfaces::animate_view(args),+        "foreground" => surfaces::foreground(args),         "save" => surfaces::save(args),         "board_shape" => surfaces::board_shape(args),         "pcb_report" => surfaces::pcb_report(args),
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.2.2",+  "version": "0.2.3",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the Altium Designer bridge (altium_* verbs). The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.2.2+0.2.3
src/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.2.2",+  "version": "0.2.3",   "description": "Drive Altium Designer from Adom: a resident DelphiScript agent (eval + the 1,100-command process launcher), the X2 -R command line, UIA over the VCL shell, dialogs by PID, screenshots of every Altium window, and Altium 365.",   "homepage": "https://wiki.adom.inc/adom/altium-bridge",   "docs": "https://wiki.adom.inc/adom/altium-bridge",