← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-shotlog"-version = "0.8.1"+version = "0.8.2" dependencies = [  "axum",  "base64",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.8.1"+version = "0.8.2" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1
@@ -1 +1 @@-0.8.1+0.8.2
bin/adom-shotlog
⋯ 1 unchanged line ⋯
dev-skills/adom-shotlog-dev/SKILL.md+9−7
@@ -106,12 +106,14 @@ rebuild** after editing them. Bump `Cargo.toml` + `VERSION` together.   and manages all the Hydrogen webviews (auto-open a slim 20% panel, bring the   right channel's tab forward - by its unique `Shots: <channel>` name so parallel   shotlogs are never cross-touched).-- **Webview activate = the BOUNCE.** Hydrogen has no "activate tab" verb;-  `webview open-or-refresh` does NOT change `activeTabIndex`, and `move-tab`-  rejects same-pane moves. The working recipe (in `bring_webview_forward`): find-  the tab by its `panelState` URL (`/log/<ch>/`), `move-tab` it to any OTHER pane,-  then `move-tab` back to `--target-index <orig>` with `--alert` - move-tab-  auto-activates in its target pane, so the tab surfaces to top with the orange-  update highlight, order preserved.+- **Webview activate = `workspace active-tab` - NEVER move-tab.** To surface a+  channel's tab: `adom-cli hydrogen workspace active-tab --name "Shots: <ch>"`+  (brings it to the front WITHIN its own pane), then `alert set --name` for the+  orange indicator. HARD-WON DON'T: an earlier version "bounced" the tab through+  another pane with two `move-tab` calls (move-tab auto-activates in its target+  pane). That re-parents iframes and re-renders the ENTIRE Hydrogen workspace,+  interrupting the user mid-keystroke in their VS Code tab. If a verb seems+  missing, search `--help` output for hyphenated forms (`active-tab` does not+  match a grep for `activate`) before building a workaround. - It's **per-container**: threads in a different container get their own server. - `shotlog channels` prints the live overview - how every thread is using it.
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.8.1",+  "version": "0.8.2",   "type": "app",   "title": "Adom Shotlog",   "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 Desktop child-window arrays via --meta.",
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.8.1",+  "version": "0.8.2",   "type": "app",   "title": "Adom Shotlog",   "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 Desktop child-window arrays via --meta.",
src/server.rs+15−81
@@ -303,37 +303,22 @@ fn bring_webview_forward(channel: &str, port: u16) {     }     let channel = channel.to_string();     std::thread::spawn(move || {-        if let Some(hit) = find_webview_tab(&channel) {-            // Hydrogen has no "activate tab" verb, and move-tab rejects same-pane-            // moves — but move-tab AUTO-ACTIVATES in its target pane. So: BOUNCE.-            // Move to any other pane, then straight back to the original position-            // with --alert: net effect = tab surfaces to top with the ORANGE-            // update highlight, order preserved via --target-index.-            if let Some(other) = hit.other_pane {-                let m1 = std::process::Command::new("adom-cli")-                    .args(["hydrogen", "workspace", "move-tab",-                           "--from-panel-id", &hit.pane, "--tab-id", &hit.tab,-                           "--to-panel-id", &other])-                    .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())-                    .status().map(|s| s.success()).unwrap_or(false);-                if m1 {-                    let m2 = std::process::Command::new("adom-cli")-                        .args(["hydrogen", "workspace", "move-tab",-                               "--from-panel-id", &other, "--tab-id", &hit.tab,-                               "--to-panel-id", &hit.pane,-                               "--target-index", &hit.index.to_string(),-                               "--alert", "--alert-duration", "6000"])-                        .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())-                        .status().map(|s| s.success()).unwrap_or(false);-                    if m2 {-                        return;-                    }-                }-            }-            // Single-pane workspace (or a move failed): at least blink the tab.+        // GENTLE activate: `workspace active-tab` brings the tab to the front+        // WITHIN ITS OWN PANE — no tab moves, no iframe re-parenting, no+        // workspace-tree re-render. (The old approach bounced the tab through+        // the user's VS Code pane via move-tab, which re-rendered the whole+        // Hydrogen page and interrupted typing. Never do that.)+        let tab = format!("Shots: {channel}");+        let activated = std::process::Command::new("adom-cli")+            .args(["hydrogen", "workspace", "active-tab", "--name", &tab])+            .output()+            .map(|o| o.status.success()+                && String::from_utf8_lossy(&o.stdout).contains("204"))+            .unwrap_or(false);+        if activated {+            // Orange update indicator on the (now-front) tab.             let _ = std::process::Command::new("adom-cli")-                .args(["hydrogen", "alert", "set", "--name",-                       &format!("Shots: {channel}"), "-d", "6000"])+                .args(["hydrogen", "alert", "set", "--name", &tab, "-d", "6000"])                 .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())                 .status();             return;@@ -343,57 +328,6 @@ fn bring_webview_forward(channel: &str, port: u16) {     }); } -/// A located webview tab: its pane, tab id, index within the pane, and some-/// OTHER pane usable for the activate-bounce (None in a single-pane workspace).-struct TabHit {-    pane: String,-    tab: String,-    index: usize,-    other_pane: Option<String>,-}--/// Locate the webview tab whose panelState URL contains `/log/<channel>/`.-fn find_webview_tab(channel: &str) -> Option<TabHit> {-    let out = std::process::Command::new("adom-cli")-        .args(["hydrogen", "workspace", "get"])-        .output()-        .ok()?;-    let v: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;-    let needle = format!("/log/{channel}/");-    // Collect all leaves in order, then find the tab.-    fn leaves<'a>(n: &'a serde_json::Value, acc: &mut Vec<&'a serde_json::Value>) {-        match n.get("type").and_then(|t| t.as_str()) {-            Some("leaf") => acc.push(n),-            Some("split") => {-                if let Some(c) = n.get("first") { leaves(c, acc); }-                if let Some(c) = n.get("second") { leaves(c, acc); }-            }-            _ => {}-        }-    }-    let root = v.get("root").unwrap_or(&v);-    let mut ls = Vec::new();-    leaves(root, &mut ls);-    let empty = Vec::new();-    for leaf in &ls {-        let pane = leaf.get("id").and_then(|i| i.as_str())?.to_string();-        for (idx, t) in leaf.get("tabs").and_then(|x| x.as_array()).unwrap_or(&empty).iter().enumerate() {-            // panelState may be a JSON object or a stringified one — either way-            // its serialization contains the viewer URL.-            let ps = t.get("panelState").map(|p| p.to_string()).unwrap_or_default();-            if ps.contains(&needle) {-                let tab = t.get("id").and_then(|i| i.as_str())?.to_string();-                let other_pane = ls.iter()-                    .filter_map(|l| l.get("id").and_then(|i| i.as_str()))-                    .find(|id| *id != pane)-                    .map(String::from);-                return Some(TabHit { pane, tab, index: idx, other_pane });-            }-        }-    }-    None-}- /// Force-open the viewer for a channel when a shot lands with NOBODY watching. /// Fallback only (see notify_surface). Debounced per-channel. Defaults to a slim /// ~20%-wide right-side panel so cross-thread shots auto-appear without taking