← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-shotlog"-version = "0.27.1"+version = "0.27.3" dependencies = [  "axum",  "base64",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.27.1"+version = "0.27.3" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1
@@ -1 +1 @@-0.27.1+0.27.3
bin/adom-shotlog
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.27.2",+  "version": "0.27.3",   "type": "app",   "title": "Shotlog - Screenshot Log",   "description": "See the shots your AI never showed you, and know it actually SAW them. adom-shotlog streams every screenshot the AI gathers into a live gallery (webview/pup/browser/phone), reports whether a human is really watching and on which surface, and ingests Adom Bridge child-window arrays via --meta.",
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.27.2",+  "version": "0.27.3",   "type": "app",   "title": "Shotlog - Screenshot Log",   "description": "See the shots your AI never showed you, and know it actually SAW them. adom-shotlog streams every screenshot the AI gathers into a live gallery (webview/pup/browser/phone), reports whether a human is really watching and on which surface, and ingests Adom Bridge child-window arrays via --meta.",
skills/adom-shotlog-surfacing/SKILL.md+1−1
@@ -121,7 +121,7 @@ just to show a shot.  You rarely need this for shotlog itself (the server handles surfacing), but when you must locate a viewer window to flash/screenshot it, these Adom Bridge verbs do it-without stealing focus - do NOT shell out to PowerShell, Adom Bridge already exposes it:+without stealing focus - do NOT shell out to PowerShell, AD already exposes it:  | Need | Verb | Notes | |------|------|-------|
src/html/index.html+9
@@ -202,6 +202,15 @@ body.compact .shot-card:has(.card-info.show) { overflow: visible; } /* natural aspect: the browser sizes the box from width x aspect (no fixed    height = no letterbox bars), capped so one giant portrait can't eat the feed */ body.compact .shot-card-img { height: auto; max-height: 420px; object-fit: contain; }+/* WIDE canvas (pup / big browser / full-width webview) - wiki issue #2: a+   height cap starved the image of width (a 1400x765 shot sat ~800px wide in a+   1500px card, centered in dead margins). On wide viewports the image takes+   the CARD'S FULL WIDTH at natural aspect, capped only by one screen height.+   Applies to classic mode too (its fixed 220px thumbnail was the original+   offender); the skinny <900px panel keeps its compact caps. */+@media (min-width: 900px) {+  .shot-card-img, body.compact .shot-card-img { height: auto; max-height: 100vh; object-fit: contain; }+} /* his most-used control stays visible, not hover-revealed */ body.compact .card-fill-btn { opacity: .85; } /* the ⓘ reveal chip, top-left over the image (mirrors the ⛶ top-right) */
src/open.rs+73−9
@@ -46,7 +46,11 @@ pub fn open_webview(         .map(|o| String::from_utf8_lossy(&o.stdout).to_string())         .unwrap_or_default();     let layout: serde_json::Value = serde_json::from_str(&layout_json).unwrap_or(serde_json::Value::Null);-    let root = layout.get("root").unwrap_or(&layout);+    // Hydrogen (desktop) and Hydrogen Cloud have wrapped the layout differently+    // over time ({root}, {layout:{root}}, {data:{root}}, or the bare tree). Find+    // the tree wherever it is instead of assuming one envelope (wiki issue #1:+    // a stock ah layout with TWO leaves was reported as "no leaf pane").+    let root = find_layout_root(&layout);      // No split yet + skinny → split the single pane to create the right panel.     if skinny {@@ -73,22 +77,45 @@ pub fn open_webview(      // Otherwise: add the tab to the preferred (right/second) leaf.     let Some(leaf_id) = find_leaf_id(root) else {-        eprintln!("Could not find a leaf pane in workspace layout.");+        // A hard failure the broker/UI can SEE (exit 2): before, this returned+        // exit 0 and the Webview button spun for 30s then silently reset.+        eprintln!("ERROR: could not find any pane in the workspace layout to put the shotlog tab in.");+        eprintln!("Hint: `adom-cli hydrogen workspace get` returned: {}", layout_json.chars().take(400).collect::<String>().replace('\n', " "));         eprintln!("URL: {url}");-        return;+        std::process::exit(2);     };     // Idempotent create-or-refresh: with --panel-id it CREATES the tab if the     // name doesn't exist, else refreshes it. (Without --panel-id it can only     // refresh — the bug that made auto-open silently fail.)-    let out = Command::new("adom-cli")+    let add = |leaf: &str| Command::new("adom-cli")         .args([             "hydrogen", "webview", "open-or-refresh",             "--name", name,             "--url", &url,-            "--panel-id", &leaf_id,+            "--panel-id", leaf,             "--display-icon", icon,         ])         .output();+    let mut out = add(&leaf_id);+    // Pane ids churn on ah when the workspace reloads between our `get` and+    // the add (issue #1 saw the empty pane's id change within minutes). On a+    // not-found, re-resolve from a FRESH layout and retry exactly once.+    let mut leaf_id = leaf_id;+    if let Ok(o) = &out {+        let text = format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));+        if !o.status.success() || text.contains("not found") {+            if let Some(fresh) = Command::new("adom-cli").args(["hydrogen", "workspace", "get"]).output().ok()+                .and_then(|o| serde_json::from_slice::<serde_json::Value>(&o.stdout).ok())+                .and_then(|v| find_leaf_id(find_layout_root(&v)))+            {+                if fresh != leaf_id {+                    eprintln!("Hint: the target pane id changed underneath us ({leaf_id} -> {fresh}); retrying with the fresh one.");+                    leaf_id = fresh;+                    out = add(&leaf_id);+                }+            }+        }+    }     report(out, name, channel, &url);      // Skinny + an existing split → resize so the shotlog pane is ~`ratio` wide.@@ -117,11 +144,34 @@ fn report(out: std::io::Result<std::process::Output>, name: &str, channel: &str,             println!("URL: {url}");             surface_tab(name);         }-        Ok(output) => eprintln!("adom-cli failed: {}", String::from_utf8_lossy(&output.stderr)),-        Err(e) => eprintln!("Failed to run adom-cli: {e}"),+        Ok(output) => {+            eprintln!("ERROR: adom-cli failed: {}{}", String::from_utf8_lossy(&output.stdout).trim(), String::from_utf8_lossy(&output.stderr).trim());+            std::process::exit(1);+        }+        Err(e) => { eprintln!("ERROR: failed to run adom-cli: {e}"); std::process::exit(1); }     } } +/// Locate the layout tree in whatever envelope `workspace get` used.+fn find_layout_root(v: &serde_json::Value) -> &serde_json::Value {+    let is_tree = |x: &serde_json::Value| matches!(x.get("type").and_then(|t| t.as_str()), Some("leaf") | Some("split"));+    if is_tree(v) { return v; }+    for k in ["root", "layout", "data", "workspace"] {+        if let Some(c) = v.get(k) {+            if is_tree(c) { return c; }+            if let Some(cc) = c.get("root") { if is_tree(cc) { return cc; } }+        }+    }+    // Last resort: any nested object that looks like a tree.+    fn deep(v: &serde_json::Value) -> Option<&serde_json::Value> {+        if matches!(v.get("type").and_then(|t| t.as_str()), Some("leaf") | Some("split")) { return Some(v); }+        if let Some(m) = v.as_object() { for c in m.values() { if let Some(f) = deep(c) { return Some(f); } } }+        if let Some(a) = v.as_array() { for c in a { if let Some(f) = deep(c) { return Some(f); } } }+        None+    }+    deep(v).unwrap_or(v)+}+ /// After opening/refreshing a webview tab, make sure the user actually LANDS on /// it: bring it to the front in its own pane (workspace active-tab - never a /// tab move) and paint the orange update highlight. Without this, a pup->webview@@ -223,6 +273,10 @@ pub fn open_pup(channel: &str, port: u16, swap: bool) {     // window leaves GHOST tab records; verbs then fail "browser is not     // connected"). So: try the cheap path, but ANY failure of switch/open_tab     // falls through to pup_open_window, which revives a dead session.+    // Remember the LAST bridge reply so a final failure can SAY WHY (wiki+    // issue #1: "ERROR: could not open the pup window" with nothing else left+    // humans guessing between a dead bridge, a missing profile, or a bad URL).+    let last_out = std::cell::RefCell::new(String::new());     let open_window = |ad: &dyn Fn(&[&str]) -> (bool, String)| -> bool {         let win_json = format!("{{\"sessionId\":\"shotlog\",\"profile\":\"shotlog\",\"url\":\"{url}\"}}");         let (mut ok, mut out) = ad(&["pup_open_window", &win_json]);@@ -242,12 +296,14 @@ pub fn open_pup(channel: &str, port: u16, swap: bool) {             ok = r.0;             out = r.1;         }+        *last_out.borrow_mut() = out;         ok     };     let (listed, list_out) = ad(&["pup_list_tabs", "{\"sessionId\":\"shotlog\"}"]);     let mut opened = false;     let mut via_window = false;     if !listed || list_out.contains("session_not_found") {+        *last_out.borrow_mut() = list_out.clone();         opened = open_window(&ad);         via_window = true;     } else {@@ -258,10 +314,14 @@ pub fn open_pup(channel: &str, port: u16, swap: bool) {             }));         if let Some(tid) = existing {             let sw = format!("{{\"sessionId\":\"shotlog\",\"tabId\":\"{tid}\"}}");-            opened = ad(&["pup_switch_tab", &sw]).0;+            let r = ad(&["pup_switch_tab", &sw]);+            opened = r.0;+            *last_out.borrow_mut() = r.1;         } else {             let tab_json = format!("{{\"sessionId\":\"shotlog\",\"url\":\"{url}\"}}");-            opened = ad(&["pup_open_tab", &tab_json]).0;+            let r = ad(&["pup_open_tab", &tab_json]);+            opened = r.0;+            *last_out.borrow_mut() = r.1;         }         if !opened {             opened = open_window(&ad);@@ -288,7 +348,11 @@ pub fn open_pup(channel: &str, port: u16, swap: bool) {     }      if !opened {+        let last_out = last_out.into_inner();         eprintln!("ERROR: could not open the pup window (is Adom Bridge connected?).");+        if !last_out.trim().is_empty() {+            eprintln!("Bridge said: {}", last_out.trim().chars().take(400).collect::<String>().replace('\n', " "));+        }         eprintln!("Hint: check `adom-bridge-cli targets`; with multiple desktops set SHOTLOG_AD_TARGET=<name>.");         std::process::exit(1);     }