← Commit history

2.2.0: dash-screenshot - capture ONLY your dashboard's webview pane in ah, not the whole window, with the tool

John Lauer ·6a6d5d3622 ·1mo ago ·parent e3e29ed
3 files changed +232−1
assets/tools/shot-webview.pyadded+100
@@ -0,0 +1,100 @@+#!/usr/bin/env python3+"""Capture ONLY a dock dashboard's webview pane inside ah.++A full-window hd_shot spends ~80% of its pixels on ah's chrome. This activates your tab,+reads your iframe's rect from the shell target, takes the shot, derives the scale from+hd_shot's OWN windowOuter metadata (never hardcoded), and crops to your pane.++  python3 shot-webview.py --tab "Parts Search" --slug dock-parts-search \+      --thread "my ai thread" --out /tmp/frame.png+  ... --box 33,160,216,240,red --box 249,160,424,240,lime      # your page's coords+"""+import argparse, json, subprocess, sys++def bridge(verb, payload, thread, reason):+    cmd = ['adom-bridge', '--target', 'AdomLapper', verb, json.dumps(payload),+           '--ai-thread', thread, '--reason', reason]+    p = subprocess.run(cmd, capture_output=True, text=True, timeout=180)+    try:+        return json.loads(p.stdout)+    except Exception:+        sys.exit('bridge call failed: ' + (p.stdout or p.stderr)[:400])++def cdp(js, target, thread, reason):+    r = bridge('hd_api', {"method": "POST", "path": "/cdp-eval",+                          "body": {"target": target, "js": js}}, thread, reason)+    return (r.get('body') or {}).get('value')++def main():+    ap = argparse.ArgumentParser()+    ap.add_argument('--tab', required=True, help='tab name exactly as the tab strip shows it')+    ap.add_argument('--slug', required=True, help='a unique substring of your iframe src, e.g. your dock slug')+    ap.add_argument('--thread', required=True, help='your ai thread name, for the activity log')+    ap.add_argument('--out', default='/tmp/webview.png')+    ap.add_argument('--pad', type=int, default=0, help='extra px of ah chrome to keep around the pane')+    ap.add_argument('--box', action='append', default=[],+                    help='x,y,w,h,colour in YOUR page coordinates; repeatable')+    a = ap.parse_args()++    # 1. ACTIVATE. An unforegrounded tab reports zero-height rects and SPECIFIED css.+    act = cdp("""(() => {+      const t = [...document.querySelectorAll('.tab-bar .tab')]+        .find(x => ((x.querySelector('span.tab-name')||{}).textContent||'').trim() === %s);+      if (!t) return 'NOTAB';+      if (!t.classList.contains('active')) t.click();+      return 'active';+    })()""" % json.dumps(a.tab), 'shell', a.thread,+        f'activate {a.tab} before measuring, an unforegrounded tab measures wrong')+    if act != 'active':+        sys.exit(f'could not activate tab {a.tab!r}: {act}')++    # 2. RECT, from the shell. Your own page cannot see where it sits in the window.+    raw = cdp("""(() => {+      const f = [...document.querySelectorAll('iframe')]+        .find(x => (x.getAttribute('src')||'').includes(%s));+      if (!f) return 'NOFRAME';+      const r = f.getBoundingClientRect();+      return JSON.stringify({x:Math.round(r.x), y:Math.round(r.y),+                             w:Math.round(r.width), h:Math.round(r.height),+                             shellW:innerWidth, dpr:devicePixelRatio});+    })()""" % json.dumps(a.slug), 'shell', a.thread,+        f'read the {a.slug} pane rect for a webview-only capture')+    if not raw or raw == 'NOFRAME':+        sys.exit(f'no iframe matching {a.slug!r}')+    rect = json.loads(raw)+    if rect['w'] == 0 or rect['h'] == 0:+        sys.exit('pane rect is zero: the tab did not actually foreground')++    # 3. SHOT, then scale from the RETURNED metadata. Never hardcode a scale or an origin.+    shot = bridge('hd_shot', {"region": "full"}, a.thread, f'capture the {a.tab} pane')+    safe = shot.get('localSafePath') or shot.get('safePath')+    if not safe:+        sys.exit('hd_shot did not return localSafePath: ' + json.dumps(shot)[:300])++    from PIL import Image, ImageDraw+    im = Image.open(safe).convert('RGB')+    # Scale from the SHELL'S CSS VIEWPORT WIDTH, because the rect above is in CSS px.+    # NOT from hd_shot's windowOuter: that is the OS window in PHYSICAL px (2582 where the+    # CSS viewport is 1706 at dpr 1.5), and using it produces a crop about 34% too small+    # that still looks like a plausible picture of a dashboard. Measured, not assumed.+    scale = im.width / float(rect['shellW'])++    if a.box:+        d = ImageDraw.Draw(im)+        for spec in a.box:+            x, y, w, h, colour = spec.split(',')+            x, y, w, h = (int(v) for v in (x, y, w, h))+            d.rectangle([(rect['x']+x)*scale, (rect['y']+y)*scale,+                         (rect['x']+x+w)*scale, (rect['y']+y+h)*scale],+                        outline=colour.strip(), width=3)++    p = a.pad+    im.crop((max(0, int((rect['x']-p)*scale)), max(0, int((rect['y']-p)*scale)),+             min(im.width,  int((rect['x']+rect['w']+p)*scale)),+             min(im.height, int((rect['y']+rect['h']+p)*scale)))).save(a.out)++    print(json.dumps({"out": a.out, "paneRect": rect,+                      "scale": round(scale, 4), "fullForShotlog": shot.get('localFullPath')}, indent=1))++if __name__ == '__main__':+    main()
package.json+3−1
@@ -1,6 +1,6 @@ {   "slug": "adom-ui-design",-  "version": "2.1.7",+  "version": "2.2.0",   "type": "skill",   "description": "The Adom UI Design System plus a bundle of focused UI skills (tooltips, floating-windows, toasts, responsive-ui, app-shell, ai-driven-apps, clickable-paths, dark-default, input-feedback, settings-layout). One install drops the whole set of skill files onto your container, plus shared icon assets (the settings cog) so every Adom app draws the same glyph.",   "dependencies": {},@@ -13,6 +13,7 @@     "assets/icons/action-send.svg",     "assets/icons/settings-cog-outline.svg",     "assets/module/newbie-panel.js",+    "assets/tools/shot-webview.py",     "install.sh",     "page.json",     "screenshots/tokens.png",@@ -22,6 +23,7 @@     "skills/dark-default/SKILL.md",     "skills/dash-demo/SKILL.md",     "skills/dash-header/SKILL.md",+    "skills/dash-screenshot/SKILL.md",     "skills/dock-dashboard/SKILL.md",     "skills/floating-windows/SKILL.md",     "skills/icons/SKILL.md",
skills/dash-screenshot/SKILL.mdadded+129
@@ -0,0 +1,129 @@+---+name: dash-screenshot+user-invocable: true+description: >-+  Screenshot ONLY your dock dashboard's webview pane inside ah, not the whole Hydrogen+  window. A full-window hd_shot spends about 80% of its pixels on ah's rail, tab strip,+  editor and status bar, and after the Claude-safe resize your dashboard's 14px text is+  unreadable and its colours are resampled, so every measurement you try to eyeball from it+  is wrong. This skill gives the exact crop recipe (activate the tab by name, read the+  iframe rect from the shell target, derive the scale from hd_shot's own metadata, crop),+  the ready-made script, and the traps: unforegrounded tabs report zero-height rects, and+  the capture is scaled so a hardcoded offset silently drifts. Read BEFORE capturing any+  dock dashboard frame for a review, an issue, or the shotlog. Trigger words: screenshot the+  dashboard, capture my panel, dock dashboard screenshot, hd_shot region, crop the webview,+  frame for the shotlog, show my dashboard in ah, webview only screenshot.+---++Parent skill: **adom-ui-design** (works with **dock-dashboard**, **newbie-panel**)++# Screenshot the webview, not the whole of ah++**The problem, John's words, 2026-08-25:** a dock thread capturing its dashboard "wastes+getting a screenshot of ALL of ah's UI".++At his working layout the ah window is about 1706 logical px wide and a dock app's pane is+**744**. So a full-window shot spends roughly **80% of its pixels on ah's chrome** and gives+your dashboard about a fifth of the frame. Then `hd_shot` resizes the result to 1400px for+Claude-safe reading, which shrinks your pane again. Your 14px phrases land around 9px, the+`0.67px` borders you are trying to check disappear into resampling, and any colour you read+off it has been through two resamples. **You cannot verify a panel from a picture of a+window.**++## There is no built-in region for this yet++`hd_shot` regions are deterministic and named, and today the list is:++```+full · titlebar · vscode · setup-panel · wiki · claude+```++**None of them is "the dock app's webview".** Adding one means extending `KNOWN_REGIONS` and+`region_selector()` in `hd_bridge.rs`, which belongs to the ah build thread. Until that lands,+crop it yourself with the recipe below. When it lands, use the region and delete your crop+code.++`hd_api` has no per-target screenshot route either (39 endpoints, the only relevant ones are+`GET /cdp-info` and `POST /eval`), so there is no way to ask ah for just your frame.++## The recipe++Three steps, and the third is the one people get wrong.++**1. ACTIVATE YOUR TAB BY NAME FIRST.** An unforegrounded tab reports **zero-height rects and+specified rather than computed CSS**. Every measurement you take from one is garbage, and it+is garbage that looks like a number. This has produced at least three wrong conclusions in+this fleet.++```js+// run against target "shell"+const t = [...document.querySelectorAll('.tab-bar .tab')]+  .find(x => ((x.querySelector('span.tab-name')||{}).textContent||'').trim() === 'Parts Search');+if (!t) return 'NOTAB';+if (!t.classList.contains('active')) t.click();+```++**2. READ YOUR IFRAME'S RECT FROM THE SHELL TARGET**, not from inside your own page. Your page+does not know where it sits in the window, and `window.frameElement` is cross-origin.++```js+// run against target "shell", after activating+const f = [...document.querySelectorAll('iframe')]+  .find(x => (x.getAttribute('src')||'').includes('dock-parts-search'));  // your dock slug+const r = f.getBoundingClientRect();+return JSON.stringify({x: Math.round(r.x), y: Math.round(r.y),+                       w: Math.round(r.width), h: Math.round(r.height)});+```++Only the ACTIVE tab's iframe has a non-zero rect. If you get `0,0,0,0`, step 1 did not take.++**3. DERIVE THE SCALE FROM hd_shot's OWN METADATA. NEVER HARDCODE IT.**++`hd_shot` returns `windowOuter:[w,h]` and the safe copy is resized to `resizeMax` (1400 by+default). So:++```python+SCALE = image.width / windowOuter[0]+```++**Do not hardcode a scale or an origin.** An earlier version of this pipeline used+`SCALE = im.width / 1706.0` with a fixed `FX, FY = 963, 117`, which worked only for one+window size and one rail state, and drifts silently the moment John resizes the window,+opens the dock rail, or moves the pane. Silently: the boxes just land slightly wrong and the+frame still looks plausible.++## The script++`assets/tools/shot-webview.py` in this pack does all three. Point it at your tab name and+your dock slug:++```sh+python3 shot-webview.py --tab "Parts Search" --slug dock-parts-search \+  --thread "your ai thread name" --out /tmp/frame.png+```++It activates the tab, reads the rect, takes the full shot, derives the scale from the+returned `windowOuter`, crops, and prints the path. Add `--box x,y,w,h,colour` to outline an+element you measured (in your page's own coordinates; it converts them for you).++## Then inject it, do not just look at it++```sh+adom-shotlog inject -c dock-dashboards -d "<what this shows>" --orig-file <full.png> <safe.png>+```++`--orig-file` matters: shotlog rejects a pre-shrunk image that cannot declare its original+resolution, so the viewer never has to show "orig missing".++## Traps++- **A shot of an unforegrounded tab looks fine and measures wrong.** Activate first, always.+- **`hd_shot` falls back to the full window when a region is not found**, and still returns+  `ok:true` with `regionFound:false`. Check that field rather than assuming you got a region.+- **`webview open-or-refresh` can report `{"action":"refreshed"}` without navigating.** Force+  the reload from inside the page (`location.replace` with a cache-buster, or CDP+  `Page.reload {ignoreCache:true}`) and confirm the version rendered.+- **Read the SAFE path, archive the FULL path.** The full-res PNG can exceed the per-image+  budget; the safe copy is already sized for reading.+- **Never open a webview in another workspace to take a picture of it.** Dock apps verify in+  ah only.