← Commit history

v0.9.116: proactive KiCad update nudge - throttled (24h) async check, surfaced as kicadUpdate{installed,latest,_hint} on status/readiness/state/bridge_status/window_info so the AI can OFFER the user the update; kicad_upgrade remains the one-call runner

John Lauer ·b180a9bd8f ·2mo ago ·parent b7da1c5
6 files changed +90−7
BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.9.115+0.9.116
adom-bridge-kicad-manifest.json+5−5
@@ -1,15 +1,15 @@ {   "manifest_version": 1,   "name": "kicad",-  "version": "0.9.115",-  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.115/adom-bridge-kicad-v0.9.115.zip",-  "sha256": "733b6ddad46b92a6ec97fc74909ca917ea63f7b610269c90209d687966d6af87",-  "size": 409411,+  "version": "0.9.116",+  "url": "https://wiki.adom.inc/download/adom/kicad-bridge/0.9.116/adom-bridge-kicad-v0.9.116.zip",+  "sha256": "bb1c9f8d334e299e69ea75099bc020815baf327e02eba97b3f1e9ca1078c4fd6",+  "size": 410551,   "verbPrefixes": [     "kicad_"   ],   "healthEndpoint": "/status",-  "released_at": "2026-08-16T13:29:16Z",+  "released_at": "2026-08-16T13:47:30Z",   "hero": "https://wiki.adom.inc/blob/app/kicad-bridge/kicad-hero.png",   "languages": [     "Python",
adom-bridge-kicad-v0.9.116.zipadded
⋯ 1 unchanged line ⋯
bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "kicad",   "displayName": "KiCad EDA",-  "version": "0.9.115",+  "version": "0.9.116",   "description": "Reverse bridge for KiCad \u2014 board/schematic introspection, lint via kicad-cli, plugin install, multi-instance probe, in-process DRC.",   "homepage": "https://wiki.adom.inc/adom/adom-bridge",   "author": "Adom Inc.",
handlers/upgrade.py+71
@@ -449,3 +449,74 @@ def handle_upgrade(kicad_info: dict, args: dict) -> dict:             )         ),     }++# ── Proactive update nudge (0.9.116, John 2026-08-16: "occasionally check the+# latest kicad version number and send a hint to the ai") ────────────────────+# The AI only ever reads verb OUTPUT, so the nudge rides as a `kicadUpdate`+# field on the everyday status verbs (injected centrally by the dispatcher).+# The network check runs at most once per 24h, always in a background thread —+# a verb response is never delayed by it.+_NUDGE_INTERVAL_S = 24 * 3600+_nudge_cache = {"claimed": 0.0, "latest": None, "installed_full": None}+++def _installed_full_version(kicad_info) -> str:+    """Full X.Y.Z of the active install via `kicad-cli version` (cached per boot;+    kicad_info['version'] is only major.minor, useless for point-release compares)."""+    if _nudge_cache["installed_full"]:+        return _nudge_cache["installed_full"]+    try:+        import subprocess+        cli = (kicad_info or {}).get("kicad_cli_exe")+        if cli:+            out = subprocess.run([cli, "version"], capture_output=True, text=True,+                                 timeout=15).stdout.strip()+            import re as _re+            m = _re.search(r"(\d+\.\d+\.\d+)", out)+            if m:+                _nudge_cache["installed_full"] = m.group(1)+                return m.group(1)+    except Exception:+        pass+    return (kicad_info or {}).get("version") or ""+++def kicad_update_nudge(kicad_info) -> dict | None:+    """Compact nudge dict when a newer KiCad exists, else None. Non-blocking."""+    import threading+    import time++    now = time.time()+    if now - _nudge_cache["claimed"] > _NUDGE_INTERVAL_S:+        _nudge_cache["claimed"] = now  # claim the slot so only one thread refreshes++        def _refresh():+            try:+                latest, _src = latest_stable_version()+                _nudge_cache["latest"] = latest+            except Exception:+                pass++        try:+            threading.Thread(target=_refresh, daemon=True, name="kicad-update-nudge").start()+        except Exception:+            pass++    latest = _nudge_cache.get("latest")+    if not latest or not kicad_info or not kicad_info.get("installed"):+        return None+    installed = _installed_full_version(kicad_info)+    try:+        if kicad_detect._version_tuple(latest) <= kicad_detect._version_tuple(installed):+            return None+    except Exception:+        return None+    return {+        "installed": installed,+        "latest": latest,+        "_hint": (f"KiCad {latest} is out (this box runs {installed}). Not urgent: mention it "+                  f"to the user when convenient and ask if they want it. If yes: kicad_close "+                  f"first, then kicad_upgrade {{}} — official downloads.kicad.org installer, "+                  f"silent, per-user, no UAC. Never update mid-design-work."),+    }+
server.py+12
@@ -755,6 +755,18 @@ def dispatch_command(command: str, args: dict) -> dict:     # Stamp the version actually used so callers can see which install ran     if isinstance(result, dict):         result.setdefault("kicadVersionUsed", selected_info.get("version"))+    # Proactive update nudge (0.9.116): at most one network check per 24h+    # (async), surfaced on the everyday status verbs so the AI can OFFER the+    # user an update — the AI only ever reads verb output.+    if isinstance(result, dict) and command in ("status", "readiness", "state",+                                                "bridge_status", "window_info"):+        try:+            from handlers.upgrade import kicad_update_nudge+            nudge = kicad_update_nudge(selected_info)+            if nudge:+                result.setdefault("kicadUpdate", nudge)+        except Exception:+            pass     # CONSTANT DIALOG COVERAGE: after EVERY GUI-affecting verb, sweep for KiCad     # dialogs (KiCad throws modal error boxes constantly — "Cannot enumerate     # ...Templates (Access denied)", save prompts, format warnings — that