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.
Add a read-only shots verb: list a channel's shots, and surface when none are tagged #2
Closes adom/adom-shotlog#5.
Adds a read-only shots verb. channels says what channels exist; this says what is IN one.
$ adom-shotlog shots -c read-captures --limit 6
REF HANDLE AGE DESCRIPTION
#146 — 16h ago Auto-captured screenshot Claude read for analysis: sheet_…
#147 — 16h ago Auto-captured screenshot Claude read for analysis: sheet_…
#151 — 16h ago Auto-captured screenshot Claude read for analysis: fin_a.…
151 shots · 0 tagged.
Hint: None of these carry a handle. A human cannot say "heroG is the one" about an
untagged shot, and shots injected by the read-hook never get one. When you iterate
variants, inject deliberately: shotlog inject -c <ch> -d "..." --tag heroG <file>
Flags: -c/--channel, -p/--port, --limit (default 25, newest last), --tagged to show only shots carrying a handle.
Why
--tag shipped in 0.28.2 this morning and works. The problem is the default path: the read-hook auto-injects and never passes one, so a channel silently fills with unreferenceable shots. On this box that is 151 of 151. Nothing surfaces that, and an agent has no way to look.
That is not hypothetical. Ray asked me to iterate hero images; I produced heroG/heroH/heroI, he could only point at them positionally ("top pic", "your last 2"), I misread which he meant, and we rebuilt the wrong one. Every image was already in read-captures, untagged. The hint above is the part that would have changed my behaviour.
Implementation notes
- Thin wrapper on the existing
GET /api/shots; no new endpoint, no new dependency. - Mirrors
run_channelsin structure, table style,fmt_ago, and its connect-error/hint handling. - Reuses the escaping convention already at
main.rs:871(channel.replace(char::from(32), "%20")) rather than adding a url crate. - Read-only. No behaviour change to inject/paste/serve.
Verified
cargo build --release: clean. 7 warnings, identical to the pre-change baseline, so no new ones.- Run against the live 0.28.2 server above: listing,
--limit,--taggedon a channel with none, and an unknown channel (falls back to thechannelshint).
Not merging this myself, adom-shotlog is yours.
One correction I owe you: I filed two issues earlier today from Ray's relay of your Chat message without checking the shipped source first. Both were already implemented, one at your own request this morning. I closed #4 and annotated #3. Sorry for the noise; this one I built and tested.
Diff Skip to comments (1)
@@ -1,1061 +1,1130 @@⋯ 280 unchanged lines ⋯ #[arg(long, default_value_t = 25)] limit: usize, },+ /// List the shots in a channel: ref, handle, age, description.+ ///+ /// `channels` answers "what channels exist"; this answers "what is IN one".+ /// Without it an agent can inject but never read back, so it cannot see its+ /// own history, cannot tell the human "compare #41 and #42", and cannot+ /// notice that everything it logged is untagged.+ Shots {+ /// Channel to list+ #[arg(short, long)]+ channel: String,+ /// Port of the running shotlog server+ #[arg(short, long, default_value_t = 8820)]+ port: u16,+ /// Max rows to show, newest last (default 25)+ #[arg(long, default_value_t = 25)]+ limit: usize,+ /// Only shots that carry a --tag handle+ #[arg(long)]+ tagged: bool,+ }, /// Install everything: skill, bash completions Install, /// Generate shell completions (bash, zsh, fish)⋯ 244 unchanged lines ⋯ Commands::Channels { port, since_min, limit } => { run_channels(port, since_min, limit); }+ Commands::Shots { channel, port, limit, tagged } => {+ run_shots(&channel, port, limit, tagged);+ } Commands::Install => { run_install(); }⋯ 407 unchanged lines ⋯ } } +fn run_shots(channel: &str, port: u16, limit: usize, tagged_only: bool) {+ // same escaping the viewer-status endpoint uses (main.rs:871), no new dep+ let endpoint = format!("/api/shots?channel={}", channel.replace(' ', "%20"));+ let val = match http_get(port, &endpoint) {+ Ok(v) => v,+ Err(e) => { err(&format!("Cannot connect to shotlog on port {port}: {e}")); hint("Start it: shotlog serve &"); std::process::exit(1); }+ };+ let rows = val.as_array().cloned().unwrap_or_default();+ if rows.is_empty() {+ println!("No shots in channel {channel}.");+ hint("Channels that DO have shots: shotlog channels");+ return;+ }+ let now = std::time::SystemTime::now()+ .duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs() as i64).unwrap_or(0);+ let fmt_ago = |secs: i64| -> String {+ let m = secs / 60;+ if m < 1 { "just now".into() } else if m < 60 { format!("{m}m ago") }+ else if m < 1440 { format!("{}h ago", m / 60) } else { format!("{}d ago", m / 1440) }+ };+ let total = rows.len();+ let tagged_total = rows.iter().filter(|r| r["tag"].as_str().is_some_and(|t| !t.is_empty())).count();+ let shown: Vec<&serde_json::Value> = rows.iter()+ .filter(|r| !tagged_only || r["tag"].as_str().is_some_and(|t| !t.is_empty()))+ .rev().take(limit).collect();+ println!("{:>6} {:<12} {:>9} {}", "REF", "HANDLE", "AGE", "DESCRIPTION");+ for row in shown.into_iter().rev() {+ let id = row["id"].as_u64().unwrap_or(0);+ let tag = row["tag"].as_str().filter(|t| !t.is_empty()).unwrap_or("—");+ let epoch = row["timestamp"].as_str()+ .and_then(|t| chrono::DateTime::parse_from_rfc3339(t).ok())+ .map(|d| d.timestamp()).unwrap_or(0);+ let ago = if epoch == 0 { "—".to_string() } else { fmt_ago(now - epoch) };+ let desc = row["description"].as_str().unwrap_or("");+ let desc = if desc.chars().count() > 58 {+ format!("{}…", desc.chars().take(57).collect::<String>())+ } else { desc.to_string() };+ println!("{:>6} {:<12} {ago:>9} {desc}", format!("#{id}"), tag);+ }+ println!("\n{total} shots · {tagged_total} tagged.");+ if tagged_total == 0 && total > 2 {+ hint("None of these carry a handle. A human cannot say \"heroG is the one\" about an untagged shot, and shots injected by the read-hook never get one. When you iterate variants, inject deliberately: shotlog inject -c <ch> -d \"...\" --tag heroG <file>");+ }+}+ fn run_install() { ensure_read_hook(); let home = std::env::var("HOME").unwrap_or_else(|_| "/home/adom".to_string());⋯ 109 unchanged lines ⋯ serde_json::from_str(body).map_err(|e| format!("Invalid JSON: {e}")) }+
Comments
Log in to comment.