small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
README.md

UI review kit #

A private daemon with seeded chats, albedo web on it and a Chrome page, driven from an albedo python kernel, for looking at the web client by hand or for a fan-out of reviewer agents. Opt-in; nothing here runs in the gate.

  • seed.py builds /tmp/albedo-review (or --base, or $ALBEDO_UX): a daemon snapshot, a web build in dist/, two git workspaces in ws/, and seed/, a home with ten named chats whose replies cover markdown, code, tables, thinking, tool cells, a file edit and a failed turn.
  • replies.py is the fake model's script (test/e2e/fake_provider.py): a message with slow, hang, fail, tool, start jobs (three background jobs), start agents (three child agents), edit, think, code, table or long gets that kind of reply; anything else a short markdown one.
  • kit.py starts a run on a copy of the seed home and drives the page. kit.scenarios() lists the boards load() builds through the UI.

running a review #

nix develop -c python3 test/manual/ux/seed.py   # again after daemon or web changes
import sys; sys.path.insert(0, "test/manual/ux"); import kit
ui = await kit.start("dock")          # runs/dock/{home,shots}; width=, height=, dark=, reduced_motion=, dist=
await ui.load("sections")             # first load builds the board, later ones restore it
await ui.windows(); await ui.layout() # w0, w1, ... and the tree, section names quoted
show_image(await ui.shot("sections"))
await ui.drag(a, b)                   # returns the dock hint and preview seen before letting go
await ui.hold(a); await ui.probe(x, y); await ui.release(x, y)
await ui.palette("layout: canvas"); await ui.key("ArrowRight", kit.ALT | kit.SHIFT)
await ui.say("w2", "slow please"); await ui.errors()
await ui.mark(); ...; await ui.remounted()   # windows made again since mark(), [] after a move
print(await ui.stop())                # [] when nothing is left running

To check a change, await kit.build(path) and kit.start(..., dist=path). To check the tethers, import tethers_check; await tethers_check.check(ui) runs the flows that move a line's target and raises on any stale tether:.

reviewer brief #

Give each reviewer one area, the scenarios to start from, and what to try there. They look and report; they do not edit the checkout. Each one starts its own run, reproduces a finding twice before reporting it, and ends with await ui.stop(). The report, in a file and in the mail body:

  • one line: what was covered and for how long.
  • findings, most important first: title · broken / confusing / sloppy / nit · scenario and exact steps · what happens vs what was expected · screenshot paths · confidence sure / likely / taste.
  • what works well, briefly.
  • outside my area.
  • kit or setup notes: what got in the way.

gotchas #

  • load() swaps the boards only. Messages sent and notes made stay in the daemon; kit.start() again for a clean home.
  • A theme picked in settings or with palette("theme: …") is saved and beats media(dark=…) until palette("theme: system").
  • key() sends what a US keyboard does: key("!") or key("1", kit.SHIFT) is ⇧1, letters type themselves, Enter clicks a focused button. page.press refuses " " and "["; use key().
  • Text fields: ui.type(text) inserts; key("Backspace") and key("a", kit.META) edit. The composer is a contenteditable .editor; say() drives it.
  • click() hovers first and leaves 80 ms between press and release; some controls ignore a bare CDP click.
  • A tile under 160 px refuses every drop ("no room"). Scenarios check that their tiles stay at 160 px or more at 1440×900; keep new ones that way.
  • A floating note dropped over another float lands on that float, not the canvas under it.
  • errors() collects page errors from every load; no listener to re-add. It also holds stale tether: … records: start() sets window.__albedoAudit, so every 500 ms at rest the tethers compare what they draw with what a fresh frame would, and a difference means something moved a line without waking it. tethers_check.py drives the flows that could.
  • shot() saves and returns a path without showing it; show_image(path) or shot(label, show=True) when you want to look.
  • Kernels outlive their daemon on purpose; stop() reaps what runs under the run's home and returns anything still alive.
  • Workspaces in ws/ are shared by every run, so file edits pile up there until the next seed.