Agents workspace: roster, runs, and a readable run log; layout now eight workspaces
This commit is contained in:
@@ -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 <!-- same-turn-2026-11-10 05:41:48 -->
|
||||
|
||||
- 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]]
|
||||
|
||||
@@ -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 <!-- same-turn-2026-11-10 05:41:48 -->
|
||||
|
||||
- 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]]
|
||||
|
||||
@@ -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 <!-- same-turn-2026-11-10 05:41:48 -->
|
||||
|
||||
- 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]]
|
||||
|
||||
@@ -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 <!-- same-turn-2026-11-10 05:41:48 -->
|
||||
|
||||
- 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]]
|
||||
|
||||
@@ -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 <!-- same-turn-2026-11-10 05:41:48 -->
|
||||
|
||||
- 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]]
|
||||
|
||||
@@ -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 <!-- same-turn-2026-11-10 05:41:48 -->
|
||||
|
||||
- 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]]
|
||||
@@ -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 <!-- same-turn-2026-11-10 05:41:48 -->
|
||||
|
||||
- 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]]
|
||||
@@ -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))),
|
||||
|
||||
BIN
bin/harness
BIN
bin/harness
Binary file not shown.
96
docs/WORKSPACES.md
Normal file
96
docs/WORKSPACES.md
Normal file
@@ -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.
|
||||
633
tui/agents.go
Normal file
633
tui/agents.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
|
||||
11
tui/main.go
11
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user