You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit e0b75d0
Browse filesBrowse the repository at this point in the historyBrowse files
Size each pane alone in the browser, keep scrolled screens in place, and persist screen layout
While no terminal is attached, the web server gives each agent pane its own
tmux window and resizes only the viewed pane's window to the viewed card's
first screen, so a side-by-side split narrows that pane and no other pane or
session changes. The poll names its session and pane. On attach, the server
and goto_session join the panes back into the agents window in the launch
order recorded in @peon_pane_order, swap them into place, and restore the
main-vertical layout.
A screen scrolled up keeps its text in view as output grows, anchored on the
first visible line with a unique three-line window. Screens, stack order, and
scroll anchors are saved per pane in localStorage and restored on reload.
README keeps only how to start the UI and the terminal size limits; UI
mechanisms move to ARCHITECTURE, and CLAUDE.md records that rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: ARCHITECTURE.md
+12-4Lines changed: 12 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -44,13 +44,21 @@ Every paste, key, and slash command to a pane runs under `with_pane_delivery` (`
44
44
45
45
The font buttons set the CSS variable `--output-font` on `:root` and save the size under the localStorage key `peon-font`, clamped to 8..32. `refresh()` in `web/app.js` is the one place that measures cols and rows; the font buttons and a 150 ms debounced `resize` listener schedule it through `scheduleRefresh()`.
46
46
47
-
The regular poll `GET /api/panes` also carries `cols` and `rows`, the visible output area divided by the measured cell size, sent only within 20 to 500 columns and 5 to 300 rows; a poll runs about every 1.2 seconds, and a window resize schedules one after 150 ms. For each session`web/server.py` works on the `agents` window. With no terminal client attached, it sizes the window to `cols` by `(rows + 1)` times the pane count, capped at 10,000 rows since each pane's top border takes a row, with `resize-window`, then applies `select-layout even-vertical`, so the panes stack at the browser's size. When a client is attached and the window's `window-size` is `manual`, the next poll unsets `window-size` and applies `select-layout main-vertical`, which restores the terminal layout. With several browser tabs open, the last poll sets the size.
47
+
The regular poll `GET /api/panes` also carries `cols` and `rows`, the first `.output` of the visible card divided by the measured cell size, sent only within 20 to 500 columns and 5 to 300 rows, together with `session` and `pane`, the session and id of the visible pane. A poll runs about every 1.2 seconds, and a window resize schedules one after 150 ms. With `cols` and `rows`, `web/server.py` visits every session of the poll.
48
48
49
-
Scrollback is a deeper live capture, not a store. Each card tracks its own depth `lines`, starting at 1,000. When the user scrolls up within 200 px of the top with no selection, and the last capture already reached the current depth, `web/app.js` adds 1,000 lines. It stops once the depth covers the pane's `#{history_size}`, which tmux's `history-limit` bounds and `web/bridge.py` reads with the pane identity, or at 200,000. The next poll sends `GET /api/panes?pane=<id>&lines=<n>` only when the depth exceeds 1,000; `web/server.py` rejects a `lines` value outside 1,000 to 200,000 with 400, and captures only that pane with `capture-pane -S -<n>`, while other panes keep 1,000. Rendering waits until a capture reaches the requested depth, then keeps the view in place by holding the distance from the bottom, or the scroll top once both captures cover the whole history. Within 40 px of the bottom the depth resets to 1,000. Polling continues at every depth, and nothing is persisted.
49
+
A detached session gets one tmux window per pane, because tmux sizes a window and panes that share it share its width. Only the session that `session` names is changed, and only when `pane` belongs to it. If the session option `@peon_pane_order` is empty, the server first records in it the pane ids of the `agents` window in their layout order. It then runs `break-pane -d` on every pane whose window holds more than one pane, which moves that pane to its own window named `p<number>`. `break-pane` keeps the pane id, so `send`, `msg`, and the watcher still reach every pane. Last, it resizes only the window of `pane` to `cols` by `rows` plus that window's border rows (`window_height` minus `pane_height`), and skips the resize when the window already has that size. A one-pane `agents` window keeps its top border row; a broken-out window has none. Other panes, other sessions, and a poll without `session` and `pane` get no resize. With several browser tabs open, the last poll for a pane sets its size.
50
50
51
-
Extra screens are a browser-only view; the server is unchanged. In `web/index.html` each card holds a `.screens` column with one `.screen` that wraps the `.output`. The **+** button (`.add-screen`) clones the first `.screen` with its current DOM, appends a `.close-screen` button, and starts the clone at the bottom. `wireScreen()` in `web/app.js` attaches the same scroll, key, and context-menu handlers to every screen, all bound to the one card, so selection, explain, key sends, and depth growth act on that pane from any screen. `updateOutput()` renders the capture with `renderAnsi()` into the first `.output` only, then copies its child nodes into each other `.output` with `cloneNode(true)`. It measures and restores the scroll position of each screen on its own, and each screen keeps its last `scrollTop` for the scroll-up check. The depth resets to 1,000 only when every screen is within 40 px of its bottom. The **Stack order** button (`.stack-order`) flips `card.stack` between `'vertical'`, the default, and `'horizontal'`, toggles the `horizontal` class on `.screens`, which sets `flex-direction: row` in `web/style.css`, restores each screen's scroll position, and calls `scheduleRefresh()`. `card.stack` lives only in memory. `refresh()` measures `cols` and `rows` from the first `.output`, so an added screen lowers `rows` in a vertical stack and lowers `cols` side by side; fewer `cols` reflows the agent's terminal. Nothing is persisted.
51
+
An attached session gets its terminal layout back, whatever `session` names. When its `agents` window has `window-size` `manual` or a pane of the session is in another window, the next poll joins each outside pane into `agents` with `join-pane -d`, in the order of `@peon_pane_order`, or in pane id order when the option is empty. It then uses `swap-pane -d` until the panes stand in that order, unsets `window-size`, sets `main-pane-width` to 60%, applies `select-layout main-vertical`, and unsets `@peon_pane_order`. `goto_session` in `lib/session.sh` runs the same join, swap, and layout steps before it attaches or switches the client, so `peon-code <session>` opens on the terminal layout; it then unsets the option. A headless caller exits before these steps. Only these two paths join the panes. A client that attaches by hand sees the separate windows until the next poll that carries `cols`, about one second later while a browser page shows a card. With no such page open, the windows stay separate until `peon-code <session>` runs or a browser polls again.
52
52
53
-
The HTTP server checks token, Host/Origin, and marked-session pane membership. It reuses `explain` (`cmd_explain` in `lib/commands.sh`) and `send` via stdin and subprocess argument arrays, preserving safe-paste checks. Browser sends are serialized to avoid simultaneous empty-input checks.
53
+
Scrollback is a deeper live capture, not a store. Each card tracks its own depth `lines`, starting at 1,000. When the user scrolls up within 200 px of the top with no selection, and the last capture already reached the current depth, `web/app.js` adds 1,000 lines. It stops once the depth covers the pane's `#{history_size}`, which tmux's `history-limit` bounds and `web/bridge.py` reads with the pane identity, or at 200,000. The next poll adds `lines=<n>` to the `pane=<id>` of the visible pane only when the depth exceeds 1,000; `web/server.py` rejects a `lines` value outside 1,000 to 200,000 with 400, and captures only that pane with `capture-pane -S -<n>`, while other panes keep 1,000. Rendering waits until a capture reaches the requested depth, then keeps the view in place as the next paragraph describes. Within 40 px of the bottom the depth resets to 1,000. Polling continues at every depth, and nothing is persisted.
54
+
55
+
A screen scrolled up keeps its text in place while output grows. A screen within 40 px of its end follows the live output. For every other screen, `updateOutput()` calls `scrollAnchor()` in `web/app.js` before the render. `scrollAnchor()` finds the line at the top left of the visible text with `caretPositionFromPoint`, or `caretRangeFromPoint` where only that exists. From that line it moves down to the first line that is not blank and whose 3-line window, that line and the next two, occurs only once in the old text. It stops with no anchor when it passes the bottom of the visible area. It records the pixel offset of that line from the top of the screen. After the render, the returned function finds each position of that window in the new text. It takes the match closest to the old index plus the change in line count, and sets `scrollTop` so the line keeps its pixel offset. When no window is unique or the new text has no match, the screen holds its distance from the bottom. Each screen keeps its own `anchorRemainder`, the fraction of a pixel that `scrollTop` rounding drops, and adds it to the next anchored scroll; a user scroll or the fallback sets it to 0.
56
+
57
+
Extra screens are a browser-only view of one pane; the server sees only the size of the first screen. In `web/index.html` each card holds a `.screens` column with one `.screen` that wraps the `.output`. The **+** button (`.add-screen`) clones the first `.screen` with its current DOM, appends a `.close-screen` button, and starts the clone at the bottom. `wireScreen()` in `web/app.js` attaches the same scroll, key, and context-menu handlers to every screen, all bound to the one card, so selection, explain, key sends, and depth growth act on that pane from any screen. `updateOutput()` renders the capture with `renderAnsi()` into the first `.output` only, then copies its child nodes into each other `.output` with `cloneNode(true)`. It measures and restores the scroll position of each screen on its own, and each screen keeps its last `scrollTop` for the scroll-up check. The depth resets to 1,000 only when every screen is within 40 px of its bottom. The **Stack order** button (`.stack-order`) flips `card.stack` between `'vertical'`, the default, and `'horizontal'`, toggles the `horizontal` class on `.screens`, which sets `flex-direction: row` in `web/style.css`, and restores each screen's scroll position. `refresh()` measures `cols` and `rows` from the first `.output`, so for a detached session a side-by-side split narrows the window of the viewed pane, and a vertical split shortens it. Other panes keep their size.
58
+
59
+
The screen layout of a card survives a reload in `localStorage` under the key `peon-screens:<session>/<pane name>`. `saveScreens()` in `web/app.js` stores `{stack, screens}`, with one `{anchor}` for each screen. `anchor` is null for a screen at the live output; otherwise it is the `scrollAnchor()` record `{text, pixelOffset, index, lineCount}`, where `text` is the 3-line window. Adding a screen, closing a screen, and the stack toggle write at once. A scroll writes after 300 ms with no further scroll, and `pagehide` writes any write that is still pending. The entry is removed when the card is one vertical screen at the live output, when the card is discarded, and when its pane is gone; a removal skips a key that another open card still uses. `createCard()` reads and validates the entry, sets the stack order, adds the saved screens, and gives each screen its saved anchor. The first `updateOutput()` passes that anchor to `scrollAnchor(output, saved)`, which finds the window in the new text as for a live render. When the saved text is not in the loaded history, the screen goes to the bottom. A read or write error in `localStorage` is ignored.
60
+
61
+
The HTTP server checks token, Host/Origin, and marked-session pane membership. It reuses `explain` (`cmd_explain` in `lib/commands.sh`) and `send` via stdin and subprocess argument arrays, preserving safe-paste checks. The **Close session** button posts to `/api/dismiss`, which checks that the session is still in the pane list and runs `peon-code.sh dismiss <session>` with a 10 second timeout. Browser sends are serialized to avoid simultaneous empty-input checks.
54
62
55
63
`buttons/*.md` holds the tracked prompt-button seeds. `web/server.py` defines `BUTTONS_DIR` as that installation directory. Its runtime `config_dir()` reads `XDG_CONFIG_HOME`, or defaults to `~/.config`, and appends `peon-code`; both PID files and custom button storage use it. Authenticated `GET /api/buttons` reads seeds and custom files from `config_dir()/buttons`, sorts by `(order, name)`, and returns file stems, one-line description frontmatter, and exact prompt bodies in a plain array. `order` is an optional frontmatter integer, 0 when absent; a value that is not an integer from 0 to 1000 makes the entry invalid. Invalid or unreadable entries are omitted independently; CRLF headers leave prompt bodies intact. `POST /api/buttons` validates the same fields, trims surrounding spaces from names, creates a custom file exclusively, rejects names already in either directory with 409, and returns the full array. Installation seeds are unchanged. The browser loads this list once and refreshes it after saving a button. Each pane renders the list and sends its prompt through `/api/send` without append.
Copy file name to clipboardExpand all lines: CLAUDE.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -20,7 +20,7 @@ Bash launcher that builds a tmux session of side-by-side AI coding agent CLIs (c
20
20
-`roles/*.md`: per-role prompt files
21
21
-`install.sh`: symlinks the script into a bin dir and seeds `~/.config/peon-code/peon-code.conf`
22
22
-`tests/test_peon_code.sh`: the test suite
23
-
-`README.md`: user-facing behavior only; `ARCHITECTURE.md`: how it works inside
23
+
-`README.md`: user-facing only: how to install, start, configure, and the limits a user cannot find by trying (an attached terminal bounds the browser size, for example). Never describe UI behavior, buttons, or layout; users find those by using the page. `ARCHITECTURE.md`: how it works inside, including every UI mechanism.
24
24
25
25
## Cross-platform: every feature must work on both macOS and Linux
The command opens your default browser on macOS or Linux. Keep it running while you use the UI; Ctrl-C stops the web server and leaves your agents running. If a browser cannot open, use the printed URL. Start teams with `peon-code`; the browser lists them as they appear.
114
114
115
-
The page has a tab for each running team and, inside it, a tab for each agent with its token total, counted as in [Usage](#usage). The **+** tab starts a team from a project path on the agent host, with an optional local team config file. Each pane card shows the agent's live output and has a message box, saved prompt buttons that you can add to, and a **Keys** row. Scroll to the top of a pane to load older lines. The **+** in a card's heading adds another screen of the same pane with its own scroll position, and the button next to it stacks the screens vertically (**↕**) or side by side (**↔**). The meta line shows the session, role, and pane id, and ends with the agent's weekly limit when the agent reports one. A **Swap agent** form changes an agent's CLI, model, or effort, as [Swap](#swap) does. **Close session** stops every agent of the team.
115
+
The page lists the running teams and their agents. The **+** tab starts a team from a project path on the agent host, with an optional local team config file. While no terminal is attached, the browser sizes each pane to its own view; once a terminal is attached, the terminal sets the size, and the browser view can show only what the terminal shows. After the browser has sized a pane, attach with `peon-code <session>`; a plain `tmux attach` shows each agent in its own window until a browser page polls again.
116
116
117
117
For SSH, install `peon-code-web` on your Mac and on the remote host, then run `peon-code-web --ssh <your-usual-SSH-host>`**on your Mac**. It starts the remote web server, forwards a loopback port, and opens your local browser. Your SSH config, keys, and authentication apply, and the Mac needs no tmux. The browser lists the running teams of all project folders on the host. Keep the command running; Ctrl-C closes the tunnel and its remote web server and leaves the agents running. A stale web server from a lost tunnel is replaced automatically.
0 commit comments