app
Shotlog - Screenshot Log
Public Made by Adomby adom
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.
← Commit history
Publish 0.27.3
9 files changed
+88−15
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); }