adom-shotlog - surfacing updates (don't shove it in the human's face)

Injecting a shot is not the goal - the human noticing it is. But noticing is not the same as being interrupted. The rule for every update:

Never steal the human's foreground. Surface the shot on the surface they're already using, as gently as that surface allows.

shotlog handles most of this for you in the server; your job is to pick the right surface and use the gentle nudge, never a focus-steal.

The surfaces, and how each should update

Webview - the preferred layout: a slim right-side panel

The nicest setup: a ~20%-wide webview panel pinned to the right of the Hydrogen page. It auto-updates over WebSocket, and you can keep several shotlog channels as tabs in that one panel. Open it that way:

shotlog open -c my-project --skinny            # slim right panel (~20% width)
shotlog open -c other-task --skinny            # stacks as another tab in the same panel
shotlog open -c my-project --skinny --ratio 0.25   # custom width fraction

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 - auto tab-switch + gentle taskbar flash, never foreground (AUTOMATIC)

On every new shot the broker also SWITCHES the shotlog pup window's active tab to the updated channel (the pup mirror of the webview pull-to-front) - a shot never lands in a buried background tab. Tab switching happens INSIDE our own window; window focus/state is never touched.

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. 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

When the tab is backgrounded, the viewer sets its title to ● N · <channel>, so the count of unseen shots shows in the browser/phone tab strip. Nothing to do.

Nobody watching - force-open fallback

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.

The HUMAN can switch themselves: every viewer header has a canvas toggle button (pup ⧉ in a webview, webview ◱ in pup). It checks pup availability via the broker (/api/canvas-check) and shows install guidance when Adom Desktop is missing. So if the human asks you to switch, you may also just tell them about the button. You do the switch in ONE command, never by hand-rolling browser calls:

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 how many are on-screen (visible) vs backgrounded.
  2. If a viewer is on-screen → done; it's genuinely visible. Don't nudge.
  3. If viewers exist but are backgrounded → nudge gently for that surface (webview: server already blinked; pup: browser_alert_window); never foreground.
  4. If nobody is watching → open the preferred surface: shotlog open -c <ch> --skinny, or ask the human to load the URL / open it on their phone. Then re-verify.
  5. Gate precisely with shotlog viewers -c <ch> - exit 0 = on-screen, 4 = open but backgrounded, 3 = nobody.

The exact gentle-cue verbs (Adom Desktop)

Use these to signal an update without foregrounding. Never use desktop_bring_to_front / raise / SendInput-click to force a window forward just to show a shot.

Surface Gentle cue Verb
Webview tab Blink the tab (auto-scrolls into view, no focus steal) adom-cli hydrogen alert set --name "Shots: <ch>" - shotlog does this for you
Webview (backgrounded editor tab) System notification adom-cli hydrogen notify …
Pup / any window Orange taskbar flash (FlashWindowEx) adom-desktop browser_alert_window {sessionId} (pup) or adom-desktop desktop_flash_window {hwnd} (any window)
Any window Glanceable taskbar status (progress bar + overlay icon) adom-desktop desktop_taskbar {hwnd, …}

Finding & targeting the right window (READ-ONLY, no foreground)

You rarely need this for shotlog itself (the server handles surfacing), but when you must locate a viewer window to flash/screenshot it, these AD verbs do it without stealing focus - do NOT shell out to PowerShell, AD already exposes it:

Need Verb Notes
List all top-level windows desktop_list_windows each entry: hwnd, title, className, rect, z (0 = topmost)
Resolve an hwnd by title/class desktop_find_window {title|class} topmost match wins
Parent → child subtree of a window desktop_ui_tree {hwnd} the "window list with parent/child": nested nodes with role, rect{centerX,centerY}, automationId, invokable/settable - each individually targetable in the BACKGROUND. maxDepth/maxNodes/scope:"popups" to narrow.
Capture a specific window (even occluded) desktop_screenshot_window {hwnd} WGC - grabs the window even if it's behind others; won't fire if the window is truly not visible
Change window state w/o foregrounding desktop_set_window_state minimize/maximize/restore without raising

Parent/child example (real): desktop_list_windows gives the top-level Edge window hwnd=5769356; desktop_ui_tree {hwnd:5769356} returns its child subtree ([Window] → [Pane] → [Pane] …), each node with a screen-px rect you can target - no foreground, no cursor move.

Environment knobs

Var Effect
SHOTLOG_MAX_SHOTS Per-channel disk cap (default 200): oldest shot's files are deleted as new ones arrive.
SHOTLOG_RETENTION_DAYS Prune channels idle this many days (default 30, 0 = off), at startup + every 6h.
SHOTLOG_NO_FORCE_OPEN=1 Never auto-open a viewer, even when nobody is watching.

The one rule, restated

Gentle by surface: webview → blink the tab, pup → orange taskbar flash, phone/browser → title badge, nobody → force-open fallback. Foregrounding the human's window is never the answer.