← Commit history
CHANGELOG.md+6
@@ -1,5 +1,11 @@ # Changelog +## 0.1.11 — 2026-08-31+- Phase 1 of the parity plan: 11 read-only board/schematic introspection verbs — list_designators, component_data, component_pins, list_nets, net_connections, layers, copper_layers, schematic_data (pins + part IDs), read_rules, set_layer_visibility, footprint_primitives. All verified live on Bluetooth Sentinel (16–350 ms), zero dialogs.+- Busy-notice reaper rewritten to go through ab's desktop verbs (find_window → find_control → ui_click) with a 4 s watch, after the X2-pid scan missed the notice twice on a live box.+- Parked: create_net_class (IPCB_ObjectClass.SuperClass/.Name are undeclared in AD26 scripting) — function kept, not dispatched.+- Last Python release of the bridge process: the next major moves the bridge to a Rust exe that calls ab's verbs and shrinks the DelphiScript agent to generic dump/apply primitives (ab#987 requests the binary runtime kind).+ ## 0.1.10 — 2026-08-31 - #3 (Colby) root-cause FIX: the agent registers the PCB/SCH scripting APIs at startup (`ServersUp`: `Client.StartServer('PCB')`/`('SCH')` before the first `PCBServer` reference) — on a fresh session that never opened a PCB document the API unit is unregistered and the old template halted with an "Undeclared identifier: PostProcess" modal on the user's screen. - #3 follow-up: `altium_agent_recover` now closes the scratch `AdomAgent_<digest>.PrjScr` out of the Projects panel after freeing the engine (only there — after a successful start the resident loop runs from it).
README.md+4−1
@@ -106,7 +106,10 @@ A halted engine (DelphiScript runtime errors are modal) recovers with `altium_ag  ## Verb reference -All 81 verbs (prefix `altium_`). Call `altium_describe` for the live catalog.+**Board & schematic introspection (0.1.11):** `list_designators`, `component_data`, `component_pins`, `list_nets`, `net_connections`, `layers`, `copper_layers`, `read_rules`, `set_layer_visibility`, `footprint_primitives`, `schematic_data` — read the live design as data (pads→nets, rules, layers, sheet components with pins), all in tens of milliseconds, no dialogs.+++All 94 verbs (prefix `altium_`). Call `altium_describe` for the live catalog.  **Readiness & lifecycle** — `readiness`, `status`, `launch`, `close`, `diagnostics`, `progress`, `agent_start/_stop/_status/_recover`, `license`, `signin_status`, `connect_workspace`, `uninstall`
SKILL.md+18
@@ -102,3 +102,21 @@ adom-bridge --ai-thread "board review" altium_export_gerber '{"outjobPath":"C:/d ## How this bridge is packaged  Wiki page `adom/altium-bridge`: the Release zip + `adom-bridge-altium-manifest.json` (what Adom Bridge installs with `bridge_install`), and this skills pkg (`adom-wiki pkg install adom/altium-bridge` — skills only; it does not install the runtime). Plan of record and every measured engine fact: the `altium-bridge-plan` skill.++## Board & schematic introspection (0.1.11)++Read-only, need a .PcbDoc (or .SchDoc for `schematic_data`) as the CURRENT document — the agent payload comes back under `data`.++| Verb | Returns |+|---|---|+| `altium_list_designators` | Every component designator on the current board. |+| `altium_component_data` | Per-component data: pattern, comment, side, x/y/rotation, bbox, pad count, pad→net map ({designator} for one, else all). |+| `altium_component_pins` | Every pad of one component: name, net, position, size, hole. |+| `altium_list_nets` | Every net with its pad count. |+| `altium_net_connections` | Every pad with net + owner (DES.PIN, x/y) and per-net span; {net} filters. |+| `altium_layers` | The board layer set with names, kinds, visibility. |+| `altium_copper_layers` | All 48 copper layers (top, mid 1-30, planes 1-16, bottom). |+| `altium_schematic_data` | Components on the current sheet with pins (number/name/electrical) and part IDs. |+| `altium_read_rules` | Every design rule: name, kind, enabled, priority, scopes. |+| `altium_set_layer_visibility` | Show/hide a layer {layerId, displayed}. |+| `altium_footprint_primitives` | Geometry dump of a placed component: tracks, arcs, pads, texts. |
adom-bridge-altium-manifest.json+4−4
@@ -1,9 +1,9 @@ {   "name": "altium",-  "version": "0.1.10",-  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.10/adom-bridge-altium-v0.1.10.zip",-  "sha256": "8b6e93dc5abaa53c024d86152329a151581170f97d28b773612e3c340ce4eafc",-  "size": 97615,+  "version": "0.1.11",+  "url": "https://wiki.adom.inc/download/adom/altium-bridge/0.1.11/adom-bridge-altium-v0.1.11.zip",+  "sha256": "3f5062567ccffa208025b7cc4bdb51b7a06bce047c5d0c8e1cf6f9019478ff8d",+  "size": 102890,   "verbPrefixes": [     "altium_"   ],
adom-bridge-altium-v0.1.11.zipadded
⋯ 1 unchanged line ⋯
bridge.json+13−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.8",+  "version": "0.1.11",   "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",@@ -103,7 +103,18 @@     "altium_set_visibility",     "altium_xcopy_footprint",     "altium_import_part",-    "altium_install_library_bundle"+    "altium_install_library_bundle",+    "altium_list_designators",+    "altium_component_data",+    "altium_component_pins",+    "altium_list_nets",+    "altium_layers",+    "altium_net_connections",+    "altium_schematic_data",+    "altium_copper_layers",+    "altium_read_rules",+    "altium_set_layer_visibility",+    "altium_footprint_primitives"   ],   "statusVerb": "altium_status",   "uninstall": "altium_uninstall",
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "altium-bridge",-  "version": "0.1.10",+  "version": "0.1.11",   "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": [
skills/altium-boards/SKILL.md+10
@@ -59,3 +59,13 @@ their nets. Trust the report, not a return code.   text bloats the bbox. - **Frames under a locked Windows session are BLACK** (DirectX): check   `desktop_get_foreground` ≠ 0 before trusting a board frame; GDI editors still capture.++## Reading the board as data (0.1.11)++`list_designators`, `component_data {designator?}`, `component_pins {designator}`, `list_nets`,+`net_connections {net?}` (pads as DES.PIN with coordinates + per-net span), `layers` /+`copper_layers`, `read_rules`, `footprint_primitives {designator}`, `set_layer_visibility+{layerId, displayed}`; `schematic_data {designator?, pins?}` on the current sheet. They read the+CURRENT document: opening a .SchDoc makes board verbs answer "no current PCB board" until a+.PcbDoc is opened again — order your calls. Payload is under `data`. Multi-part symbols list+once per part (`partId`).
skills/altium-bridge-dev/SKILL.md+37−1
@@ -32,6 +32,22 @@ Everything you need to change the bridge without re-learning what this session p   running old code); to load NEW PYTHON (server.py/handlers), `bridge_kill` (reloads the   process). Both often needed together. +## Deploying a big template (measured 2026-08-31)++`write_file` passes the content as a CLI argument; once `AdomAgent.pas.tmpl` passed ~130 KB it+failed with "Argument list too long" and NOTHING deployed (the reload then ran old code —+"Unknown command" for the new verbs, zero dialogs, easy to misread as a compile pass). Deploy+big files with `send_files {filePaths:[...]}` (streams; ignores destDir and lands in+`C:\Users\john\Downloads`) then `Move-Item` into `bridges-cache\altium\{agent\,}`.++## "Current document" semantics++`PCBServer.GetCurrentPCBBoard` / `SchServer.GetCurrentSchDocument` follow the ACTIVE document.+Opening a SchDoc makes board verbs answer "no current PCB board" until a PcbDoc is opened+again. Read verbs return that error with a `_hint`; order your calls, or open the right doc first.+Schematic iteration needs the sheet SHOWN (activated) — a background `open_document` can leave+`SchIterator` empty (measured: count 0 until `show_schematic`).+ ## Dialog pre-emption (John: a dismissed dialog is a defect)  The DelphiScript debugger breaks on ANY raised exception even inside Try/Except — a raise IS@@ -47,7 +63,7 @@ Warning. When you add a mutating command, wrap file reads and never leave a tran `IPCB_BoardOutline.Clear`, `eProcessFreePrimitives`, `IPCB_Board.GroupIterator_Create` (use `BoardIterator_Create`), `IPCB_Component.Name.Text :=` (use `NameOn`/`Inst.Name.MoveToXY`), `GroupIterator` without `AddFilter_LayerSet(AllLayers)` (mech-layer children invisible),-`RunProcessWithParameters` (use ResetParameters+AddStringParameter+RunProcess), a nested `{}`+`RunProcessWithParameters` (use ResetParameters+AddStringParameter+RunProcess), `IPCB_ObjectClass.SuperClass :=` (not declared in AD26 scripting — a new class from `PCBClassFactory` is already non-super; just omit it), a nested `{}` inside a brace comment (breaks compilation). An unknown identifier is a RUNTIME error that halts the debugger; `altium_agent_recover` clears it. @@ -57,6 +73,26 @@ Result strings build JSON with `'{' + ... + '}'`; that's fine in a STRING LITERA BRACE COMMENTS that cannot contain a nested `{`. Lint new commands: `re.sub(r"'[^']*'","",code)` then assert no `\{[^}]*\{`. +## Two sessions on one engine = a modal storm (measured 2026-08-31)++The resident loop is ONE script engine. When a second session (another Claude thread, a+delegated agent) also drives `altium_*` verbs, a halt raised by one session's command surfaces+as the OTHER session's error (it saw "Undeclared identifier: SuperClass/Name" on a read-only+verb that was innocent), both sessions then run `agent_recover`, and every recover's `-R`+helper puts the single-instance busy notice on the user's screen. Rule: exactly one driver+per Altium instance. Coordinate ownership (cross-session message) before any second session+touches the engine, and never delegate live-engine work without that handshake.++## Recover costs the user a modal — measured 2026-08-31 on John's screen++Each `agent_recover` fires `X2 -REditScript:Stop`; against a halted (busy) instance Windows raises the+single-instance notice ("There is another instance of running, but it is busy") on the USER's+screen, and 0.1.9's `_reap_busy_notice` did NOT catch it (twice). Until the reaper is proven:+never iterate a suspected-halting command live on the user's box — halt → recover → retry is a+modal per cycle. Eval-probe identifiers in a throwaway session, or ship the verb unwired.+`create_net_class` is parked unwired for exactly this reason (`SuperClass` halts; a second+identifier halts after it).+ ## When the bridge "vanishes" from ab  `bridge_list` without `altium` + `Unknown verb altium_*` = ab could not parse the cached `bridge.json`
src/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.10\ No newline at end of file+0.1.11
src/agent/AdomAgent.pas.tmpl+591−1
@@ -2910,6 +2910,585 @@ Begin End;  ++{ ---- Phase 1: board introspection, read-only (altium-bridge parity plan) ---- }+{ Every pad of a component as name=net pairs, all pads, capped. }+Function AllPadNets(Comp : IPCB_Component) : String;+Var+    GIt : IPCB_GroupIterator;+    Pad : IPCB_Pad;+    S, N : String;+    K : Integer;+Begin+    S := ''; K := 0;+    GIt := Comp.GroupIterator_Create;+    GIt.AddFilter_ObjectSet(MkSet(ePadObject));+    GIt.AddFilter_LayerSet(AllLayers);+    Pad := GIt.FirstPCBObject;+    While (Pad <> Nil) And (K < 400) Do+    Begin+        N := '';+        If Pad.Net <> Nil Then N := Pad.Net.Name;+        If S <> '' Then S := S + ';';+        S := S + Pad.Name + '=' + N;+        K := K + 1;+        Pad := GIt.NextPCBObject;+    End;+    Comp.GroupIterator_Destroy(GIt);+    Result := S;+End;++Function FindCompByDes(Board : IPCB_Board; Des : String) : IPCB_Component;+Var+    BIt  : IPCB_BoardIterator;+    Comp : IPCB_Component;+Begin+    Result := Nil;+    BIt := Board.BoardIterator_Create;+    BIt.AddFilter_ObjectSet(MkSet(eComponentObject));+    BIt.AddFilter_LayerSet(AllLayers);+    BIt.AddFilter_Method(eProcessAll);+    Comp := BIt.FirstPCBObject;+    While (Comp <> Nil) And (Result = Nil) Do+    Begin+        If UpperCase(Comp.Name.Text) = UpperCase(Des) Then Result := Comp;+        Comp := BIt.NextPCBObject;+    End;+    Board.BoardIterator_Destroy(BIt);+End;++Function CompJson(Comp : IPCB_Component) : String;+Var+    R : TCoordRect;+Begin+    R := Comp.BoundingRectangle;+    Result := '{' + JStr('designator', Comp.Name.Text) + ',' ++        JStr('pattern', Comp.Pattern) + ',' ++        JStr('comment', Comp.Comment.Text) + ',' ++        JRaw('layerId', IntToStr(Comp.Layer)) + ',' ++        JRaw('bottom', JBool(Comp.Layer = eBottomLayer)) + ',' ++        JRaw('xMil', IntToStr(Round(CoordToMils(Comp.x)))) + ',' ++        JRaw('yMil', IntToStr(Round(CoordToMils(Comp.y)))) + ',' ++        JRaw('rotation', IntToStr(Round(Comp.Rotation))) + ',' ++        JRaw('wMil', IntToStr(Round(CoordToMils(R.Right - R.Left)))) + ',' ++        JRaw('hMil', IntToStr(Round(CoordToMils(R.Top - R.Bottom)))) + ',' ++        JRaw('pads', IntToStr(GroupCount(Comp, ePadObject))) + ',' ++        JStr('nets', AllPadNets(Comp)) + '}';+End;++{ list_designators: every component designator on the current board }+Function CmdListDesignators(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    BIt   : IPCB_BoardIterator;+    Comp  : IPCB_Component;+    S     : String;+    N     : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    S := ''; N := 0;+    BIt := Board.BoardIterator_Create;+    BIt.AddFilter_ObjectSet(MkSet(eComponentObject));+    BIt.AddFilter_LayerSet(AllLayers);+    BIt.AddFilter_Method(eProcessAll);+    Comp := BIt.FirstPCBObject;+    While Comp <> Nil Do+    Begin+        If S <> '' Then S := S + ',';+        S := S + '"' + JsonEsc(Comp.Name.Text) + '"';+        N := N + 1;+        Comp := BIt.NextPCBObject;+    End;+    Board.BoardIterator_Destroy(BIt);+    Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('designators', '[' + S + ']') + '}';+End;++{ component_data: designator=<one> or all (limit=N, default 500) }+Function CmdComponentData(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    BIt   : IPCB_BoardIterator;+    Comp  : IPCB_Component;+    Des, S : String;+    N, Limit : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    Des := Param(Lines, 'designator');+    Limit := StrToIntDef(Param(Lines, 'limit'), 500);+    If Des <> '' Then+    Begin+        Comp := FindCompByDes(Board, Des);+        If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;+        Result := '{' + JRaw('ok','true') + ',' + JRaw('component', CompJson(Comp)) + '}';+        Exit;+    End;+    S := ''; N := 0;+    BIt := Board.BoardIterator_Create;+    BIt.AddFilter_ObjectSet(MkSet(eComponentObject));+    BIt.AddFilter_LayerSet(AllLayers);+    BIt.AddFilter_Method(eProcessAll);+    Comp := BIt.FirstPCBObject;+    While (Comp <> Nil) And (N < Limit) Do+    Begin+        If S <> '' Then S := S + ',';+        S := S + CompJson(Comp);+        N := N + 1;+        Comp := BIt.NextPCBObject;+    End;+    Board.BoardIterator_Destroy(BIt);+    Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('components', '[' + S + ']') + '}';+End;++{ component_pins: designator=<one> -> every pad with net, position, size, hole }+Function CmdComponentPins(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    Comp  : IPCB_Component;+    GIt   : IPCB_GroupIterator;+    Pad   : IPCB_Pad;+    Des, S, N : String;+    K : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    Des := Param(Lines, 'designator');+    If Des = '' Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator is required') + '}'; Exit; End;+    Comp := FindCompByDes(Board, Des);+    If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;+    S := ''; K := 0;+    GIt := Comp.GroupIterator_Create;+    GIt.AddFilter_ObjectSet(MkSet(ePadObject));+    GIt.AddFilter_LayerSet(AllLayers);+    Pad := GIt.FirstPCBObject;+    While (Pad <> Nil) And (K < 1000) Do+    Begin+        N := '';+        If Pad.Net <> Nil Then N := Pad.Net.Name;+        If S <> '' Then S := S + ',';+        S := S + '{' + JStr('name', Pad.Name) + ',' + JStr('net', N) + ',' ++            JRaw('xMil', IntToStr(Round(CoordToMils(Pad.x)))) + ',' ++            JRaw('yMil', IntToStr(Round(CoordToMils(Pad.y)))) + ',' ++            JRaw('layerId', IntToStr(Pad.Layer)) + ',' ++            JRaw('wMil', IntToStr(Round(CoordToMils(Pad.TopXSize)))) + ',' ++            JRaw('hMil', IntToStr(Round(CoordToMils(Pad.TopYSize)))) + ',' ++            JRaw('holeMil', IntToStr(Round(CoordToMils(Pad.HoleSize)))) + '}';+        K := K + 1;+        Pad := GIt.NextPCBObject;+    End;+    Comp.GroupIterator_Destroy(GIt);+    Result := '{' + JRaw('ok','true') + ',' + JStr('designator', Comp.Name.Text) + ',' + JRaw('count', IntToStr(K)) + ',' + JRaw('pins', '[' + S + ']') + '}';+End;++{ list_nets: every net on the board with its pad count (tallied over all pads) }+Function CmdListNets(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    BIt   : IPCB_BoardIterator;+    Pad   : IPCB_Pad;+    SL    : TStringList;+    S, N  : String;+    I, C  : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    SL := TStringList.Create;+    BIt := Board.BoardIterator_Create;+    BIt.AddFilter_ObjectSet(MkSet(ePadObject));+    BIt.AddFilter_LayerSet(AllLayers);+    BIt.AddFilter_Method(eProcessAll);+    Pad := BIt.FirstPCBObject;+    While Pad <> Nil Do+    Begin+        If Pad.Net <> Nil Then+        Begin+            N := Pad.Net.Name;+            C := StrToIntDef(SL.Values[N], 0);+            SL.Values[N] := IntToStr(C + 1);+        End;+        Pad := BIt.NextPCBObject;+    End;+    Board.BoardIterator_Destroy(BIt);+    S := '';+    For I := 0 To SL.Count - 1 Do+    Begin+        If S <> '' Then S := S + ',';+        S := S + '{' + JStr('name', SL.Names[I]) + ',' + JRaw('pads', SL.Values[SL.Names[I]]) + '}';+    End;+    Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(SL.Count)) + ',' + JRaw('nets', '[' + S + ']') + '}';+    SL.Free;+End;++Function LayerJson(Board : IPCB_Board; L : Integer; Kind : String) : String;+Begin+    Result := '{' + JStr('name', Board.LayerStack.LayerObject[L].Name) + ',' ++        JRaw('id', IntToStr(L)) + ',' + JStr('kind', Kind) + ',' ++        JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}';+End;++{ layers: the fixed layer set with names and visibility (stackup detail is a later verb) }+Function CmdLayers(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    S : String;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    S := LayerJson(Board, eTopLayer, 'signal') + ',' + LayerJson(Board, eBottomLayer, 'signal') + ',' ++        LayerJson(Board, eTopOverlay, 'overlay') + ',' + LayerJson(Board, eBottomOverlay, 'overlay') + ',' ++        LayerJson(Board, eTopPaste, 'paste') + ',' + LayerJson(Board, eBottomPaste, 'paste') + ',' ++        LayerJson(Board, eTopSolder, 'solder') + ',' + LayerJson(Board, eBottomSolder, 'solder') + ',' ++        LayerJson(Board, eKeepOutLayer, 'keepout') + ',' + LayerJson(Board, eMultiLayer, 'multi') + ',' ++        LayerJson(Board, eDrillGuide, 'drill') + ',' + LayerJson(Board, eDrillDrawing, 'drill') + ',' ++        LayerJson(Board, eMechanical1, 'mechanical') + ',' + LayerJson(Board, eMechanical2, 'mechanical') + ',' ++        LayerJson(Board, eMechanical3, 'mechanical') + ',' + LayerJson(Board, eMechanical4, 'mechanical') + ',' ++        LayerJson(Board, eMechanical5, 'mechanical') + ',' + LayerJson(Board, eMechanical6, 'mechanical') + ',' ++        LayerJson(Board, eMechanical7, 'mechanical') + ',' + LayerJson(Board, eMechanical8, 'mechanical') + ',' ++        LayerJson(Board, eMechanical9, 'mechanical') + ',' + LayerJson(Board, eMechanical10, 'mechanical') + ',' ++        LayerJson(Board, eMechanical11, 'mechanical') + ',' + LayerJson(Board, eMechanical12, 'mechanical') + ',' ++        LayerJson(Board, eMechanical13, 'mechanical') + ',' + LayerJson(Board, eMechanical14, 'mechanical') + ',' ++        LayerJson(Board, eMechanical15, 'mechanical') + ',' + LayerJson(Board, eMechanical16, 'mechanical');+    Result := '{' + JRaw('ok','true') + ',' + JRaw('layers', '[' + S + ']') + '}';+End;+++{ ---- Phase 1 batch 2 ---------------------------------------------------- }+{ net_connections: every pad on the board with its net and owner, plus per-net span.+  net=<name> filters to one net. limit caps pads (default 4000). }+Function CmdNetConnections(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    BIt   : IPCB_BoardIterator;+    Pad   : IPCB_Pad;+    Want, S, N, Own, Spans, V : String;+    K, Limit, X, Y, I : Integer;+    SL    : TStringList;+    P     : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    Want := Param(Lines, 'net');+    Limit := StrToIntDef(Param(Lines, 'limit'), 4000);+    SL := TStringList.Create;+    S := ''; K := 0;+    BIt := Board.BoardIterator_Create;+    BIt.AddFilter_ObjectSet(MkSet(ePadObject));+    BIt.AddFilter_LayerSet(AllLayers);+    BIt.AddFilter_Method(eProcessAll);+    Pad := BIt.FirstPCBObject;+    While (Pad <> Nil) And (K < Limit) Do+    Begin+        If Pad.Net <> Nil Then+        Begin+            N := Pad.Net.Name;+            If (Want = '') Or (UpperCase(N) = UpperCase(Want)) Then+            Begin+                Own := '';+                If Pad.Component <> Nil Then Own := Pad.Component.Name.Text;+                X := Round(CoordToMils(Pad.x)); Y := Round(CoordToMils(Pad.y));+                If S <> '' Then S := S + ',';+                S := S + '{' + JStr('net', N) + ',' + JStr('pad', Own + '.' + Pad.Name) + ',' ++                    JRaw('xMil', IntToStr(X)) + ',' + JRaw('yMil', IntToStr(Y)) + '}';+                K := K + 1;+                V := SL.Values[N];+                If V = '' Then SL.Values[N] := IntToStr(X) + '|' + IntToStr(Y) + '|' + IntToStr(X) + '|' + IntToStr(Y) + '|1'+                Else+                Begin+                    P := Pos('|', V); Spans := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+                    I := StrToIntDef(Spans, X); If X < I Then I := X; Spans := IntToStr(I);+                    P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+                    I := StrToIntDef(N, Y); If Y < I Then I := Y; Spans := Spans + '|' + IntToStr(I);+                    P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+                    I := StrToIntDef(N, X); If X > I Then I := X; Spans := Spans + '|' + IntToStr(I);+                    P := Pos('|', V); N := Copy(V, 1, P - 1); V := Copy(V, P + 1, Length(V));+                    I := StrToIntDef(N, Y); If Y > I Then I := Y; Spans := Spans + '|' + IntToStr(I);+                    I := StrToIntDef(V, 0) + 1; Spans := Spans + '|' + IntToStr(I);+                    SL.Values[Pad.Net.Name] := Spans;+                End;+            End;+        End;+        Pad := BIt.NextPCBObject;+    End;+    Board.BoardIterator_Destroy(BIt);+    Spans := '';+    For I := 0 To SL.Count - 1 Do+    Begin+        If Spans <> '' Then Spans := Spans + ',';+        Spans := Spans + '{' + JStr('net', SL.Names[I]) + ',' + JStr('minXminYmaxXmaxYpads', SL.Values[SL.Names[I]]) + '}';+    End;+    SL.Free;+    Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(K)) + ',' + JRaw('pads', '[' + S + ']') + ',' + JRaw('nets', '[' + Spans + ']') + '}';+End;++{ schematic_data: components on the CURRENT schematic sheet with pins.+  designator=<one> filters. pins=0 skips pin detail. limit caps components. }+Function CmdSchematicData(Lines : TStringList) : String;+Var+    Doc  : ISch_Document;+    Iter : ISch_Iterator;+    PIt  : ISch_Iterator;+    Comp : ISch_Component;+    Pin  : ISch_Pin;+    Want, S, PS : String;+    N, Limit : Integer;+    WantPins : Boolean;+Begin+    Doc := SchServer.GetCurrentSchDocument;+    If Doc = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current schematic document (open a .SchDoc first)') + '}'; Exit; End;+    Want := Param(Lines, 'designator');+    Limit := StrToIntDef(Param(Lines, 'limit'), 500);+    WantPins := Param(Lines, 'pins') <> '0';+    S := ''; N := 0;+    Iter := Doc.SchIterator_Create;+    Iter.AddFilter_ObjectSet(MkSet(eSchComponent));+    Comp := Iter.FirstSchObject;+    While (Comp <> Nil) And (N < Limit) Do+    Begin+        If (Want = '') Or (UpperCase(Comp.Designator.Text) = UpperCase(Want)) Then+        Begin+            PS := '';+            If WantPins Then+            Begin+                PIt := Comp.SchIterator_Create;+                PIt.AddFilter_ObjectSet(MkSet(ePin));+                Pin := PIt.FirstSchObject;+                While Pin <> Nil Do+                Begin+                    If PS <> '' Then PS := PS + ',';+                    PS := PS + '{' + JStr('number', Pin.Designator) + ',' + JStr('name', Pin.Name) + ',' ++                        JRaw('electrical', IntToStr(Pin.Electrical)) + '}';+                    Pin := PIt.NextSchObject;+                End;+                Comp.SchIterator_Destroy(PIt);+            End;+            If S <> '' Then S := S + ',';+            S := S + '{' + JStr('designator', Comp.Designator.Text) + ',' ++                JRaw('partId', IntToStr(Comp.CurrentPartID)) + ',' ++                JStr('libReference', Comp.LibReference) + ',' ++                JStr('comment', Comp.Comment.Text) + ',' ++                JRaw('xMil', IntToStr(Round(CoordToMils(Comp.Location.X)))) + ',' ++                JRaw('yMil', IntToStr(Round(CoordToMils(Comp.Location.Y)))) + ',' ++                JRaw('orientation', IntToStr(Comp.Orientation)) + ',' ++                JRaw('pins', '[' + PS + ']') + '}';+            N := N + 1;+        End;+        Comp := Iter.NextSchObject;+    End;+    Doc.SchIterator_Destroy(Iter);+    Result := '{' + JRaw('ok','true') + ',' + JStr('document', Doc.DocumentName) + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('components', '[' + S + ']') + '}';+End;++{ copper_layers: the full copper set (top, mid 1-30, planes 1-16, bottom) with names and visibility }+Function CmdCopperLayers(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    S : String;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    S := LayerJson(Board, eTopLayer, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer1, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer2, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer3, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer4, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer5, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer6, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer7, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer8, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer9, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer10, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer11, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer12, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer13, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer14, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer15, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer16, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer17, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer18, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer19, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer20, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer21, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer22, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer23, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer24, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer25, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer26, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer27, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer28, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer29, 'signal');+    S := S + ',' + LayerJson(Board, eMidLayer30, 'signal');+    S := S + ',' + LayerJson(Board, eInternalPlane1, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane2, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane3, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane4, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane5, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane6, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane7, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane8, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane9, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane10, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane11, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane12, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane13, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane14, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane15, 'plane');+    S := S + ',' + LayerJson(Board, eInternalPlane16, 'plane');+    S := S + ',' + LayerJson(Board, eBottomLayer, 'signal');+    Result := '{' + JRaw('ok','true') + ',' + JRaw('layers', '[' + S + ']') + '}';+End;+++{ ---- Phase 1 batch 3 ---------------------------------------------------- }+{ read_rules: every design rule on the board }+Function CmdReadRules(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    BIt   : IPCB_BoardIterator;+    Rule  : IPCB_Rule;+    S     : String;+    N     : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    S := ''; N := 0;+    BIt := Board.BoardIterator_Create;+    BIt.AddFilter_ObjectSet(MkSet(eRuleObject));+    BIt.AddFilter_LayerSet(AllLayers);+    BIt.AddFilter_Method(eProcessAll);+    Rule := BIt.FirstPCBObject;+    While Rule <> Nil Do+    Begin+        If S <> '' Then S := S + ',';+        S := S + '{' + JStr('name', Rule.Name) + ',' + JRaw('kind', IntToStr(Rule.RuleKind)) + ',' ++            JRaw('enabled', JBool(Rule.Enabled)) + ',' + JRaw('priority', IntToStr(Rule.Priority)) + ',' ++            JStr('scope1', Rule.Scope1Expression) + ',' + JStr('scope2', Rule.Scope2Expression) + ',' ++            JStr('comment', Rule.Comment) + '}';+        N := N + 1;+        Rule := BIt.NextPCBObject;+    End;+    Board.BoardIterator_Destroy(BIt);+    Result := '{' + JRaw('ok','true') + ',' + JRaw('count', IntToStr(N)) + ',' + JRaw('rules', '[' + S + ']') + '}';+End;++{ set_layer_visibility: layerId=<int> displayed=1|0 }+Function CmdSetLayerVisibility(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    L     : Integer;+    Show  : Boolean;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    L := StrToIntDef(Param(Lines, 'layerId'), -1);+    If L < 0 Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','layerId (from altium_layers) is required') + '}'; Exit; End;+    Show := Param(Lines, 'displayed') <> '0';+    Board.LayerIsDisplayed[L] := Show;+    Board.ViewManager_UpdateLayerTabs;+    Board.ViewManager_FullUpdate;+    Result := '{' + JRaw('ok','true') + ',' + JRaw('layerId', IntToStr(L)) + ',' + JStr('name', Board.LayerStack.LayerObject[L].Name) + ',' + JRaw('displayed', JBool(Board.LayerIsDisplayed[L])) + '}';+End;++{ create_net_class: name=<class> nets=<A;B;C> }+Function CmdCreateNetClass(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    Cls   : IPCB_ObjectClass;+    Name, Nets, N : String;+    P, K  : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    Name := Param(Lines, 'name');+    Nets := Param(Lines, 'nets');+    If (Name = '') Or (Nets = '') Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','name and nets (semicolon list) are required') + '}'; Exit; End;+    PCBServer.PreProcess;+    Cls := PCBServer.PCBClassFactory(eClassMemberKind_Net);+    Cls.Name := Name;+    K := 0;+    While Nets <> '' Do+    Begin+        P := Pos(';', Nets);+        If P = 0 Then Begin N := Nets; Nets := ''; End+        Else Begin N := Copy(Nets, 1, P - 1); Nets := Copy(Nets, P + 1, Length(Nets)); End;+        If N <> '' Then Begin Cls.AddMemberByName(N); K := K + 1; End;+    End;+    Board.AddPCBObject(Cls);+    PCBServer.PostProcess;+    Result := '{' + JRaw('ok','true') + ',' + JStr('class', Name) + ',' + JRaw('members', IntToStr(K)) + '}';+End;++{ footprint_primitives: designator=<board component> -> geometry dump (tracks, arcs, pads, texts, others counted) }+Function CmdFootprintPrimitives(Lines : TStringList) : String;+Var+    Board : IPCB_Board;+    Comp  : IPCB_Component;+    GIt   : IPCB_GroupIterator;+    Prim  : IPCB_Primitive;+    Trk   : IPCB_Track;+    Arc   : IPCB_Arc;+    Pad   : IPCB_Pad;+    Txt   : IPCB_Text;+    Des, S, Item : String;+    K, Other : Integer;+Begin+    Board := PCBServer.GetCurrentPCBBoard;+    If Board = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','no current PCB board (open a .PcbDoc first)') + '}'; Exit; End;+    Des := Param(Lines, 'designator');+    If Des = '' Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator is required') + '}'; Exit; End;+    Comp := FindCompByDes(Board, Des);+    If Comp = Nil Then Begin Result := '{' + JRaw('ok','false') + ',' + JStr('error','designator not found: ' + Des) + '}'; Exit; End;+    S := ''; K := 0; Other := 0;+    GIt := Comp.GroupIterator_Create;+    GIt.AddFilter_LayerSet(AllLayers);+    Prim := GIt.FirstPCBObject;+    While (Prim <> Nil) And (K < 2000) Do+    Begin+        Item := '';+        If Prim.ObjectId = eTrackObject Then+        Begin+            Trk := Prim;+            Item := '{' + JStr('kind','track') + ',' + JRaw('layerId', IntToStr(Trk.Layer)) + ',' ++                JRaw('x1', IntToStr(Round(CoordToMils(Trk.x1)))) + ',' + JRaw('y1', IntToStr(Round(CoordToMils(Trk.y1)))) + ',' ++                JRaw('x2', IntToStr(Round(CoordToMils(Trk.x2)))) + ',' + JRaw('y2', IntToStr(Round(CoordToMils(Trk.y2)))) + ',' ++                JRaw('wMil', IntToStr(Round(CoordToMils(Trk.Width)))) + '}';+        End+        Else If Prim.ObjectId = eArcObject Then+        Begin+            Arc := Prim;+            Item := '{' + JStr('kind','arc') + ',' + JRaw('layerId', IntToStr(Arc.Layer)) + ',' ++                JRaw('cx', IntToStr(Round(CoordToMils(Arc.XCenter)))) + ',' + JRaw('cy', IntToStr(Round(CoordToMils(Arc.YCenter)))) + ',' ++                JRaw('rMil', IntToStr(Round(CoordToMils(Arc.Radius)))) + ',' + JRaw('start', IntToStr(Round(Arc.StartAngle))) + ',' ++                JRaw('end', IntToStr(Round(Arc.EndAngle))) + ',' + JRaw('wMil', IntToStr(Round(CoordToMils(Arc.LineWidth)))) + '}';+        End+        Else If Prim.ObjectId = ePadObject Then+        Begin+            Pad := Prim;+            Item := '{' + JStr('kind','pad') + ',' + JStr('name', Pad.Name) + ',' + JRaw('layerId', IntToStr(Pad.Layer)) + ',' ++                JRaw('x', IntToStr(Round(CoordToMils(Pad.x)))) + ',' + JRaw('y', IntToStr(Round(CoordToMils(Pad.y)))) + ',' ++                JRaw('wMil', IntToStr(Round(CoordToMils(Pad.TopXSize)))) + ',' + JRaw('hMil', IntToStr(Round(CoordToMils(Pad.TopYSize)))) + ',' ++                JRaw('holeMil', IntToStr(Round(CoordToMils(Pad.HoleSize)))) + '}';+        End+        Else If Prim.ObjectId = eTextObject Then+        Begin+            Txt := Prim;+            Item := '{' + JStr('kind','text') + ',' + JRaw('layerId', IntToStr(Txt.Layer)) + ',' + JStr('text', Txt.Text) + ',' ++                JRaw('x', IntToStr(Round(CoordToMils(Txt.XLocation)))) + ',' + JRaw('y', IntToStr(Round(CoordToMils(Txt.YLocation)))) + '}';+        End+        Else Other := Other + 1;+        If Item <> '' Then+        Begin+            If S <> '' Then S := S + ',';+            S := S + Item;+            K := K + 1;+        End;+        Prim := GIt.NextPCBObject;+    End;+    Comp.GroupIterator_Destroy(GIt);+    Result := '{' + JRaw('ok','true') + ',' + JStr('designator', Comp.Name.Text) + ',' + JStr('pattern', Comp.Pattern) + ',' ++        JRaw('count', IntToStr(K)) + ',' + JRaw('otherPrimitives', IntToStr(Other)) + ',' + JRaw('primitives', '[' + S + ']') + '}';+End;+ Function Dispatch(Cmd : String; Lines : TStringList; Payload : String) : String; Begin     If Cmd = 'ping' Then Result := CmdPing(Lines)@@ -2932,6 +3511,17 @@ Begin     Else If Cmd = 'place_on_sch' Then Result := CmdPlaceOnSch(Lines)     Else If Cmd = 'place_on_pcb' Then Result := CmdPlaceOnPcb(Lines)     Else If Cmd = 'pcb_report' Then Result := CmdPcbReport(Lines)+    Else If Cmd = 'list_designators' Then Result := CmdListDesignators(Lines)+    Else If Cmd = 'component_data' Then Result := CmdComponentData(Lines)+    Else If Cmd = 'component_pins' Then Result := CmdComponentPins(Lines)+    Else If Cmd = 'list_nets' Then Result := CmdListNets(Lines)+    Else If Cmd = 'layers' Then Result := CmdLayers(Lines)+    Else If Cmd = 'net_connections' Then Result := CmdNetConnections(Lines)+    Else If Cmd = 'schematic_data' Then Result := CmdSchematicData(Lines)+    Else If Cmd = 'copper_layers' Then Result := CmdCopperLayers(Lines)+    Else If Cmd = 'read_rules' Then Result := CmdReadRules(Lines)+    Else If Cmd = 'set_layer_visibility' Then Result := CmdSetLayerVisibility(Lines)+    Else If Cmd = 'footprint_primitives' Then Result := CmdFootprintPrimitives(Lines)     Else If Cmd = 'build_footprint' Then Result := CmdBuildFootprint(Lines)     Else If Cmd = 'build_symbol_modes' Then Result := CmdBuildSymbolModes(Lines)     Else If Cmd = 'symbol_mode' Then Result := CmdSymbolMode(Lines)@@ -2956,7 +3546,7 @@ Begin         gRunning := False;         Result := '{' + JRaw('ok', 'true') + ',' + JStr('note', 'agent loop stopping') + '}';     End-    Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop') + '}';+    Else Result := '{' + JRaw('ok', 'false') + ',' + JStr('error', 'unknown command: ' + Cmd) + ',' + JStr('known', 'ping,state,run_process,open,eval,lib_list,lib_select,compile,violations,outjob,create_footprint,create_symbol,install_library,compile_intlib,extract_intlib,set_params,add_body,stop,list_designators,component_data,component_pins,list_nets,layers,net_connections,schematic_data,copper_layers,read_rules,set_layer_visibility,footprint_primitives') + '}'; End;  { ------------------------------------------------------------------ loop --- }
src/bridge.json+13−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "altium",   "displayName": "Altium Designer",-  "version": "0.1.10",+  "version": "0.1.11",   "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",@@ -103,7 +103,18 @@     "altium_set_visibility",     "altium_xcopy_footprint",     "altium_import_part",-    "altium_install_library_bundle"+    "altium_install_library_bundle",+    "altium_list_designators",+    "altium_component_data",+    "altium_component_pins",+    "altium_list_nets",+    "altium_layers",+    "altium_net_connections",+    "altium_schematic_data",+    "altium_copper_layers",+    "altium_read_rules",+    "altium_set_layer_visibility",+    "altium_footprint_primitives"   ],   "statusVerb": "altium_status",   "uninstall": "altium_uninstall",
src/handlers/agent.py+49−13
@@ -32,6 +32,7 @@ import uuid from pathlib import Path  from handlers import altium_detect+import caller_identity  _inbox_lock = threading.Lock() _no_recover = False@@ -205,21 +206,56 @@ def _kill_stray_helpers() -> list[int]:     return out  -def _reap_busy_notice() -> list:+def _reap_busy_notice(wait_sec: float = 4.0) -> list:     """A stray -R helper fired at a busy main instance makes Windows raise the single-instance-    notice ("There is another instance ... but it is busy at the moment") as a modal on the-    USER's screen — owned by the helper X2, not the main one (issue #3 follow-up, Colby). Scan-    every X2 dialog and dismiss that specific notice in the background so it never lingers."""-    from handlers import dialogs+    notice ("There is another instance of running, but it is busy at the moment") as a modal on+    the USER's screen. Measured 2026-08-31: it can appear a beat AFTER the helper is reaped and+    our own X2-pid enumeration missed it twice. So: ask ab (the desktop authority) — find any+    "Information" window whose text carries the notice, press its OK, and keep looking for a+    short window. Never blind-dismiss anything else."""     reaped = []-    try:-        for d in dialogs.scan(main_pid_only=False):-            blob = ((d.get("body") or "") + " " + (d.get("title") or "")).lower()-            if "another instance" in blob or "busy at the moment" in blob or "cannot proceed with the request" in blob:-                dialogs.press(d["hwnd"], "OK")-                reaped.append(d.get("title") or "notice")-    except Exception:-        pass+    deadline = time.time() + wait_sec+    seen = set()+    while time.time() < deadline:+        try:+            r = caller_identity.ad_callback("desktop_find_window", {"titleContains": "Information"}, on_behalf=False, timeout=8) or {}+            wins = r.get("matches") or r.get("windows") or ([r.get("best")] if r.get("best") else [])+        except Exception:+            wins = []+        hit = False+        for w in wins:+            hw = (w or {}).get("hwnd")+            if not hw or hw in seen:+                continue+            try:+                t = caller_identity.ad_callback("desktop_find_control", {"hwnd": hw, "contains": "another instance", "role": "text"}, on_behalf=False, timeout=8) or {}+                found = bool(t.get("control") or t.get("matches") or t.get("found"))+            except Exception:+                found = False+            if not found:+                continue+            seen.add(hw)+            try:+                caller_identity.ad_callback("desktop_ui_click", {"hwnd": hw, "name": "OK", "role": "button"}, on_behalf=False, timeout=8)+                reaped.append(hw)+                hit = True+            except Exception:+                pass+        # fallback: our own X2 scan (covers a notice parented to a live helper we still track)+        try:+            from handlers import dialogs+            for d in dialogs.scan(main_pid_only=False):+                blob = ((d.get("body") or "") + " " + (d.get("title") or "")).lower()+                if d["hwnd"] not in seen and ("another instance" in blob or "busy at the moment" in blob or "cannot proceed with the request" in blob):+                    dialogs.press(d["hwnd"], "OK"); seen.add(d["hwnd"]); reaped.append(d["hwnd"]); hit = True+        except Exception:+            pass+        if hit:+            time.sleep(0.5)+            continue+        if reaped:+            break+        time.sleep(0.5)     return reaped  
src/server.py+86−1
@@ -1501,6 +1501,91 @@ def _ps(script, timeout=60):     return _shell(f"powershell -NoProfile -EncodedCommand {enc}", timeout=timeout)  +# ---- Phase 1: board introspection (read-only; parity with coffeenmusic/eda-agent) ----+def _board_read(cmd, args, params=None, timeout=90):+    g = _need_licensed()+    if g:+        return g+    r = agent.call(cmd, params or {}, timeout=float(args.get("timeout", timeout)))+    if not r.get("success", True) and "no current PCB board" in (r.get("error") or ""):+        r["_hint"] = "Open a .PcbDoc first (altium_open_document / altium_show_2d_board), then retry."+    return r+++def h_list_designators(args):+    return _board_read("list_designators", args)+++def h_component_data(args):+    return _board_read("component_data", args, {"designator": args.get("designator", ""), "limit": str(args.get("limit", 500))})+++def h_component_pins(args):+    if not args.get("designator"):+        return {"success": False, "error": "designator is required (e.g. U1)"}+    return _board_read("component_pins", args, {"designator": args["designator"]})+++def h_list_nets(args):+    return _board_read("list_nets", args)+++def h_layers(args):+    return _board_read("layers", args)+++def h_net_connections(args):+    return _board_read("net_connections", args, {"net": args.get("net", ""), "limit": str(args.get("limit", 4000))}, timeout=120)+++def h_schematic_data(args):+    g = _need_licensed()+    if g:+        return g+    r = agent.call("schematic_data", {"designator": args.get("designator", ""), "limit": str(args.get("limit", 500)),+                                       "pins": "0" if args.get("pins") is False else "1"}, timeout=float(args.get("timeout", 120)))+    if "no current schematic" in (r.get("error") or ""):+        r["_hint"] = "Open a .SchDoc first (altium_open_document / altium_show_schematic), then retry."+    return r+++def h_copper_layers(args):+    return _board_read("copper_layers", args)+++def h_read_rules(args):+    return _board_read("read_rules", args)+++def h_set_layer_visibility(args):+    g = _need_licensed()+    if g:+        return g+    if args.get("layerId") is None:+        return {"success": False, "error": "layerId is required (from altium_layers / altium_copper_layers)"}+    return agent.call("set_layer_visibility", {"layerId": str(args["layerId"]), "displayed": "0" if args.get("displayed") is False else "1"}, timeout=float(args.get("timeout", 60)))+++def h_create_net_class(args):+    g = _need_licensed()+    if g:+        return g+    if not args.get("name") or not args.get("nets"):+        return {"success": False, "error": "name and nets (list or 'A;B;C') are required"}+    nets = args["nets"]+    if isinstance(nets, list):+        nets = ";".join(str(n) for n in nets)+    r = agent.call("create_net_class", {"name": args["name"], "nets": nets}, timeout=float(args.get("timeout", 60)))+    r.setdefault("_hint", "Net class added to the board (unsaved). altium_save to persist; altium_read_rules to scope rules to it.")+    return r+++def h_footprint_primitives(args):+    if not args.get("designator"):+        return {"success": False, "error": "designator is required (a placed component, e.g. U1)"}+    return _board_read("footprint_primitives", args, {"designator": args["designator"]}, timeout=120)++ def _todo(name, phase):     def h(args):         return {"success": False, "errorCode": "not_implemented", "verb": f"altium_{name}",@@ -1588,7 +1673,7 @@ COMMAND_HANDLERS = {     "install_library": h_install_library, "install_library_bundle": h_install_library_bundle,     "license_status": h_license_status, "ensure_licensed": h_ensure_licensed, "setup_trial": h_setup_trial, "compile_intlib": h_compile_intlib, "extract_intlib": h_extract_intlib,     "set_params": h_set_params, "add_body": h_add_body,-    "place_on_sch": h_place_on_sch, "place_on_pcb": h_place_on_pcb, "pcb_report": h_pcb_report, "build_footprint": h_build_footprint, "build_symbol_modes": h_build_symbol_modes, "add_models": h_add_models, "remove_symbol": h_remove_symbol, "clone_symbol": h_clone_symbol, "rename_symbol": h_rename_symbol, "read_symbol": h_read_symbol, "copy_footprint": h_copy_footprint, "rename_footprint": h_rename_footprint, "remove_footprint": h_remove_footprint, "cleanup_libs": h_cleanup_libs, "set_visibility": h_set_visibility, "xcopy_footprint": h_xcopy_footprint, "import_part": h_import_part, "normalize_bodies": h_normalize_bodies, "test_board": h_test_board, "board_shape": h_board_shape, "swap_footprint": h_swap_footprint, "symbol_mode": h_symbol_mode,+    "place_on_sch": h_place_on_sch, "place_on_pcb": h_place_on_pcb, "pcb_report": h_pcb_report, "list_designators": h_list_designators, "component_data": h_component_data, "component_pins": h_component_pins, "list_nets": h_list_nets, "layers": h_layers, "net_connections": h_net_connections, "schematic_data": h_schematic_data, "copper_layers": h_copper_layers, "read_rules": h_read_rules, "set_layer_visibility": h_set_layer_visibility, "footprint_primitives": h_footprint_primitives, "build_footprint": h_build_footprint, "build_symbol_modes": h_build_symbol_modes, "add_models": h_add_models, "remove_symbol": h_remove_symbol, "clone_symbol": h_clone_symbol, "rename_symbol": h_rename_symbol, "read_symbol": h_read_symbol, "copy_footprint": h_copy_footprint, "rename_footprint": h_rename_footprint, "remove_footprint": h_remove_footprint, "cleanup_libs": h_cleanup_libs, "set_visibility": h_set_visibility, "xcopy_footprint": h_xcopy_footprint, "import_part": h_import_part, "normalize_bodies": h_normalize_bodies, "test_board": h_test_board, "board_shape": h_board_shape, "swap_footprint": h_swap_footprint, "symbol_mode": h_symbol_mode,     "install_extension": h_install_extension, "extension_ping": h_extension_ping,     "export_step": h_export_step, "import_kicad": h_import_kicad,     "a365_configure": h_a365_configure, "a365_status": h_a365_status, "a365_query": h_a365_query,