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.24.5
12 files changed
+66−19
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.24.4"+version = "0.24.5" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.24.4"+version = "0.24.5" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
VERSION+1−1@@ -1 +1 @@-0.24.4+0.24.5
bin/adom-shotlog⋯ 1 unchanged line ⋯
dev-skills/adom-shotlog-dev/SKILL.md+11@@ -356,3 +356,14 @@ resets the backFromFill toggle, and exiting with the modal closed just returns to the list (openModal resets state on the next open, so no stale flag). NOTE (bridge drift): the pup verb is now `browser_eval` (not browser_evaluate) and the eval result is TOP-LEVEL `d.result` (not d.output.result).++## Webview auto-size is OPT-IN (John, 2026-08-08)+open_webview took an `autosize` param. When FALSE (the new default, pref+`webview_autosize`), the RESIZE step (snap an existing split back to+webview_width on every open) is SKIPPED - the human's manually-dragged column+width sticks. The width pref now only SEEDS a brand-new panel (the initial+split). ON re-forces the width every open (old behavior). John found the+constant snap-back annoying: "rely on the user deciding what their webview+column width is." All 3 open_webview callers (CLI open, force_open_viewer,+force_open_pup fallback) thread the pref through notify_surface. An explicit+CLI --ratio still counts as autosize (deliberate size request).
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.24.4",+ "version": "0.24.5", "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.24.4",+ "version": "0.24.5", "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/html/index.html+15−2@@ -567,13 +567,21 @@ header { <button data-v="pup">Pup</button> </div> </div>+ <div class="pref-row">+ <div class="pref-info">+ <div class="pref-name">Auto-size the webview panel</div>+ <div class="pref-desc">When ON, shotlog re-forces the panel to the width below every time it opens the webview. When OFF (default), shotlog leaves the column at whatever width YOU last dragged it to; the width below only seeds a brand-new panel.</div>+ <div class="ai-chip"><span class="lbl">or say to your AI</span>turn on shotlog webview auto-sizing</div>+ </div>+ <button class="toggle" id="prefAutosize" role="switch" aria-checked="false" data-tooltip="ON: snap the webview column to your preferred width on every open. OFF (default): respect the width you set manually."></button>+ </div> <div class="pref-row"> <div class="pref-info"> <div class="pref-name">Webview panel width</div>- <div class="pref-desc">How wide the slim webview panel is when shotlog opens it, as a percent of your workspace (10-50).</div>+ <div class="pref-desc">The width shotlog uses when it first CREATES the slim webview panel, as a percent of your workspace (10-50). With auto-size OFF (default) this only seeds a new panel and never resizes an existing one; with it ON, every open snaps back to this.</div> <div class="ai-chip"><span class="lbl">or say to your AI</span>make the shotlog webview panel 25% wide</div> </div>- <span class="num-wrap" data-tooltip="Percent of the workspace width for the skinny shotlog panel (10-50). Applies to newly opened/auto-opened panels.">+ <span class="num-wrap" data-tooltip="Percent of the workspace width for the skinny shotlog panel (10-50). Seeds a new panel; only re-applied on every open when auto-size is ON."> <input class="num-input" id="prefWidth" type="number" min="10" max="50" step="5"><span class="unit">%</span> </span> </div>@@ -1397,6 +1405,7 @@ header { hook.setAttribute('aria-checked', String(!!p.read_hook)); $('prefAlerts').setAttribute('aria-checked', String(p.update_alerts!==false)); $('prefAutoswitch').setAttribute('aria-checked', String(p.update_autoswitch!==false));+ $('prefAutosize').setAttribute('aria-checked', String(p.webview_autosize===true)); APREFS=p; seg.querySelectorAll('button').forEach(b=>b.classList.toggle('on', b.dataset.v===p.default_canvas)); $('prefWidth').value = p.webview_width ?? 20;@@ -1422,6 +1431,10 @@ header { 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.'); });+ $('prefAutosize').addEventListener('click',()=>{+ const now=$('prefAutosize').getAttribute('aria-checked')==='true';+ patch({webview_autosize: !now}, !now?'auto-size ON: the webview column snaps to your width on every open.':'auto-size OFF: shotlog leaves the webview column at the width you set.');+ }); seg.addEventListener('click',e=>{ const b=e.target.closest('button[data-v]'); if(!b) return; patch({default_canvas: b.dataset.v}, 'channels now default to '+b.dataset.v+'.');
src/main.rs+7−1@@ -468,13 +468,19 @@ async fn main() { } else { // Webview is ALWAYS the managed slim panel at the preferred // width - an unmanaged add-tab once landed at 50% of the screen.+ let ratio_flag_set = ratio.is_some(); let ratio = ratio.unwrap_or_else(|| { prefs.as_ref() .and_then(|v| v.get("webview_width").and_then(|w| w.as_u64())) .map(|w| (w as f64 / 100.0).clamp(0.10, 0.50)) .unwrap_or(0.2) });- open::open_webview(&channel, name.as_deref(), icon.as_deref(), port, true, ratio);+ // An explicit --ratio on the CLI is a deliberate size request, so+ // honor it (autosize); otherwise follow the human's pref (default off).+ let autosize = ratio_flag_set || prefs.as_ref()+ .and_then(|v| v.get("webview_autosize").and_then(|w| w.as_bool()))+ .unwrap_or(false);+ open::open_webview(&channel, name.as_deref(), icon.as_deref(), port, true, ratio, autosize); if swap { if open::close_pup_tab(&channel) { ok(&format!("closed the pup tab for '{channel}' - webview is now this channel's canvas."));
src/open.rs+7−1@@ -16,6 +16,10 @@ pub fn open_webview( port: u16, skinny: bool, ratio: f64,+ // Re-force the column to `ratio` on every open. When false, we only set the+ // width when CREATING the panel (initial split) and never resize an existing+ // one, so the human's manual column width sticks. Default off (see AppPrefs).+ autosize: bool, ) { let default_name = "Shotlog".to_string(); let name = display_name.unwrap_or(&default_name);@@ -88,7 +92,9 @@ pub fn open_webview( report(out, name, channel, &url); // Skinny + an existing split → resize so the shotlog pane is ~`ratio` wide.- if skinny {+ // Only when AUTOSIZE is on: otherwise the human's manually-set column width+ // is respected and never snapped back on each open (their explicit request).+ if skinny && autosize { if let Some((split_id, side)) = find_enclosing_split(root, &leaf_id) { // ratio is the FIRST child's fraction. If shotlog is the second // (right) child, first gets (1 - ratio); if it's the first, it gets ratio.
src/server.rs+13−9@@ -460,7 +460,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);+ notify_surface(&req.channel, state.port, &infos, &prefs.default_canvas, prefs.update_alerts, prefs.webview_width as f64 / 100.0, prefs.webview_autosize); Json(serde_json::json!({ "ok": true, "consolidated": true, "viewers": infos.len(), "surfaces": surfaces, }))@@ -523,6 +523,7 @@ struct PrefsPatch { update_alerts: Option<bool>, webview_width: Option<u8>, update_autoswitch: Option<bool>,+ webview_autosize: Option<bool>, } /// Patch global preferences from the viewer's Preferences dialog.@@ -561,6 +562,9 @@ async fn set_prefs( if let Some(a) = patch.update_autoswitch { p.update_autoswitch = a; }+ if let Some(a) = patch.webview_autosize {+ p.webview_autosize = a;+ } if let Some(w) = patch.webview_width { if !(10..=50).contains(&w) { return Json(serde_json::json!({"ok": false, "error": "webview_width must be 10-50 (percent)"}));@@ -818,7 +822,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) {+fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], pref: &str, alerts: bool, ratio: f64, autosize: 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).@@ -856,9 +860,9 @@ fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], // a pup channel whose viewer blipped must NOT get a webview shoved at it. println!("{} DECISION force-open '{channel}' canvas={pref} (nobody watching)", ts()); if pref == "pup" {- force_open_pup(channel, ratio);+ force_open_pup(channel, ratio, autosize); } else {- force_open_viewer(channel, port, ratio);+ force_open_viewer(channel, port, ratio, autosize); } } }@@ -866,7 +870,7 @@ fn notify_surface(channel: &str, port: u16, infos: &[crate::store::ViewerInfo], /// Force-open the PUP canvas for a channel (its chosen canvas) when a shot /// lands with nobody watching. Runs via a self-subprocess (open_pup can exit). /// If pup genuinely cannot open, falls back to a webview - never invisible.-fn force_open_pup(channel: &str, fallback_ratio: f64) {+fn force_open_pup(channel: &str, fallback_ratio: f64, autosize: bool) { // Debounce longer than the whole retry window so attempts never overlap. if !debounce_ok(&format!("openpup:{channel}"), 240) { return;@@ -899,7 +903,7 @@ fn force_open_pup(channel: &str, fallback_ratio: f64) { } println!("{} WARN: pup never came up for '{channel}' - falling back to a webview so the shots aren't invisible (no choice recorded).", ts()); set_notice("Tried to open pup (your default) - it was unreachable, so I fell back to this webview so shots stay visible. I'll keep retrying and move you back to pup automatically once it's up.");- crate::open::open_webview(&channel, None, None, 8820, true, fallback_ratio);+ crate::open::open_webview(&channel, None, None, 8820, true, fallback_ratio, autosize); }); } @@ -1020,7 +1024,7 @@ fn bring_webview_forward(channel: &str, port: u16, alerts: bool) { /// ~20%-wide right-side panel so cross-thread shots auto-appear without taking /// over the workspace — and CRUCIALLY passes a --panel-id (via open_webview) so /// the tab is actually CREATED, not silently skipped.-fn force_open_viewer(channel: &str, port: u16, ratio: f64) {+fn force_open_viewer(channel: &str, port: u16, ratio: f64, autosize: bool) { if std::env::var("VSCODE_PROXY_URI").is_err() { return; // not in an Adom container; nothing to open }@@ -1030,7 +1034,7 @@ fn force_open_viewer(channel: &str, port: u16, ratio: f64) { let channel = channel.to_string(); // Best-effort; don't block the inject response on the adom-cli round-trip. std::thread::spawn(move || {- crate::open::open_webview(&channel, None, None, port, true, ratio);+ crate::open::open_webview(&channel, None, None, port, true, ratio, autosize); }); } @@ -1201,7 +1205,7 @@ async fn post_shots( } 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);+ notify_surface(&req.channel, state.port, &infos, &canvas_pref, alerts_on, ratio, app_prefs.webview_autosize); // 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+8−1@@ -124,9 +124,16 @@ pub struct AppPrefs { /// that just received a shot (vs sit where you are with a badge + flash). #[serde(default = "default_true")] pub update_autoswitch: bool,+ /// Re-force the webview panel to `webview_width` every time shotlog opens+ /// it. OFF (default): shotlog leaves the column at whatever width the human+ /// last set it to; the width pref only seeds a brand-new panel. ON: every+ /// open snaps the column back to the pref width.+ #[serde(default = "default_false")]+ pub webview_autosize: bool, } fn default_canvas() -> String { "webview".into() } fn default_true() -> bool { true }+fn default_false() -> bool { false } fn default_max_shots() -> usize { 200 } fn default_retention_days() -> u64 { 30 } fn default_webview_width() -> u8 { 20 }@@ -134,7 +141,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: true, webview_width: default_webview_width(),- update_autoswitch: true } }+ update_autoswitch: true, webview_autosize: false } } } /// The flag file the read-hook script checks; present = hook disabled.