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.9.0
13 files changed
+174−34
Cargo.lock+1−1@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-shotlog"-version = "0.8.2"+version = "0.9.0" dependencies = [ "axum", "base64",
Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-shotlog"-version = "0.8.3"+version = "0.9.0" edition = "2021" description = "Screenshot log viewer and CLI for Adom" license = "MIT"
README.md+1−1@@ -45,7 +45,7 @@ The layout adapts to the narrow screen, and while the tab is backgrounded the pa ### Pup and plain browser -If the viewer is a pup window, shotlog leaves your foreground alone; the gentle cue is a taskbar flash, not a window that jumps in front of you. In any plain browser tab, the same live gallery and unseen-shots title badge apply.+Pup is the **full-browser canvas**: zooming a card uses nearly the whole screen, so fine text is readable (the skinny webview can never zoom past its panel width). Ask your AI to switch any channel with one command: `shotlog open -c <ch> --pup --swap` (opens a tab in the shared shotlog pup window and closes the webview tab). shotlog leaves your foreground alone; the gentle cue is an automatic **orange taskbar flash** on updates. In any plain browser tab, the same live gallery and unseen-shots title badge apply. ## Is a human actually seeing it?
SKILL.md+6−1@@ -156,9 +156,14 @@ every gallery card. Make it specific. ## The viewer ```bash-adom-shotlog open -c my-project # opens the gallery as a Hydrogen webview tab+adom-shotlog open -c my-project --skinny # slim 20% webview panel (ambient watching)+adom-shotlog open -c my-project --pup # FULL-BROWSER pup canvas (real zoom); --swap closes the webview ``` +The human picks the canvas: webview for ambient, pup when they want images big+(zoom in the skinny panel cannot exceed the panel). `--pup --swap` is the+one-command switch; the broker auto-flashes the pup taskbar on updates.+ - **Gallery**, newest-first. Each card: description, timestamp, size (shown as `1400×788 · orig 1920×1080` when they differ), KB, source, a **+N child windows** badge when the capture grabbed child windows, and an **ⓘ meta** chip when it has
VERSION+1−1@@ -1 +1 @@-0.8.3+0.9.0
bin/adom-shotlog⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-shotlog",- "version": "0.8.3",+ "version": "0.9.0", "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.3",+ "version": "0.9.0", "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.",
skills/adom-shotlog-surfacing/SKILL.md+34−20@@ -28,30 +28,22 @@ shotlog open -c other-task --skinny # stacks as another tab in the sa shotlog open -c my-project --skinny --ratio 0.25 # custom width fraction ``` -On a new shot, the server **blinks that channel's tab** (`hydrogen alert set`) --a gentle attention cue that auto-scrolls the tab into view **without** stealing-window focus. If the user likes the panel to *jump* to whichever channel just-updated, set `SHOTLOG_WEBVIEW_AUTOSWITCH=1` and the server also activates the tab.+On a new shot, the broker pulls that channel's tab to the front IN ITS OWN PANE+(`workspace active-tab`, never a tab move) and paints an **orange update alert**+on it. It never touches other panes, so the human's typing is never interrupted. ### Pup - gentle orange taskbar flash, never foreground (AUTOMATIC) -Open pup viewers with `?surface=pup&session=<pup sessionId>` in the URL (e.g. one-pup window, session `shots`, with a tab per channel). The viewer reports that-session over its WebSocket, so on every new shot the broker AUTOMATICALLY runs+Open the pup canvas with `shotlog open -c <ch> --pup`: one shared pup window+(session `shotlog`), a tab per channel, idempotent. The URL carries+`?surface=pup&session=shotlog`, the viewer reports that session over its+WebSocket, and on every new shot the broker AUTOMATICALLY runs `adom-desktop browser_alert_window {sessionId}` - the orange taskbar flash - debounced per session. Set `SHOTLOG_AD_TARGET=<desktop>` on the server when more-than one Adom Desktop is connected. No AI action needed; omit `session=` and the-flash falls back to being your job:--If the viewer is a **pup** window, do **NOT** raise/foreground it. The gentle-Windows-native cue is the **orange taskbar flash**:--```bash-adom-desktop browser_alert_window '{"sessionId":"<the pup session>"}'-```--`inject` will hint you to do this when a pup viewer is backgrounded. The viewer's-own page title also shows a `● N` unseen-shot count in the pup tab.+than one Adom Desktop is connected. No AI action needed. If a pup viewer has no+`session=` handle the broker can't flash it; `inject` hints you to do it+yourself (`adom-desktop browser_alert_window`) - never raise/foreground the+window. The viewer's page title also shows a `● N` unseen-shot count. ### Phone / plain browser - in-page title badge @@ -64,6 +56,29 @@ If **no** viewer is connected, the server force-opens a webview as a safety net (so a shot never lands invisibly). Opt out with `SHOTLOG_NO_FORCE_OPEN=1` for users who never want anything auto-opened. +## Switching canvases (webview <-> pup): one surface at a time++The human picks the canvas, and they usually want exactly ONE per channel:++- **Skinny webview** = ambient watching next to their editor. But its zoom can+ never exceed the panel width, so images stay small.+- **Pup** = the full-browser canvas: zoom uses nearly the whole screen, text is+ readable. When the human says "show it in pup", they want the BIG view.++Do the switch in ONE command, in code, not by hand-rolling browser calls:++```bash+shotlog open -c <ch> --pup --swap # open the pup canvas AND close the webview tab+shotlog open -c <ch> --pup # pup alongside (webview stays connected)+shotlog open -c <ch> --skinny # back to the slim webview panel+```++`--pup` uses ONE shared pup window (session `shotlog`) with a tab per channel,+and is idempotent: re-running switches to the existing tab. The URL carries+`?surface=pup&session=shotlog`, so the broker flashes that window's taskbar+orange automatically on every update. If a channel ends up on BOTH surfaces the+broker's inject hints call it out ("did you mean to?"): resolve with `--swap`.+ ## Decision flow for an AI injecting a shot 1. `inject` - read its presence report: how many viewers, which **surfaces**, and@@ -114,7 +129,6 @@ target - no foreground, no cursor move. | Var | Effect | |-----|--------|-| `SHOTLOG_WEBVIEW_AUTOSWITCH=1` | On update, also **activate** (auto-switch to) the channel's webview tab, not just blink it. | | `SHOTLOG_NO_FORCE_OPEN=1` | Never auto-open a viewer, even when nobody is watching. | ## The one rule, restated
src/html/index.html+4−2@@ -170,14 +170,16 @@ header { align-items: flex-start; justify-content: center; z-index: 100; overflow-y: auto; padding: 16px; padding-top: max(16px, env(safe-area-inset-top)); } .modal.open { display: flex; }+/* Zoom uses ALL the real estate the surface has: near-full viewport in a big+ pup/browser window, and still fits a skinny panel. Never a fixed cap. */ .modal-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;- overflow: hidden; width: 100%; max-width: 820px; flex-shrink: 0; }+ overflow: hidden; width: 100%; max-width: min(96vw, 2200px); flex-shrink: 0; } .modal-card .m-desc { padding: 14px 16px 6px; font: 500 15px/1.4 'Satoshi', sans-serif; color: var(--text); } .modal-card .m-meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 4px 16px 10px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--text3); } .modal-card .m-meta .dim-main { color: var(--text2); } .modal-card .m-meta .dim-missing { color: var(--danger); font-weight: 600; } .modal-card .m-meta .sep { color: var(--border); }-.modal-card img { display: block; width: 100%; object-fit: contain; background: #000; }+.modal-card img { display: block; width: 100%; max-height: 84vh; object-fit: contain; background: #000; } /* Popup array inside the modal */ .modal-card .m-popups { padding: 10px 16px 14px; } .modal-card .m-popups .pop-h { font: 600 12px 'Familjen Grotesk', sans-serif; color: var(--text2);
src/main.rs+16−2@@ -113,6 +113,15 @@ enum Commands { /// Width of the skinny panel as a fraction of the workspace (default 0.2). #[arg(long, default_value_t = 0.2)] ratio: f64,+ /// Open in the FULL-BROWSER pup canvas instead: one shared pup window+ /// ('shotlog') with a tab per channel. Real zoom at near-full screen,+ /// and the broker flashes the window's taskbar orange on every update.+ #[arg(long)]+ pup: bool,+ /// With --pup: also CLOSE the channel's skinny webview tab, making pup+ /// this channel's only canvas (the human asked for the big view).+ #[arg(long)]+ swap: bool, }, /// Resize (if needed) and inject a screenshot (like inject but applies 1400px max-width) Paste {@@ -326,9 +335,14 @@ async fn main() { run_inject(&channel, &desc, &source, port, &file, false, orig_w, orig_h, cap_meta, popups); } Commands::Open {- channel, name, icon, port, skinny, ratio,+ channel, name, icon, port, skinny, ratio, pup, swap, } => {- open::open_webview(&channel, name.as_deref(), icon.as_deref(), port, skinny, ratio);+ if pup {+ open::open_pup(&channel, port, swap);+ } else {+ open::open_webview(&channel, name.as_deref(), icon.as_deref(), port, skinny, ratio);+ hint("Skinny webview too small for the human to inspect images? `shotlog open -c <ch> --pup` opens the full-browser pup canvas (add --swap to close the webview tab). Zoom there uses nearly the whole screen.");+ } } Commands::Paste { channel, desc, source, port, file,
src/open.rs+86@@ -158,3 +158,89 @@ fn find_enclosing_split(val: &serde_json::Value, leaf_id: &str) -> Option<(Strin } None }++/// Open (or add a tab to) the ONE shared shotlog pup window for a channel.+/// The full-browser canvas: real zoom, full-size images. The URL carries+/// `?surface=pup&session=shotlog`, so the broker flashes this window's taskbar+/// orange automatically on every update. `swap` also closes the channel's+/// skinny webview tab so pup becomes the channel's primary canvas.+pub fn open_pup(channel: &str, port: u16, swap: bool) {+ let proxy_uri = match std::env::var("VSCODE_PROXY_URI") {+ Ok(uri) => uri.replace("{{port}}", &port.to_string()),+ Err(_) => {+ eprintln!("ERROR: VSCODE_PROXY_URI not set - cannot build the proxy URL for pup.");+ std::process::exit(1);+ }+ };+ let url = format!("{proxy_uri}log/{channel}/?surface=pup&session=shotlog");++ let ad = |args: &[&str]| -> (bool, String) {+ let mut cmd = Command::new("adom-desktop");+ if let Ok(t) = std::env::var("SHOTLOG_AD_TARGET") {+ cmd.args(["--target", &t]);+ }+ match cmd.args(args).output() {+ Ok(o) => {+ let out = format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr));+ (o.status.success() && !out.contains("\"error\""), out)+ }+ Err(e) => (false, e.to_string()),+ }+ };++ // One shotlog pup window for ALL channels, one tab PER channel (idempotent):+ // if this channel's tab already exists, switch to it; else add a tab; else+ // open the window.+ let needle = format!("/log/{channel}/");+ let (listed, list_out) = ad(&["browser_list_tabs", "{\"sessionId\":\"shotlog\"}"]);+ let mut opened = false;+ if listed {+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(&list_out) {+ if let Some(tab) = v.get("tabs").and_then(|t| t.as_array()).and_then(|tabs| {+ tabs.iter().find(|t| t.get("url").and_then(|u| u.as_str()).map(|u| u.contains(&needle)).unwrap_or(false))+ }) {+ if let Some(tid) = tab.get("tabId").or_else(|| tab.get("id")).and_then(|x| x.as_str()) {+ let sw = format!("{{\"sessionId\":\"shotlog\",\"tabId\":\"{tid}\"}}");+ opened = ad(&["browser_switch_tab", &sw]).0;+ }+ }+ }+ }+ if !opened {+ let tab_json = format!("{{\"sessionId\":\"shotlog\",\"url\":\"{url}\"}}");+ let (ok, out) = ad(&["browser_open_tab", &tab_json]);+ opened = if ok {+ true+ } else if out.contains("session_not_found") || out.contains("not found") {+ let win_json = format!("{{\"sessionId\":\"shotlog\",\"profile\":\"shotlog\",\"url\":\"{url}\"}}");+ ad(&["browser_open_window", &win_json]).0+ } else {+ false+ };+ }++ if !opened {+ eprintln!("ERROR: could not open the pup window (is adom-desktop connected?).");+ eprintln!("Hint: check `adom-desktop targets`; with multiple desktops set SHOTLOG_AD_TARGET=<name>.");+ std::process::exit(1);+ }+ println!("OK: pup canvas open for '{channel}' (tab in the shared 'shotlog' pup window).");+ println!("URL: {url}");+ eprintln!("Hint: the broker will flash this window's taskbar ORANGE automatically on every update (the URL registered session=shotlog). Do NOT foreground or refresh it yourself.");+ eprintln!("Hint: pup is the full-browser canvas: zooming a card now uses nearly the whole screen, so text is readable. The skinny webview is better for ambient watching; switch back any time with `shotlog open -c {channel} --skinny`.");++ if swap {+ let removed = Command::new("adom-cli")+ .args(["hydrogen", "workspace", "remove-tab", "--name", &format!("Shots: {channel}")])+ .output()+ .map(|o| o.status.success())+ .unwrap_or(false);+ if removed {+ println!("OK: closed the skinny webview tab for '{channel}' - pup is now this channel's canvas.");+ } else {+ eprintln!("Hint: no webview tab found to close for '{channel}' (already closed, or named differently).");+ }+ } else {+ eprintln!("Hint: the webview tab (if open) stays connected and will still pull forward on updates. Pass --swap to close it and make pup the ONLY canvas for this channel.");+ }+}
src/server.rs+22−3@@ -144,6 +144,8 @@ async fn viewer_status( let surfaces: Vec<String> = infos.iter().map(|i| i.surface.clone()).collect(); let visible: usize = infos.iter().filter(|i| i.visible).count(); let visible_surfaces: Vec<String> = infos.iter().filter(|i| i.visible).map(|i| i.surface.clone()).collect();+ let pup_sessions: Vec<String> = infos.iter()+ .filter(|i| i.surface == "pup").filter_map(|i| i.session.clone()).collect(); Json(serde_json::json!({ "channel": channel, "viewers": viewers,@@ -151,6 +153,7 @@ async fn viewer_status( "surfaces": surfaces, "visible": visible, "visible_surfaces": visible_surfaces,+ "pup_sessions": pup_sessions, })) } @@ -495,18 +498,34 @@ async fn post_shots( // Progressive mini-skill hints — taught at the moment they're relevant, then // quiet (hint_gate caps how many times each lesson repeats). let mut hints: Vec<String> = Vec::new();- if entry.id == 1 && hint_gate("teach:new-channel", 6) {+ if entry.id == 1 && hint_gate(&format!("teach:new-channel:{}", req.channel), 1) { hints.push(format!( "mini-skill [surfacing]: first shot in '{ch}'. The human watches via a slim right-side webview (`shotlog open -c {ch} --skinny`), a pup window, or their phone. I auto-open a 20% webview when nobody is watching, pull the channel's tab to the front with an orange alert on updates, and flash pup taskbars — never foreground anything yourself. Gate on visibility with `shotlog viewers -c {ch}` (exit 0=on-screen, 4=backgrounded, 3=nobody); see every channel with `shotlog channels`.", ch = req.channel)); }- if source_str.contains("desktop_screenshot") && !had_meta && hint_gate("teach:meta", 3) {+ if source_str.contains("desktop_screenshot") && !had_meta && hint_gate(&format!("teach:meta:{}", req.channel), 2) { hints.push("mini-skill [child-windows]: this source looks like an Adom Desktop window capture. AD returns the window's CHILD WINDOWS (owned dropdowns/dialogs, `screenshots[]`/`ownedPopupCount`) + coordMap in the SAME response — pass that whole JSON via `--meta` and they all render inline on this card, and the original resolution comes free.".into()); }- if infos.iter().any(|i| i.surface == "pup" && i.session.is_none()) && hint_gate("teach:pup-session", 3) {+ if infos.iter().any(|i| i.surface == "pup" && i.session.is_none()) && hint_gate(&format!("teach:pup-session:{}", req.channel), 2) { hints.push("mini-skill [surfacing]: a pup viewer is watching WITHOUT a ?session= handle, so I cannot flash its taskbar for you. Open pup viewers with `?surface=pup&session=<pup sessionId>` and I flash that window orange automatically on every update.".into()); } + // Dual-surface check: same channel on webview AND pup is usually an accident+ // (the human asked to "switch" and the AI opened pup without closing webview).+ let has_webview_v = infos.iter().any(|i| i.surface == "webview");+ let has_pup_v = infos.iter().any(|i| i.surface == "pup");+ if has_webview_v && has_pup_v && hint_gate(&format!("teach:dual-surface:{}", req.channel), 3) {+ hints.push(format!(+ "you are showing '{ch}' on TWO surfaces at once (webview + pup). Did you mean to? Most users just want one surface. If you picked pup, close the webview and stop sending to it: `shotlog open -c {ch} --pup --swap` (or `adom-cli hydrogen workspace remove-tab --name \"Shots: {ch}\"`). The broker then stops pulling the webview tab forward on updates.",+ ch = req.channel));+ }+ let webview_only = !infos.is_empty() && infos.iter().all(|i| i.surface == "webview");+ if webview_only && hint_gate(&format!("teach:canvas:{}", req.channel), 2) {+ hints.push(format!(+ "mini-skill [surfacing]: '{ch}' is only being watched in the SKINNY webview. If the human wants to inspect images full-size (zoom in the skinny panel can't get bigger than the panel), switch canvases: `shotlog open -c {ch} --pup` opens the full-browser pup window (one shared window, a tab per channel, taskbar flashes orange on updates). Add `--swap` to close the webview tab so pup becomes the channel's canvas.",+ ch = req.channel));+ }+ Ok(Json(InjectResponse { ok: true, id: entry.id,