← Commit history
Cargo.lock+1−1
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"  [[package]] name = "adom-shotlog"-version = "0.28.5"+version = "0.28.6" dependencies = [  "axum",  "base64",
Cargo.toml+1−1
@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.28.5"+version = "0.28.6" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
README.md+1
@@ -115,6 +115,7 @@ The viewer has a **Settings** dialog: the outline cog at the right end of the he |---|---|---| | **Auto-capture what AIs read** (read-hook) | ON | Auto-inject every image any AI thread reads into the shared `read-captures` channel. Off = only deliberate injects show. | | **Update alerts** (orange flashes) | OFF | The orange cues when new shots arrive: the webview tab highlight, the in-app channel-tab flash, and the pup taskbar flash. Tabs still come forward and unseen badges still show; turn on if you want the flashing. |+| **Bring the webview tab forward** | ON | When a shot lands and the Shotlog webview sits behind another tab in its Hydrogen pane, pull it to the front. Turn it off if you live in webviews: you stay on the tab you were reading; the webview still receives every shot (read-hook captures included) in the background and is fully current the moment you switch to it. | | **Auto-switch to updated channel** | ON | When a shot arrives for another channel, jump that channel's tab to the front. Off = the tab just flashes + shows an unseen badge; you stay put. | | **Compact cards (image-first)** | ON | The gallery shows mostly thumbnails: info text hides behind the ⓘ chip on each image (hover to reveal); the ⛶ fullscreen button stays on every thumbnail. Off = classic text-above-image cards. | | **Default canvas** | webview | Where shotlog auto-opens a channel when nobody's watching: `webview` or `pup`. |
SKILL.md+5
@@ -316,3 +316,8 @@ Read a channel BACK before you talk about it: `adom-shotlog shots -c <channel>` - ONE click on a thumbnail fills the window; the next click, Esc, or the margin comes back. There is no modal step any more. - The thumbnail carries no overlay text (the ref sits ABOVE it) because humans screenshot thumbnails to send back to you. - Settings > Own column (off by default) opens shotlog as its own ~10% third column at the right edge instead of joining the webview pane; never resize the human's webview pane yourself.++## The webview tab pull is a setting (0.28.6)++Settings > Notifications > "Bring the webview tab forward" (default ON) is what pulls the Shotlog Hydrogen tab to the front when a shot lands. A human who lives in webviews may turn it OFF: then the webview still receives every shot in the background, `shotlog viewers` can report a connected-but-backgrounded viewer (exit 4), and that is fine: they see everything when they switch to the tab. Never "fix" that by opening or foregrounding anything yourself.+
VERSION+1−1
@@ -1 +1 @@-0.28.5+0.28.6
bin/adom-shotlog
⋯ 1 unchanged line ⋯
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-shotlog",-  "version": "0.28.5",+  "version": "0.28.6",   "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.28.5",+  "version": "0.28.6",   "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.",
src/html/index.html+14−3
@@ -748,10 +748,10 @@ body.compact .card-popups .ph { padding-top: 4px; }     </section>     <section class="settings-card" id="cardNotify">       <h3 class="card-title">Notifications <span class="card-status" id="stNotify"></span></h3>-      <p class="card-desc">How a new shot gets your attention. shotlog never steals the OS foreground: the strongest cue it will ever use is an orange flash. Unseen badges on channel tabs and the page title work regardless of these two switches.</p>+      <p class="card-desc">How a new shot gets your attention. shotlog never steals the OS foreground: the strongest cue it will ever use is an orange flash. Unseen badges on channel tabs and the page title work regardless of these switches, and the webview keeps receiving every shot in the background no matter what: switch to it yourself and everything is already there.</p>             <div class="set-row">         <h4 class="set-name" title="update_alerts"><span class="prefix">Notifications: </span><span class="title">Update alerts (orange flashes)</span></h4>-        <p class="set-desc">When new shots arrive: the webview tab highlights orange, the in-app channel tab flashes, and the pup window flashes its taskbar. Tabs still come forward and unseen badges still show either way; this only controls the orange flashing.</p>+        <p class="set-desc">When new shots arrive: the webview tab highlights orange, the in-app channel tab flashes, and the pup window flashes its taskbar. Unseen badges still show either way; this only controls the orange flashing.</p>         <div class="set-ctl"><button class="toggle" id="prefAlerts" role="switch" aria-checked="false" aria-label="Update alerts" data-tooltip="Off (default): no orange flashing when shots arrive. On: webview tab + in-app tab + pup taskbar flash orange."><span class="toggle-track"><span class="toggle-thumb"></span></span></button></div>         <div class="set-foot"><span class="default-hint">Default: Off</span><span class="ai-chip"><span class="lbl">or say to your AI</span>turn on the shotlog orange flashes</span></div>       </div>@@ -761,6 +761,12 @@ body.compact .card-popups .ph { padding-top: 4px; }         <div class="set-ctl"><button class="toggle" id="prefAutoswitch" role="switch" aria-checked="true" aria-label="Auto-switch to updated channel" data-tooltip="Auto-switch the in-app channel tab when a new shot arrives for it."><span class="toggle-track"><span class="toggle-thumb"></span></span></button></div>         <div class="set-foot"><span class="default-hint">Default: On</span><span class="ai-chip"><span class="lbl">or say to your AI</span>stop shotlog auto-switching tabs</span></div>       </div>+      <div class="set-row">+        <h4 class="set-name" title="webview_bring_forward"><span class="prefix">Notifications: </span><span class="title">Bring the webview tab forward</span></h4>+        <p class="set-desc">When a shot lands and the Shotlog webview is open but behind another tab in its pane, pull it to the front (default). Turn it off if you live in webviews and do not want to be pulled away: you stay on whatever tab you were reading, and the webview still receives every shot (including auto-captures from the read-hook) in the background, so it is fully up to date the moment you switch to it. This is about the Hydrogen tab; the switch above is about channel tabs inside shotlog.</p>+        <div class="set-ctl"><button class="toggle" id="prefBringFwd" role="switch" aria-checked="true" aria-label="Bring the webview tab forward" data-tooltip="On (default): every new shot pulls the Shotlog tab to the front of its pane. Off: the tab stays where it is; shots keep arriving in the background."><span class="toggle-track"><span class="toggle-thumb"></span></span></button></div>+        <div class="set-foot"><span class="default-hint">Default: On</span><span class="ai-chip"><span class="lbl">or say to your AI</span>stop shotlog pulling its webview tab to the front</span></div>+      </div>     </section>     <section class="settings-card" id="cardGallery">       <h3 class="card-title">Gallery <span class="card-status" id="stGallery"></span></h3>@@ -1820,7 +1826,8 @@ body.compact .card-popups .ph { padding-top: 4px; }       // Live status in every card title (recomputed truth, never a cached claim).       const st=(id,txt,ok)=>{ const el=$(id); if(!el) return; el.textContent=txt; el.classList.toggle('ok', !!ok); };       st('stHook', p.read_hook!==false ? 'active' : 'off', p.read_hook!==false);-      st('stNotify', (p.update_alerts===true?'flashes on':'flashes off')+' · '+(p.update_autoswitch!==false?'auto-switch on':'auto-switch off'), false);+      $('prefBringFwd').setAttribute('aria-checked', String(p.webview_bring_forward!==false));+      st('stNotify', (p.update_alerts===true?'flashes on':'flashes off')+' · '+(p.update_autoswitch!==false?'auto-switch on':'auto-switch off')+' · '+(p.webview_bring_forward!==false?'tab comes forward':'tab stays put'), false);       st('stGallery', p.compact!==false ? 'compact' : 'classic', false);       st('stCanvas', p.default_canvas||'webview', false);       st('stLayout', own ? ('own column, '+(p.own_column_width??10)+'% wide') : ('shared pane'+(p.webview_autosize===true?', auto-sized':'')), false);@@ -1851,6 +1858,10 @@ body.compact .card-popups .ph { padding-top: 4px; }       const now=$('prefAlerts').getAttribute('aria-checked')==='true';       patch({update_alerts: !now}, !now?'orange update flashes ON.':'orange update flashes OFF: tabs still come forward, no flashing.');     });+    $('prefBringFwd').addEventListener('click',()=>{+      const now=$('prefBringFwd').getAttribute('aria-checked')==='true';+      patch({webview_bring_forward: !now}, !now?'bring-forward ON: new shots pull the Shotlog tab to the front.':'bring-forward OFF: the Shotlog tab stays where it is; shots keep arriving in the background.');+    });     $('prefAutoswitch').addEventListener('click',()=>{       const now=$('prefAutoswitch').getAttribute('aria-checked')==='true';       patch({update_autoswitch: !now}, !now?'auto-switch ON: updated channels come to the front.':'auto-switch OFF: tabs flash + badge, you stay put.');
src/server.rs+38−20
@@ -464,7 +464,7 @@ async fn post_show(     });     let _ = state.store.ctrl_tx.send(frame.to_string());     println!("{} DECISION show '{}' consolidated into the open {desired} window ({} viewer(s)) - no new surface.", ts(), req.channel, infos.len());-    notify_surface(&req.channel, state.port, &infos, &prefs.default_canvas, prefs.update_alerts, prefs.webview_width as f64 / 100.0, prefs.webview_autosize, own_column(&prefs));+    notify_surface(&req.channel, state.port, &infos, &prefs.default_canvas, prefs.update_alerts, prefs.webview_width as f64 / 100.0, prefs.webview_autosize, own_column(&prefs), prefs.webview_bring_forward);     Json(serde_json::json!({         "ok": true, "consolidated": true, "viewers": infos.len(), "surfaces": surfaces,     }))@@ -529,6 +529,7 @@ struct PrefsPatch {     update_autoswitch: Option<bool>,     webview_autosize: Option<bool>,     webview_own_column: Option<bool>,+    webview_bring_forward: Option<bool>,     own_column_width: Option<u8>,     compact: Option<bool>, }@@ -591,6 +592,9 @@ async fn set_prefs(     if let Some(o) = patch.webview_own_column {         p.webview_own_column = o;     }+    if let Some(b) = patch.webview_bring_forward {+        p.webview_bring_forward = b;+    }     if let Some(w) = patch.own_column_width {         if !(5..=30).contains(&w) {             return Json(serde_json::json!({"ok": false, "error": "own_column_width must be 5-30 (percent of the screen)"}));@@ -876,7 +880,7 @@ fn debounce_ok(key: &str, secs: u64) -> bool { ///    hinted to flash the pup taskbar. ///  - **nobody** watching → force-open a slim ~20% webview, unless ///    `SHOTLOG_NO_FORCE_OPEN` is set.-fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], pref: &str, alerts: bool, ratio: f64, autosize: bool, own: Option<f64>) {+fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], pref: &str, alerts: bool, ratio: f64, autosize: bool, own: Option<f64>, bring_forward: bool) {     // Pup viewers get an automatic ORANGE TASKBAR FLASH (FlashWindowEx) on their     // hosting window — a gentle "update" cue, never a foreground steal. Works when     // the viewer URL carried ?session=<pup sessionId> (reported in the WS hello).@@ -897,7 +901,7 @@ fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo],      let has_webview = infos.iter().any(|i| i.surface == "webview");     if has_webview {-        bring_webview_forward(channel, port, alerts);+        bring_webview_forward(channel, port, alerts, bring_forward);         // The webview FALLBACK is a lifeboat, not a destination. If the human's         // canvas says pup and the only thing connected is a webview (classic:         // Bridge was offline, we fell back, Bridge came back), each new shot buys one@@ -1036,7 +1040,10 @@ fn flash_pup_session(session: &str) { /// shotlog tabs are never cross-touched), then `move-tab` to its own pane: /// Hydrogen auto-activates it (surfaces to top) and `--alert` paints the ORANGE /// update highlight on the tab. Falls back to create-or-refresh if not found.-fn bring_webview_forward(channel: &str, port: u16, alerts: bool) {+fn bring_webview_forward(channel: &str, port: u16, alerts: bool, bring_forward: bool) {+    if !bring_forward && !alerts {+        return;   // nothing to do: the background webview already has the shot+    }     if !debounce_ok(&format!("fwd:{channel}"), 2) {         return;     }@@ -1049,20 +1056,26 @@ fn bring_webview_forward(channel: &str, port: u16, alerts: bool) {         // Hydrogen page and interrupted typing. Never do that.)         let _ = &channel;         let tab = "Shotlog".to_string();-        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 - user-toggleable.-            if alerts {-                let _ = std::process::Command::new("adom-cli")-                    .args(["hydrogen", "alert", "set", "--name", &tab, "-d", "6000"])-                    .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())-                    .status();-            }+        // Setting "Bring the webview tab forward" (default OFF): only when ON+        // does the Shotlog tab get pulled to the front of its pane. OFF leaves+        // the human where they are; the webview still receives every shot.+        let activated = if bring_forward {+            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)+        } else { false };+        // Orange update indicator on the tab (front or not) - user-toggleable,+        // independent of the pull so a background tab can still say "new".+        if alerts {+            let _ = std::process::Command::new("adom-cli")+                .args(["hydrogen", "alert", "set", "--name", &tab, "-d", "6000"])+                .stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null())+                .status();+        }+        if activated || !bring_forward {             return;         }         // Tab not found: do NOTHING. A webview VIEWER with no Hydrogen tab is a@@ -1254,14 +1267,19 @@ async fn post_shots(     }     if has_webview {         actions.push(serde_json::json!({"action":"webview_surface","tab":format!("Shots: {}", req.channel),-            "cue": if alerts_on {"tab pulled to front + orange alert"} else {"tab pulled to front (update alerts off)"}}));+            "cue": match (app_prefs.webview_bring_forward, alerts_on) {+                (true, true) => "tab pulled to front + orange alert",+                (true, false) => "tab pulled to front (update alerts off)",+                (false, true) => "shot delivered to the background webview + orange alert (bring-forward off)",+                (false, false) => "shot delivered to the background webview; the tab stays where it is (bring-forward off, alerts off)",+            }}));     } else if infos.is_empty() && force_open_on {         actions.push(serde_json::json!({"action":"force_open","canvas": canvas_pref,             "layout": if canvas_pref=="pup" {"tab in the shared pup window"} else {"slim 20% right-side webview"}}));     }     let surfacing = Some(serde_json::json!({"actions": actions}));     let ratio = (app_prefs.webview_width as f64 / 100.0).clamp(0.10, 0.50);-    notify_surface(&req.channel, state.port, &infos, &canvas_pref, alerts_on, ratio, app_prefs.webview_autosize, own_column(&app_prefs));+    notify_surface(&req.channel, state.port, &infos, &canvas_pref, alerts_on, ratio, app_prefs.webview_autosize, own_column(&app_prefs), app_prefs.webview_bring_forward);      // Progressive mini-skill hints — taught at the moment they're relevant, then     // quiet (hint_gate caps how many times each lesson repeats).
src/store.rs+10−1
@@ -137,6 +137,15 @@ pub struct AppPrefs {     /// open snaps the column back to the pref width.     #[serde(default = "default_false")]     pub webview_autosize: bool,+    /// BRING THE WEBVIEW TAB FORWARD (default ON, John 2026-09-14: "for all+    /// adom users"): when a shot lands and a webview is connected but sits+    /// behind another tab in its pane, pull the Shotlog tab to the front. OFF+    /// is for people who live in webviews and find the pull annoying: the+    /// webview keeps receiving every shot in the background and shows them all+    /// the moment they switch to it. The orange tab alert (update_alerts)+    /// applies either way.+    #[serde(default = "default_true")]+    pub webview_bring_forward: bool,     /// OWN COLUMN (default off): when the workspace already has a webview pane,     /// shotlog opens as its OWN third column at the right edge, `own_column_width`     /// percent of the screen, instead of joining that pane's tabs. Never resizes@@ -165,7 +174,7 @@ impl Default for AppPrefs {     fn default() -> Self { Self { default_canvas: default_canvas(), read_hook: true,         max_shots: default_max_shots(), retention_days: default_retention_days(),         update_alerts: false, webview_width: default_webview_width(),-        update_autoswitch: true, webview_autosize: false, webview_own_column: false, own_column_width: 10, compact: true } }+        update_autoswitch: true, webview_autosize: false, webview_bring_forward: true, webview_own_column: false, own_column_width: 10, compact: true } } }  /// The flag file the read-hook script checks; present = hook disabled.