← Commit history
dashboard/kicad_dashboard.py+1−1
@@ -49,7 +49,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from urllib.parse import unquote, urlparse -VERSION = "2.1.2"+VERSION = "2.1.3" AI_THREAD = "kicad-dashboard" DEFAULT_PORT = 8931 UI_DIR = Path(__file__).resolve().parent / "ui"
dashboard/ui/assets/shots/taskbar-both.png
⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-flash.png
⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-none.png
⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-overlay.png
⋯ 1 unchanged line ⋯
dashboard/ui/assets/shots/taskbar-progress.png
⋯ 1 unchanged line ⋯
dashboard/ui/index.html+11−11
@@ -97,7 +97,7 @@ dialog{margin:auto;background:var(--surface);color:var(--text-body);border:1px s dialog::backdrop{background:rgba(0,0,0,.25)}  /* ---- Settings modal: the Adom settings surface (settings-layout skill) ---- */-dialog.settings{width:min(560px,92vw);max-width:none;padding:0;overflow:hidden;border-radius:12px;background:var(--surface)}+dialog.settings{width:min(660px,92vw);max-width:none;padding:0;overflow:hidden;border-radius:12px;background:var(--surface)} dialog.settings:focus{outline:none} .settings-head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px 12px 18px;border-bottom:1px solid var(--border);cursor:move;user-select:none} .settings-head h2{margin:0;font:600 15px/1.3 var(--brand-heading);color:var(--text);display:flex;align-items:center;gap:10px}@@ -121,14 +121,14 @@ dialog.settings:focus{outline:none} .default-hint{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2)} .setting-shot{margin:0 0 .75rem} .shot-pair{display:flex;flex-wrap:wrap;gap:.75rem}-.shot-pair .setting-shot{flex:1 1 168px;max-width:200px}+.shot-pair .setting-shot{flex:1 1 272px;max-width:272px} .setting-shot figcaption b{color:var(--text-body);font-weight:600} .setting-shot img{display:block;max-width:100%;height:auto;border:1px solid var(--border);border-radius:8px;background:var(--surface)} .setting-shot figcaption{margin-top:.35rem;font-family:var(--brand-body);font-size:.75rem;color:var(--text-2)} .choice-set{border:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:.75rem}-.choice{flex:1 1 200px;display:flex;flex-direction:column;gap:.35rem;padding:.6rem;cursor:pointer;position:relative;background:var(--surface);border:1px solid var(--border);border-radius:8px;transition:border-color .15s ease,background .15s ease,transform .06s ease}+.choice{flex:1 1 280px;display:flex;flex-direction:column;gap:.35rem;padding:.6rem;cursor:pointer;position:relative;background:var(--surface);border:1px solid var(--border);border-radius:8px;transition:border-color .15s ease,background .15s ease,transform .06s ease} .choice input{position:absolute;opacity:0;pointer-events:none}-.choice img{display:block;width:100%;max-width:168px;height:auto;border-radius:4px;background:var(--bg);image-rendering:auto}+.choice img{display:block;width:100%;max-width:272px;height:auto;border-radius:4px;background:var(--bg);image-rendering:auto} .choice-name{font-family:var(--brand-heading);font-size:.875rem;font-weight:600;color:var(--text)} .choice-desc{font-family:var(--brand-body);font-size:.75rem;color:var(--text-2);line-height:1.5} .choice:hover{border-color:var(--accent);background:var(--elevated)}@@ -246,28 +246,28 @@ main a:hover{border-bottom-color:var(--accent)}       <div class="wrapper">         <h4 class="name" title="activityIndicator"><span class="prefix">Taskbar: </span><span class="title">Activity indicator</span></h4>         <fieldset class="choice-set" role="radiogroup" aria-labelledby="activityLabel">-          <legend id="activityLabel" class="set-check-label" style="padding:0;margin-bottom:.35rem">What the KiCad taskbar button does while Adom is driving it. Each picture is a capture of this machine's taskbar.</legend>+          <legend id="activityLabel" class="set-check-label" style="padding:0;margin-bottom:.35rem">What the KiCad taskbar button does while Adom is driving it. Each picture is a 1:1 capture of this machine's taskbar at its native resolution.</legend>           <label class="choice" data-mode="progress" data-selected="false">             <input type="radio" name="activityIndicator" value="progress">-            <img src="assets/shots/taskbar-progress.png" width="168" height="56" loading="lazy" alt="The KiCad taskbar button with a blue progress bar about two thirds full under its icon, neighbours untouched">+            <img src="assets/shots/taskbar-progress.png" width="272" height="88" loading="lazy" alt="The KiCad taskbar button with a blue progress bar about two thirds full under its icon, neighbours untouched">             <span class="choice-name">Progress bar</span>             <span class="choice-desc">Fills over the verb's usual duration on this box, then clears itself. Cancels the OS flash.</span>           </label>           <label class="choice" data-mode="flash" data-selected="false">             <input type="radio" name="activityIndicator" value="flash">-            <img src="assets/shots/taskbar-flash.png" width="168" height="56" loading="lazy" alt="The same KiCad taskbar button tinted salmon with a highlighted underline, no progress bar">+            <img src="assets/shots/taskbar-flash.png" width="272" height="88" loading="lazy" alt="The same KiCad taskbar button tinted salmon with a highlighted underline, no progress bar">             <span class="choice-name">Attention flash</span>             <span class="choice-desc">Windows' own highlight, left alone. Says something happened, not how long is left, and stays until you click it.</span>           </label>           <label class="choice" data-mode="both" data-selected="false">             <input type="radio" name="activityIndicator" value="both">-            <img src="assets/shots/taskbar-both.png" width="168" height="56" loading="lazy" alt="The same KiCad taskbar button tinted salmon and carrying the blue progress bar at the same time">+            <img src="assets/shots/taskbar-both.png" width="272" height="88" loading="lazy" alt="The same KiCad taskbar button tinted salmon and carrying the blue progress bar at the same time">             <span class="choice-name">Bar and flash</span>             <span class="choice-desc">Both at once. The bar still clears itself; the tint stays until you click.</span>           </label>           <label class="choice" data-mode="none" data-selected="false">             <input type="radio" name="activityIndicator" value="none">-            <img src="assets/shots/taskbar-none.png" width="168" height="56" loading="lazy" alt="The same KiCad taskbar button with no bar and no highlight, only the running dot">+            <img src="assets/shots/taskbar-none.png" width="272" height="88" loading="lazy" alt="The same KiCad taskbar button with no bar and no highlight, only the running dot">             <span class="choice-name">Nothing</span>             <span class="choice-desc">The button never changes. You rely on the overlay badge below, or on nothing.</span>           </label>@@ -281,11 +281,11 @@ main a:hover{border-bottom-color:var(--accent)}       <p class="card-desc">The Adom mark is stamped in the corner of a KiCad window's taskbar button while a verb is running against that window, and comes off by itself five minutes after the last verb, so a badge always means "Adom is driving this right now". It is a per-window overlay through the same taskbar API, so it never changes the button's own icon or how Windows groups it, and it is never stamped by read-only verbs like status checks. A bridge restart clears every badge, because a freshly started bridge is driving nothing.</p>       <div class="shot-pair">         <figure class="setting-shot">-          <img src="assets/shots/taskbar-overlay.png" width="168" height="56" loading="lazy" alt="The KiCad taskbar button with the small teal Adom mark in its top right corner">+          <img src="assets/shots/taskbar-overlay.png" width="272" height="88" loading="lazy" alt="The KiCad taskbar button with the small teal Adom mark in its top right corner">           <figcaption><b>On:</b> the Adom mark sits on the KiCad PCB Editor button while a verb drives it.</figcaption>         </figure>         <figure class="setting-shot">-          <img src="assets/shots/taskbar-none.png" width="168" height="56" loading="lazy" alt="The same KiCad taskbar button with no mark on it, only its own icon">+          <img src="assets/shots/taskbar-none.png" width="272" height="88" loading="lazy" alt="The same KiCad taskbar button with no mark on it, only its own icon">           <figcaption><b>Off:</b> the same button, same moment, nothing added.</figcaption>         </figure>       </div>
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "kicad-bridge",-  "version": "1.0.53",+  "version": "1.0.54",   "type": "app",   "description": "Skills for your container so your AI knows how to drive the KiCad bridge. The bridge runtime itself is the release zip; Adom Bridge loads that.",   "tags": [
page.json+1−1
@@ -4,7 +4,7 @@   "slug": "kicad-bridge",   "title": "KiCad - the KiCad Bridge",   "brief": "Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.",-  "version": "1.0.53",+  "version": "1.0.54",   "tags": [     "kicad",     "pcb",
tools/capture_taskbar_shots.py+65−28
@@ -1,57 +1,94 @@ #!/usr/bin/env python3-"""Re-capture the REAL taskbar screenshots the dashboard settings modal shows-(settings-layout skill: never fabricate). Drives AdomLapper: finds the pcbnew-button by its green icon, crops 168x56 at 1:1. Usage: <name> [frames]; put the-button in the state first (desktop_taskbar progress/overlay, FlashWindowEx)."""-import base64, io, json, subprocess, sys, time+"""NATIVE-resolution capture of the pcbnew taskbar button on AdomLapper.++desktop_screenshot_screen returns a downscaled frame (1568 px on a 2560 px+display), which made the first set of settings shots grainy. This captures+the taskbar band on the box itself (DPI-aware CopyFromScreen), pulls the PNG+over, and crops 1:1 around the button found by its green icon.+Usage: cap2.py <name> [frames]   (frames>1 keeps the frame with the most+salmon/orange tint, for the attention-flash states)."""+import base64, json, os, subprocess, sys, time from PIL import Image  SHOTS = "/home/adom/project/adom-desktop-kicad-bridge/dashboard/ui/assets/shots"-W, H = 168, 56+PULL = "/tmp/claude-1001/-home-adom-project/a82462cd-99a1-4a08-b142-383081e6ec8a/scratchpad/pull"+W, H = 272, 88+os.makedirs(PULL, exist_ok=True)++PS = r'''+Add-Type -AssemblyName System.Drawing+Add-Type -TypeDefinition 'using System.Runtime.InteropServices; public class D2 { [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] public static extern int GetSystemMetrics(int n); }'+[D2]::SetProcessDPIAware() | Out-Null+$w=[D2]::GetSystemMetrics(0); $h=[D2]::GetSystemMetrics(1)+$band = 110+$bmp = New-Object System.Drawing.Bitmap $w, $band+$g = [System.Drawing.Graphics]::FromImage($bmp)+$g.CopyFromScreen(0, $h - $band, 0, 0, $bmp.Size)+$out = Join-Path $env:TEMP "adom-tb-%NAME%.png"+$bmp.Save($out, [System.Drawing.Imaging.ImageFormat]::Png)+Write-Output $out+''' -def grab():+def ab(verb, args):     out = subprocess.run(["timeout", "120", "adom-bridge", "--ai-thread", "kicad-icon",-                          "--target", "AdomLapper", "desktop_screenshot_screen", "{}"],+                          "--target", "AdomLapper", verb, json.dumps(args)],                          capture_output=True, text=True).stdout-    d = json.loads(out); inner = json.loads(d["output"])["data"]-    return Image.open(io.BytesIO(base64.b64decode(inner["image"]))).convert("RGB")+    return json.loads(out)++def grab(tag):+    ps = PS.replace("%NAME%", tag)+    r = ab("run_script", {"interpreter": "powershell",+                          "scriptB64": base64.b64encode(ps.encode()).decode(),+                          "reason": "Capture the taskbar band at native resolution for a real settings screenshot"})+    remote = (r.get("stdout") or "").strip().splitlines()[-1]+    p = ab("pull_file", {"filePaths": [remote], "saveTo": PULL})+    local = None+    for cand in (p.get("files") or p.get("saved") or []):+        local = cand if isinstance(cand, str) else (cand.get("localPath") or cand.get("path") or cand.get("saveTo"))+    if not local:+        local = os.path.join(PULL, os.path.basename(remote))+    return Image.open(local).convert("RGB")  def button_center(im):-    w, h = im.size-    band = im.crop((0, h - 58, w, h)); px = band.load()+    w, h = im.size; px = im.load()     cols = []-    for x in range(band.width):-        n = sum(1 for y in range(band.height)-                if (lambda r, g, b: g > 110 and g > r + 40 and g > b + 40 and r < 120)(*px[x, y]))-        if n >= 6: cols.append(x)+    for x in range(w):+        n = sum(1 for y in range(h) if (lambda r, g, b: g > 110 and g > r + 40 and g > b + 40 and r < 120)(*px[x, y]))+        if n >= 10: cols.append(x)     runs, s, p = [], None, None     for x in cols:         if s is None: s = x         elif x != p + 1: runs.append((s, p)); s = x         p = x     if s is not None: runs.append((s, p))-    runs = [r for r in runs if 14 <= r[1] - r[0] <= 30]-    if not runs: raise SystemExit("green icon not found")+    runs = [r for r in runs if 22 <= r[1] - r[0] <= 50]+    if not runs: raise SystemExit("green icon not found; runs=%r" % runs)     a, b = runs[-1]-    return (a + b) // 2, h - 30+    # icon centre x; the button spans the full band height: centre ~ h-46 at 150%+    return (a + b) // 2, h - 47 -def orange(im):+def tint(im):     px = im.load(); n = 0     for x in range(im.width):         for y in range(im.height):             r, g, b = px[x, y]-            if r > 170 and 70 < g < 180 and b < 90: n += 1+            if r > 120 and r > g + 35 and r > b + 20 and g < 150: n += 1     return n  name = sys.argv[1]; frames = int(sys.argv[2]) if len(sys.argv) > 2 else 1-best, bestScore = None, -1+best, score = None, -1 for i in range(frames):-    im = grab(); cx, cy = button_center(im)+    im = grab(f"{name}-{i}")+    if os.environ.get('CENTER'):+        cx, cy = [int(v) for v in os.environ['CENTER'].split(',')]+    else:+        cx, cy = button_center(im)     crop = im.crop((cx - W // 2, cy - H // 2, cx + W // 2, cy + H // 2))-    sc = orange(crop)-    print(f"frame {i}: center=({cx},{cy}) orange={sc}")-    if sc > bestScore: best, bestScore = crop, sc+    sc = tint(crop)+    print(f"frame {i}: band={im.size} center=({cx},{cy}) tint={sc}")+    if sc > score: best, score = crop, sc     if frames > 1: time.sleep(0.45) best.save(f"{SHOTS}/{name}.png")-best.resize((W * 4, H * 4), Image.NEAREST).save(f"/tmp/claude-1001/-home-adom-project/a82462cd-99a1-4a08-b142-383081e6ec8a/scratchpad/shot-{name}-x4.png")-print("saved", name, "orange", bestScore)+best.resize((W * 2, H * 2), Image.NEAREST).save(+    f"/tmp/claude-1001/-home-adom-project/a82462cd-99a1-4a08-b142-383081e6ec8a/scratchpad/n-{name}-x2.png")+print("saved", name, "tint", score)