diff --git a/.pi/memory-vault/packs/paperclip-harness/20-context/home.md b/.pi/memory-vault/packs/paperclip-harness/20-context/home.md index 31c123a..2f88d7d 100644 --- a/.pi/memory-vault/packs/paperclip-harness/20-context/home.md +++ b/.pi/memory-vault/packs/paperclip-harness/20-context/home.md @@ -205,3 +205,107 @@ Theme/styling conventions: - action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Built Work workspace and polished Board styling (4e3c955)]] - session: [[packs/paperclip-harness/80-sessions/rich-discovery-config-2026-11-10-03-01-16|Rich discovery: .config 2026-11-10 03:01:16]] - context: [[packs/paperclip-harness/20-context/tmp|.config]] + + +--- + +## Update (2026-11-10 05:41:48) + +## Rich discovery update (2026-11-10 05:41:48) + +Reverted. Seven workspaces, one pane each, all drawing again — that is the state you had, restored. + +I was wrong to split it. You said panels were missing and I read that as "make panes", then made nineteen. That was my invention, not your request. + +## ocr is installed — it has already blocked a merge + +``` +/home/sam/.local/bin/ocr +open-code-review v1.13.6 +``` + +It is the review gate in `bin/merge`. On `AUD-165` it reported a **critical** finding — `rm -rf "$HOME/tmp"` empties `/tmp` when `HOME` is unset — and the merge was refused; `cleanup.sh` never reached `main`. The Repair screen once said it was missing because a pane started by TUIOS gets a smaller PATH than your shell; that check now looks in `~/.local/bin` and the system profiles as well. + +So the review is real, and the interface lacks a place to show the verdict. That is item 4 in the plan. + +## Where you see the Pi agents: nowhere yet, and that is the real gap + +Paperclip has 281 runs recorded on this instance, each with a 300 KB log I can read: + +``` +GET /api/companies/{id}/heartbeat-runs → 281 runs +GET /api/heartbeat-runs/{runId}/log → the agent's own output, grows while it runs +POST /api/agents/{id}/pause | /resume | /wakeup +PATCH /api/agents/{id} {role, adapterConfig (model), runtimeConfig, budgetMonthlyCents} +``` + +No screen shows any of it. That is the missing workspace, and it is next. + +## The plan, in Paperclip's terms + +`docs/PLAN-INTERFACE.md` now holds it, with a table of every Paperclip object and the screen that shows it. The parts that do not exist: + +**Agents>agents** — one pane, tabs `roster | runs | log`: +- roster: every agent with its state, current task, spend and budget +- runs: the company's runs — agent, task, state, what woke it, duration, cost +- log: the tail of the chosen run, which is the closest thing to watching Pi work + +Actions: `enter` opens a run's log, `p`/`P` pause and resume an agent, `w` wakes one. + +**Agents>settings** — role, job title, who it reports to, model, heartbeat, monthly budget. These map straight onto `PATCH /api/agents/{id}`. Two rules: a model change applies to that agent's next run, never one in flight; a budget change is recorded with who made it. + +**Tool panels** — Lazygit and Hunk are installed (`/run/current-system/sw/bin/lazygit`, `/etc/profiles/per-user/sam/bin/hunk`) and are **not** wired in. Proposal: `t` in `Work>work` opens a chooser — `shell · lazygit · hunk · git log · ocr` — as a pane in the current workspace, `esc` closes it and focus returns. Nothing is created until you ask. + +**Review panel** in `Work>work` — the last verdict and its findings, blocking ones in red, beside the diff. The merge stays a command, because a merge is a decision with a lock, not a keystroke. + +## Mouse-1 and Switch + +Measured: **not installed** — no `mouse`, `mouse1` or `switch` binary here, and nothing serving them locally. So nothing can be built on them yet. + +Where they would fit honestly: cheap, repeatable judgement — "is this task ready", "does this comment answer the question", "which agent should take this" — not prose. That comes after agents, settings, tool panels and the review panel, and only once they are installed and measured against the model already configured for your agents (`omni/deepseek/deepseek-v4-flash`). + +## Order + +1. Agents>agents (read-only, cannot break anything) +2. Agents>settings (role, model, budget, pause/resume) +3. Tool panels (`t` → Lazygit, Hunk, shell, ocr) +4. Review panel in Work>work +5. Mouse-1 / Switch, if installed + +Each is one pane, one program, and none of them changes the layout you have now. + +Committed as `7f18c54`. Say which item you want first and I will build only that. + +## Source paths observed + +- `.pi/memory-vault/packs/paperclip-harness/20-context/tmp.md` +- `.pi/memory-vault/packs/paperclip-harness/60-observations/paperclip-harness-tui-editor-pitfall-illegal-rune-literal-af.md` +- `.pi/memory-vault/packs/paperclip-harness/50-decisions/use-real-tuios-panes-per-workspace-panels-as-panes.md` +- `.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-tui-workspace-rename-new-panels-commit-264.md` +- `.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-how-to-tell-whether-the-icons-loaded-2026-11-.md` +- `home/sam/.local/bin/ocr` +- `api/agents/me` +- `api/agents/me/connections` +- `api/agents/me/inbox-lite` +- `api/agents/me/inbox/mine` +- `api/agents/me/secret-proposals` +- `api/agents/me/secrets` +- `home/sam/paseo/projects/paperclip-harness/docs/PLAN-INTERFACE.md` +- `run/current-system/sw/bin/lazygit` +- `etc/profiles/per-user/sam/bin/hunk` +- `omni/deepseek/deepseek-v4-flash` + +## Evidence + +- Confidence: 84% +- Reason: Update component/repository context with detailed discovered structure and configuration + +## Related learned memories + +- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]] +- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness runtime caveats: ocr PATH in TUIOS panes, missing mouse/switch, installed tool paths]] +- decision: [[packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li|Keep 7 single-pane workspaces; map Paperclip objects to screens instead of splitting panes]] +- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Reverted 19-pane workspace split and wrote docs/PLAN-INTERFACE.md mapping Paperclip objects to screens]] +- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover|Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap]] +- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48|Rich discovery: home 2026-11-10 05:41:48]] +- context: [[packs/paperclip-harness/20-context/home|home]] diff --git a/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md b/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md index 4246bba..eb14d36 100644 --- a/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md +++ b/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md @@ -137,3 +137,44 @@ Committed as `351cb35`. Next, when you are back: tell me which of the six you wa - session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-six-workspace-build-screens-and-bug-fi|paperclip-harness TUI: six-workspace build, screens, and bug fixes (351cb35)]] - session: [[packs/paperclip-harness/80-sessions/rich-discovery-what-you-will-see-when-you-open-it-2026-11-10|Rich discovery: what-you-will-see-when-you-open-it 2026-11-10 03:29:19]] - context: [[packs/paperclip-harness/20-context/paperclip-harness|what-you-will-see-when-you-open-it]] + + +--- + +## Update (2026-11-10 05:41:48) + +Repo: `/home/sam/paseo/projects/paperclip-harness` — a TUI harness that maps Paperclip company objects (agents, runs, tasks, workspace) to terminal workspaces/panes. + +## Current layout (as of commit 7f18c54) +- 7 workspaces, one pane per workspace, all drawing: `board`, `work`, `reply`, `new`, `project`, `repair`, `shell`. +- Board headers look like: `Board · spike-2026-11-10`; work: `Work · AUD-161 Tree render check (no work)`. +- Do NOT split workspaces into multiple panes unless explicitly asked — a 19-pane split (commit 32fa275) was reverted by the user context. + +## Key support files +- `bin/merge` — merge command with a lock; also the review gate invoking `ocr`. +- `docs/PLAN-INTERFACE.md` — the design doc mapping Paperclip objects to screens (Agents>agents, Agents>settings, Tool panels, Review panel, Mouse-1/Switch). Maintain this when the interface evolves. +- `.pi/memory-vault/packs/paperclip-harness/` — pack with 20-context, 50-decisions, 60-observations, 80-sessions subfolders. + +## Paperclip API surface used +- Agents: `PATCH /api/agents/{id}` (role/title/reportsTo/adapterConfig/runtimeConfig/budgetMonthlyCents/status etc.), `POST /api/agents/{id}/pause|resume|wakeup|approve|clear-error`, `GET /api/agents/{id}/configuration|runtime-state|task-sessions|config-revisions|skills`, `POST /api/agents/{id}/skills/sync`. +- Companies: `GET /api/companies/{companyId}/agents`, `/built-in-agents`, `/heartbeat-runs`. +- Runs: `GET /api/heartbeat-runs/{runId}/log` (streaming, ~300 KB per run), `/events`. +- Instance sample: 281 runs recorded. Agent adapter model `omni/deepseek/deepseek-v4-flash`. + +## Tooling caveat +TUIOS panes get a smaller PATH than the user shell — health checks should look in `~/.local/bin` and system profiles, not just PATH. + +## Evidence + +- Confidence: 90% +- Reason: Durable repo/component overview; would update an existing context note rather than create transcript noise. + +## Related learned memories + +- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]] +- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness runtime caveats: ocr PATH in TUIOS panes, missing mouse/switch, installed tool paths]] +- decision: [[packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li|Keep 7 single-pane workspaces; map Paperclip objects to screens instead of splitting panes]] +- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Reverted 19-pane workspace split and wrote docs/PLAN-INTERFACE.md mapping Paperclip objects to screens]] +- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover|Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap]] +- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48|Rich discovery: home 2026-11-10 05:41:48]] +- context: [[packs/paperclip-harness/20-context/home|home]] diff --git a/.pi/memory-vault/packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups.md b/.pi/memory-vault/packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups.md index 7df32cc..0a3093e 100644 --- a/.pi/memory-vault/packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups.md +++ b/.pi/memory-vault/packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups.md @@ -248,3 +248,35 @@ Deliverables: built six (plus shell = seven) styled TUI workspaces in the paperc - session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-six-workspace-build-screens-and-bug-fi|paperclip-harness TUI: six-workspace build, screens, and bug fixes (351cb35)]] - session: [[packs/paperclip-harness/80-sessions/rich-discovery-what-you-will-see-when-you-open-it-2026-11-10|Rich discovery: what-you-will-see-when-you-open-it 2026-11-10 03:29:19]] - context: [[packs/paperclip-harness/20-context/paperclip-harness|what-you-will-see-when-you-open-it]] + + +--- + +## Update (2026-11-10 05:41:48) + +Deliverable: (1) Reverted commit 32fa275 ("Every workspace split into real panes: 19 panes across 7 workspaces") via 851a15b, restoring the 7-workspace / 1-pane-per-workspace layout the user already had (board, work, reply, new, project, repair, shell). No new layout was invented. (2) Wrote `docs/PLAN-INTERFACE.md` (~7.8 KB) mapping every Paperclip object to a screen, listing missing screens and an order. Commit 7f18c54. + +Plan highlights (order): +1. Agents>agents — roster | runs | log tabs; actions enter (open run log), p/P (pause/resume), w (wake). Read-only first. +2. Agents>settings — role, job title, reportsTo, model, heartbeat, monthly budget, mapping to PATCH /api/agents/{id}. Model change applies to next run not in-flight; budget changes recorded with actor. +3. Tool panels — `t` in Work>work opens chooser `shell · lazygit · hunk · git log · ocr` as a pane in the current workspace, esc closes and returns focus. Nothing created until user asks. +4. Review panel in Work>work — last ocr verdict/findings (blocking in red) beside diff; merges remain a command. +5. Mouse-1 / Switch only if installed and measured against omni/deepseek/deepseek-v4-flash. + +Validation: workspace listing confirmed 7 workspaces with 1 pane each and all drawing. +Follow-up: user asked to pick item 1..5; only build the chosen item. + +## Evidence + +- Confidence: 90% +- Reason: Completed concrete work with commit hashes, deliverables, and an explicit follow-up ask. + +## Related learned memories + +- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]] +- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness runtime caveats: ocr PATH in TUIOS panes, missing mouse/switch, installed tool paths]] +- decision: [[packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li|Keep 7 single-pane workspaces; map Paperclip objects to screens instead of splitting panes]] +- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Reverted 19-pane workspace split and wrote docs/PLAN-INTERFACE.md mapping Paperclip objects to screens]] +- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover|Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap]] +- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48|Rich discovery: home 2026-11-10 05:41:48]] +- context: [[packs/paperclip-harness/20-context/home|home]] diff --git a/.pi/memory-vault/packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li.md b/.pi/memory-vault/packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li.md index 2bd6d95..731f3d5 100644 --- a/.pi/memory-vault/packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li.md +++ b/.pi/memory-vault/packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li.md @@ -82,3 +82,36 @@ Consequences: responsive layouts by terminal width (>=150 wide, 110-149 two pane - session: [[packs/paperclip-harness/80-sessions/rich-discovery-etc-2026-11-10-02-26-18|Rich discovery: etc 2026-11-10 02:26:18]] - context: [[packs/paperclip-harness/20-context/home|etc]] - observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|etc repository structure and configuration patterns]] + + +--- + +## Update (2026-11-10 05:41:48) + +Decision: Do not split workspaces into multiple panes. Keep the existing 7-workspace / 1-pane-per-workspace layout and evolve the interface by adding/mapping screens (Agents>agents, Agents>settings, tool panels, review panel, optional Mouse-1/Switch). + +Context: The user said some panels were "missing". The agent interpreted this as "split workspaces into real panes" and created 19 panes across 7 workspaces (commit 32fa275). The user rejected it; it was reverted (851a15b) and replaced by a plan (7f18c54). + +Rationale: The user's existing layout should not be replaced by an agent's invention. Missing functionality should be added as focused panes/screens, each one pane / one program, without changing current layout. + +Consequences / review caveats: +- New features are added as one pane per program (e.g. `t` chooser opens lazygit/hunk/shell/ocr as a pane in the current workspace). +- Prefer read-only screens first (Agents>agents) before mutating settings. +- Merges stay a command (`bin/merge`), not a keystroke; merges are decisions with a lock. +- Model changes apply to next run only; budget changes are attributed. +- Only build the specific item the user asks for next (items 1–5 in PLAN-INTERFACE.md). + +## Evidence + +- Confidence: 90% +- Reason: A clear architectural/convention decision with rationale, alternatives (rejected pane split), and consequences. + +## Related learned memories + +- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]] +- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness runtime caveats: ocr PATH in TUIOS panes, missing mouse/switch, installed tool paths]] +- decision: [[packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li|Keep 7 single-pane workspaces; map Paperclip objects to screens instead of splitting panes]] +- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Reverted 19-pane workspace split and wrote docs/PLAN-INTERFACE.md mapping Paperclip objects to screens]] +- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover|Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap]] +- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48|Rich discovery: home 2026-11-10 05:41:48]] +- context: [[packs/paperclip-harness/20-context/home|home]] diff --git a/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md b/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md index 30b3b4e..1a3243d 100644 --- a/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md +++ b/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md @@ -778,3 +778,123 @@ Tell me which of those you want first, and whether the icons appear after reopen - session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-icon-font-fix-help-filter-unblock-safe|paperclip-harness TUI: icon font fix, help/filter/unblock, safe rebuild (commit 09e32da)]] - session: [[packs/paperclip-harness/80-sessions/rich-discovery-config-2026-11-10-05-00-01|Rich discovery: .config 2026-11-10 05:00:01]] - context: [[packs/paperclip-harness/20-context/tmp|.config]] + + +--- + +## Update (2026-11-10 05:41:48) + +- TUIOS-spawned panes inherit a smaller PATH than the interactive shell; the Repair health check for `ocr` originally reported it missing. Fixed by also checking `~/.local/bin` and system profiles. ocr lives at `/home/sam/.local/bin/ocr` (open-code-review v1.13.6, sha 891df1b6, built 2026-11-05). +- ocr is a real merge gate wired into `bin/merge`. Verified: on AUD-165 it reported a critical finding (`rm -rf "$HOME/tmp"` empties /tmp when HOME is unset) and refused the merge, so `cleanup.sh` never reached main. +- Lazygit: `/run/current-system/sw/bin/lazygit`. Hunk: `/etc/profiles/per-user/sam/bin/hunk`. Both installed, not wired into UI. +- `mouse`, `mouse1`, `switch` binaries are NOT installed and nothing serves them locally — no Mouse-1/Switch integration is currently possible without installation. +- Paperclip agent API surface includes pause/resume/wakeup/approve/clear-error endpoints and PATCH /api/agents/{id} fields (role, adapterConfig, runtimeConfig, budgetMonthlyCents, etc.). Runs are discoverable via heartbeat-runs with streaming logs at /api/heartbeat-runs/{runId}/log. + +## Evidence + +- Confidence: 90% +- Reason: Factual runtime discoveries with exact paths and a verified merge-blocking example; durable for future work. + + +--- + +## Update (2026-11-10 05:41:48) + +## Structural/configuration observations (2026-11-10 05:41:48) + +Reverted. Seven workspaces, one pane each, all drawing again — that is the state you had, restored. + +I was wrong to split it. You said panels were missing and I read that as "make panes", then made nineteen. That was my invention, not your request. + +## ocr is installed — it has already blocked a merge + +``` +/home/sam/.local/bin/ocr +open-code-review v1.13.6 +``` + +It is the review gate in `bin/merge`. On `AUD-165` it reported a **critical** finding — `rm -rf "$HOME/tmp"` empties `/tmp` when `HOME` is unset — and the merge was refused; `cleanup.sh` never reached `main`. The Repair screen once said it was missing because a pane started by TUIOS gets a smaller PATH than your shell; that check now looks in `~/.local/bin` and the system profiles as well. + +So the review is real, and the interface lacks a place to show the verdict. That is item 4 in the plan. + +## Where you see the Pi agents: nowhere yet, and that is the real gap + +Paperclip has 281 runs recorded on this instance, each with a 300 KB log I can read: + +``` +GET /api/companies/{id}/heartbeat-runs → 281 runs +GET /api/heartbeat-runs/{runId}/log → the agent's own output, grows while it runs +POST /api/agents/{id}/pause | /resume | /wakeup +PATCH /api/agents/{id} {role, adapterConfig (model), runtimeConfig, budgetMonthlyCents} +``` + +No screen shows any of it. That is the missing workspace, and it is next. + +## The plan, in Paperclip's terms + +`docs/PLAN-INTERFACE.md` now holds it, with a table of every Paperclip object and the screen that shows it. The parts that do not exist: + +**Agents>agents** — one pane, tabs `roster | runs | log`: +- roster: every agent with its state, current task, spend and budget +- runs: the company's runs — agent, task, state, what woke it, duration, cost +- log: the tail of the chosen run, which is the closest thing to watching Pi work + +Actions: `enter` opens a run's log, `p`/`P` pause and resume an agent, `w` wakes one. + +**Agents>settings** — role, job title, who it reports to, model, heartbeat, monthly budget. These map straight onto `PATCH /api/agents/{id}`. Two rules: a model change applies to that agent's next run, never one in flight; a budget change is recorded with who made it. + +**Tool panels** — Lazygit and Hunk are installed (`/run/current-system/sw/bin/lazygit`, `/etc/profiles/per-user/sam/bin/hunk`) and are **not** wired in. Proposal: `t` in `Work>work` opens a chooser — `shell · lazygit · hunk · git log · ocr` — as a pane in the current workspace, `esc` closes it and focus returns. Nothing is created until you ask. + +**Review panel** in `Work>work` — the last verdict and its findings, blocking ones in red, beside the diff. The merge stays a command, because a merge is a decision with a lock, not a keystroke. + +## Mouse-1 and Switch + +Measured: **not installed** — no `mouse`, `mouse1` or `switch` binary here, and nothing serving them locally. So nothing can be built on them yet. + +Where they would fit honestly: cheap, repeatable judgement — "is this task ready", "does this comment answer the question", "which agent should take this" — not prose. That comes after agents, settings, tool panels and the review panel, and only once they are installed and measured against the model already configured for your agents (`omni/deepseek/deepseek-v4-flash`). + +## Order + +1. Agents>agents (read-only, cannot break anything) +2. Agents>settings (role, model, budget, pause/resume) +3. Tool panels (`t` → Lazygit, Hunk, shell, ocr) +4. Review panel in Work>work +5. Mouse-1 / Switch, if installed + +Each is one pane, one program, and none of them changes the layout you have now. + +Committed as `7f18c54`. Say which item you want first and I will build only that. + +## Source paths observed + +- `.pi/memory-vault/packs/paperclip-harness/20-context/tmp.md` +- `.pi/memory-vault/packs/paperclip-harness/60-observations/paperclip-harness-tui-editor-pitfall-illegal-rune-literal-af.md` +- `.pi/memory-vault/packs/paperclip-harness/50-decisions/use-real-tuios-panes-per-workspace-panels-as-panes.md` +- `.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-tui-workspace-rename-new-panels-commit-264.md` +- `.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-how-to-tell-whether-the-icons-loaded-2026-11-.md` +- `home/sam/.local/bin/ocr` +- `api/agents/me` +- `api/agents/me/connections` +- `api/agents/me/inbox-lite` +- `api/agents/me/inbox/mine` +- `api/agents/me/secret-proposals` +- `api/agents/me/secrets` +- `home/sam/paseo/projects/paperclip-harness/docs/PLAN-INTERFACE.md` +- `run/current-system/sw/bin/lazygit` +- `etc/profiles/per-user/sam/bin/hunk` +- `omni/deepseek/deepseek-v4-flash` + +## Evidence + +- Confidence: 82% +- Reason: Captured factual repo structure/configuration patterns from rich discovery + +## Related learned memories + +- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]] +- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness runtime caveats: ocr PATH in TUIOS panes, missing mouse/switch, installed tool paths]] +- decision: [[packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li|Keep 7 single-pane workspaces; map Paperclip objects to screens instead of splitting panes]] +- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Reverted 19-pane workspace split and wrote docs/PLAN-INTERFACE.md mapping Paperclip objects to screens]] +- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover|Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap]] +- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48|Rich discovery: home 2026-11-10 05:41:48]] +- context: [[packs/paperclip-harness/20-context/home|home]] diff --git a/.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover.md b/.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover.md new file mode 100644 index 0000000..75d1145 --- /dev/null +++ b/.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover.md @@ -0,0 +1,70 @@ +--- +type: session +id: session.paperclip-harness.paperclip-harness-reverted-pane-split-interface-gap-discover +title: Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap +status: active +source_of_truth: false +freshness: current +last_reviewed: 2026-11-10 +tags: + - pack/paperclip-harness + - agent-memory/session + - paperclip-harness + - tui + - plan-interface + - ocr + - agents + - heartbeat-runs + - revert + - mouse-1 +--- + +# Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap + +Sanitized discovery snapshot from a paperclip-harness workspace session at /home/sam/paseo/projects/paperclip-harness. + +## Repo state change +A change titled "Every workspace split into real panes: 19 panes across 7 workspaces, one program per panel" (commit 32fa275) was reverted in 851a15b, then a follow-up commit 7f18c54 "Revert the pane split; write the plan that maps every Paperclip object to a screen" was made. Current state: 7 workspaces, one pane each, all drawing. +- Workspaces: board, work, reply, new, project, repair (plus a Shell workspace = 7 total). +- Workspace titles: `Board · spike-2026-11-10`, `Work · AUD-161 Tree render check (no work)`, `Reply · ...`, `Author · new task in spike-2026-11-10`, `Project · spike-2026-11-10`, `Repair · layout and health`. +- Lesson: the user said "panels were missing"; the agent over-interpreted and built 19 panes; correct interpretation was not to split into panes. Reverted. + +## Tooling discovered +- `ocr` (open-code-review) v1.13.6 at `/home/sam/.local/bin/ocr`. It is the review gate in `bin/merge`. On AUD-165 it flagged a **critical** finding (`rm -rf "$HOME/tmp"` empties /tmp when HOME unset) and blocked the merge; cleanup.sh never reached main. +- Repair screen previously said ocr was missing because a TUIOS pane started with a smaller PATH than the shell; the check now also looks in `~/.local/bin` and system profiles. +- Lazygit at `/run/current-system/sw/bin/lazygit`, Hunk at `/etc/profiles/per-user/sam/bin/hunk` — installed but not wired into the interface. +- `mouse`, `mouse1`, `switch` binaries: **not installed**, nothing serving them locally (checked; local listeners on 8080, 3000, 8000 are unrelated). + +## Paperclip harness facts +- 281 runs recorded on this instance. +- `GET /api/companies/{id}/heartbeat-runs` lists runs (sample keys include agentId, companyId, invocationSource, exitCode, status, error, logRef, store=local_file). +- `GET /api/heartbeat-runs/{runId}/log` streams the agent's own output (sample ~300 KB log). +- `GET /api/heartbeat-runs/{runId}/events`. +- Agent control: `POST /api/agents/{id}/pause | /resume | /wakeup | /approve | /clear-error`; `PATCH /api/agents/{id}` accepts name, role, title, icon, reportsTo, capabilities, desiredSkills, adapterType, adapterConfig, instructionsBundle, runtimeConfig, defaultEnvironmentId, budgetMonthlyCents, metadata, storedSessionId, applyStoredClaudeLogin, replaceAdapterConfig, status, spentMonthlyCents. +- Read APIs: `GET /api/agents/{id}/configuration`, `/runtime-state`, `/task-sessions`, `/config-revisions`, `/skills`; `POST /api/agents/{id}/skills/sync`; `GET /api/companies/{companyId}/agents`, `/built-in-agents`. +- Adapter/model currently configured for the user's agents: `omni/deepseek/deepseek-v4-flash` (referenced in the roadmap for Mouse-1/Switch). + +## Missing screens / roadmap written by the assistant +- `docs/PLAN-INTERFACE.md` (written, 7,843 bytes) maps every Paperclip object to a screen and lists missing screens. +- Identified gap: no screen shows agents or runs — this is the missing workspace. +- Proposed order: 1) Agents>agents (roster/runs/log, read-only; enter opens run log, p/P pause/resume, w wake), 2) Agents>settings (role, model, heartbeat, budget — maps to PATCH /api/agents/{id}), 3) Tool panels (`t` in Work>work opens chooser shell · lazygit · hunk · git log · ocr as a pane in current workspace), 4) Review panel in Work>work (last ocr verdict/findings), 5) Mouse-1/Switch only if installed. +- Rules stated: model change applies to agent's next run, never in-flight; budget changes recorded with who made it; merges stay a command, not a keystroke. + +## Evidence + +- Confidence: 90% +- Reason: Rich discovery: repo state change, tooling, API surface, and a written roadmap document with concrete commit hashes and paths. + +## Related + +- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]] + +## Related learned memories + +- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]] +- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness runtime caveats: ocr PATH in TUIOS panes, missing mouse/switch, installed tool paths]] +- decision: [[packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li|Keep 7 single-pane workspaces; map Paperclip objects to screens instead of splitting panes]] +- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Reverted 19-pane workspace split and wrote docs/PLAN-INTERFACE.md mapping Paperclip objects to screens]] +- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover|Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap]] +- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48|Rich discovery: home 2026-11-10 05:41:48]] +- context: [[packs/paperclip-harness/20-context/home|home]] diff --git a/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48.md b/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48.md new file mode 100644 index 0000000..1fd3399 --- /dev/null +++ b/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48.md @@ -0,0 +1,123 @@ +--- +type: session +id: session.paperclip-harness.rich-discovery-home-2026-11-10-05-41-48 +title: Rich discovery: home 2026-11-10 05:41:48 +status: active +source_of_truth: false +freshness: current +last_reviewed: 2026-11-10 +tags: + - pack/paperclip-harness + - agent-memory/session + - autolearn + - rich-discovery + - repo/home +--- + +# Rich discovery: home 2026-11-10 05:41:48 + +Sanitized rich discovery snapshot captured automatically from the completed turn. + + +## Source paths observed + +- `.pi/memory-vault/packs/paperclip-harness/20-context/tmp.md` +- `.pi/memory-vault/packs/paperclip-harness/60-observations/paperclip-harness-tui-editor-pitfall-illegal-rune-literal-af.md` +- `.pi/memory-vault/packs/paperclip-harness/50-decisions/use-real-tuios-panes-per-workspace-panels-as-panes.md` +- `.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-tui-workspace-rename-new-panels-commit-264.md` +- `.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-how-to-tell-whether-the-icons-loaded-2026-11-.md` +- `home/sam/.local/bin/ocr` +- `api/agents/me` +- `api/agents/me/connections` +- `api/agents/me/inbox-lite` +- `api/agents/me/inbox/mine` +- `api/agents/me/secret-proposals` +- `api/agents/me/secrets` +- `home/sam/paseo/projects/paperclip-harness/docs/PLAN-INTERFACE.md` +- `run/current-system/sw/bin/lazygit` +- `etc/profiles/per-user/sam/bin/hunk` +- `omni/deepseek/deepseek-v4-flash` + +## Final answer snapshot + +Reverted. Seven workspaces, one pane each, all drawing again — that is the state you had, restored. + +I was wrong to split it. You said panels were missing and I read that as "make panes", then made nineteen. That was my invention, not your request. + +## ocr is installed — it has already blocked a merge + +``` +/home/sam/.local/bin/ocr +open-code-review v1.13.6 +``` + +It is the review gate in `bin/merge`. On `AUD-165` it reported a **critical** finding — `rm -rf "$HOME/tmp"` empties `/tmp` when `HOME` is unset — and the merge was refused; `cleanup.sh` never reached `main`. The Repair screen once said it was missing because a pane started by TUIOS gets a smaller PATH than your shell; that check now looks in `~/.local/bin` and the system profiles as well. + +So the review is real, and the interface lacks a place to show the verdict. That is item 4 in the plan. + +## Where you see the Pi agents: nowhere yet, and that is the real gap + +Paperclip has 281 runs recorded on this instance, each with a 300 KB log I can read: + +``` +GET /api/companies/{id}/heartbeat-runs → 281 runs +GET /api/heartbeat-runs/{runId}/log → the agent's own output, grows while it runs +POST /api/agents/{id}/pause | /resume | /wakeup +PATCH /api/agents/{id} {role, adapterConfig (model), runtimeConfig, budgetMonthlyCents} +``` + +No screen shows any of it. That is the missing workspace, and it is next. + +## The plan, in Paperclip's terms + +`docs/PLAN-INTERFACE.md` now holds it, with a table of every Paperclip object and the screen that shows it. The parts that do not exist: + +**Agents>agents** — one pane, tabs `roster | runs | log`: +- roster: every agent with its state, current task, spend and budget +- runs: the company's runs — agent, task, state, what woke it, duration, cost +- log: the tail of the chosen run, which is the closest thing to watching Pi work + +Actions: `enter` opens a run's log, `p`/`P` pause and resume an agent, `w` wakes one. + +**Agents>settings** — role, job title, who it reports to, model, heartbeat, monthly budget. These map straight onto `PATCH /api/agents/{id}`. Two rules: a model change applies to that agent's next run, never one in flight; a budget change is recorded with who made it. + +**Tool panels** — Lazygit and Hunk are installed (`/run/current-system/sw/bin/lazygit`, `/etc/profiles/per-user/sam/bin/hunk`) and are **not** wired in. Proposal: `t` in `Work>work` opens a chooser — `shell · lazygit · hunk · git log · ocr` — as a pane in the current workspace, `esc` closes it and focus returns. Nothing is created until you ask. + +**Review panel** in `Work>work` — the last verdict and its findings, blocking ones in red, beside the diff. The merge stays a command, because a merge is a decision with a lock, not a keystroke. + +## Mouse-1 and Switch + +Measured: **not installed** — no `mouse`, `mouse1` or `switch` binary here, and nothing serving them locally. So nothing can be built on them yet. + +Where they would fit honestly: cheap, repeatable judgement — "is this task ready", "does this comment answer the question", "which agent should take this" — not prose. That comes after agents, settings, tool panels and the review panel, and only once they are installed and measured against the model already configured for your agents (`omni/deepseek/deepseek-v4-flash`). + +## Order + +1. Agents>agents (read-only, cannot break anything) +2. Agents>settings (role, model, budget, pause/resume) +3. Tool panels (`t` → Lazygit, Hunk, shell, ocr) +4. Review panel in Work>work +5. Mouse-1 / Switch, if installed + +Each is one pane, one program, and none of them changes the layout you have now. + +Committed as `7f18c54`. Say which item you want first and I will build only that. + +## Evidence + +- Confidence: 86% +- Reason: Preserve full sanitized discovery detail from a rich investigation turn + +## Related + +- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]] + +## Related learned memories + +- context: [[packs/paperclip-harness/20-context/paperclip-harness|paperclip-harness]] +- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness runtime caveats: ocr PATH in TUIOS panes, missing mouse/switch, installed tool paths]] +- decision: [[packs/paperclip-harness/50-decisions/paperclip-harness-ui-six-workspaces-spec-first-four-panel-li|Keep 7 single-pane workspaces; map Paperclip objects to screens instead of splitting panes]] +- action: [[packs/paperclip-harness/40-actions/2026-11-10-wrote-docs-ui-layout-md-and-three-live-layout-mockups|Reverted 19-pane workspace split and wrote docs/PLAN-INTERFACE.md mapping Paperclip objects to screens]] +- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-reverted-pane-split-interface-gap-discover|Paperclip-harness — reverted pane split, interface gap discovery, and PLAN-INTERFACE roadmap]] +- session: [[packs/paperclip-harness/80-sessions/rich-discovery-home-2026-11-10-05-41-48|Rich discovery: home 2026-11-10 05:41:48]] +- context: [[packs/paperclip-harness/20-context/home|home]] diff --git a/bin/board b/bin/board index 15594e4..775b666 100755 --- a/bin/board +++ b/bin/board @@ -47,6 +47,7 @@ def pages(cfg, folder): ("shell", "Shell", "exec zsh -i"), ("board", "Board", "%s -screen board -project %s -folder %s" % (shlex.quote(bin_path), proj, shlex.quote(folder))), ("work", "Work", "%s -screen work -folder %s" % (shlex.quote(bin_path), shlex.quote(folder))), + ("agents", "Agents", "%s -screen agents -folder %s" % (shlex.quote(bin_path), shlex.quote(folder))), ("reply", "Reply", "%s -screen reply -folder %s" % (shlex.quote(bin_path), shlex.quote(folder))), ("new", "New task", "%s -screen new -project %s" % (shlex.quote(bin_path), proj)), ("project", "Project", "%s -screen project -folder %s" % (shlex.quote(bin_path), shlex.quote(folder))), diff --git a/bin/harness b/bin/harness index 4e29747..fad101b 100755 Binary files a/bin/harness and b/bin/harness differ diff --git a/docs/WORKSPACES.md b/docs/WORKSPACES.md new file mode 100644 index 0000000..4d7ee85 --- /dev/null +++ b/docs/WORKSPACES.md @@ -0,0 +1,96 @@ +# Every workspace and pane, at the end + +The finished shape, so a gap is visible at a glance. One pane per workspace unless stated; +panels are what the pane's program draws, switched with Tab or the tab keys. + +Status: **built** means running in the project session now. **missing** means planned, not +written yet. + +## 1. Shell + +| Pane | Shows | Status | +|---|---|---| +| `Shell>shell` | a prompt in the project folder | built | + +## 2. Board + +| Pane | Shows | Status | +|---|---|---| +| `Board>tasks` | tasks grouped todo, running, blocked, review, done; each row: identifier, state, assignee, title, what it waits on | built | +| `Board>detail` | the chosen task: state, assignee, blockers, verify command, last run, cost | built | +| `Board>needs` | decisions waiting on a person; second tab: the chosen task's activity | built | + +## 3. Work + +| Pane | Shows | Status | +|---|---|---| +| `Work>runs` | run timeline and activity, with cost per run | built | +| `Work>change` | worktree, branch, base, commits, changed files, the diff | built | +| `Work>facts` | task, state, worktree path, branch, base, cost | built | +| `Work>review` | the last ocr verdict for this task: findings, blocking ones in red, and whether a merge would be refused | **missing** | + +## 4. Reply + +| Pane | Shows | Status | +|---|---|---| +| `Reply>task` | the task above the conversation: identifier, state, assignee, verify command | built | +| `Reply>thread` | every message, with author and time | built | +| `Reply>replybox` | the reply box, and the state that will be sent with it | built | + +## 5. New task + +| Pane | Shows | Status | +|---|---|---| +| `New task>form` | title, what it must do, what must finish first, the command that proves it is done, whether to assign | built | +| `New task>guidance` | what a task needs: one thing, a command, a boundary, a blocker | built | +| `New task>preview` | exactly the description the agent will receive | built | + +## 6. Agents — the missing workspace + +| Pane | Shows | Status | +|---|---|---| +| `Agents>roster` | every agent: name, state, spend this month, budget | built | +| `Agents>runs` | the company's runs, newest first: agent, task, state, what woke it, duration | built | +| `Agents>log` | the chosen run's output, deltas gathered back into paragraphs — this is watching Pi work | built | +| `Agents>settings` | role, job title, who it reports to, model, heartbeat, monthly budget | **missing** | + +## 7. Project + +| Pane | Shows | Status | +|---|---|---| +| `Project>project` | project, folder, machine, session, agents | built | +| `Project>costs` | spend per task and the total | built | +| `Project>settings` | the project's own file, and the limits Paperclip holds | built | + +## 8. Repair + +| Pane | Shows | Status | +|---|---|---| +| `Repair>repair` | health: three-state checks and an icon test row | built | +| `Repair>layout` | what TUIOS actually has, read live | built | +| `Repair>rebuild-log` | the last rebuild's output | built | + +## Tool panes — any of these, in any workspace, on demand + +A tool pane is not part of a layout. `t` opens a chooser in the workspace you are in, the +tool appears beside what you were reading, `esc` closes it and focus returns. This is how +Yazi, fzf, Neovim, Lazygit, Hunk and anything else fits without being rebuilt by us. + +| Tool | What it is for | Status | +|---|---|---| +| Lazygit | branches, commits, staging, history | installed, **not wired** | +| Hunk | reading a change as a review | installed, **not wired** | +| Yazi | files | installed, **not wired** | +| Neovim | editing a file the task touched | installed, **not wired** | +| fzf | picking anything | installed, **not wired** | +| shell | any command the interface does not cover | built (`Shell>shell`) | +| ocr | the review of the current change | installed, behind `bin/review` | + +## Count + +- **21 panels built**, across 8 workspaces. +- **2 panels missing**: `Work>review` and `Agents>settings`. +- **6 tool panes** to wire: Lazygit, Hunk, Yazi, Neovim, fzf, ocr. + +Nothing in the missing list changes the layout: each is one more panel inside a pane that +already exists, or one pane-opener for a tool. diff --git a/tui/agents.go b/tui/agents.go new file mode 100644 index 0000000..19666b0 --- /dev/null +++ b/tui/agents.go @@ -0,0 +1,633 @@ +package main + +// The Agents workspace: who is working, on what, and for how much. +// +// This is the view the project was missing. Paperclip already records every +// agent, every run and every run's output; nothing here is invented, and +// nothing here is stored. Three panels in one pane, switched with tabs: +// +// roster every agent, its state, its spend and its budget +// runs every run in the company, newest first +// log the output of the chosen run — watching Pi work +// +// Pause, resume and wake are the only writes, and each one is a single call to +// Paperclip, which owns the agent. + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "paperclip-harness/tui/paperclip" +) + +type agentsMsg struct { + agents []paperclip.Agent + names map[string]string + err error +} + +// runsMsg is separate from agentsMsg on purpose: the company's run list is over +// a megabyte and takes eight seconds, while the roster is small and instant. +// Reading them together made the whole screen wait for the slow one. +type runsMsg struct { + runs []paperclip.AgentRun + err error +} + +type runLogMsg struct { + runID string + text string + err error +} + +type agentsModel struct { + client *paperclip.Client + folder string + + agents []paperclip.Agent + runs []paperclip.AgentRun + names map[string]string + logRunID string + logText string + err error + notice string + readAt time.Time + + tab int + cursor int + scroll int + showHelp bool + + width, height int +} + +var agentTabs = []string{"roster", "runs", "log"} + +func newAgentsModel(c *paperclip.Client, folder string) agentsModel { + return agentsModel{client: c, folder: folder} +} + +func (m agentsModel) Init() tea.Cmd { + return tea.Batch(m.load(), tickEvery(5*time.Second)) +} + +func (m agentsModel) load() tea.Cmd { + c := m.client + return func() tea.Msg { + agents, err := c.Agents() + if err != nil { + return agentsMsg{err: err} + } + names := map[string]string{} + for _, a := range agents { + names[a.ID] = a.Name + } + return agentsMsg{agents: agents, names: names} + } +} + +// loadRuns reads the company's runs. Called when the runs tab is opened and +// when it is asked for, never on the timer: it is a megabyte and eight seconds. +func (m agentsModel) loadRuns() tea.Cmd { + c := m.client + return func() tea.Msg { + runs, err := c.CompanyRuns() + if err != nil { + return runsMsg{err: err} + } + if len(runs) > 200 { + runs = runs[:200] + } + return runsMsg{runs: runs} + } +} + +func (m agentsModel) loadLog(runID string) tea.Cmd { + c := m.client + return func() tea.Msg { + if runID == "" { + return runLogMsg{} + } + // The tail, not the whole thing: a run log here is tens of thousands of + // lines and only the end says what is happening now. + // Read the whole log: the stored form is one JSON envelope, and a + // partial read cuts the envelope in half and makes it unreadable. + text, err := c.RunLog(runID, 8<<20) + return runLogMsg{runID: runID, text: text, err: err} + } +} + +func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width, m.height = msg.Width, msg.Height + return m, nil + + case tickMsg: + return m, tea.Batch(m.load(), tickEvery(5*time.Second)) + + case tea.KeyMsg: + if m.showHelp { + m.showHelp = false + return m, nil + } + switch msg.String() { + case "?": + m.showHelp = true + return m, nil + case "q", "ctrl+c": + return m, tea.Quit + case "r": + switch m.tab { + case 1: + return m, m.loadRuns() + case 2: + return m, m.loadLog(m.logRunID) + } + return m, m.load() + case "R": + if err := startRebuild(m.folder); err != nil { + m.err = err + return m, nil + } + return m, tea.Quit + case "[", "h": + if m.tab > 0 { + m.tab-- + m.scroll = 0 + } + return m, nil + case "]", "l", "tab": + if m.tab < len(agentTabs)-1 { + m.tab++ + m.scroll = 0 + if m.tab == 1 && len(m.runs) == 0 { + return m, m.loadRuns() + } + } + return m, nil + case "up", "k": + m.scroll-- + return m, nil + case "down", "j": + m.scroll++ + return m, nil + case "g": + m.scroll = 0 + return m, nil + case "G": + m.scroll = 1 << 30 + return m, nil + case "enter": + if m.tab == 1 { + if r := m.chosenRun(); r != nil { + m.logRunID = r.ID + m.tab = 2 + m.scroll = 1 << 30 + return m, m.loadLog(r.ID) + } + } + return m, nil + case "p", "P", "w": + agent := m.chosenAgent() + if agent == nil { + return m, nil + } + c := m.client + action := msg.String() + return m, func() tea.Msg { + var err error + switch action { + case "p": + err = c.PauseAgent(agent.ID) + case "P": + err = c.ResumeAgent(agent.ID) + case "w": + err = c.WakeAgent(agent.ID) + } + if err != nil { + return errMsg{err} + } + agents, err := c.Agents() + if err != nil { + return errMsg{err} + } + return agentsMsg{agents: agents} + } + case "esc": + return m, nil + } + + case agentsMsg: + if msg.agents != nil { + m.agents = msg.agents + } + m.names = msg.names + if msg.err != nil { + m.err = msg.err + } else { + m.err = nil + } + m.readAt = time.Now() + return m, nil + + case runsMsg: + if msg.runs != nil { + m.runs = msg.runs + m.readAt = time.Now() + } + if msg.err != nil { + m.err = msg.err + } else { + m.err = nil + } + return m, nil + + case runLogMsg: + if msg.runID != "" { + m.logRunID = msg.runID + } + m.logText = msg.text + if msg.err != nil { + m.err = msg.err + } + return m, nil + + case errMsg: + m.err = msg.err + return m, nil + } + return m, nil +} + +// chosenRun is the run the roster cursor is on, in the runs tab. +func (m agentsModel) chosenRun() *paperclip.AgentRun { + if m.cursor >= 0 && m.cursor < len(m.runs) { + return &m.runs[m.cursor] + } + return nil +} + +// chosenAgent is the agent whose name matches the chosen run, or the first one. +func (m agentsModel) chosenAgent() *paperclip.Agent { + if r := m.chosenRun(); r != nil { + for i := range m.agents { + if m.agents[i].ID == r.AgentID { + return &m.agents[i] + } + } + } + if len(m.agents) > 0 { + return &m.agents[0] + } + return nil +} + +func (m agentsModel) View() string { + if m.width == 0 || m.height == 0 { + return "reading…" + } + if m.width < 80 || m.height < 24 { + return lipgloss.NewStyle().Width(m.width).Height(m.height).Render( + fmt.Sprintf("This workspace needs 80 × 24.\nThis panel is %d × %d.", m.width, m.height)) + } + + if m.showHelp { + return helpOverlay("agents", [][2]string{ + {"[ ] h l", "roster, runs, log"}, + {"enter", "open the chosen run's log"}, + {"p / P", "pause / resume the agent"}, + {"w", "wake the agent now"}, + {"↑ ↓ j k", "scroll"}, + {"r / R", "read again / rebuild"}, + {"q", "leave"}, + }, m.width, m.height) + } + + head := titleStyle.Render("Agents") + mutedStyle2().Render(" · who is working, on what, for how much") + state := "read " + m.readAt.Format("15:04:05") + if m.readAt.IsZero() { + state = "reading…" + } + + var body []string + switch m.tab { + case 0: + body = m.rosterLines(m.width - 4) + case 1: + body = m.runLines(m.width - 4) + default: + body = m.logLines(m.width - 4) + } + + p := panel{title: fmt.Sprintf("agents · %d agent(s), %d run(s)", len(m.agents), len(m.runs)), + width: m.width, height: m.height - 2, tabs: agentTabs, active: m.tab, focused: true} + visible := p.bodyHeight() + maxScroll := max(0, len(body)-visible) + if m.scroll > maxScroll { + m.scroll = maxScroll + } + if m.scroll < 0 { + m.scroll = 0 + } + from := m.scroll + to := min(len(body), from+visible) + + footer := hintLine([]string{ + keyHint("[ ]", "roster, runs, log"), + keyHint("enter", "open a run"), + keyHint("p P w", "pause, resume, wake"), + keyHint("r", "read again"), + keyHint("?", "keys"), + }, m.width) + if m.notice != "" { + footer = lipgloss.NewStyle().Foreground(lipgloss.Color(active.Success)).Render(m.notice) + } + if m.err != nil { + footer = lipgloss.NewStyle().Foreground(lipgloss.Color(active.Error)).Render(truncateVisible("error: "+m.err.Error(), m.width)) + } + + gap := max(2, m.width-lipgloss.Width(head)-lipgloss.Width(state)-1) + return strings.Join([]string{ + head + strings.Repeat(" ", gap) + mutedStyle2().Render(state), + p.render(body[from:to]), + footer, + }, "\n") +} + +func (m agentsModel) rosterLines(width int) []string { + out := []string{"", dimStyle().Render(fmt.Sprintf(" %-16s %-10s %-18s %8s %8s", "agent", "state", "working on", "spent", "budget"))} + for _, a := range m.agents { + mark := icons.Todo + if a.Status == "idle" { + mark = icons.Done + } + working := "—" + for _, r := range m.runs { + if r.AgentID == a.ID && r.Status == "running" { + working = m.taskLabel(r) + mark = icons.Running + break + } + } + out = append(out, fmt.Sprintf(" %s %-16s %-10s %-18s %6d c %6d c", + mark, a.Name, a.Status, truncateVisible(working, 18), a.SpentCents, a.BudgetCents)) + } + out = append(out, "") + out = append(out, dimStyle().Render(" c means cents. Budget is set in Paperclip; change it there, not here.")) + return out +} + +func (m agentsModel) taskLabel(r paperclip.AgentRun) string { + if r.Snapshot.IssueID != "" { + return short(r.Snapshot.IssueID) + } + if r.Snapshot.TaskKey != "" { + return short(r.Snapshot.TaskKey) + } + return "—" +} + +func (m agentsModel) runLines(width int) []string { + if len(m.runs) == 0 { + return []string{"", mutedStyle2().Render("no runs recorded")} + } + out := []string{"", dimStyle().Render(fmt.Sprintf(" %-10s %-11s %-9s %-12s %-8s %s", + "when", "agent", "state", "woken by", "took", "task"))} + for _, r := range m.runs { + when := "-" + if t, err := time.Parse(time.RFC3339, r.StartedAt); err == nil { + when = t.Local().Format("15:04:05") + } + took := "-" + if r.FinishedAt != "" { + if s, err := time.Parse(time.RFC3339, r.StartedAt); err == nil { + if f, err := time.Parse(time.RFC3339, r.FinishedAt); err == nil { + took = f.Sub(s).Round(time.Second).String() + } + } + } else if r.Status == "running" { + took = "running" + } + colour := active.Text + switch r.Status { + case "running": + colour = active.Info + case "succeeded": + colour = active.Success + case "failed": + colour = active.Error + } + out = append(out, fmt.Sprintf(" %-10s %-11s %-9s %-12s %-8s %s", + when, truncateVisible(m.names[r.AgentID], 11), + lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(fmt.Sprintf("%-9s", r.Status)), + r.Source, took, m.taskLabel(r))) + } + return out +} + +// logEntry is one readable line out of a run log. +type logEntry struct { + when string + stream string + line string +} + +// decodeRunLog turns what Paperclip stores into something a person can read. +// +// A run log is not lines of prose: it is an envelope around newline-separated +// JSON, and most of those are token-by-token streaming events. Printed raw they +// are unreadable and they redraw the panel. So deltas are gathered back into +// paragraphs, and everything else becomes one labelled line. +func decodeRunLog(raw string) []logEntry { + var envelope struct { + Content string `json:"content"` + } + content := raw + if json.Unmarshal([]byte(raw), &envelope) == nil && envelope.Content != "" { + content = envelope.Content + } + + var out []logEntry + var buf strings.Builder + bufWhen := "" + flush := func() { + text := strings.TrimSpace(cleanText(buf.String())) + buf.Reset() + if text == "" { + return + } + for _, para := range strings.Split(text, "\n") { + para = strings.TrimSpace(para) + if para != "" { + out = append(out, logEntry{when: bufWhen, stream: "thinking", line: para}) + } + } + } + + for _, line := range strings.Split(content, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + var rec struct { + TS string `json:"ts"` + Stream string `json:"stream"` + Chunk string `json:"chunk"` + } + text := line + when := "" + if json.Unmarshal([]byte(line), &rec) == nil && rec.Chunk != "" { + text, when = rec.Chunk, shortTime(rec.TS) + } + + var ev struct { + Type string `json:"type"` + Delta string `json:"delta"` + Message struct { + Role string `json:"role"` + Content []struct { + Type string `json:"type"` + Text string `json:"text"` + } `json:"content"` + } `json:"message"` + AssistantMessageEvent struct { + Type string `json:"type"` + Delta string `json:"delta"` + } `json:"assistantMessageEvent"` + } + + if json.Unmarshal([]byte(text), &ev) != nil { + // Not JSON we know: show it as a line, trimmed. + if t := strings.TrimSpace(cleanText(text)); t != "" { + out = append(out, logEntry{when: when, line: t}) + } + continue + } + + kind := ev.AssistantMessageEvent.Type + if ev.Type == "message_update" && strings.HasSuffix(kind, "_delta") { + if bufWhen == "" { + bufWhen = when + } + buf.WriteString(ev.AssistantMessageEvent.Delta) + continue + } + + // Anything else ends the paragraph that was being gathered. + flush() + bufWhen = when + + switch ev.Type { + case "message_start", "message_end": + role := ev.Message.Role + if role == "" { + continue + } + for _, c := range ev.Message.Content { + t := strings.TrimSpace(cleanText(c.Text)) + if t == "" { + continue + } + if i := strings.Index(t, "\n"); i > 0 { + t = t[:i] + } + out = append(out, logEntry{when: when, stream: "prompt", line: role + ": " + t}) + break + } + case "tool_call", "tool_result", "tool_use": + out = append(out, logEntry{when: when, stream: "tool", line: ev.Type}) + case "turn_start", "turn_end", "session", "agent_start", "agent_end": + out = append(out, logEntry{when: when, stream: "event", line: ev.Type}) + default: + if kind != "" { + out = append(out, logEntry{when: when, stream: "event", line: kind}) + } + } + } + flush() + + if len(out) == 0 { + for _, line := range strings.Split(strings.TrimRight(content, "\n"), "\n") { + if t := strings.TrimSpace(cleanText(line)); t != "" { + out = append(out, logEntry{line: t}) + } + } + } + return out +} + +// shortTime is the clock time of a log line, in the reader's timezone. +func shortTime(ts string) string { + if t, err := time.Parse(time.RFC3339Nano, ts); err == nil { + return t.Local().Format("15:04:05") + } + return "" +} + +// cleanText removes escape sequences and control characters, so a log cannot +// repaint the panel it is drawn in. +func cleanText(s string) string { + var b strings.Builder + skip := false + for _, r := range s { + if r == 0x1b { + skip = true + continue + } + if skip { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + skip = false + } + continue + } + if r == '\t' { + b.WriteString(" ") + continue + } + if r < 0x20 && r != '\n' { + continue + } + b.WriteRune(r) + } + return b.String() +} + +func (m agentsModel) logLines(width int) []string { + if m.logRunID == "" { + return []string{"", mutedStyle2().Render("choose a run on the runs tab, then press enter")} + } + if m.logText == "" { + return []string{"", mutedStyle2().Render("no output recorded for this run")} + } + var out []string + out = append(out, "", dimStyle().Render(" run "+short(m.logRunID))) + entries := decodeRunLog(m.logText) + if len(entries) > 2000 { + entries = entries[len(entries)-2000:] + } + for _, e := range entries { + if lipgloss.Width(e.line) > width { + e.line = truncateVisible(e.line, width) + } + colour := active.Text + switch e.stream { + case "thinking": + colour = active.Muted + case "prompt": + colour = active.Info + case "tool": + colour = active.Warning + case "event": + colour = active.Overlay + } + out = append(out, " "+dimStyle().Render(e.when)+" "+ + lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(e.line)) + } + return out +} diff --git a/tui/frame.go b/tui/frame.go index f9a72f6..b718239 100644 --- a/tui/frame.go +++ b/tui/frame.go @@ -289,3 +289,12 @@ func helpOverlay(title string, entries [][2]string, w, h int) string { p := panel{title: title, width: boxW, height: boxH, focused: true} return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, p.render(lines)) } + +// short is the first eight characters of an identifier: enough to recognise a +// run or a task in a column, never enough to confuse two of them on one screen. +func short(id string) string { + if len(id) <= 8 { + return id + } + return id[:8] +} diff --git a/tui/main.go b/tui/main.go index 388eefa..5d72f7e 100644 --- a/tui/main.go +++ b/tui/main.go @@ -19,7 +19,7 @@ import ( const defaultAgent = "403de1f7-645c-4d35-bc7e-19b01d77bacc" func main() { - screen := flag.String("screen", "board", "which screen to run: board, work, reply, new, project, repair") + screen := flag.String("screen", "board", "which screen to run: board, work, reply, new, agents, project, repair") folder := flag.String("folder", "", "project folder, for the board screen (defaults to the current directory)") light := flag.String("theme", "", "colour variant: dark, light, or empty to follow the terminal") ascii := flag.Bool("ascii", false, "plain characters instead of Nerd Font icons") @@ -54,6 +54,13 @@ func main() { } m = newBoardModel(client, proj, dir) + case "agents": + dir := *folder + if dir == "" { + dir, _ = os.Getwd() + } + m = newAgentsModel(client, dir) + case "project": dir := *folder if dir == "" { @@ -127,7 +134,7 @@ func main() { m = newReplyModel(client, dir, id) default: - fmt.Fprintf(os.Stderr, "unknown screen %q: use board, work, reply, new, project or repair\n", *screen) + fmt.Fprintf(os.Stderr, "unknown screen %q: use board, work, reply, new, agents, project or repair\n", *screen) os.Exit(1) } diff --git a/tui/paperclip/client.go b/tui/paperclip/client.go index 1834a95..9c0f2d8 100644 --- a/tui/paperclip/client.go +++ b/tui/paperclip/client.go @@ -373,3 +373,71 @@ func (c *Client) CostSummary(issueID string) (Cost, error) { func (c *Client) Health() error { return c.do("GET", "/api/health", nil, nil) } + +// AgentRun is one heartbeat run in the company, whichever agent ran it. +type AgentRun struct { + ID string `json:"id"` + AgentID string `json:"agentId"` + Status string `json:"status"` + Source string `json:"invocationSource"` + StartedAt string `json:"createdAt"` + FinishedAt string `json:"finishedAt"` + ErrorCode string `json:"errorCode"` + LastOutputAt string `json:"lastOutputAt"` + LastOutputBytes int `json:"lastOutputBytes"` + // The task a run worked on lives inside the run's context snapshot, not on + // the run itself. + Snapshot struct { + IssueID string `json:"issueId"` + TaskKey string `json:"taskKey"` + } `json:"contextSnapshot"` +} + +// Runs returns every run in the company, newest first. +func (c *Client) CompanyRuns() ([]AgentRun, error) { + var out []AgentRun + err := c.do("GET", "/api/companies/"+c.Company+"/heartbeat-runs", nil, &out) + return out, err +} + +// RunLog returns the recorded output of one run. +func (c *Client) RunLog(runID string, maxBytes int) (string, error) { + var raw []byte + req, err := http.NewRequest("GET", c.BaseURL+"/api/heartbeat-runs/"+runID+"/log", nil) + if err != nil { + return "", err + } + req.Header.Set("Authorization", "Bearer "+c.token) + resp, err := c.http.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode > 299 { + b, _ := io.ReadAll(io.LimitReader(resp.Body, 400)) + return "", fmt.Errorf("run log returned %d: %s", resp.StatusCode, snippet(b)) + } + raw, err = io.ReadAll(io.LimitReader(resp.Body, int64(maxBytes))) + if err != nil { + return "", err + } + if len(raw) == maxBytes { + return string(raw) + "\n… (only the last " + fmt.Sprint(maxBytes) + " bytes are shown)", nil + } + return string(raw), nil +} + +// PauseAgent and ResumeAgent are the two ways to stop and start an agent. +func (c *Client) PauseAgent(agentID string) error { + return c.do("POST", "/api/agents/"+agentID+"/pause", map[string]any{}, nil) +} + +func (c *Client) ResumeAgent(agentID string) error { + return c.do("POST", "/api/agents/"+agentID+"/resume", map[string]any{}, nil) +} + +// WakeAgent asks Paperclip to start a run for this agent now. +func (c *Client) WakeAgent(agentID string) error { + return c.do("POST", "/api/agents/"+agentID+"/wakeup", + map[string]any{"source": "on_demand", "triggerDetail": "manual", "forceFreshSession": false}, nil) +}