diff --git a/AGENTS.md b/AGENTS.md index ad689cd9..a8ea6934 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ Kotlin and Jetpack Compose; see the README's **Layout** for the modules and [`do Every change to what the app shows is checked by rendering its screens on the desktop JVM, not by starting an Android emulator: ```sh -./gradlew -p tools/ui-preview render -Ponly=board,task # just the ones the change touches, a few seconds each +./gradlew -p tools/ui-preview render -Ponly=agents,chat # just the ones the change touches, a few seconds each ./gradlew -p tools/ui-preview render # all scenes, dark and light, about half an hour ``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b1a7be7..9278d469 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,16 +15,16 @@ A debug build is `com.makefunstuff.herdr.debug`, so it installs next to the rele ### The host side -The app talks to [herdr](https://herdr.dev) over SSH, so you need a machine running herdr that your phone can reach by SSH: install herdr there and add the machine in the app ([Setting up a machine](README.md#setting-up-a-machine)). The host tools in `host/` (`herdr-push` for notifications, `herdr-usage` for the usage page and `herdr-logins` for the agent logins) are copied to a machine by the app when a feature needs them; each folder's README says how to run it by hand. +The app talks to [herdr](https://herdr.dev) over SSH, so you need a machine running herdr that your phone can reach by SSH: install herdr there and add the machine in the app ([Setting up a machine](README.md#setting-up-a-machine)). The host tools in `host/` (`herdr-push` for notifications, `herdr-usage` for the usage page, `herdr-logins` for the agent logins and `herdr-app-build` for changing the app from the phone) are copied to a machine by the app when a feature needs them; each folder's README says how to run it by hand. -Without a machine of your own, [`docs/screenshots/demo-host`](docs/screenshots/demo-host) starts throwaway herdr hosts in containers with made-up projects, the ones the README's screenshots come from. +Without a machine of your own, the screen previews (below) show every screen against a fake machine with made-up projects; they are also where the README's screenshots come from. ## Tests ```sh ./gradlew -p core test # core: SSH, herdr API, board, transcripts, terminal, host tools ./gradlew :app:testDebugUnitTest # the app's JVM tests (needs the Android SDK) -./gradlew -p tools/ui-preview render -Ponly=board # screen previews of the screens you changed +./gradlew -p tools/ui-preview render -Ponly=agents # screen previews of the screens you changed ``` The core tests run the host scripts themselves, so they want `python3` (and `sshd` and `ss` for a few); a test whose tool is missing is skipped, not failed. CI runs the core tests, builds the release APK and renders every screen on each push and pull request. @@ -38,7 +38,7 @@ The core tests run the host scripts themselves, so they want `python3` (and `ssh - **`core/` is plain Kotlin/JVM**: SSH, the herdr API, parsing and logic go there, with tests. `app/` is the Android app and its Compose screens. - **The look lives in `ui/theme` and `ui/design`** ([`docs/DESIGN.md`](docs/DESIGN.md)). Screens use its colours, type, spacing and components and nothing else; a new one is added there first. - **Android-only calls stay out of shared code** the previews compile; see "When it doesn't compile" in [`tools/ui-preview/README.md`](tools/ui-preview/README.md). -- **Host scripts** (`host/`) stay one file that needs nothing installed: `herdr-push`, `herdr-usage` and `herdr-logins` are Python 3.6+ with the standard library only. Raise a script's `version:` line near its top when you change it, so the app replaces older copies on machines. +- **Host scripts** (`host/`) stay one file that needs nothing installed: `herdr-push`, `herdr-usage`, `herdr-logins` and `herdr-app-build` are Python 3.6+ with the standard library only. Raise a script's `version:` line near its top when you change it, so the app replaces older copies on machines. - **Words on screen and in docs are plain and short.** A feature users see is described in the README. - **The repo is public**: no real hostnames, IPs, user names, emails or keys in code, tests, demo data or screenshots. Use the demo machines (`devbox`, `gpu-box`, `ci-runner`, user `demo`). @@ -65,7 +65,7 @@ CI must pass. Pull requests from forks build with the debug key, which is expect The repo is worked on with AI coding agents. [`AGENTS.md`](AGENTS.md) holds the instructions they read: which checks to run, and to check UI changes with the screen previews instead of an emulator. If you use an agent, point it there; you are still the one who reviews and stands behind what you send. -The owner runs their own work through [herdr-projects](https://github.com/eliasstravik/herdr-projects), a work board for herdr: that is where the task names (`AND-55`) and `work/-…` branches on its pull requests come from. Outside contributors don't need it; a normal fork and pull request is all it takes. +The owner runs their own work through [simple-herdr-projects](https://github.com/makefunstuff/simple-herdr-projects), a work board for herdr: that is where the task names (`HERD-33`) and `task/-…` branches on its pull requests come from. Outside contributors don't need it; a normal fork and pull request is all it takes. ## License diff --git a/README.md b/README.md index 430efbfd..0e41c880 100644 --- a/README.md +++ b/README.md @@ -4,26 +4,26 @@ A native Android client for [herdr](https://herdr.dev) hosts: see your agents ac - + + - - - - - + + + + - - - - + + + +
Agents list
Agents: what needs you on every machine first, answerable on the card
The Tasks list
Tasks: what needs you on every machine first, answerable on the row
A space's Assistant chat
Assistant: talk to a space; its plans, news lines and Accept cards
An agent's chat
Agent chat: the agent's own transcript, each turn's work folded to one line
A blocked agent asking for approval
Needs you: the question docked over the composer, answered with a thumb
Coordinator chat
Coordinator: talk to the work board
Machines list
Machines: status per host, also through a jump host
A machine's workspaces
A machine: its workspaces, tabs and panes
Terminal
Terminal: any pane, live, sized to the phone
Settings
Settings: the SSH key, with backup and restore
A task ready to accept
A task: its chat, files, changes, PR and browser as tabs, Accept on top
A task's pull request
Pull request: checks, approve or request changes from the phone
A task's live browser
Browser: what the task's agent sees in its browser, live
A herdr.doc investigation
Docs: the agents' herdr.doc write-ups, drawn natively
The work board
Board: tasks in the board's colours, the ones that need you first
A task's details
A task: next step, reviews, links, one tap to approve
Agents in light mode
Light mode: follows the phone
The board in light mode
Board, light
The spaces drawer
Spaces: every machine's spaces in the drawer, Settings at its foot
A machine's spaces
A machine: health, usage and logins, then its spaces and panes
Terminal
Terminal: any pane, live, sized to the phone
The Tasks list in light mode
Light mode: follows the phone
-The screenshots come from a demo host with made-up projects (see [`docs/screenshots/demo-host`](docs/screenshots/demo-host)). The look (colours, type, icons, spacing, components) is described in [`docs/DESIGN.md`](docs/DESIGN.md); the theme and art are the Herdr desktop's workbench (Apache 2.0), and the fonts and icons are credited in [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md). +The screenshots are the [screen previews](#screen-previews): the app's own screens rendered against a fake machine with made-up projects. `./gradlew -p tools/ui-preview render -Ponly=agents-v2,chat-tab,chat,chat-blocked,task-v2-ready,task-v2-pr,task-v2-browser,doc,machines,machine,terminal`, then each shot `magick .png -strip -resize 540x -quality 82 .webp` (agents-v2 → tasks, agents-v2-light → tasks-light, chat-tab → assistant, chat → chat, chat-blocked → blocked, task-v2-ready → task, task-v2-pr → pull-request, task-v2-browser → browser, doc → doc, machines → spaces, machine → machine, terminal → terminal), the tall `agents-v2`, `agents-v2-light` and `doc` cut to their first screen first (`-gravity north -crop 824x1784+0+0 +repage` before `-strip`). ## Getting the app @@ -71,16 +71,16 @@ Without these secrets, and for local builds, the APK is signed with the normal d ``` (On Windows the interpreter is usually `python`, see below.) It shows a QR code with where the machine is (its Tailscale name and address, its address on the local network), its user, its SSH host key's fingerprint and a one-time code, valid five minutes. -2. In **Machines**, tap **Add machine**, then **Scan the code**. The phone's own code scanner reads it (Google Play services': the app asks for no camera permission). Without one, **Paste the link** takes the link printed under the code. +2. At the foot of the spaces drawer, tap **Add machine**, then **Scan the code**. The phone's own code scanner reads it (Google Play services': the app asks for no camera permission). Without one, **Paste the link** takes the link printed under the code. 3. The app finds the machine at the first address that answers with that host key and shows what it found. **Pair and add** logs in with the one-time code, which can do nothing but hand over this phone's public key: it goes into `~/.ssh/authorized_keys` there, the code is spent, and the app pins the host key and opens the machine. How it works and what it touches: [`host/herdr-push/README.md`](host/herdr-push/README.md#pairing-a-phone). **By hand** (**Add machine → Type the address instead**): -1. Open **Settings** (the gear in the Machines bar) and copy the app's public key. +1. Open **Settings** (at the foot of the spaces drawer) and copy the app's public key. 2. Add it to `~/.ssh/authorized_keys` on the machine running herdr. -3. In **Machines**, tap **Add machine**, then **Type the address instead**, and enter `user@host[:port]`. The app pins the host key on first connect. +3. Tap **Add machine**, then **Type the address instead**, and enter `user@host[:port]`. The app pins the host key on first connect. **A Windows machine** works with the Herdr desktop app's herdr-core: its agents and their terminals. Classic herdr, Shepherdr and App change need Linux or macOS. It needs OpenSSH Server running with [Git for Windows](https://gitforwindows.org)' Bash as its shell, once, in a PowerShell opened as administrator: @@ -92,7 +92,7 @@ Then pair as above with `python herdr-push pair`. For a user in Administrators, For notifications, the PC has to be awake and signed in: the watcher runs in your signed-in session, and herdr-push needs a Python 3 there (the app finds `python3`, `python` or `py -3`, and skips the Microsoft Store's stand-in). See [Notifications while the app is closed](#notifications-while-the-app-is-closed). -Machines reachable only through another one can use it as a jump host ("Connect through"). For zero-touch or read-only machines, **Allow host scripts** (on by default) can be turned off so the app skips copying its helper scripts (`herdr-usage`, `herdr-logins`, `herdr-push`) there. From a machine's menu, **Import herdr machines** adds the machines its herdr already knows (`herdr machine list`). +Machines reachable only through another one can use it as a jump host ("Connect through"). For zero-touch or read-only machines, **Allow host scripts** (on by default) can be turned off so the app skips copying its helper scripts (`herdr-usage`, `herdr-logins`, `herdr-push`) there. ## How it talks to herdr @@ -100,37 +100,39 @@ The app opens an SSH connection and reaches herdr's socket API through `direct-s ## Needs you, first -The **Agents** screen opens with one **Needs you** list across every machine: agents asking a question, Shepherdr tasks waiting for you (Ready to accept, or asking), and Assistants that ask. An agent and the task it works on show once, as one card (the app ties them by the task Shepherdr labels its terminal with, or the workspace it opens for the task). Each card carries its answers: +The app is laid out as Shepherdr 3.1's phone web app: a drawer of spaces grouped by machine (each machine with a meter: CPU, memory and its tightest plan; a tap opens the machine; Search on top, **Settings** and **Add machine** at its foot) and a bottom bar of **Chat** (the space picked in the drawer: its Assistant, or its agent's chat), **Tasks** and **Artifacts**. + +The **Tasks** tab opens with one **Needs you** list across every machine: agents asking a question, Shepherdr tasks waiting for you (asking, or wanting a secret), and Assistants that ask; then the **Ready** tasks, each with its **Accept**; then the work by state: Working, Queued, To do, Done and Idle. A filter field and chips (All, Needs you, Ready, Working…, each with its count) narrow it. An agent and the task it works on show once, as one row (the app ties them by the task Shepherdr labels its terminal with, or the workspace it opens for the task). Each row carries its answers: - a question: **Yes** and **No** (or the menu's two choices), and **Reply**. Like a notification's buttons, Yes and No read the agent's screen again first and press nothing if the question changed; a longer menu opens in the chat. -- a task: its question card's choices, or **Accept** for one that is Ready (Shepherdr's Accept card first), and **Open**, its chat on the board. +- a task: its question card's choices, **Give it** for a secret it asks for, or **Accept** for one that is Ready (Shepherdr's Accept card first); a tap opens its chat. -Below it, the agents at work are one line each, with a thin bar. Finished ones wait under **Done** until you open their chat on the phone, swipe them right, press **Mark all seen**, or look at their pane in herdr itself (herdr then says idle; the app reads its `completion_seq` to tell one finish from the next); a finish seen rests with the idle ones. +Agents at work are one line each, with a thin bar. Finished ones wait under **Done** until you open their chat on the phone, swipe them right, press **Mark all seen**, or look at their pane in herdr itself (herdr then says idle; the app reads its `completion_seq` to tell one finish from the next); a finish seen rests with the idle ones. -Swipe an agent left for **Pin** (it stays on top, under Needs you, in the order pinned; kept on the phone, and gone when the pane is). Its long-press sheet has Pin too, and its space's **Notify**: **All** (its questions and each finish), **Needs me** (only its questions) or **Off** (nothing: no alerts, no Live Update while it works, no in-app alert; its rows show a crossed-out bell). The same choice is a long press on the space in the drawer, as in Shepherdr 3.1. A space that picks nothing follows the switches in Settings (Needs me by default). A question always shows in Needs you, whatever its space's Notify. Mutes and watches from before Notify move onto their spaces the first time the app reads their machine: a muted agent's space becomes Off, a watched one's All. One line on top says how many machines are online and filters the list to one. **Ask** opens the Coordinator: the one used last, or it asks which machine. The tabs are **Agents**, **Board** and **Machines**, each badge counting what waits for you; Settings is the gear in the Machines bar. The boards are read every 15 seconds while the list is in front (every 10 on the Board tab). +Swipe an agent left for **Pin** (it stays on top, under Needs you, in the order pinned; kept on the phone, and gone when the pane is). Its long-press sheet has Pin too, and its space's **Notify**: **All** (its questions and each finish), **Needs me** (only its questions) or **Off** (nothing: no alerts, no Live Update while it works, no in-app alert; its rows show a crossed-out bell). The same choice is a long press on the space in the drawer, as in Shepherdr 3.1. A space that picks nothing follows the switches in Settings (Needs me by default). A question always shows in Needs you, whatever its space's Notify. Mutes and watches from before Notify move onto their spaces the first time the app reads their machine: a muted agent's space becomes Off, a watched one's All. The **+** offers **New task**, the **Assistant** and **Start an agent** (on which machine, when there are several). The Tasks badge counts what waits for an answer; Ready tasks have their Accept on the list instead. The boards are read every 15 seconds while the list is in front. ## Search -The search icon on **Agents**, **Board** and a machine (or **Ctrl+K** with a hardware keyboard, from anywhere) opens one box for the agents, the boards' tasks, the machines, their workspaces and the words in the recent chats. Chips above the results filter them to what **Needs you**, what is **Working** and what is **Done**, each with its count. From a machine it searches only that machine, until its chip is taken off. Every word typed has to be in a result, and a word at the start of a name counts most; with nothing typed it lists the agents, the tasks waiting for you and **Start work…**. +The search icon on **Tasks**, the spaces drawer and a machine (or **Ctrl+K** with a hardware keyboard, from anywhere) opens one box for the agents, the boards' tasks, the machines, their workspaces and the words in the recent chats. Chips above the results filter them to what **Needs you**, what is **Working** and what is **Done**, each with its count. From a machine it searches only that machine, until its chip is taken off. Every word typed has to be in a result, and a word at the start of a name counts most; with nothing typed it lists the agents, the tasks waiting for you and **Start work…**. -Words in the chats are looked for on the machines themselves: each running agent's own transcript (Claude Code, codex, pi and omp) is searched where it is, a moment after you stop typing (three letters at the least), and what was said around the words shows with who said it and when; tool calls and their output don't count. Under the results, **New task about "…"** starts work about the words, and **Open a terminal in …** opens a shell in the workspace found (each workspace row of classic herdr has the same terminal button; herdr-core's workspaces open no terminal from the phone). Enter opens the first result, Esc goes back. +Words in the chats are looked for on the machines themselves: each running agent's own transcript (Claude Code, codex, pi and omp) is searched where it is, a moment after you stop typing (three letters at the least), and what was said around the words shows with who said it and when; tool calls and their output don't count. Under the results, **New task about "…"** starts work about the words, and **Open a terminal in …** opens a shell in the workspace found (each pane on a machine's screen has the same terminal button). Enter opens the first result, Esc goes back. ## Start work -**Start work** asks first what you want, then how and where. It opens from a long press on **Ask** (or the **Start work** button when no machine has the work board), from the search, and from the launcher's shortcut. +**Start work** asks first what you want, then how and where. It opens from the Tasks tab's **+** (**New task** or **Start an agent**), a space's long-press sheet in the drawer, the search, and the launcher's shortcut. -- **Board task**: the sentence becomes a task on the chosen machine's board (its first line the title, the rest the description), in one of the board's repos or a folder, with a worktree, checks and a review; the board picks its agent (shown, with what is left of its limit), at once or later. +- **Board task**: the sentence goes to the chosen space's Assistant (**Send to the Assistant**), picked among the machine's spaces, which makes the task: Shepherdr makes tasks only through a space's Assistant. - **Agent now**: an agent starts in the folder, in a new tab of the workspace that sits there (else a workspace of its own), named after the sentence's first words, and the sentence is its first message. The agents offered are the ones installed on that machine: those herdr's integrations find, and every other kind herdr can start whose program is on the machine's PATH (gemini, amp…). -The folders offered are the ones work was started in from the phone, then the workspaces' (a task's worktree counts as its repository); **Browse…** walks the machine's folders, Git repositories marked. The phone never starts an agent in a mode that skips its permission prompts: it asks before it acts, as it does at the desk. A machine's **New › Agent** sheet has the same first-message field, lists the agents installed there, and starts the agent in a new split beside the workspace's focused pane. +The folders offered are the ones work was started in from the phone, then the workspaces' (a task's worktree counts as its repository); **Browse…** walks the machine's folders, Git repositories marked. The phone never starts an agent in a mode that skips its permission prompts: it asks before it acts, as it does at the desk. ## Managing from the phone -A machine's screen opens with its places as tiles in one row, each with a live line: **Board** (how many tasks need you, else how many are at work), **Coordinator** (idle, working, waits for you) and **Health**; **Usage** is a line under them (a tile on a machine without the board). Its workspaces follow, each with its branch and pull request under its name and one row per pane: the agent and its state, or the shell and its folder, a tap opening its chat (or terminal) and the terminal icon its terminal. Tab chips and the layout thumbnail show only for a workspace with several tabs. The bar says when the machine last answered ("Online · answered 4 s ago"). +A machine's screen (a tap on the machine in the spaces drawer) opens with its places as tiles in one row, each with a live line: **Health**, **Usage** and **Logins**, framed in a state colour only when something there is wrong. Its spaces follow (classic herdr's workspaces and herdr-core's spaces alike), each with its branch and pull request under its name and one row per pane: the agent and its state, or the shell and its folder, a tap opening its chat (or terminal) and the terminal icon its terminal. Tab chips show only for a workspace with several tabs. The bar says when the machine last answered ("Online · answered 4 s ago"). -Everything that exists in a session is editable from the app, not just readable. A workspace or tab header has an actions button (a workspace's one tab is under its actions, **Its tab**), and panes and agents answer to a long press: rename, focus, reorder, move, swap, zoom, clear a pane's scrollback, read what a pane is running, and ask `agent.explain` why an agent is in the state it shows. Closing a workspace, tab or pane asks first. herdr's own refusals ("There's no pane in that direction.") come back as plain sentences. +Everything that exists in a classic herdr session is editable from the app, not just readable (herdr-core's spaces are the desktop app's: their panes open, nothing more). A workspace or tab header has an actions button (a workspace's one tab is under its actions, **Its tab**), and panes and agents answer to a long press: rename, focus, reorder, move, swap, zoom, clear a pane's scrollback, read what a pane is running, and ask `agent.explain` why an agent is in the state it shows. Closing a workspace, tab or pane asks first. herdr's own refusals ("There's no pane in that direction.") come back as plain sentences. -The screens follow herdr's `events.subscribe` stream, so a change made anywhere — desktop or phone — appears here. Notifications are the exception: herdr has no list of them, so the app derives them from agents finishing or blocking and shows them behind the bell in the top bar; the menu can also show a notification on the desktop running herdr. +The screens follow herdr's `events.subscribe` stream, so a change made anywhere — desktop or phone — appears here. Notifications are the exception: herdr has no list of them, so the app derives them from agents finishing or blocking (see Notifications below). ## The terminal @@ -138,8 +140,8 @@ Any pane opens as a live terminal, sized to the phone. - **The key bar** over the keyboard has two rows of keys on a phone: Esc, Tab, ^C, ^R (the shell's history), ^W (delete a word), `|`, `~`, **fn** (a page of F1 to F12, Page Up and Down), Ctrl, Alt, Space and the arrows. Ctrl, Alt and Shift are sticky: a tap holds one for the next key (the bar's or the keyboard's: Ctrl then ← is a word back), a second tap keeps it on (underlined) until a third. **Hold Space and drag** to move the cursor: a cell for each 12 dp across, a line for each 24 dp up or down. - **Your own keys.** The pencil at the end of the bar (or **⋯ → Edit key bar…**) lists the bar's keys, each moved earlier or later or taken off, and offers more by kind: modifiers and pages (Shift, the keyboard key), shortcuts (^D, ^Z, ^L, ^A, ^E, ^U, ^K, ^B, Alt+B, Alt+F, Alt+., Shift+Tab), keys (Enter, Backspace, Delete, Home, End, Page Up and Down…) and characters; a tap puts a key on the bar or takes it off. **A shortcut of your own** builds any other: Ctrl, Alt and Shift with a letter, a sign or a key's name (`F5`, `Home`, `Left`). **Back to the first keys** undoes it all. The bar is the phone's, the same in every terminal, and stays through restarts. -- **Selected text** (a long press, or **⋯ → Select text**, then a drag) gets **Copy**, **Quote in chat** and **Send to Coordinator**. Quoting puts it in a fenced block, with where it came from (`From the terminal (shell on devbox):`), at the end of an agent's message box and opens its chat: the workspace's agent, or the one you pick when there are several. **Send to Coordinator** puts it in the machine's Coordinator chat the same way (on a machine with the board). Nothing is sent until you send it. -- **Keep screen on** (the sun in the bar, or the **⋯**): the screen stays on while a terminal is open, for a long build or a log you follow. It is kept for every terminal until turned off. +- **Selected text** (a long press, or **⋯ → Select text**, then a drag) gets **Copy**, **Quote in chat** and **Send to the Assistant**. Quoting puts it in a fenced block, with where it came from (`From the terminal (shell on devbox):`), at the end of an agent's message box and opens its chat: the workspace's agent, or the one you pick when there are several. **Send to the Assistant** puts it in the shown space's Assistant chat the same way (on a machine with Shepherdr). Nothing is sent until you send it. +- **Keep screen on** (in the **⋯**): the screen stays on while a terminal is open, for a long build or a log you follow. It is kept for every terminal until turned off. ## Reading a chat @@ -201,7 +203,7 @@ Each machine has its own logins, so each has a **Usage** page (the **Usage** lin - **Local models** (llama.cpp, say, serving Qwen): local, no limit, and whether the server answers. - On every card: the tasks and agents that used it most this week and a 7-day chart. -**At a glance**, the top bar of the Agents screen carries the tightest limit across the machines ("Claude 82% · resets 17:00", amber near it), a tap from its usage page; the status widget shows the same line. **Where agents are chosen**, **Start an agent** on a task lists its agent and the ones the board falls back to, each with what it has left ("18% left · resets 11:00", "$7.63 left", "local, no limit"), to start it with another one; adding a task that starts at once names its agent and what that has left. **Warnings** come as notifications at 80% and 95%, when a limit runs out, and when the board's fallback takes a task over because of a limit ("codex took over from claude: out of allowance until 17:00"); **Limits** in the notification settings switches them off, and quiet hours hold them. +**At a glance**, each machine's meter in the spaces drawer has a bar for its tightest plan, amber near a limit and red past it; the status widget carries the tightest limit across the machines ("Claude 82% · resets 17:00"). **Warnings** come as notifications at 80% and 95% and when a limit runs out; **Limits** in the notification settings switches them off, and quiet hours hold them. It is all read on the machine, over SSH, by [herdr-usage](host/herdr-usage/README.md) (Python 3, copied there by the app): Claude Code's and Codex's own usage APIs with their own logins, the providers' balance and spend APIs with the keys already there, and the agents' session records for what each task and agent used. No key comes to the phone. Readings are cached on the machine for a few minutes and on the phone until the next one, so the page opens at once; while the app is in front, each connected machine is read again every 10 minutes, and while it is closed its own looks (and herdr-push's warnings) keep the limits current for the widget. A limit whose reset time has passed since it was read counts as fresh everywhere, and an agent whose provider's key or login failed says so in the agent choices. Which source each provider's numbers come from, and why: [`host/herdr-usage/README.md`](host/herdr-usage/README.md). @@ -228,7 +230,7 @@ Each state is shown once, however it arrives, and comes down again when it is ov - **Hold while I'm at the machine** (on by default): while you typed in herdr in the last two minutes, or a machine's screen is unlocked, notifications arrive silently, without a sound or a pop-up. herdr-push tells, with every push, how long ago someone typed in a herdr window there (the last input of its terminal, as `w` reads it), and whether a desktop session there is unlocked (logind's lock, or a screen locker running; on macOS the console's lock). Anything that touches `~/.local/state/herdr-push/present` counts as typing too, a shell prompt or an editor hook, say. Any machine counts: you are at your desk, whichever machine pings. - **Spaces**: each space's own **Notify** (All / Needs me / Off), picked in the drawer or an agent's long-press sheet, overrides Needs you and Finished for its agents. The screen lists the spaces that picked their own, each with **Reset**. -- **In the app**: when another agent starts asking (or a Shepherdr task starts waiting) while you are in a chat or anywhere but the Agents list, one black line drops from the top, "api-server · needs you: run db:migrate?", with the agent's mark. A tap opens it; it leaves after five seconds; it never shows for the chat or the task open in front. Not for a space set to Notify Off. +- **In the app**: when another agent starts asking (or a Shepherdr task starts waiting) while you are in a chat or anywhere but the Tasks list, one black line drops from the top, "api-server · needs you: run db:migrate?", with the agent's mark. A tap opens it; it leaves after five seconds; it never shows for the chat or the task open in front. Not for a space set to Notify Off. ### Live Updates @@ -244,7 +246,7 @@ They come from herdr-push, which also sends what runs to a phone that asked for ## The brief -The **Brief** (the newspaper icon above the agents, or **Settings → Brief**) tells what the agents and Shepherdr's tasks did since you last looked, on every machine: what merged, what waits for you now (tasks to accept or answer, agents that ask), your own agents that finished, and what is still at work. Each line opens its task on the board or its agent's chat. "Since you last looked" is since the app was last on screen, or a brief was read aloud to its end; **Last 24 hours** is there too. +The **Brief** (**Settings → Brief → Open the brief**, or its notification) tells what the agents and Shepherdr's tasks did since you last looked, on every machine: what merged, what waits for you now (tasks to accept or answer, agents that ask), your own agents that finished, and what is still at work. Each line opens its task on the board or its agent's chat. "Since you last looked" is since the app was last on screen, or a brief was read aloud to its end; **Last 24 hours** is there too. - **At the times you choose.** **When the brief comes** sends it as a notification, at up to four times (say 07:30, and 17:30 for the drive home), every day or on the days you pick. Its lines say what waits and what merged; a tap opens it. - **Read aloud, for the drive.** **Read aloud** (on the brief, and on its notification) reads it with the phone's voice, over the music: what waits for you, what was done, then what is at work. Titles rather than task keys, at most five of each. The reading goes on with the screen off or the app closed; its notification and the brief's bar have **Next** and **Stop**. **Read it aloud as it comes** can also read the scheduled brief by itself, in the car (car mode: Android Auto or a car dock) or always. @@ -253,19 +255,20 @@ To put it together the app reaches every machine over SSH, as for notifications ## Code -The code your agents work on, to read and comment on from the phone, where you check their work: a Shepherdr task's view has 3.1's tabs under its bar, **Chat**, **Files**, **Changes** and, once it has one, **Pull request**. Files and Changes are the folder the task works in (its worktree, as Shepherdr names it); a file or a change opens full screen. An agent chat's **⋯** (**Browse the code**: the folder it works in) and a workspace's actions open the same screens on their folder. There is no machine-wide code browser: the code is reached from the work. +The code your agents work on, to read and comment on from the phone, where you check their work: a task's view has 3.1's tabs under its bar, **Chat**, **Files**, **Changes**, once it has one **Pull request**, and **Browser** where Shepherdr has a browser for the task. Files and Changes are the folder the task works in (its worktree, as Shepherdr names it); a file or a change opens full screen. An agent chat's **⋯** (**Browse the code**: the folder it works in) and a workspace's actions open the same screens on their folder. There is no machine-wide code browser: the code is reached from the work. -- **Pull request.** The task's pull request as GitHub says it now (asked with `gh` on the task's machine through Shepherdr, again every 30 s while it shows): open, draft, merged or closed, its number, files and lines changed, its branch, **Open on GitHub** and **Copy link**, then its **Checks**, failed first, then running, passed and skipped, each opening on GitHub. The strip's **PR #** opens this tab. +- **Pull request.** The task's pull request as GitHub says it now, read with `gh` on the task's machine over SSH (again every 30 s while it shows): open, draft, merged or closed, its number, files and lines changed, its branch, **Open on GitHub** and **Copy link**, its description, its **Checks** (failed first, then running, passed and skipped, each opening on GitHub), its open threads with the code they sit on (resolved ones folded to a line) and the conversation. **Approve**, **Request changes** (with words), **Comment** and **Reply** in a thread each open a sheet and post only on its button, as the machine's gh login; **Resolve** and **Reopen** act at once. What you write goes to `gh api` on its input, never on a command line. On your own pull request only Comment is offered (GitHub takes no approval from its author); without gh, or not signed in, the tab shows the board's own answer and says why. The strip's **PR #** opens this tab. +- **Browser.** The task's agent-browser session, live, through Shepherdr 3.1's browser relay: the page, its address, and who drives (**LIVE**, **YOU'RE DRIVING**, **OFF**). Touch the page to take it over (the agent is told to hold off); **Hand back**, or a quiet minute, gives it back; Back, Reload and the phone's keyboard work on the page. **Open browser** starts one that isn't open. - **Browse.** A checkout's files are folders; **Go to file** finds a file by a few letters of its name; the search icon searches the code itself, on the machine (`git grep`: words or a regular expression, with or without case). - **Read.** Numbered lines in the herdr web UI's code colours, long lines wrapped or scrolled sideways, the text size you set (**Aa**), **Go to line** (**#**), Markdown as it reads (and its source a tap away). A big file opens at once: its first 4 MB, said so, drawn a line at a time as you scroll. - **Changes.** Git's status (branch, ahead and behind, what isn't committed), the diff against the branch it left (`main`, or whatever `origin/HEAD` names), or only what isn't committed yet, new files included; a file's diff with both line numbers, stepping through the files. **History** lists the commits, those not in `main` yet first, and each commit's own diff. - **Comment.** Tap a line number (or long-press a line), then another to take the lines between, in a file or in a diff, and **Comment**. It goes to the task's agent (into its chat, from a task's view) or an agent at work in that checkout, as a review note: the path, the lines, the quoted code (a diff's with its `+` and `-`) and your words. **Keep for later** adds it to the **Comments** waiting on that machine (the speech-bubble icon, with a count), to change, drop, or send together as one message. -Everything is read over the machine's SSH connection with `git` and the shell, and nothing is changed there: Git runs with `GIT_OPTIONAL_LOCKS=0`, so not even `git status` touches the index. Only sending a comment does something, and that is a message. +Everything is read over the machine's SSH connection with `git` and the shell, and nothing is changed there: Git runs with `GIT_OPTIONAL_LOCKS=0`, so not even `git status` touches the index. Only sending a comment does something, and that is a message; a review on the Pull request tab is posted to GitHub by the machine's `gh`. ## The work board -The **Board** tab shows every machine's task board in one list, grouped by machine (what needs you, then what is at work and what is to do), with a chip per machine; a task opens on its machine's board, a machine's heading opens that whole board. A machine's board is [Shepherdr](#shepherdr) 3.1's where its service runs, or [simple-herdr-projects](#simple-herdr-projects) where `shp` is there; the choice is per machine and detected. +The **Tasks** tab shows every machine's tasks in one list (see [Needs you, first](#needs-you-first)); a task opens as its task view, with Chat, Files, Changes, Pull request and Browser tabs. A machine's board is [Shepherdr](#shepherdr) 3.1's where its service runs, or [simple-herdr-projects](#simple-herdr-projects) where `shp` is there; the choice is per machine and detected. ### What an agent did, step by step @@ -290,19 +293,19 @@ Both chats, an agent's and an Assistant's, take pictures: **+** offers **Take a ## Asking from anywhere -The Coordinator is a tap away from outside the app too: +A space's Assistant is a tap away from outside the app too: -- **Share to it.** Share a link, some text, an error or a screenshot to **Herdr Coordinator** from any app's share sheet. It lands in the Coordinator chat's message box: the words at the end of what you had typed, a picture among its pictures (made smaller, to look at, crop or mark up before it goes, as above), any other file copied to that machine and named as `@path`, as **+** does. You read it, add to it, and send it, so the Coordinator can make a task of it. With several machines the app asks which one first (the one used last on top); a machine without the work board can't be picked. A share waiting for a machine, and files waiting for a machine that isn't connected, are kept through the app being closed: the app asks again when it next opens; a picture waits in the message box, and the chat says which other file waits and attaches it once the machine connects. -- **Send selected text.** **Send to Coordinator** sits next to Copy in the menu of selected text, and does the same with just the text. -- **The Quick Settings tile.** **Coordinator** opens the chat at once: the Coordinator used last, or the only machine that can have one; otherwise it asks. Under its name it says what needs you ("2 need you · 3 working"). -- **The launcher's shortcuts.** A long press on the app's icon offers **Needs you** (with its count), the **Coordinator** used last, **Start work**, and the two agents whose chats were opened last. -- **Three home-screen widgets.** **Ask the Coordinator** opens the chat as the tile does. **Herdr** shows the counts across your machines, such as "2 need you · 3 working", and when they were last known; a tap opens the board with the most tasks waiting for you (or the agents, when it is an agent that asks). **Needs you** lists what waits on you, newest first, with its notification's buttons on each row: **Accept** on a finished task, **Approve** and **Deny** on an agent's yes-or-no question (the fingerprint first where Settings ask for it), and **Answer** opening what is answered in words. A row stays until it is answered or over, even when its notification was swiped away; a tap on it opens it, a tap on its heading opens Needs you. +- **Share to it.** Share a link, some text, an error or a screenshot to **Herdr Assistant** from any app's share sheet. It lands in the Assistant chat's message box: the words at the end of what you had typed, a picture among its pictures (made smaller, to look at, crop or mark up before it goes, as above), any other file copied to that machine and named as `@path`, as **+** does. You read it, add to it, and send it, so the Assistant can make a task of it. With several machines the app asks which one first (the one used last on top); a machine without Shepherdr can't be picked. A share waiting for a machine, and files waiting for a machine that isn't connected, are kept through the app being closed: the app asks again when it next opens; a picture waits in the message box, and the chat says which other file waits and attaches it once the machine connects. +- **Send selected text.** **Send to the Assistant** sits next to Copy in the menu of selected text, and does the same with just the text. +- **The Quick Settings tile.** **Assistant** opens the Chat tab at once, on the space shown last. Under its name it says what needs you ("2 need you · 3 working"). +- **The launcher's shortcuts.** A long press on the app's icon offers **Needs you** (with its count), the **Assistant** used last, **Start work**, and the two agents whose chats were opened last. +- **Three home-screen widgets.** **Ask the Assistant** opens the chat as the tile does. **Herdr** shows the counts across your machines, such as "2 need you · 3 working", and when they were last known; a tap opens the Tasks tab. **Needs you** lists what waits on you, newest first, with its notification's buttons on each row: **Accept** on a finished task, **Approve** and **Deny** on an agent's yes-or-no question (the fingerprint first where Settings ask for it), and **Answer** opening what is answered in words. A row stays until it is answered or over, even when its notification was swiped away; a tap on it opens it, a tap on its heading opens Needs you. The counts are what the app knows: what it sees while open (and each Shepherdr board as the app reads it while one of its lists is on screen), its own looks while closed, and the machines' pushes (see Notifications above). An agent that needs you and the task it works for count once, as do a task and its own agent at work. What works and nothing confirmed for 75 minutes stops counting. ## Floating chats -One agent's chat or the Coordinator chat can float over other apps as an Android bubble (Android 11 and newer), to steer it while reading something else. **Float over other apps** is in an agent chat's **⋯** menu and in the Coordinator's top bar; an agent's notification has **Float chat** too, when it has room for a third button (a finished agent, one that asks something without a yes or no), and so has the Live Update of an agent at work (the Coordinator's own agent floats the Coordinator chat). The bubble is the chat itself, with its draft, attachments and terminal; **Open in the app** (in its menu, or the Coordinator's top bar) opens it full size. Each chat gets its own bubble, and Android keeps them as conversations: dragging one away closes it, and the launcher's long press on the app lists them. +An agent's chat can float over other apps as an Android bubble (Android 11 and newer), to steer it while reading something else. **Float over other apps** is in an agent chat's **⋯** menu; an agent's notification has **Float chat** too, when it has room for a third button (a finished agent, one that asks something without a yes or no), and so has the Live Update of an agent at work. The bubble is the chat itself, with its draft, attachments and terminal; **Open in the app** (in its menu) opens it full size. Each chat gets its own bubble, and Android keeps them as conversations: dragging one away closes it, and the launcher's long press on the app lists them. Android lets an app's chats bubble only once you allow it. The first time, the app says so and opens Herdr's bubble settings: choose **All conversations can bubble**, and the chat floats as you come back. With **Selected conversations**, the chat shows as a notification instead, whose bubble button floats it (and marks it as one that may). @@ -312,7 +315,7 @@ Everything a long press opens is also offered to TalkBack as actions on the card ## Big screens -On a window 840 dp wide or wider (a tablet, an open foldable, a large window or desktop windowing), the app shows three panes side by side, like Puggie: the agents list (with Agents, Board and Machines under it), then what you opened from it (an agent's chat, a terminal, an Assistant, the Board tab, a machine, Settings), then a machine's work board. What is open in the middle is marked in the list, and opening another agent replaces it instead of piling up; Back closes it, and then a task open on the board. A board opened from a notification, the widget, the Board tab or a machine's Board card shows on the right, with its task open (a board still open in the middle, from before the window grew, moves there). A narrower window keeps the phone layout. Rotating or resizing across the line keeps what is open as it was: the chat, its draft and scroll, the board's search, filters and open task (kept while a narrow window hides the board, and back when it is wide again). +On a window 840 dp wide or wider (a tablet, an open foldable, a large window or desktop windowing), the app shows the spaces drawer as a sidebar that stays on the left, as Shepherdr 3.1's web app does on a desktop, and the app (its tabs and what they open) fills the rest; a tap on a space opens it beside the sidebar from any screen. What a list opens takes the place of what was shown instead of piling up. A narrower window keeps the phone layout, with the drawer. Rotating or resizing across the line keeps what is open as it was: the chat, its draft and scroll, the Tasks list's filter and place. ## Changing the app from the phone @@ -340,10 +343,10 @@ UI changes are checked, and shown in review, with screen previews instead of an ```sh ./gradlew -p tools/ui-preview render # every scene → tools/ui-preview/build/shots/*.png -./gradlew -p tools/ui-preview render -Ponly=board,chat # some of them +./gradlew -p tools/ui-preview render -Ponly=agents,chat # some of them ``` -It needs JDK 17, no Android SDK, and takes about half an hour for all 248 scenes (agents, search, start work and its folder browser, machines, adding one by its code (found, not answering) or by hand, the code (files, a file, a comment, changes, a diff, history, search, comments waiting), a task's Files, Changes and Pull request tabs, a machine, a machine's health and cleanup, agent chats, pictures in a chat (what + offers, two screenshots in the message box, one marked up and one cropped, sent ones in the chat and one full size), the Assistants' chats, floating chats, the board and a task, an agent's activity timeline, the brief and when it comes, a machine's usage and limits (its providers, one out of its allowance, one whose reset passed, a reading that failed), the terminal (its menu, a password asked there, sticky modifiers, the F-keys, the key bar's editor and a bar of one's own, selected text quoted in a chat or sent to the Coordinator), settings with its notifications and text sizes, TalkBack's actions on a card, the dialogs and sheets, and the three panes of a wide window, also across a rotation). CI renders them too, a quarter of them on each of four runners, and uploads them as the `herdr-screens-` artifact. See [`tools/ui-preview/README.md`](tools/ui-preview/README.md) for how it works and how to add a scene, and [`AGENTS.md`](AGENTS.md) for how changes are reviewed. +It needs JDK 17, no Android SDK, and takes about half an hour for all 343 scenes (the Tasks list and what waits on it, the spaces drawer, search, start work, the Chat tab and a space's tab menu, the Assistants' chats with their plans, news lines, Accept cards, reactions, replies, find and voice, artifacts, docs and a document, a task's Chat, Files, Changes, Pull request (its review, approving, replying, a refusal) and Browser tabs, simple-herdr-projects' board and tasks, machines and adding one by its code or by hand, a machine, its health, usage and agent logins, agent chats (turns, held messages, `/` and `@`, the model pill, voice messages, dictation, pictures), the blocked agent's question, an agent's activity timeline, floating chats, the brief, the code screens, the terminal (its menu, a password, sticky keys, the key bar's editor, selected text), settings with notifications, Autopilot and the fingerprint, changing the app from the phone, machines on the Herdr desktop's herdr-core, the owner's real data's shape (`real-*`), TalkBack's actions, and the wide window). CI renders them too, a quarter of them on each of four runners, and uploads them as the `herdr-screens-` artifact. See [`tools/ui-preview/README.md`](tools/ui-preview/README.md) for how it works and how to add a scene, and [`AGENTS.md`](AGENTS.md) for how changes are reviewed. ## Shepherdr @@ -369,7 +372,7 @@ The desktop's Docs panel on the phone: the herdr.doc/2 documents (the desktop's ## Layout -- `core/`: pure Kotlin/JVM: SSH, herdr API, herdr-core's API (`core/hc`, [`docs/HERDR_CORE.md`](docs/HERDR_CORE.md)), host sessions, terminal emulator and its key bar (`core/terminal`), reading code (`core/code`), herdr.doc documents (`core/docs`), port forwarding (`core/ssh/PortForward.kt`, for Shepherdr's service and voice). `./gradlew -p core test`. +- `core/`: pure Kotlin/JVM: SSH, herdr API, herdr-core's API (`core/hc`, [`docs/HERDR_CORE.md`](docs/HERDR_CORE.md)), host sessions, terminal emulator and its key bar (`core/terminal`), reading code (`core/code`), herdr.doc documents (`core/docs`), Shepherdr 3's client (`core/shepherdr`), simple-herdr-projects over SSH (`core/shp`), pull request reviews with gh (`core/github`), port forwarding (`core/ssh/PortForward.kt`, for Shepherdr's service and voice). `./gradlew -p core test`. - `app/`: the Android app. `ui/theme` and `ui/design` hold its look and shared components ([`docs/DESIGN.md`](docs/DESIGN.md)). - `tools/ui-preview/`: the screen previews. `./gradlew -p tools/ui-preview render`. - `host/herdr-push/`: the watcher that runs on a machine and pushes to the phone (Python 3; core carries it as a resource, and its tests run it). diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 1d0a677d..732ecb77 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -51,16 +51,16 @@ Near-black panels, amber frames, status lights. Flat and minimal: frames and fla HERDR chip, *HERDR*, an amber-framed *WORKBENCH* tag, search), *SPACES*, then each machine as a numbered bus (`BUS 1 · DEVBOX`, its meter: thin bars for CPU, memory and its tightest plan, amber near a limit and red past it; its connection LED; why, when it doesn't answer; a tap opens the - machine) with its spaces as numbered slots (`SlotNumber`, the name in capitals, the program's chip - or *ASSISTANT*, its one LED; the shown one on `selected` with an amber bar at its start and its + machine) with its spaces as numbered slots (`SlotNumber`, the name in sentence case, the program's chip + only where an agent really is, its one LED; the shown one on `selected` with an amber bar at its start and its number on amber). Classic herdr's and herdr-core's workspaces of one name are one slot. At its foot, *Settings* and *Add machine*. The circuit board shows, quietly, below the last bus. - **Chat tab**: the shown space's Assistant (Shepherdr), else its agent (the one that needs you first), else its terminals. The bar's leading button opens the drawer; its layers glyph opens the space's menu: its tabs (each agent's chat and terminal, each shell), *New task*, *Assistant*, *Start an agent*, and its machine. -- **Tasks tab** (3.1's Tasks list): a filter row (All, then each group with its count), then - *Pinned*, *Needs you* (cards with their answers), *Ready* (with Accept), *Working*, *Queued*, +- **Tasks tab** (3.1's Tasks list): a *Filter tasks* field and a filter row (All, then each group + with its count), then *Pinned*, *Needs you* (one row each, its answer beside it), *Ready* (with Accept), *Working*, *Queued*, *To do*, *Done* (five, then *Show N more*), *Idle*. One row per task: a task's agent is its row. A + offers *New task*, *Assistant* and *Start an agent*. - **Wide** (840dp and more): the drawer stays open on the left as a sidebar; the app fills the rest. @@ -176,12 +176,12 @@ drawer's last bus. Never under rows of text; text over an empty state sits on th | `HerdrNavBar` / `NavItem` | See Layout. | | `SectionHeading` | Mono capitals in `muted` (or a state's colour when it needs you), then a 1dp `line` rule to the edge or to its trailing action. | | `QuietCard` | A module: a `lineStrong` frame, flat. Holds rows and sections. | -| Needs you cards | The agent's chip, what it works on, its key and where; the question; its answers as square buttons (the main one amber). No status badge: the section says it. | +| Needs you rows (`WaitingRow`) | One row per item: the agent's chip, the title, its key and space, the question in two lines, and its one action beside it (Accept, Give it, Answer; a yes/no keeps both answers as square buttons, the main one amber). No status badge: the section says it. The full card stays in the task's chat. | | `ListRow`, `ActionRow`, `MenuRow` | Optional leading cell, title, a dim line, trailing or chevron. A chosen action row sits on `selected` with a 3dp amber bar at its start. | | `PrimaryButton` / `SecondaryButton` / `QuietButton` / `DangerButton` | Square, 48dp, mono labels: amber with ink / framed on the ground / bare / the danger tint framed in red. | | `HerdrChip` | A framed cell; chosen: amber frame, amber text on amber's tint. | | `SegmentedPills` | One framed track; the chosen segment amber with ink. | -| `ViewTabs` | The views of one thing (a task's Chat, Files, Changes, Pull request) under its bar: on `panel` over a `lineStrong` rule, names in mono capitals, the shown one amber with a 2dp amber bar along its bottom edge; scrolls sideways when they don't fit. | +| `ViewTabs` | The views of one thing (a task's Chat, Files, Changes, Pull request, Browser) under its bar: on `panel` over a `lineStrong` rule, names in mono capitals, the shown one amber with a 2dp amber bar along its bottom edge; scrolls sideways when they don't fit. | | `HerdrField` | Label in mono capitals, the box on `input` framed in `lineStrong`, amber while focused. | | `HerdrSwitch`, `SwitchRow` | Square: a framed track with a square knob; on, amber with an ink knob. | | `StatusBadge`, `StatusLabel`, `Led` | See States. | diff --git a/docs/FEATURE_AUDIT.md b/docs/FEATURE_AUDIT.md index 0fa5ca8b..c6729063 100644 --- a/docs/FEATURE_AUDIT.md +++ b/docs/FEATURE_AUDIT.md @@ -18,7 +18,59 @@ What the owner's machine runs today (read on this machine): herdr-core and the d Shepherdr 3.1 service, and still a classic `herdr server` holding one idle workspace; herdr-projects is installed, but its data folder was last written on 2026-11-01. -## How the app is laid out now +## Where it stands (2026-11-08, HERD-33) + +The audit below is kept as written on 2026-11-04; this is what became of each item, checked against +`main` at `8916b0f`. The layout it called "now" is gone: the app is 3.1's phone layout (drawer of +spaces, Chat · Tasks · Artifacts), README "Needs you, first". + +| Item | Now | +| --- | --- | +| R1 herdr-projects 1.x board | **Done**, HERD-11 (its board, reviews, Routines and Coordinator removed) | +| R2 three ways into a Coordinator | **Done**, HERD-9: the tile, widgets and shortcut open the Chat tab's Assistant; `AskScreen` stays for shares only | +| R3 places twice on a machine | **Done**, HERD-9: one row of tiles (Health, Usage, Logins); the ⋯ menu is Docs, Edit machine, Change this screen | +| R4 classic herdr's admin | **Done**, HERD-9 (`ui/admin`, Import herdr machines removed) | +| R5 the machine's Refresh | **Done**, HERD-9 | +| R6 Keep screen on twice | **Done**: a ⋯ row only | +| R7 the brief's bar button | **Done**, HERD-9: the brief is in Settings › Brief and its notification; the brief itself kept | +| R8 classic workspace tools | **Done**, HERD-12 | +| R9 machine-wide code browser | **Done**, HERD-16 | +| F1 two Needs you badges | **Done**, HERD-9: only Tasks has the badge, and since HERD-31 it leaves Ready tasks out | +| F2 status said twice on cards | **Done**, HERD-9; HERD-31 made each a one-line `WaitingRow` | +| F3 one task twice | **Done**, HERD-9 with HERD-8's task↔agent link: one row per task | +| F4 navigation unlike 3.1 | **Done**, HERD-9, reworked in HERD-31 | +| F5 the same project twice on a machine | **Done**, HERD-9: one Spaces list, the backend in a detail line | +| F6 six blocks before the workspaces | **Done**, HERD-9 | +| F7 13-row machine menu | **Done**: three rows | +| F8 Online twice, Settings behind a tab | **Done**, HERD-9: the Machines tab is gone; machines are buses in the drawer, Settings at its foot | +| F9 Start work behind a long press | **Done**, HERD-9: the Tasks tab's + | +| F10 nine-row agent sheet | **Done**, HERD-13 | +| F11 four controls in the bar | **Done**, HERD-9: search only; usage is the drawer's meter | +| F12 "Not connected yet" on launch | **Done**, HERD-9: *Connecting to N machines* | +| F13 herdr-core chat is its screen | **Done**, HERD-14, and HERD-30 for the right conversation | +| F14 herdr-core terminal without Find | **Done**, HERD-15 | +| F15 a space's dot vs. its row | **Done**, HERD-9: on a machine's screen the tab that needs you (or works, or finished) carries its own light on its chip | +| F16 "Working" three times | **Done**, HERD-9: one working line | +| F17 placeholders differ | **Done**, HERD-9: "Message ", "Message the Assistant" | +| A1 task alerts | **Done**, HERD-21 | +| A2 Files, Changes, Pull request tabs | **Done**, HERD-16 | +| A3 dictation | **Done**, HERD-17 | +| A4 spaces drawer | **Done** in part, HERD-9: machines as buses with a meter, Settings at the foot, Notify per space (HERD-13). Not done: Pinned, sections, unread spaces in bold | +| A5 Tasks list as 3.1's | **Done** in part, HERD-9 and HERD-31: filter field and status chips. Not done: Group by, Archived, Automations | +| A6 secret asks | **Done**, HERD-18 | +| A7 Autopilot switch | **Done**, HERD-19 | +| A8 tab menu | **Done**, HERD-20 | +| A9 chat parity | **Done**, HERD-22 | +| A10 Needs you widget | **Done**, HERD-23 | +| A11 pull request review | **Done**, HERD-24 | +| A12 live browser | **Done**, HERD-25 | +| A13 docs viewer | **Done**, HERD-26 | + +Since then, outside this list: changing the app from the phone (HERD-27), the terminal reachable +again (HERD-29), simple-herdr-projects as a board (HERD-32), Windows machines over Git Bash, and +the wide window's sidebar (HERD-35). + +## How the app is laid out now (2026-11-04) - Bottom bar (`AppNav.kt` `AppNavBar`): **Agents**, **Board**, **Machines**. Settings sits behind a gear in the Machines bar (`ui/machines/MachinesScreen.kt:64`); the code browser behind `<>` beside it. diff --git a/docs/HERDR_CORE.md b/docs/HERDR_CORE.md index 558f440e..6545abc1 100644 --- a/docs/HERDR_CORE.md +++ b/docs/HERDR_CORE.md @@ -37,7 +37,7 @@ hc itself has terminals only. The desktop app adds its spaces and tabs as labels What follows on the screens: -- **Agents and Needs you**: hc's agents appear with the classic ones (`HomeAgent` reads `SessionSnapshot`). A blocked one can be answered with its keys (`terminal.keys`: `y`, `enter`, `esc`). `terminal.send` refuses a blocked program (kind `blocked`) unless forced. +- **The Tasks list and Needs you**: hc's agents appear with the classic ones (`HomeAgent` reads `SessionSnapshot`). A blocked one can be answered with its keys (`terminal.keys`: `y`, `enter`, `esc`). `terminal.send` refuses a blocked program (kind `blocked`) unless forced. - **Notifications**: the app derives them from the status changes it follows (`Notices.between` on two snapshots), so herdr-core's snapshots raise the same notices for working → done and → blocked; the events stream keeps them live. - **Terminals**: `HcTerminalStream` and classic herdr's `TerminalStream` are both a `LiveTerminal` (emulator, revision, status, send, resize, scroll, close), so the Terminal screen takes either. It is fed by `terminal.attach`; hc sends the program's raw bytes, so the app's own emulator draws them. Attaching takes the terminal's size at once (hc: the last active viewer sets it), as classic herdr's takeover does; a watch-only attach (`observe`) never types or resizes the person's window. When the desktop window takes the size back (someone typed there), hc's `size` event says so; HA-6 passes it to `programResized` so the emulator follows. - **Chats**: hc reports no transcript, so the app finds it from the agent's process (`foreground.pid`), as Shepherdr 3.1's chat does: Claude Code names its session in `~/.claude/sessions/.json`, codex keeps its `rollout-*.jsonl` open (`/proc//fd`); the process's children are tried too. Several agents share a `cwd` (an Assistant and its tasks' workers), so the folder alone can't tell them apart. Where none is found (another agent, a Claude Code that hasn't written its first turn, a terminal on a machine hc added), the chat is its screen (`terminal.read`), as a classic agent without a session's is. Where Shepherdr 3.1 answers a task's or an Assistant's chat with "Can't find this conversation" (`notice: none`) while the app finds that agent's transcript itself, the app shows the transcript in its place (`ChatOwner.readsOwn`). `LiveChatMapTest` (with `HERDR_LIVE_LOCAL=1`) lists every chat on the machine it runs on and what it maps to. @@ -55,13 +55,13 @@ What follows on the screens: ## On the screens -- **One view**: `HostState.view` is classic herdr's snapshot and herdr-core's together (`Sessions.merge`, classic first, herdr-core's spaces numbered after). The Agents list, Needs you, notices, Live Updates, search, the chats, the machine list and the Terminal screen read it; `HostState.snapshot` stays classic herdr's alone, for what only classic herdr has (layouts, workspace metadata, the brief). When classic herdr stops on a machine running both, the machine stays online for herdr-core and shows only what is live; classic herdr's last snapshot is kept aside, and its agents' pins, and the mutes and watches still to move onto their spaces, wait for it (`HostState.agentsKnown`). +- **One view**: `HostState.view` is classic herdr's snapshot and herdr-core's together (`Sessions.merge`, classic first, herdr-core's spaces numbered after). The Tasks list, Needs you, notices, Live Updates, search, the chats, the spaces drawer, the machine screen and the Terminal screen read it; `HostState.snapshot` stays classic herdr's alone, for what only classic herdr has (workspace metadata, the brief). When classic herdr stops on a machine running both, the machine stays online for herdr-core and shows only what is live; classic herdr's last snapshot is kept aside, and its agents' pins, and the mutes and watches still to move onto their spaces, wait for it (`HostState.agentsKnown`). - **Keys and words**: `HostSession.awaitAgent(paneId)` gives an `AgentPort`: herdr-core's (`HcAgentPort`) for a pane id with `hc:`, else classic herdr's. `AgentInput` (the chat, the Needs you card's Yes and No, a notification's buttons, Stop on a Live Update) speaks only to it, so an answer is read off the screen again and pressed the same way on either. - **Chat**: Claude Code's and codex's transcripts, found from the agent's process (`AgentSessionRef` kind `pid`, which `TranscriptFollower.FIND` looks up on the machine), folded as a classic agent's are; else the agent's screen (`terminal.read`). A question on it gets its card and buttons as a classic agent's does. Classic herdr's own actions (rename, focus, why this state, activity) are left out for herdr-core's agents. - **Terminal**: `HcTerminalStream`; the desktop window taking the size back (`size` event, `HerdrCoreState.sizes`) resizes the phone's emulator. hc has no scroll request, so the phone keeps the history, as a classic terminal does: the emulator keeps the 2000 lines that scroll off its main screen, and on attaching `terminal.read`'s history (asked just before the attach, 1000 lines, as text without colours) goes above the first paint where its tail meets it (`TerminalHistory.above`). Swiping scrolls through it, typing or **Back to the screen** returns; Find searches it on the phone (`TerminalFind`), marking the match and scrolling to it. Selection by herdr and links are classic herdr's; selected text is read off the phone's own screen (or the history scrolled to) instead. -- **Machine screen**: classic herdr's workspaces, then a **Desktop app** section with herdr-core's spaces, tabs and terminals (no actions on them). The machine list shows herdr-core's version and its agents in the counts. +- **Machine screen**: one **Spaces** list, classic herdr's workspaces and herdr-core's spaces together by name (no actions on herdr-core's); on a machine running both, each says which it is in a detail line (`classic herdr`, `herdr-core 0.1.14`), never a heading. - **Notifications**: herdr-core's snapshots raise the session's notices as classic herdr's do (none on the first look). While the app is closed, herdr-push (version 9) reads the default core's agents on its socket (plain JSON-RPC, `terminal.list` and `terminal.read`; connecting never starts a core) and pushes them with the classic ones; the app's own look reads them as far as herdr-core was found. herdr-push still reads the default session alone: other sessions' agents raise notices while the app runs, not yet while it is closed. - **Which machine**: herdr-core belongs to the host, not a herdr session, so the app follows it from the machine entry for classic herdr's default session only: two machines in the app for two sessions of one host show its agents once. -- **Several herdr-core sessions**: `HostSession` follows each session that runs (one `hc connect` each, at most three besides the default one: each holds an SSH channel). A session with no live terminal (a preview's) is let go again after its first look and looked at again a minute later. Ids keep the session apart: the default session's stay `hc:t7`, another's are `hc:@:t7` (`HcMapping.scope`, `HcMapping.session`), so the same `t7` in two sessions is two panes, and each terminal is in exactly one. `HostState.herdrCore` is every session's state as one (`HerdrCoreState.merge`: the default session's spaces first; `sessions` names them), and `awaitHerdrCore(paneId = …)`/`awaitAgent` reach the pane's own session. The machine screen's section is then called **herdr-core** and names the sessions; the machine list adds them after the version. A machine where classic herdr is missing is online while any session's core runs. -- **Shepherdr's agents**: Shepherdr 3 labels the agents it starts (`herdr.role`, `herdr.coordinator.space` on a space's Assistant; `herdr.task` (`#`) and `herdr.task.space` on a task's worker, which herdr-task names `-`). The app calls them by their task (`HERD-8`, `HERD-8 reviewer`) or as `<space> Assistant`, and keeps the task and space on the agent (`AgentInfo.shepherdrTask`, `shepherdrSpace`; `HcMapping.shepherdrTask` takes the ref's letters from the worker's name only when its number is the label's). A worker sits in its Assistant's space and tab. On a machine whose board is Shepherdr, opening one from the Agents list, Needs you, a notice or notification, search or the brief opens its task's chat or its space's Assistant, where its cards are (`Panes.openAgent`); the machine screen still opens its screen and terminal. -- **Order**: classic herdr's `stateChangeSeq` is a counter, herdr-core's the time in ms, so on the Agents list a herdr-core agent sorts before classic ones of the same state. +- **Several herdr-core sessions**: `HostSession` follows each session that runs (one `hc connect` each, at most three besides the default one: each holds an SSH channel). A session with no live terminal (a preview's) is let go again after its first look and looked at again a minute later. Ids keep the session apart: the default session's stay `hc:t7`, another's are `hc:@<session>:t7` (`HcMapping.scope`, `HcMapping.session`), so the same `t7` in two sessions is two panes, and each terminal is in exactly one. `HostState.herdrCore` is every session's state as one (`HerdrCoreState.merge`: the default session's spaces first; `sessions` names them), and `awaitHerdrCore(paneId = …)`/`awaitAgent` reach the pane's own session. A machine where classic herdr is missing is online while any session's core runs. +- **Shepherdr's agents**: Shepherdr 3 labels the agents it starts (`herdr.role`, `herdr.coordinator.space` on a space's Assistant; `herdr.task` (`<list>#<n>`) and `herdr.task.space` on a task's worker, which herdr-task names `<ref>-<title>`). The app calls them by their task (`HERD-8`, `HERD-8 reviewer`) or as `<space> Assistant`, and keeps the task and space on the agent (`AgentInfo.shepherdrTask`, `shepherdrSpace`; `HcMapping.shepherdrTask` takes the ref's letters from the worker's name only when its number is the label's). A worker sits in its Assistant's space and tab. On a machine whose board is Shepherdr, opening one from the Tasks list, Needs you, a notice or notification, search or the brief opens its task's chat or its space's Assistant, where its cards are (`Panes.openAgent`); the machine screen still opens its screen and terminal. +- **Order**: classic herdr's `stateChangeSeq` is a counter, herdr-core's the time in ms, so on the Tasks list a herdr-core agent sorts before classic ones of the same state. diff --git a/docs/screenshots/agents-light.webp b/docs/screenshots/agents-light.webp deleted file mode 100644 index cbfea7ed..00000000 Binary files a/docs/screenshots/agents-light.webp and /dev/null differ diff --git a/docs/screenshots/agents.webp b/docs/screenshots/agents.webp deleted file mode 100644 index d7f41a46..00000000 Binary files a/docs/screenshots/agents.webp and /dev/null differ diff --git a/docs/screenshots/assistant.webp b/docs/screenshots/assistant.webp new file mode 100644 index 00000000..11e20727 Binary files /dev/null and b/docs/screenshots/assistant.webp differ diff --git a/docs/screenshots/blocked.webp b/docs/screenshots/blocked.webp index fb254b26..4ab7807c 100644 Binary files a/docs/screenshots/blocked.webp and b/docs/screenshots/blocked.webp differ diff --git a/docs/screenshots/browser.webp b/docs/screenshots/browser.webp new file mode 100644 index 00000000..9cccc170 Binary files /dev/null and b/docs/screenshots/browser.webp differ diff --git a/docs/screenshots/chat.webp b/docs/screenshots/chat.webp index 4b3818c2..0d024d20 100644 Binary files a/docs/screenshots/chat.webp and b/docs/screenshots/chat.webp differ diff --git a/docs/screenshots/coordinator.webp b/docs/screenshots/coordinator.webp deleted file mode 100644 index c3a24d49..00000000 Binary files a/docs/screenshots/coordinator.webp and /dev/null differ diff --git a/docs/screenshots/demo-host/Dockerfile b/docs/screenshots/demo-host/Dockerfile deleted file mode 100644 index e8bc4760..00000000 --- a/docs/screenshots/demo-host/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -# A throwaway herdr host for the README screenshots: user "demo", no real data. -FROM alpine:3.24.2 -RUN apk add --no-cache openssh bash coreutils findutils git ncurses procps python3 \ - && adduser -D -s /bin/bash demo && echo 'demo:*' | chpasswd -e -COPY herdr /usr/local/bin/herdr -RUN ssh-keygen -A \ - && sed -i 's/^#\?AllowTcpForwarding.*/AllowTcpForwarding yes/; s/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config \ - && echo 'AllowStreamLocalForwarding yes' >> /etc/ssh/sshd_config -CMD ["/usr/sbin/sshd", "-D", "-e"] diff --git a/docs/screenshots/demo-host/README.md b/docs/screenshots/demo-host/README.md deleted file mode 100644 index 0ed424ac..00000000 --- a/docs/screenshots/demo-host/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Demo host for the screenshots - -The README's screenshots come from the real app talking to real herdr servers, in three throwaway containers (`devbox`, `gpu-box`, `ci-runner`) with a user called `demo` and made-up projects. Nothing in the app changes for them. - -- `up.sh [authorized_keys]` builds the image (it copies the `herdr` on your PATH into it), recreates the containers and lays out their workspaces. `devbox` listens on `127.0.0.1:2201`; the other two are reachable only from `devbox`, so the app adds them with **Connect through devbox**. -- `setup.sh` starts herdr in a container and opens the panes. The "agents" are stand-in scripts named `claude` and `codex` that draw a screen (`content.py` writes them), so herdr detects them, and their sessions point at transcripts `content.py` writes in the agents' own formats. The chat view follows those like any other. -- `herdr-projects` stands in for the work board: `remote list` / `remote dispatch` for the Coordinator chat, and `list`, `details` and the task commands (which only say "Done") for the Board screen, with made-up tasks. -- `demo-session` and `mkrepo.sh` fill the shell pane that the Terminal screenshot shows. - -To take the shots on an emulator (Pixel 7 size, API 35, started with `-writable-system`): - -```sh -adb root && adb remount # once, then reboot the emulator -adb root && adb remount -adb shell "echo '10.0.2.2 devbox' >> /system/etc/hosts" -adb shell iptables -t nat -A OUTPUT -p tcp -d 10.0.2.2 --dport 22 -j DNAT --to-destination 10.0.2.2:2201 -adb shell settings put global sysui_demo_allowed 1 -adb shell am broadcast -a com.android.systemui.demo -e command enter -adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 0941 -``` - -A rebuilt image has new host keys, which the app refuses for machines it already pinned: add them again. Make a key in the app, save its public key to a file, run `./up.sh that-file`, and add `demo@devbox`, then `demo@gpu-box` and `demo@ci-runner` through devbox. Screens were captured with `adb exec-out screencap -p` (light and dark: `adb shell cmd uimode night yes|no`) and converted with `magick in.png -strip -resize 540x -quality 82 out.webp`. - -`docker rm -f herdr-devbox herdr-gpu-box herdr-ci-runner && docker network rm herdr-demo-net` removes it all. diff --git a/docs/screenshots/demo-host/content.py b/docs/screenshots/demo-host/content.py deleted file mode 100755 index c2609615..00000000 --- a/docs/screenshots/demo-host/content.py +++ /dev/null @@ -1,237 +0,0 @@ -#!/usr/bin/env python3 -"""Writes the demo transcripts and agent screens into the demo user's home (run inside a demo host).""" -import json, os, sys, uuid -from datetime import datetime, timedelta, timezone - -HOME = os.path.expanduser("~") -role = sys.argv[1] - - -def w(path, text, mode=0o644): - path = os.path.join(HOME, path) - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w") as f: - f.write(text) - os.chmod(path, mode) - return path - - -def jsonl(path, rows): - return w(path, "".join(json.dumps(r) + "\n" for r in rows)) - - -# The demo's clock: the app's previews look at it on 17 October 2026, 07:42 UTC (Demo.NOW). -def at(text): - return datetime.fromisoformat(text).replace(tzinfo=timezone.utc) - - -def iso(t): - return t.strftime("%Y-%m-%dT%H:%M:%S.") + f"{t.microsecond // 1000:03d}Z" - - -# ---------- Claude Code transcripts ---------- -class Claude: - """Rows of a Claude Code session; each row `after` seconds past the one before, from `start`.""" - - def __init__(self, start, cwd, context=(0, 0)): - self.rows = [] - self.n = 0 - self.t = at(start) - self.cwd = cwd - # The context each answer filled: the first answer's, then more by a step each (the model pill reads the last). - self.context, self.step = context - - def _id(self): - self.n += 1 - return f"00000000-0000-4000-8000-{self.n:012d}" - - def _row(self, row, after): - self.t += timedelta(seconds=after) - self.rows.append({**row, "timestamp": iso(self.t), "cwd": self.cwd}) - - def user(self, text, after=0): - self._row({"type": "user", "uuid": self._id(), "message": {"role": "user", "content": text}}, after) - - def _answer(self, content): - """An answer as Claude Code records it: the model, what the request filled, the effort it ran at.""" - used = self.context - self.context += self.step - usage = {"input_tokens": 4, "cache_creation_input_tokens": 1200, "cache_read_input_tokens": used - 1204, "output_tokens": 310} - return {"type": "assistant", "uuid": self._id(), "effort": "high", "message": {"role": "assistant", "model": "claude-opus-5-5", "content": content, "usage": usage}} - - def say(self, text, after=0): - self._row(self._answer([{"type": "text", "text": text}]), after) - - def tool(self, name, inp, result=None, error=False, after=0, took=0.2): - tid = "toolu_" + self._id()[-8:] - self._row(self._answer([{"type": "tool_use", "id": tid, "name": name, "input": inp}]), after) - if result is not None: - self._row({"type": "user", "uuid": self._id(), "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": tid, "content": result, "is_error": error}]}}, took) - - -def claude_app(): - # The first ask took 2m 36s; the second came 38 minutes later and is still at work at 07:42. - # Its context ends at 124k of Opus's 200k: "Opus 5.5 · 62%". - c = Claude("2026-10-17T06:58:10", HOME + "/src/herdr-android", context=(106_000, 2_000)) - p = HOME + "/src/herdr-android/app/src/main/kotlin/com/makefunstuff/herdr/ui/machines/MachinesScreen.kt" - c.user("The Machines list only refreshes when the app is reopened. Add pull-to-refresh that reconnects every machine, and keep the last known state on screen while it does.") - c.say("I'll look at how the Machines screen gets its state first.", after=6) - c.tool("Read", {"file_path": p}, "156 lines", after=2) - c.tool("Grep", {"pattern": "fun retry", "path": HOME + "/src/herdr-android/app"}, "app/src/main/kotlin/com/makefunstuff/herdr/data/HostManager.kt:62: fun retryAll()", after=4.8) - c.say("`HostManager.retryAll()` already reconnects every session, so the screen only needs a `PullToRefreshBox` around the list. Adding it now.", after=19.8) - c.tool("Edit", {"file_path": p, "old_string": "LazyColumn(", "new_string": "PullToRefreshBox(isRefreshing = refreshing, onRefresh = { hosts.retryAll() }) {\n LazyColumn("}, "The file has been updated.", after=39.8) - c.tool("Bash", {"command": "./gradlew :app:testDebugUnitTest", "description": "Run the app's unit tests"}, "BUILD SUCCESSFUL in 38s\n27 actionable tasks: 6 executed, 21 up-to-date", after=4.8, took=38.4) - c.say(after=40, text= - "Pull-to-refresh is in.\n\n" - "**What changed**\n" - "- `MachinesScreen` wraps the list in a `PullToRefreshBox` that calls `retryAll()`.\n" - "- The spinner stays until every machine has answered or failed, so a slow host doesn't end it early.\n" - "- Cards keep the last snapshot and show *Showing the last known state* while they reconnect.\n\n" - "| Check | Result |\n|---|---|\n| Unit tests | 44 passed |\n| Emulator, 3 machines | refreshes in ~1 s |" - ) - c.user("Nice. Also show when each machine last answered, like \"2 min ago\".", after=38 * 60 + 16) - c.say("Sure — `HostState` has no timestamp yet, so I'll add one where a snapshot arrives.", after=7) - c.tool("Read", {"file_path": HOME + "/src/herdr-android/core/src/main/kotlin/com/makefunstuff/herdr/core/host/HostSession.kt"}, "320 lines", after=3) - c.tool("Edit", {"file_path": HOME + "/src/herdr-android/core/src/main/kotlin/com/makefunstuff/herdr/core/host/HostSession.kt", "old_string": "val snapshot", "new_string": "val snapshot: Snapshot?,\n val updatedAt: Long?"}, after=29.8) - return c.rows - - -def claude_api(): - c = Claude("2026-10-17T07:31:05", HOME + "/src/api-server", context=(21_000, 3_000)) - c.user("The /jobs endpoint takes 4 s on the big tenant. Find out why and fix it.") - c.tool("Bash", {"command": "psql -c 'EXPLAIN ANALYZE SELECT * FROM jobs ORDER BY created_at DESC LIMIT 50'", "description": "Explain the slow jobs query"}, "Seq Scan on jobs (cost=0.00..48211.20 rows=1201480)\nExecution Time: 3912.448 ms", after=9, took=4.1) - c.say("It's a sequential scan over 1.2 M rows: `jobs` has no index on `created_at`. I wrote a migration that adds one concurrently, so the table stays writable.", after=21) - c.tool("Write", {"file_path": HOME + "/src/api-server/migrations/0042_jobs_created_at.sql", "content": "CREATE INDEX CONCURRENTLY jobs_created_at_idx ON jobs (created_at DESC);"}, "File created successfully", after=12) - c.tool("Bash", {"command": "npm run db:migrate", "description": "Apply the new migration"}, after=6) - return c.rows - - -def codex_rows(start): - """A codex rollout's completed items, each `after` seconds past the one before, from `start`.""" - rows = [] - clock = [at(start)] - def item(i, after=0): - clock[0] += timedelta(seconds=after) - rows.append({"timestamp": iso(clock[0]), "type": "event_msg", "payload": {"type": "item_completed", "item": i}}) - return rows, item - - -def codex_site(): - rows, item = codex_rows("2026-10-16T09:12:40") - item({"type": "UserMessage", "id": "u1", "content": [{"type": "text", "text": "Rewrite the install page so it starts with the one-line installer, and check every link on it."}]}) - item({"type": "AgentMessage", "id": "a1", "content": [{"type": "text", "text": "Reading the current page and the link checker config."}]}, after=5) - item({"type": "CommandExecution", "id": "c1", "command": ["bash", "-lc", "rg -n 'install' content/docs/install.md | head"], "aggregated_output": "3:## Install\n12:curl -fsSL https://example.com/install.sh | sh", "exit_code": 0, "status": "completed"}, after=3) - item({"type": "FileChange", "id": "f1", "changes": {"content/docs/install.md": {"type": "update", "unified_diff": "@@ -1,4 +1,9 @@\n # Install\n-Download the archive for your platform and unpack it.\n+\n+```sh\n+curl -fsSL https://example.com/install.sh | sh\n+```\n \n-## Install\n+## Manual install\n+\n+## Updating\n"}}, "status": "completed"}, after=48) - item({"type": "CommandExecution", "id": "c2", "command": ["bash", "-lc", "npm run check-links"], "aggregated_output": "Checked 38 links, 0 broken", "exit_code": 0, "status": "completed"}, after=21) - item({"type": "AgentMessage", "id": "a2", "content": [{"type": "text", "text": "Done. The page now opens with the installer one-liner, then *Manual install* and *Updating*. All 38 links resolve."}]}, after=12) - # The model it ran at, and what its last request filled of the window Codex states. - rows.insert(0, {"timestamp": rows[0]["timestamp"], "type": "turn_context", "payload": {"cwd": HOME + "/src/website", "model": "gpt-5.5", "effort": "medium"}}) - rows.append({"timestamp": rows[-1]["timestamp"], "type": "event_msg", "payload": {"type": "token_count", "info": { - "last_token_usage": {"input_tokens": 51_000, "cached_input_tokens": 44_800, "output_tokens": 800, "total_tokens": 51_800}, - "model_context_window": 258_400}}}) - return rows - - -def codex_eval(): - rows, item = codex_rows("2026-10-17T07:05:31") - item({"type": "UserMessage", "id": "u1", "content": [{"type": "text", "text": "Run the long-context eval on the new checkpoint and compare it with last week's."}]}) - item({"type": "AgentMessage", "id": "a1", "content": [{"type": "text", "text": "Starting the 128k suite on both GPUs. It takes about 40 minutes; I'll compare against `ckpt-0605` when it finishes."}]}, after=8) - item({"type": "CommandExecution", "id": "c1", "command": ["bash", "-lc", "nvidia-smi --query-gpu=name,memory.used --format=csv"], "aggregated_output": "name, memory.used\nGPU 0, 61 GiB\nGPU 1, 58 GiB", "exit_code": 0, "status": "completed"}, after=4) - return rows - - - -SCREEN_CLAUDE_APP = """\033[38;5;174m╭───────────────────────────────────────────────╮\033[0m -\033[38;5;174m│\033[0m \033[38;5;174m✻\033[0m Welcome to \033[1mClaude Code\033[0m! \033[38;5;174m│\033[0m -\033[38;5;174m│\033[0m \033[38;5;174m│\033[0m -\033[38;5;174m│\033[0m \033[2mcwd: ~/src/herdr-android\033[0m \033[38;5;174m│\033[0m -\033[38;5;174m╰───────────────────────────────────────────────╯\033[0m - -\033[2m>\033[0m The Machines list only refreshes when the app is - reopened. Add pull-to-refresh that reconnects every - machine, and keep the last known state on screen. - -\033[32m●\033[0m \033[1mRead\033[0m(app/…/machines/MachinesScreen.kt) - ⎿ Read \033[1m156\033[0m lines - -\033[32m●\033[0m \033[1mUpdate\033[0m(app/…/machines/MachinesScreen.kt) - ⎿ Updated with \033[1m14\033[0m additions and \033[1m3\033[0m removals - -\033[32m●\033[0m \033[1mBash\033[0m(./gradlew :app:testDebugUnitTest) - ⎿ BUILD SUCCESSFUL in 38s - -\033[32m●\033[0m Pull-to-refresh is in. The spinner stays until - every machine has answered or failed. - -\033[2m>\033[0m Nice. Also show when each machine last answered. - -\033[32m●\033[0m \033[1mUpdate\033[0m(core/…/host/HostSession.kt) - -\033[38;5;174m✻ Adding a timestamp… \033[2m(12s · esc to interrupt)\033[0m - -\033[2m╭───────────────────────────────────────────────╮\033[0m -\033[2m│\033[0m > \033[2m│\033[0m -\033[2m╰───────────────────────────────────────────────╯\033[0m - \033[2m⏵⏵ accept edits on\033[0m -""" - -SCREEN_CLAUDE_API = """\033[32m●\033[0m \033[1mWrite\033[0m(migrations/0042_jobs_created_at.sql) - ⎿ Wrote 1 line - -──────────────────────────────────────────────────────── - Bash command - npm run db:migrate - Apply the new migration -╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ - Do you want to proceed? - ❯ 1. Yes - 2. Yes, and don't ask again for npm run commands - 3. No, and tell Claude what to do differently - - Esc to cancel · Tab to amend -""" - -SCREEN_CODEX = """\033[1m>_ OpenAI Codex\033[0m \033[2m~/src/website\033[0m - -\033[1muser\033[0m -Rewrite the install page so it starts with the one-line -installer, and check every link on it. - -\033[32m✓\033[0m rg -n 'install' content/docs/install.md -\033[32m✓\033[0m Edited content/docs/install.md -\033[32m✓\033[0m npm run check-links \033[2m38 links, 0 broken\033[0m - -\033[1mcodex\033[0m -Done. The page now opens with the installer one-liner. - -\033[2m› Ask Codex to do anything\033[0m -""" - -SCREEN_CODEX_EVAL = """\033[1m>_ OpenAI Codex\033[0m \033[2m~/llm-eval\033[0m - -Run the long-context eval on the new checkpoint and -compare it with last week's. - -Starting the 128k suite on both GPUs. -\033[33m$ python eval.py --suite long-context --ckpt ckpt-0612\033[0m - suite long-context ████████████░░░░░░░░ 61% (734/1200) - -• Working \033[2m(2m 14s • esc to interrupt)\033[0m - -\033[2m› Ask Codex to do anything\033[0m -""" - -if role == "devbox": - s1 = "3f6c2a1e-8b7d-4c2a-9e51-0a1b2c3d4e5f" - jsonl(f".claude/projects/-home-demo-src-herdr-android/{s1}.jsonl", claude_app()) - s2 = "9d2e4b7a-1c3f-4e6a-8b90-5f4e3d2c1b0a" - jsonl(f".claude/projects/-home-demo-src-api-server/{s2}.jsonl", claude_api()) - s3 = "0199a3b2-7c41-7e10-9a2b-3c4d5e6f7a8b" - jsonl(f".codex/sessions/2026/10/16/rollout-2026-10-16T09-12-40-{s3}.jsonl", codex_site()) - w(".demo/screen-claude-app", SCREEN_CLAUDE_APP) - w(".demo/screen-claude-api", SCREEN_CLAUDE_API) - w(".demo/screen-codex", SCREEN_CODEX) -elif role == "gpu-box": - s4 = "0199a3c4-1d2e-7f30-8a4b-5c6d7e8f9a0b" - jsonl(f".codex/sessions/2026/10/16/rollout-2026-10-16T08-02-11-{s4}.jsonl", codex_eval()) - w(".demo/screen-codex-eval", SCREEN_CODEX_EVAL) diff --git a/docs/screenshots/demo-host/demo-session b/docs/screenshots/demo-host/demo-session deleted file mode 100755 index a5459c2c..00000000 --- a/docs/screenshots/demo-host/demo-session +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash -P=$'\e[1;32mdemo@devbox\e[0m:\e[1;34m~/src/herdr-android\e[0m$ ' -printf '%s%s\n' "$P" "git log --oneline --graph -7" -git --no-pager -c color.ui=always log --oneline --graph --decorate -7 -printf '%s%s\n' "$P" "git status -sb" -git --no-pager -c color.ui=always status -sb -printf '%s%s\n' "$P" "./gradlew -p core test" -G=$'\e[32m'; R=$'\e[0m'; B=$'\e[1m' -cat <<OUT -> Task :compileKotlin UP-TO-DATE -> Task :test - -BoardTest > coordinatorChatMerges ${G}PASSED${R} -ChatTest > claudeTranscriptReadsTools ${G}PASSED${R} -ChatTest > blockedPromptParsesMenu ${G}PASSED${R} -PainterTest > drawsWideCharacters ${G}PASSED${R} -TranscriptFollowerCancelTest > closes ${G}PASSED${R} - -${B}${G}BUILD SUCCESSFUL${R} in 21s -4 actionable tasks: 1 executed, 3 up-to-date -OUT -exec bash -i diff --git a/docs/screenshots/demo-host/herdr-projects b/docs/screenshots/demo-host/herdr-projects deleted file mode 100755 index 15b3699e..00000000 --- a/docs/screenshots/demo-host/herdr-projects +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# Demo stand-in for the work board, for screenshots only: the `remote` answers the Coordinator -# chat reads, and `list` / `details` / task commands for the Board screen. Made-up tasks. -import base64, json, os, sys -store = os.path.expanduser("~/.demo/talk.json") -talk = [ - {"you": True, "text": "The Android app needs screenshots in its README, and the jobs endpoint is slow on the big tenant."}, - {"you": False, "text": "Added two tasks:\n\n- **AND-6** Add app screenshots to the README\n- **API-3** Speed up `/jobs` on large tables\n\nAPI-3 has an agent on **devbox** now. AND-6 waits for the chat view (AND-2) to land."}, - {"you": True, "text": "AND-2 is merged, start AND-6 too."}, - {"you": False, "text": "Started an agent on AND-6 in `herdr-android`. It runs the app against a demo host, so no real hostnames end up in the images."}, -] -if os.path.exists(store): - talk = json.load(open(store)) - -GH = "https://github.com/demo" -TASKS = [ - ("AND-9", {"id": "herdr-android/9", "title": "Show when each machine last answered", "state": "blocked", "stage": "approval", "kind": "permission", - "app": "claude", "msg": "Review passed. Ready to merge.", "pr": GH + "/herdr-android/pull/9", "reviewRound": 1, "review": {"pass": True}}), - ("API-3", {"id": "api-server/3", "title": "Speed up /jobs on large tables", "state": "blocked", "stage": "working", "kind": "question", - "app": "claude", "msg": "Asks whether the migration may lock the jobs table for a few seconds"}), - ("WEB-4", {"id": "website/4", "title": "Rewrite the install page", "state": "blocked", "stage": "approval", "kind": "permission", - "app": "codex", "msg": "Review found 2 problems", "pr": GH + "/website/pull/4", "reviewRound": 1, "review": {"pass": False}}), - ("AND-6", {"id": "herdr-android/6", "title": "Add app screenshots to the README", "state": "working", "stage": "working", - "app": "claude", "msg": "Taking the Coordinator screenshot", "progress": 3, "eta": 12}), - ("EVAL-2", {"id": "llm-eval/2", "title": "Long-context eval on ckpt-0612", "state": "working", "stage": "working", "research": True, - "app": "codex", "msg": "Scoring the 64k needle set", "progress": 2, "eta": 40}), - ("API-5", {"id": "api-server/5", "title": "Rate-limit the export endpoint per tenant", "state": "working", "stage": "reviewing", - "app": "claude", "msg": "Reviewer is running the load test", "pr": GH + "/api-server/pull/5", "reviewRound": 1}), - ("WEB-5", {"id": "website/5", "title": "Dark mode for the docs", "state": "idle", "stage": "queued", "app": "", "msg": "Waits for WEB-4"}), - ("AND-10", {"id": "herdr-android/10", "title": "Haptics when an agent needs you", "state": "idle", "stage": "", "app": "", "msg": ""}), - ("AND-2", {"id": "herdr-android/2", "title": "Replace agent terminals with native chat views", "state": "done", "stage": "done", - "app": "claude", "msg": "Merged", "pr": GH + "/herdr-android/pull/3"}), -] -DETAILS = { - "herdr-android/9": { - "task": {"id": "AND-9", "title": "Show when each machine last answered", "stage": "approval", - "msg": "Review passed. Ready to merge.", - "description": "Machines that stop answering look the same as quiet ones. Show **when each machine last answered**, like \"2 min ago\", on its card and on the machine's screen.\n\n- Keep the last snapshot while reconnecting\n- Say *Showing the last known state* when it is old"}, - "failure": None, - "reviews": [{"round": 1, "by": "codex", "meets": True, "pass": True, - "text": "Meets the task. The time updates every minute and survives reconnects.", - "findings": [{"id": "F1", "severity": "trivial", "subject": "\"0 min ago\" shows for a few seconds after connecting", - "where": "app/…/MachinesScreen.kt", "proof": "Say \"just now\" under a minute."}], - "checks": ["./gradlew -p core test: 61 passed", "Emulator: three machines, one stopped"], "limitations": []}], - "agent": "claude", "handovers": [], "research": False, "plan": "", - "pr": GH + "/herdr-android/pull/9", "branch": "work/9-last-answered", "worktree": "~/src/herdr-android-9", - "limits": {"time": {"limit": "2h", "used": "38m"}, "tokens": {"limit": "4M", "used": 1180000}}, - }, - "api-server/3": { - "task": {"id": "API-3", "title": "Speed up /jobs on large tables", "stage": "working", - "msg": "Asks whether the migration may lock the jobs table for a few seconds", - "description": "`GET /jobs` takes 4 s on the big tenant. Find out why and fix it without downtime."}, - "failure": None, "reviews": [], "agent": "claude", "handovers": [], "research": False, "plan": "", - "pr": "", "branch": "work/3-jobs-index", "worktree": "~/src/api-server-3", - "limits": {"time": {"limit": "1h", "used": "21m"}}, - }, - "website/4": { - "task": {"id": "WEB-4", "title": "Rewrite the install page", "stage": "approval", "msg": "Review found 2 problems", - "description": "The install page still describes the old tarball. Rewrite it around the one-line installer."}, - "failure": None, - "reviews": [{"round": 1, "by": "claude", "meets": False, "pass": False, - "text": "Close, but two things are wrong.", - "findings": [{"id": "F1", "severity": "major", "subject": "The macOS command installs the Linux binary", - "where": "content/install.md, line 14", "proof": "`uname -s` is never checked."}, - {"id": "F2", "severity": "minor", "subject": "The checksum link is a 404", - "where": "content/install.md, line 31", "proof": "GET /releases/sha256.txt → 404"}], - "checks": ["npm run build", "Link check: 1 broken"], "limitations": []}], - "agent": "codex", "handovers": [], "research": False, "plan": "", - "pr": GH + "/website/pull/4", "branch": "work/4-install-page", "worktree": "~/src/website-4", "limits": {}, - }, -} - -def find(tid): - for k, t in TASKS: - if t["id"] == tid or k == tid: - return k, t - return None, None - -def details(tid): - k, t = find(tid) - if t is None: - print(f"No task {tid}"); sys.exit(1) - d = DETAILS.get(t["id"]) or { - "task": {"id": k, "title": t["title"], "stage": t.get("stage", ""), "msg": t.get("msg", ""), "description": ""}, - "failure": None, "reviews": [], "agent": t.get("app", ""), "handovers": [], "research": t.get("research", False), - "plan": "", "pr": t.get("pr", ""), "branch": "", "worktree": "", "limits": {}, - } - print(json.dumps(d, indent=2)) - -args = sys.argv[1:] -if args[:2] == ["remote", "list"]: - print(json.dumps({"host": "devbox", "version": "0.9.0", "tasks": [], "coordinator": {"state": "idle", "queue": 0, "readable": True, "said": talk[-1]["text"], "sent": "", "talk": talk}})) -elif args[:2] == ["remote", "dispatch"]: - req = json.loads(base64.b64decode(args[2])) - talk.append({"you": True, "text": req["text"]}) - json.dump(talk, open(store, "w")) - print(json.dumps({"ok": True, "msg": "Sent to the Coordinator"})) -elif args[:1] == ["list"]: - shown = [] if "--archived" in args else TASKS - if "--query" in args: - q = args[args.index("--query") + 1].lower() - shown = [(k, t) for k, t in shown if q in t["title"].lower() or q in k.lower()] - if "--json" in args: - print(json.dumps([t for _, t in shown], indent=2)) - else: - for k, t in shown: - print(f"{k} {t['title']}") -elif args[:1] == ["details"] and len(args) > 1: - details(args[1]) -elif args and args[0] in ("delegate", "tell", "stop", "retry", "fix", "review", "finish", "approve", "pick", "archive", "restore", "clear", "pin", "add", "edit"): - print("Done (demo board)") -else: - print(json.dumps({"ok": False, "msg": "demo board"})) diff --git a/docs/screenshots/demo-host/mkrepo.sh b/docs/screenshots/demo-host/mkrepo.sh deleted file mode 100755 index b070b0ed..00000000 --- a/docs/screenshots/demo-host/mkrepo.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e -cd ~/src/herdr-android; git init -q -b main; git config user.name Demo; git config user.email demo@example.com -msgs=("Initial Compose client" "Machines list with live status" "Terminal: fit the phone by default" "Native Coordinator chat" "Replace the app icon with the herdr icon" "Installable releases that keep the SSH key" "Replace agent terminals with native chat views") -i=0; for m in "${msgs[@]}"; do i=$((i+1)); echo $i > f$i; git add .; GIT_AUTHOR_DATE="2026-10-0${i}T10:00:00" GIT_COMMITTER_DATE="2026-10-0${i}T10:00:00" git commit -qm "$m"; done -git update-index --assume-unchanged demo-session -git checkout -qb pull-to-refresh -mkdir -p app core; echo x > app/MachinesScreen.kt; echo y > core/HostSession.kt; git add -N app core diff --git a/docs/screenshots/demo-host/setup.sh b/docs/screenshots/demo-host/setup.sh deleted file mode 100755 index 75160911..00000000 --- a/docs/screenshots/demo-host/setup.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash -# Runs as demo inside a demo container: starts herdr and lays out workspaces with scripted agents. -set -e -role=$1 -cd ~ -mkdir -p ~/.local/bin -# Stand-ins named like the agents, so herdr detects them; they just draw a screen. -for a in claude codex; do printf '#!/bin/bash\nclear; printf "\\033[?25l"; cat ~/.demo/$DEMO_SCREEN\nwhile :; do sleep 3600; done\n' > ~/.local/bin/$a; chmod +x ~/.local/bin/$a; done -python3 ~/.demo/content.py $role -cat > ~/.bashrc <<'RC' -PS1='\[\e[1;32m\]demo@\h\[\e[0m\]:\[\e[1;34m\]\w\[\e[0m\]$ ' -alias ls='ls --color=auto' -export PATH=$HOME/.local/bin:$PATH -RC -echo '[ -f ~/.bashrc ] && . ~/.bashrc' > ~/.bash_profile -pgrep -u demo -f "herdr server" >/dev/null || { SHELL=/bin/bash nohup herdr server > /tmp/herdr.log 2>&1 & sleep 2; } -H=herdr -ws() { $H workspace create --no-focus --cwd "$1" --label "$2" | python3 -c 'import json,sys; r=json.load(sys.stdin)["result"]; print(r["root_pane"]["pane_id"])'; } -split() { $H pane split "$1" --direction "${2:-right}" | python3 -c 'import json,sys; r=json.load(sys.stdin)["result"]; print(r["pane"]["pane_id"] if "pane" in r else r)'; } -now() { date +%s%N; } -agent() { # pane agent state session-id session-path title screen - local pane=$1 a=$2 st=$3 sid=$4 sp=$5 title=$6 screen=$7 - $H pane run "$pane" "DEMO_SCREEN=$screen $a" >/dev/null - sleep 0.5 - $H pane report-agent-session --source herdr:$a --agent $a --seq $(now) --agent-session-id "$sid" ${sp:+--agent-session-path "$sp"} "$pane" >/dev/null - $H pane report-agent --source herdr:$a --agent $a --state $st --seq $(now) "$pane" >/dev/null - [ -n "$title" ] && $H pane report-metadata "$pane" --title "$title" --source herdr:$a >/dev/null || true -} -case $role in -devbox) - mkdir -p src/herdr-android src/api-server src/website - p=$(ws ~/src/herdr-android herdr-android) - agent $p claude working 3f6c2a1e-8b7d-4c2a-9e51-0a1b2c3d4e5f "" "Show when each machine last answered" screen-claude-app - q=$(split $p right) - $H pane rename "$q" shell >/dev/null - $H pane run "$q" "cd ~/src/herdr-android && clear && ./demo-session" >/dev/null - p=$(ws ~/src/api-server api-server) - agent $p claude blocked 9d2e4b7a-1c3f-4e6a-8b90-5f4e3d2c1b0a "" "Speed up /jobs on large tables" screen-claude-api - p=$(ws ~/src/website website) - agent $p codex working 0199a3b2-7c41-7e10-9a2b-3c4d5e6f7a8b "" "Rewrite the install page" screen-codex - sleep 1 - $H pane report-agent --source herdr:codex --agent codex --state idle --seq $(now) "$p" >/dev/null - ;; -gpu-box) - mkdir -p llm-eval notebooks - p=$(ws ~/llm-eval llm-eval) - agent $p codex working 0199a3c4-1d2e-7f30-8a4b-5c6d7e8f9a0b "" "Long-context eval on ckpt-0612" screen-codex-eval - ws ~/notebooks notebooks >/dev/null - ;; -ci-runner) - mkdir -p runners deploy - ws ~/runners runners >/dev/null - ws ~/deploy deploy >/dev/null - ;; -esac -$H agent list diff --git a/docs/screenshots/demo-host/up.sh b/docs/screenshots/demo-host/up.sh deleted file mode 100755 index 5323fd77..00000000 --- a/docs/screenshots/demo-host/up.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -# Recreates the three demo hosts. $1: optional authorized_keys file. -D=$(cd "$(dirname "$0")" && pwd) -# The image takes this machine's herdr binary (static, x86_64). -ctx=$(mktemp -d); cp "$D/Dockerfile" "$ctx/"; cp "$(command -v herdr)" "$ctx/herdr" -docker build -q -t herdr-demo "$ctx" >/dev/null && rm -rf "$ctx" -docker network inspect herdr-demo-net >/dev/null 2>&1 || docker network create herdr-demo-net >/dev/null -for h in devbox gpu-box ci-runner; do - docker rm -f herdr-$h >/dev/null 2>&1 - extra=""; [ $h = devbox ] && extra="-p 127.0.0.1:2201:22" - docker run -d --name herdr-$h --hostname $h --network herdr-demo-net --network-alias $h $extra herdr-demo >/dev/null - docker exec herdr-$h mkdir -p /home/demo/.demo /home/demo/.ssh - docker cp $D/content.py herdr-$h:/home/demo/.demo/content.py; docker cp $D/setup.sh herdr-$h:/home/demo/.demo/setup.sh - [ -n "$1" ] && docker cp "$1" herdr-$h:/home/demo/.ssh/authorized_keys -done -docker exec herdr-devbox mkdir -p /home/demo/.herdr-projects/data/bin /home/demo/src/herdr-android -docker cp $D/herdr-projects herdr-devbox:/home/demo/.herdr-projects/data/bin/herdr-projects -docker cp $D/mkrepo.sh herdr-devbox:/home/demo/.demo/; docker cp $D/demo-session herdr-devbox:/home/demo/src/herdr-android/demo-session -for h in devbox gpu-box ci-runner; do - docker exec herdr-$h sh -c 'chown -R demo:demo /home/demo; chmod 700 /home/demo/.ssh; chmod 600 /home/demo/.ssh/authorized_keys 2>/dev/null; true' -done -docker exec -u demo -w /home/demo herdr-devbox bash /home/demo/.demo/mkrepo.sh -for h in devbox gpu-box ci-runner; do docker exec -u demo -w /home/demo -e HOME=/home/demo herdr-$h bash /home/demo/.demo/setup.sh $h 2>&1 | grep -v '^{"id":"cli:agent:list"'; done diff --git a/docs/screenshots/doc.webp b/docs/screenshots/doc.webp new file mode 100644 index 00000000..62d7f78a Binary files /dev/null and b/docs/screenshots/doc.webp differ diff --git a/docs/screenshots/machine.webp b/docs/screenshots/machine.webp index 3c35c247..2ee4e562 100644 Binary files a/docs/screenshots/machine.webp and b/docs/screenshots/machine.webp differ diff --git a/docs/screenshots/machines.webp b/docs/screenshots/machines.webp deleted file mode 100644 index 1741eec1..00000000 Binary files a/docs/screenshots/machines.webp and /dev/null differ diff --git a/docs/screenshots/pull-request.webp b/docs/screenshots/pull-request.webp new file mode 100644 index 00000000..c3524103 Binary files /dev/null and b/docs/screenshots/pull-request.webp differ diff --git a/docs/screenshots/settings.webp b/docs/screenshots/settings.webp deleted file mode 100644 index 2d1ef09e..00000000 Binary files a/docs/screenshots/settings.webp and /dev/null differ diff --git a/docs/screenshots/spaces.webp b/docs/screenshots/spaces.webp new file mode 100644 index 00000000..41ae60e3 Binary files /dev/null and b/docs/screenshots/spaces.webp differ diff --git a/docs/screenshots/task.webp b/docs/screenshots/task.webp index 8b90f50c..edebabe8 100644 Binary files a/docs/screenshots/task.webp and b/docs/screenshots/task.webp differ diff --git a/docs/screenshots/tasks-light.webp b/docs/screenshots/tasks-light.webp new file mode 100644 index 00000000..d9ba9d4c Binary files /dev/null and b/docs/screenshots/tasks-light.webp differ diff --git a/docs/screenshots/tasks.webp b/docs/screenshots/tasks.webp new file mode 100644 index 00000000..47a17a1c Binary files /dev/null and b/docs/screenshots/tasks.webp differ diff --git a/docs/screenshots/terminal.webp b/docs/screenshots/terminal.webp index 14ee5c92..c33ef7b2 100644 Binary files a/docs/screenshots/terminal.webp and b/docs/screenshots/terminal.webp differ diff --git a/host/herdr-usage/README.md b/host/herdr-usage/README.md index d03d5b3e..15928d7b 100644 --- a/host/herdr-usage/README.md +++ b/host/herdr-usage/README.md @@ -39,7 +39,7 @@ Every provider card has a 7-day chart (tokens a day, and their cost where it is | pi, omp | `~/.pi/agent/sessions/`, `~/.omp/agent/sessions/` | each reply's `usage`, with its provider and cost | | opencode | `~/.local/share/opencode/opencode.db` (read only) | each assistant message's tokens and cost | -Tokens are input, output and cache writes; cache reads are left out (they would drown everything else, at a tenth of the price). A session belongs to the board task whose worktree it ran in (`~/.herdr/worktrees/<repo>/work-<n>-…` is `<repo>/<n>`, its title from the board's record `~/.herdr-projects/data/tasks/<repo>/<n>/task.json`), or to the Coordinator (`~/.herdr-projects/data/coordinator`). An agent is its harness and model (`claude · claude-opus-5-5`). +Tokens are input, output and cache writes; cache reads are left out (they would drown everything else, at a tenth of the price). A session belongs to the board task whose worktree it ran in (`~/.herdr/worktrees/<repo>/work-<n>-…` is `<repo>/<n>`, its title from the board's record `~/.herdr-projects/data/tasks/<repo>/<n>/task.json`), or to the Coordinator (`~/.herdr-projects/data/coordinator`). That is herdr-projects 1.x's layout, which herdr-usage still reads; the tasks of Shepherdr 3 and simple-herdr-projects (worktrees `task-<n>-…`) and their Assistants aren't told apart yet, so their sessions count under the agent only. An agent is its harness and model (`claude · claude-opus-5-5`). The days are the machine's. diff --git a/tools/ui-preview/README.md b/tools/ui-preview/README.md index ebf01006..7a63d5ac 100644 --- a/tools/ui-preview/README.md +++ b/tools/ui-preview/README.md @@ -4,8 +4,8 @@ Renders the app's real screens to PNGs on the desktop JVM, in dark and light, wi ```sh ./gradlew -p tools/ui-preview render # every scene → tools/ui-preview/build/shots/ -./gradlew -p tools/ui-preview render -Ponly=board,chat # some scenes (both themes) -./gradlew -p tools/ui-preview render -Ponly=board-light # one shot +./gradlew -p tools/ui-preview render -Ponly=agents,chat # some scenes (both themes) +./gradlew -p tools/ui-preview render -Ponly=agents-light # one shot ./gradlew -p tools/ui-preview render -Pout=/tmp/shots # somewhere else ./gradlew -p tools/ui-preview render -Pshard=2/4 # every 4th scene from the 2nd, as CI does ``` @@ -31,13 +31,14 @@ In CI the Android workflow's four `render (k/4)` jobs each render a quarter of t - `MainActivity.kt` is left out; the scenes render `HerdrRoot` (`AppNav.kt`), as the activity does. - `share/ShareActivity.kt`, `share/CoordinatorTile.kt` and `share/Widgets.kt` (the share target, the Quick Settings tile and the home-screen widgets) are left out: what they open, the `share` and `ask` scenes show. The widgets are Android `RemoteViews`, which Compose on the desktop can't draw. `share/AndroidShortcuts.kt` (the launcher's shortcuts, through ShortcutManager) is left out too: the container stand-in gives `NoShortcuts`, and the `start` and `search` scenes show what they open. - `brief/AndroidBrief.kt` and `brief/AndroidVoice.kt` (the brief's notification and alarm, and text to speech) are left out: the container stand-in gives `PreviewBrief` (nothing posted, not in a car) and `PreviewVoice` (it keeps what would be read, and where, without a sound). The brief runs at the demo's fixed time (`Demo.NOW`, UTC), and devbox and gpu-box answer `herdr-push journal` with `Demo.devboxJournal()` and `Demo.gpuJournal()`. - - `guard/AndroidUnlock.kt` and `guard/ConfirmActivity.kt` (androidx.biometric's prompt, and the window a guarded notification button opens) are left out: the container stand-in gives `PreviewUnlock`, which has a screen lock unless a scene says `screenLock = false` and answers yes at once (`unlockAnswer = null` leaves it up, for the `app-lock` scene). Android's prompt is a system window the harness can't draw; a phone without a screen lock shows the app's own confirmation (`prompt-confirm`, `review-merge-confirm`). + - `guard/AndroidUnlock.kt` and `guard/ConfirmActivity.kt` (androidx.biometric's prompt, and the window a guarded notification button opens) are left out: the container stand-in gives `PreviewUnlock`, which has a screen lock unless a scene says `screenLock = false` and answers yes at once (`unlockAnswer = null` leaves it up, for the `app-lock` scene). Android's prompt is a system window the harness can't draw; a phone without a screen lock shows the app's own confirmation (`prompt-confirm`). - `bubble/AndroidBubbles.kt` and `bubble/BubbleActivity.kt` (a chat floating as an Android bubble: its notification, shortcut and window) are left out: the container stand-in gives `PreviewBubbles` (bubbles allowed unless a scene says `bubblesAllowed = false`, nothing floats), and a scene with `bubble` renders the bubble's window (`BubbleRoot`) instead of the whole app. - `src/main/kotlin/shims/` has the handful of `android.*` and Android-only androidx calls the shared code makes (`Context`, `SharedPreferences`, `Uri`, `LocalContext`, `BackHandler`, `AndroidView`, `Font(resId)`, `R.font`, `BuildConfig`). The app's own fonts come from `app/src/main/res/font`. - `preview/FakeMachine.kt` is a herdr machine without SSH: it answers herdr's socket API (`session.snapshot`, `pane.read`, …), the shell commands the app runs (the herdr probe, `session list`, Shepherdr's service in `FakeShepherdr.kt`, the health probe in `FakeHealth.kt`), and the streams it opens (an agent's transcript, a terminal, the activity timeline's sessions). Everything above it, `HostSession`, `HerdrApi`, the Shepherdr client, the transcript readers, is the app's real code. - `preview/FakeShepherdr.kt` is Shepherdr 3.1's service on a fake machine (`shepherdr` on a `FakeMachine`): `/api/health`, the live link's first snapshot and the chats as `bun` reads them there, `/api/action` (a message, an answer, the Accept card's evidence and Accept itself) and a recipe artifact's `recipe.json` and pictures (`preview/DemoArtifacts.kt`). Its demo board (`DemoV2`, `demo/shepherdr/`) is in 3.1's shapes, `web/shared/types.ts`; `DemoCore.shepherdr()` is devbox's herdr-core with Shepherdr 3's agents, labelled as it labels them. +- `preview/FakeShp.kt` is simple-herdr-projects on a fake machine (`shp list --json`, `shp show`, `shp accept`, `shp say`, a worker's terminal through `hc read`), with its demo board `DemoShp` (`shp-*` scenes). `preview/FakeGitHub.kt` answers `gh` for a task's pull request review (`DemoGitHub`, `task-v2-pr*`); `preview/DemoBrowser.kt` is Shepherdr's browser relay with a made-up page (`task-v2-browser*`); `preview/FakeDocs.kt` is a machine's `~/.herdr/docs` folder of herdr.doc documents (`doc*` scenes); `preview/FakeCore.kt` and `DemoCore.kt` are the Herdr desktop's herdr-core (`*-desktop` scenes); `preview/RealData.kt` has the shape of the owner's real work (eleven spaces, thirteen tasks, long titles), with the project names changed (`real-*` scenes). - `preview/FakeCode.kt` is devbox's code: its repositories and checkouts, answering the code browser's commands (`CodeCommands`) with what they printed on a real repository: `code-demo/make.sh` builds a small made-up herdr-android repository with Git (task AND-9's worktree with two commits, a change not committed and a new file) and writes its files and those outputs to `src/main/resources/demo/code`. Run it again after changing `code-demo/files`. -- `preview/Demo.kt` is the data: the README screenshots' demo host (`docs/screenshots/demo-host`). devbox runs three agents (and Shepherdr 3, `DemoV2`, in the scenes that set it up); gpu-box and ci-runner are reached through it; mac-mini refuses the app's key. The transcripts and agent screens in `src/main/resources/demo/` were written by that demo host's `content.py` (the transcripts with their times, so the chat's turns read "Worked for 2m 36s" against `Demo.NOW`); `claude-app-more.jsonl` (what the app agent writes next: a failed test run, the fix, its answer, for `chat-done` and `chat-new-messages`) was written by hand in the same format. The activity timeline's data (`demo/activity/`: the app agent's session while it works, with times, for its chat's timeline) was written by hand, by a script, in its harness's own format. +- `preview/Demo.kt` is the data: the demo host the README's screenshots were once taken on (containers with made-up projects, since retired; the README's screenshots are now these scenes). devbox runs three agents (and Shepherdr 3, `DemoV2`, in the scenes that set it up); gpu-box and ci-runner are reached through it; mac-mini refuses the app's key. The transcripts and agent screens in `src/main/resources/demo/` were written by that demo host's `content.py` (still in the history as `8916b0f:docs/screenshots/demo-host/content.py`; the transcripts with their times, so the chat's turns read "Worked for 2m 36s" against `Demo.NOW`); `claude-app-more.jsonl` (what the app agent writes next: a failed test run, the fix, its answer, for `chat-done` and `chat-new-messages`) was written by hand in the same format. The activity timeline's data (`demo/activity/`: the app agent's session while it works, with times, for its chat's timeline) was written by hand, by a script, in its harness's own format. - `preview/Main.kt` renders a scene: it saves the machines, starts the app's container, waits until every machine has answered, composes `HerdrRoot` in a 412×892 dp window, opens the scene's route, runs its actions, and saves the window with its dialogs and sheets. ## Adding a scene @@ -45,21 +46,21 @@ In CI the Android workflow's four `render (k/4)` jobs each render a quarter of t Scenes are in `preview/Scenes.kt`, in render order: ```kotlin -Scene("board", route = Routes.board(DEVBOX)), -Scene("task", route = Routes.board(DEVBOX), actions = { tapText("Show when each machine last answered") }), +Scene("board-v2", route = Routes.board(DEVBOX), setup = { v2() }), +Scene("task-v2-ready", route = Routes.board(DEVBOX, "AND-20"), setup = { v2() }), Scene("agent-actions", actions = { longPressText("Speed up /jobs") }), Scene("key-setup", key = IdentityStore.State.Missing), Scene("chat-blocked", route = Routes.agent(DEVBOX, API_AGENT), setup = { host(DEVBOX).screens += API_AGENT to "…" }), ``` - `bubble` shows a chat as its floating bubble does (`BubbleRoot`), in place of the app (`bubble*` scenes). -- `widthDp` and `heightDp` set the window (412×892 dp by default); 840 dp wide or more shows the three panes (`wide*` scenes). `resize` changes the window after the actions, as a rotation or a resize does, and the shot is the new size (`rotate-*` scenes). -- `route` opens a screen by its `Routes` entry; `actions` taps what is on screen: `tapText`, `tapExact` (only that text, not a longer line that starts with it), `tapLabel` (an icon's content description), `longPressText`, `type`, `typeInto` (a form's field, by its label), and `settle()` to let the app catch up. Scrolling never settles in the desktop harness: a scene that needs what is further down sets a taller `heightDp`. `ctx.data.host(id)` changes a machine while the app runs, and `ctx.refresh(id)` makes the app read it again (see the `notifications` scene). +- `widthDp` and `heightDp` set the window (412×892 dp by default); 840 dp wide or more shows the spaces sidebar beside the app (`wide*` scenes). `resize` changes the window after the actions, as a rotation or a resize does, and the shot is the new size (`rotate-*` scenes). +- `route` opens a screen by its `Routes` entry; `actions` taps what is on screen: `tapText`, `tapExact` (only that text, not a longer line that starts with it), `tapLabel` (an icon's content description), `longPressText`, `type`, `typeInto` (a form's field, by its label), and `settle()` to let the app catch up. Scrolling never settles in the desktop harness: a scene that needs what is further down sets a taller `heightDp`. `ctx.data.host(id)` changes a machine while the app runs, and `ctx.refresh(id)` makes the app read it again (see the `pill` scene). - `talkback(text)` draws TalkBack's Actions menu for the element showing `text` (a card, a row): its custom actions as the app's semantics give them, with TalkBack's green outline round it; they are printed to the log too. `talkbackDo(text, label)` carries one of them out (`agent-why`). TalkBack itself is a system service the harness can't run; the menu is drawn in Android's look, not the app's. - `drag(label, a to b, …)` puts a finger down on the element with that content description and moves it through the points (fractions of its size, or of a part of it given by `within`): marking up a picture in the editor (`picture-*` scenes). - `hold(label, a to b, …)` is `drag` without letting go: the shot shows the finger still down (holding the mic, `chat-voice-*` scenes). - `scrollTo(index)` scrolls the first list to an item at once (in a chat, which runs bottom up, 0 is the newest); with `ctx.data.host(id).append(session, lines)` the agent writes more to its transcript while the chat follows it, as `tail -F` gives it (`chat-new-messages`). -- `setup` changes the demo data before the app starts: `looked` (when each agent's chat was last on screen, `LookedStore`, by `<machine>/<pane>`: its "While you were away" card counts from it), `images` (pictures waiting in a chat's composer, by its DraftStore key; the README's own screenshots in `docs/screenshots` stand for the phone's, `Demo.picture`), `notes` and `codeView` (comments kept for later and how code is shown, `CodeStore`), `push` (the notification settings, `Demo.push()`), `brief` (the brief's schedule and when you last looked, `Demo.brief()`), `pushApps` and `notificationsAllowed` (the phone's side, `PreviewPhone`), `bubblesAllowed` (`PreviewBubbles`), `guard`, `screenLock` and `unlockAnswer` (the fingerprint's settings and the phone's side, `GuardStore` and `PreviewUnlock`), `composer` (messages held over the agents' composers and the quick replies, `ComposerStore`), `textSizes` (the chats' and the terminal's text sizes, `TextSizeStore`), `keyBar` (the terminal's key bar and Keep screen on, `KeyBarStore`), `selection` (what "Select text" selects in the terminal, in viewport cells, as a drag would: the harness can't drag across the grid), `scanned` and `reachable` (what the code scanner reads on Scan the code, and the host key each address shows; `pair-*` scenes), `machines`, `hosts` (each a `FakeMachine`: `snapshot`, `screens`, `transcripts`, `activity`, `records` and `said` (the timeline's sessions and board records), `board`, `refuse`, `health`, `journal`, `pictures` (pictures a chat reads back by their path there), `agentCommands` (what the composer's `/` list reads there; its `@` list reads the machine's `code`), or `on("method") { params -> result }` for one API call), `drafts`, `mic` (the microphone and what it hears, `PreviewMic.Setup`) and `voiceSent` (the messages sent as voice messages, `VoiceStore`). +- `setup` changes the demo data before the app starts: `looked` (when each agent's chat was last on screen, `LookedStore`, by `<machine>/<pane>`: its "While you were away" card counts from it), `images` (pictures waiting in a chat's composer, by its DraftStore key; old screenshots of the app in `src/main/resources/demo/pictures` stand for the phone's, `Demo.picture`), `notes` and `codeView` (comments kept for later and how code is shown, `CodeStore`), `push` (the notification settings, `Demo.push()`), `brief` (the brief's schedule and when you last looked, `Demo.brief()`), `pushApps` and `notificationsAllowed` (the phone's side, `PreviewPhone`), `bubblesAllowed` (`PreviewBubbles`), `guard`, `screenLock` and `unlockAnswer` (the fingerprint's settings and the phone's side, `GuardStore` and `PreviewUnlock`), `composer` (messages held over the agents' composers and the quick replies, `ComposerStore`), `textSizes` (the chats' and the terminal's text sizes, `TextSizeStore`), `keyBar` (the terminal's key bar and Keep screen on, `KeyBarStore`), `selection` (what "Select text" selects in the terminal, in viewport cells, as a drag would: the harness can't drag across the grid), `scanned` and `reachable` (what the code scanner reads on Scan the code, and the host key each address shows; `pair-*` scenes), `machines`, `hosts` (each a `FakeMachine`: `snapshot`, `screens`, `transcripts`, `activity`, `records` and `said` (the timeline's sessions and board records), `board`, `refuse`, `health`, `journal`, `pictures` (pictures a chat reads back by their path there), `agentCommands` (what the composer's `/` list reads there; its `@` list reads the machine's `code`), or `on("method") { params -> result }` for one API call), `drafts`, `mic` (the microphone and what it hears, `PreviewMic.Setup`) and `voiceSent` (the messages sent as voice messages, `VoiceStore`). - Keep the data fixed: no clocks, random ids or files from outside the repo, so a scene renders the same every time. ## When it doesn't compile diff --git a/tools/ui-preview/src/main/kotlin/preview/Demo.kt b/tools/ui-preview/src/main/kotlin/preview/Demo.kt index e4f5771f..94d01d48 100644 --- a/tools/ui-preview/src/main/kotlin/preview/Demo.kt +++ b/tools/ui-preview/src/main/kotlin/preview/Demo.kt @@ -1,5 +1,5 @@ -// The made-up world every scene starts from: the README screenshots' demo host -// (docs/screenshots/demo-host) as fixed data. devbox runs three agents and the work board; +// The made-up world every scene starts from: the demo host the README's screenshots were once +// taken on (containers, since retired) as fixed data. devbox runs three agents and the work board; // gpu-box and ci-runner are reached through it; mac-mini refuses the app's key. package preview @@ -160,13 +160,15 @@ object Demo { fun sharedFile(): java.io.File = kotlin.io.path.createTempFile("shared", ".png").toFile().apply { writeBytes(picture("task").readBytes()) } /** - * A screenshot on the phone, for the composer and the chat's pictures: the README's own - * screenshots of the app (540×1200, `docs/screenshots/<name>.webp`), as a phone's would be. + * A screenshot on the phone, for the composer and the chat's pictures: old screenshots of the app + * (540×1200, `src/main/resources/demo/pictures/<name>.webp`), as a phone's would be. */ fun picture(name: String): java.io.File { val start = java.io.File("").absoluteFile - val repo = generateSequence(start) { it.parentFile }.firstOrNull { java.io.File(it, "docs/screenshots").isDirectory } ?: error("no docs/screenshots above $start") - return java.io.File(repo, "docs/screenshots/$name.webp") + val dir = generateSequence(start) { it.parentFile }.flatMap { + sequenceOf(java.io.File(it, "src/main/resources/demo/pictures"), java.io.File(it, "tools/ui-preview/src/main/resources/demo/pictures")) + }.firstOrNull { it.isDirectory } ?: error("no demo pictures above $start") + return java.io.File(dir, "$name.webp") } /** Where devbox keeps the pictures sent to its agents, as `HostSession.upload` puts them. */ diff --git a/docs/screenshots/board-light.webp b/tools/ui-preview/src/main/resources/demo/pictures/board-light.webp similarity index 100% rename from docs/screenshots/board-light.webp rename to tools/ui-preview/src/main/resources/demo/pictures/board-light.webp diff --git a/docs/screenshots/board.webp b/tools/ui-preview/src/main/resources/demo/pictures/board.webp similarity index 100% rename from docs/screenshots/board.webp rename to tools/ui-preview/src/main/resources/demo/pictures/board.webp diff --git a/tools/ui-preview/src/main/resources/demo/pictures/task.webp b/tools/ui-preview/src/main/resources/demo/pictures/task.webp new file mode 100644 index 00000000..8b90f50c Binary files /dev/null and b/tools/ui-preview/src/main/resources/demo/pictures/task.webp differ