Agents pane: project-scoped runs with selection, roster cursor, editable settings
This commit is contained in:
@@ -385,3 +385,84 @@ 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/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]]
|
- 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]]
|
- context: [[packs/paperclip-harness/20-context/tmp|.config]]
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update (2026-11-10 05:59:07)
|
||||||
|
|
||||||
|
## Rich discovery update (2026-11-10 05:59:07)
|
||||||
|
|
||||||
|
The full list, at the end of the project, with what exists today marked.
|
||||||
|
|
||||||
|
## Workspaces and panes
|
||||||
|
|
||||||
|
| Workspace | Pane | Shows | Now |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Shell** | shell | a prompt in the project folder | built |
|
||||||
|
| **Board** | board | tasks by group · chosen task · needs you | built |
|
||||||
|
| **Work** | work | runs · change (diff) · facts | built |
|
||||||
|
| **Work** | review | the last ocr verdict and its blocking findings | **missing** |
|
||||||
|
| **Agents** | agents | roster · runs · log — who is working, on what | built today |
|
||||||
|
| **Agents** | settings | role, title, reports-to, model, heartbeat, budget | **missing** |
|
||||||
|
| **Reply** | reply | the task · the thread · the reply box | built |
|
||||||
|
| **New task** | new | the form · guidance · preview | built |
|
||||||
|
| **Project** | project | facts · costs · settings and limits | built |
|
||||||
|
| **Repair** | repair | health · layout · rebuild log | built |
|
||||||
|
| **any** | tool | Lazygit, Hunk, Yazi, Neovim, fzf, shell, ocr — opened with `t`, closed with `esc` | **not wired** |
|
||||||
|
|
||||||
|
So: 8 workspaces, 10 named panes, 21 panels built, 2 panels and the tool panes still to do.
|
||||||
|
|
||||||
|
## What the Agents pane does now
|
||||||
|
|
||||||
|
Workspace `Agents`, pane `agents`, three tabs:
|
||||||
|
|
||||||
|
**roster** — every agent with its state, spend and budget:
|
||||||
|
```
|
||||||
|
researcher idle 0 c 2000 c
|
||||||
|
Builder idle 10 c 5000 c
|
||||||
|
```
|
||||||
|
|
||||||
|
**runs** — the company's runs, newest first, with agent, state, what woke it, duration and task:
|
||||||
|
```
|
||||||
|
15:56:27 Builder succeeded automation 32s bb4dc72b
|
||||||
|
12:41:47 Builder succeeded assignment 1m27s bb4dc72b
|
||||||
|
```
|
||||||
|
|
||||||
|
**log** — `enter` on a run shows its output. This is watching Pi work: the stored log is a wall of JSON and token-by-token deltas, so it is gathered back into readable paragraphs:
|
||||||
|
```
|
||||||
|
15:56:41 session
|
||||||
|
15:56:42 user: ## Paperclip Resume Delta
|
||||||
|
15:56:43 The user posted "Here is a test reply" — a test comment…
|
||||||
|
15:56:43 Let me verify the file still exists and commit present…
|
||||||
|
```
|
||||||
|
|
||||||
|
`p`, `P` and `w` pause, resume and wake the selected agent. I tested the pause/resume calls directly against Paperclip on `Documenter`: `idle → paused → idle`.
|
||||||
|
|
||||||
|
One bug worth knowing: the company run list is 1.1 MB and takes 8 seconds, while the roster is instant. Reading them together made the whole pane appear empty. They are now separate reads — the roster on the timer, the runs when you open that tab.
|
||||||
|
|
||||||
|
## Still missing, in order
|
||||||
|
|
||||||
|
1. **Agents>settings** — role, title, reports-to, model, heartbeat, budget. All of it is a single `PATCH /api/agents/{id}`, which accepts exactly those fields.
|
||||||
|
2. **Work>review** — the ocr verdict beside the diff. ocr is installed and already refused a merge on `AUD-165`.
|
||||||
|
3. **Tool panes** — `t` opens Lazygit, Hunk, Yazi, Neovim, fzf or ocr in the current workspace; `esc` closes it. This is the answer to "any pane can work with any terminal tool" and needs no reimplementation.
|
||||||
|
|
||||||
|
The layout is now eight workspaces, one pane each, and `R` rebuilds them. `docs/WORKSPACES.md` holds this table and is updated; committed as `b15216a`.
|
||||||
|
|
||||||
|
Say which of the three you want next, and I will build only that.
|
||||||
|
|
||||||
|
## Source paths observed
|
||||||
|
|
||||||
|
- `d4921013-e5da-4d88-8016-57d5746e2d03/403de1f7-645c-4d35-bc7e-19b01d77bacc/df306e7b-f56b-4cd8-bf68-bab0cbfb27fa.ndjson`
|
||||||
|
- `tmp/pc-sandbox-2026-11-10/.paperclip/worktrees/AUD-166-add-a-changelog-entry-for-the-harness`
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Confidence: 84%
|
||||||
|
- Reason: Update component/repository context with detailed discovered structure and configuration
|
||||||
|
|
||||||
|
## Related learned memories <!-- same-turn-2026-11-10 05:59:07 -->
|
||||||
|
|
||||||
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-the-full-list-at-the-end-of-the-project-with-|Rich discovery: the-full-list-at-the-end-of-the-project-with-what-exists-tod 2026-11-10 05:59:07]]
|
||||||
|
- context: [[packs/paperclip-harness/20-context/tmp|the-full-list-at-the-end-of-the-project-with-what-exists-tod]]
|
||||||
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|the-full-list-at-the-end-of-the-project-with-what-exists-tod repository structure and configuration patterns]]
|
||||||
|
|||||||
@@ -898,3 +898,84 @@ Committed as `7f18c54`. Say which item you want first and I will build only that
|
|||||||
- 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/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]]
|
- 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]]
|
- context: [[packs/paperclip-harness/20-context/home|home]]
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update (2026-11-10 05:59:07)
|
||||||
|
|
||||||
|
## Structural/configuration observations (2026-11-10 05:59:07)
|
||||||
|
|
||||||
|
The full list, at the end of the project, with what exists today marked.
|
||||||
|
|
||||||
|
## Workspaces and panes
|
||||||
|
|
||||||
|
| Workspace | Pane | Shows | Now |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Shell** | shell | a prompt in the project folder | built |
|
||||||
|
| **Board** | board | tasks by group · chosen task · needs you | built |
|
||||||
|
| **Work** | work | runs · change (diff) · facts | built |
|
||||||
|
| **Work** | review | the last ocr verdict and its blocking findings | **missing** |
|
||||||
|
| **Agents** | agents | roster · runs · log — who is working, on what | built today |
|
||||||
|
| **Agents** | settings | role, title, reports-to, model, heartbeat, budget | **missing** |
|
||||||
|
| **Reply** | reply | the task · the thread · the reply box | built |
|
||||||
|
| **New task** | new | the form · guidance · preview | built |
|
||||||
|
| **Project** | project | facts · costs · settings and limits | built |
|
||||||
|
| **Repair** | repair | health · layout · rebuild log | built |
|
||||||
|
| **any** | tool | Lazygit, Hunk, Yazi, Neovim, fzf, shell, ocr — opened with `t`, closed with `esc` | **not wired** |
|
||||||
|
|
||||||
|
So: 8 workspaces, 10 named panes, 21 panels built, 2 panels and the tool panes still to do.
|
||||||
|
|
||||||
|
## What the Agents pane does now
|
||||||
|
|
||||||
|
Workspace `Agents`, pane `agents`, three tabs:
|
||||||
|
|
||||||
|
**roster** — every agent with its state, spend and budget:
|
||||||
|
```
|
||||||
|
researcher idle 0 c 2000 c
|
||||||
|
Builder idle 10 c 5000 c
|
||||||
|
```
|
||||||
|
|
||||||
|
**runs** — the company's runs, newest first, with agent, state, what woke it, duration and task:
|
||||||
|
```
|
||||||
|
15:56:27 Builder succeeded automation 32s bb4dc72b
|
||||||
|
12:41:47 Builder succeeded assignment 1m27s bb4dc72b
|
||||||
|
```
|
||||||
|
|
||||||
|
**log** — `enter` on a run shows its output. This is watching Pi work: the stored log is a wall of JSON and token-by-token deltas, so it is gathered back into readable paragraphs:
|
||||||
|
```
|
||||||
|
15:56:41 session
|
||||||
|
15:56:42 user: ## Paperclip Resume Delta
|
||||||
|
15:56:43 The user posted "Here is a test reply" — a test comment…
|
||||||
|
15:56:43 Let me verify the file still exists and commit present…
|
||||||
|
```
|
||||||
|
|
||||||
|
`p`, `P` and `w` pause, resume and wake the selected agent. I tested the pause/resume calls directly against Paperclip on `Documenter`: `idle → paused → idle`.
|
||||||
|
|
||||||
|
One bug worth knowing: the company run list is 1.1 MB and takes 8 seconds, while the roster is instant. Reading them together made the whole pane appear empty. They are now separate reads — the roster on the timer, the runs when you open that tab.
|
||||||
|
|
||||||
|
## Still missing, in order
|
||||||
|
|
||||||
|
1. **Agents>settings** — role, title, reports-to, model, heartbeat, budget. All of it is a single `PATCH /api/agents/{id}`, which accepts exactly those fields.
|
||||||
|
2. **Work>review** — the ocr verdict beside the diff. ocr is installed and already refused a merge on `AUD-165`.
|
||||||
|
3. **Tool panes** — `t` opens Lazygit, Hunk, Yazi, Neovim, fzf or ocr in the current workspace; `esc` closes it. This is the answer to "any pane can work with any terminal tool" and needs no reimplementation.
|
||||||
|
|
||||||
|
The layout is now eight workspaces, one pane each, and `R` rebuilds them. `docs/WORKSPACES.md` holds this table and is updated; committed as `b15216a`.
|
||||||
|
|
||||||
|
Say which of the three you want next, and I will build only that.
|
||||||
|
|
||||||
|
## Source paths observed
|
||||||
|
|
||||||
|
- `d4921013-e5da-4d88-8016-57d5746e2d03/403de1f7-645c-4d35-bc7e-19b01d77bacc/df306e7b-f56b-4cd8-bf68-bab0cbfb27fa.ndjson`
|
||||||
|
- `tmp/pc-sandbox-2026-11-10/.paperclip/worktrees/AUD-166-add-a-changelog-entry-for-the-harness`
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Confidence: 82%
|
||||||
|
- Reason: Captured factual repo structure/configuration patterns from rich discovery
|
||||||
|
|
||||||
|
## Related learned memories <!-- same-turn-2026-11-10 05:59:07 -->
|
||||||
|
|
||||||
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-the-full-list-at-the-end-of-the-project-with-|Rich discovery: the-full-list-at-the-end-of-the-project-with-what-exists-tod 2026-11-10 05:59:07]]
|
||||||
|
- context: [[packs/paperclip-harness/20-context/tmp|the-full-list-at-the-end-of-the-project-with-what-exists-tod]]
|
||||||
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|the-full-list-at-the-end-of-the-project-with-what-exists-tod repository structure and configuration patterns]]
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
type: session
|
||||||
|
id: session.paperclip-harness.rich-discovery-the-full-list-at-the-end-of-the-project-with-
|
||||||
|
title: Rich discovery: the-full-list-at-the-end-of-the-project-with-what-exists-tod 2026-11-10 05:59:07
|
||||||
|
status: active
|
||||||
|
source_of_truth: false
|
||||||
|
freshness: current
|
||||||
|
last_reviewed: 2026-11-10
|
||||||
|
tags:
|
||||||
|
- pack/paperclip-harness
|
||||||
|
- agent-memory/session
|
||||||
|
- autolearn
|
||||||
|
- rich-discovery
|
||||||
|
- repo/the-full-list-at-the-end-of-the-project-with-what-exists-tod
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rich discovery: the-full-list-at-the-end-of-the-project-with-what-exists-tod 2026-11-10 05:59:07
|
||||||
|
|
||||||
|
Sanitized rich discovery snapshot captured automatically from the completed turn.
|
||||||
|
|
||||||
|
|
||||||
|
## Source paths observed
|
||||||
|
|
||||||
|
- `d4921013-e5da-4d88-8016-57d5746e2d03/403de1f7-645c-4d35-bc7e-19b01d77bacc/df306e7b-f56b-4cd8-bf68-bab0cbfb27fa.ndjson`
|
||||||
|
- `tmp/pc-sandbox-2026-11-10/.paperclip/worktrees/AUD-166-add-a-changelog-entry-for-the-harness`
|
||||||
|
|
||||||
|
## Final answer snapshot
|
||||||
|
|
||||||
|
The full list, at the end of the project, with what exists today marked.
|
||||||
|
|
||||||
|
## Workspaces and panes
|
||||||
|
|
||||||
|
| Workspace | Pane | Shows | Now |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Shell** | shell | a prompt in the project folder | built |
|
||||||
|
| **Board** | board | tasks by group · chosen task · needs you | built |
|
||||||
|
| **Work** | work | runs · change (diff) · facts | built |
|
||||||
|
| **Work** | review | the last ocr verdict and its blocking findings | **missing** |
|
||||||
|
| **Agents** | agents | roster · runs · log — who is working, on what | built today |
|
||||||
|
| **Agents** | settings | role, title, reports-to, model, heartbeat, budget | **missing** |
|
||||||
|
| **Reply** | reply | the task · the thread · the reply box | built |
|
||||||
|
| **New task** | new | the form · guidance · preview | built |
|
||||||
|
| **Project** | project | facts · costs · settings and limits | built |
|
||||||
|
| **Repair** | repair | health · layout · rebuild log | built |
|
||||||
|
| **any** | tool | Lazygit, Hunk, Yazi, Neovim, fzf, shell, ocr — opened with `t`, closed with `esc` | **not wired** |
|
||||||
|
|
||||||
|
So: 8 workspaces, 10 named panes, 21 panels built, 2 panels and the tool panes still to do.
|
||||||
|
|
||||||
|
## What the Agents pane does now
|
||||||
|
|
||||||
|
Workspace `Agents`, pane `agents`, three tabs:
|
||||||
|
|
||||||
|
**roster** — every agent with its state, spend and budget:
|
||||||
|
```
|
||||||
|
researcher idle 0 c 2000 c
|
||||||
|
Builder idle 10 c 5000 c
|
||||||
|
```
|
||||||
|
|
||||||
|
**runs** — the company's runs, newest first, with agent, state, what woke it, duration and task:
|
||||||
|
```
|
||||||
|
15:56:27 Builder succeeded automation 32s bb4dc72b
|
||||||
|
12:41:47 Builder succeeded assignment 1m27s bb4dc72b
|
||||||
|
```
|
||||||
|
|
||||||
|
**log** — `enter` on a run shows its output. This is watching Pi work: the stored log is a wall of JSON and token-by-token deltas, so it is gathered back into readable paragraphs:
|
||||||
|
```
|
||||||
|
15:56:41 session
|
||||||
|
15:56:42 user: ## Paperclip Resume Delta
|
||||||
|
15:56:43 The user posted "Here is a test reply" — a test comment…
|
||||||
|
15:56:43 Let me verify the file still exists and commit present…
|
||||||
|
```
|
||||||
|
|
||||||
|
`p`, `P` and `w` pause, resume and wake the selected agent. I tested the pause/resume calls directly against Paperclip on `Documenter`: `idle → paused → idle`.
|
||||||
|
|
||||||
|
One bug worth knowing: the company run list is 1.1 MB and takes 8 seconds, while the roster is instant. Reading them together made the whole pane appear empty. They are now separate reads — the roster on the timer, the runs when you open that tab.
|
||||||
|
|
||||||
|
## Still missing, in order
|
||||||
|
|
||||||
|
1. **Agents>settings** — role, title, reports-to, model, heartbeat, budget. All of it is a single `PATCH /api/agents/{id}`, which accepts exactly those fields.
|
||||||
|
2. **Work>review** — the ocr verdict beside the diff. ocr is installed and already refused a merge on `AUD-165`.
|
||||||
|
3. **Tool panes** — `t` opens Lazygit, Hunk, Yazi, Neovim, fzf or ocr in the current workspace; `esc` closes it. This is the answer to "any pane can work with any terminal tool" and needs no reimplementation.
|
||||||
|
|
||||||
|
The layout is now eight workspaces, one pane each, and `R` rebuilds them. `docs/WORKSPACES.md` holds this table and is updated; committed as `b15216a`.
|
||||||
|
|
||||||
|
Say which of the three you want next, 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:59:07 -->
|
||||||
|
|
||||||
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-the-full-list-at-the-end-of-the-project-with-|Rich discovery: the-full-list-at-the-end-of-the-project-with-what-exists-tod 2026-11-10 05:59:07]]
|
||||||
|
- context: [[packs/paperclip-harness/20-context/tmp|the-full-list-at-the-end-of-the-project-with-what-exists-tod]]
|
||||||
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|the-full-list-at-the-end-of-the-project-with-what-exists-tod repository structure and configuration patterns]]
|
||||||
BIN
bin/harness
BIN
bin/harness
Binary file not shown.
@@ -52,7 +52,7 @@ written yet.
|
|||||||
| `Agents>roster` | every agent: name, state, spend this month, budget | built |
|
| `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>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>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** |
|
| `Agents>settings` | role, job title, model, monthly budget, heartbeat — each one editable, with pause and resume | built |
|
||||||
|
|
||||||
## 7. Project
|
## 7. Project
|
||||||
|
|
||||||
@@ -88,8 +88,8 @@ Yazi, fzf, Neovim, Lazygit, Hunk and anything else fits without being rebuilt by
|
|||||||
|
|
||||||
## Count
|
## Count
|
||||||
|
|
||||||
- **21 panels built**, across 8 workspaces.
|
- **22 panels built**, across 8 workspaces.
|
||||||
- **2 panels missing**: `Work>review` and `Agents>settings`.
|
- **1 panel missing**: `Work>review`.
|
||||||
- **6 tool panes** to wire: Lazygit, Hunk, Yazi, Neovim, fzf, ocr.
|
- **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
|
Nothing in the missing list changes the layout: each is one more panel inside a pane that
|
||||||
|
|||||||
388
tui/agents.go
388
tui/agents.go
@@ -16,6 +16,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -35,8 +36,9 @@ type agentsMsg struct {
|
|||||||
// a megabyte and takes eight seconds, while the roster is small and instant.
|
// 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.
|
// Reading them together made the whole screen wait for the slow one.
|
||||||
type runsMsg struct {
|
type runsMsg struct {
|
||||||
runs []paperclip.AgentRun
|
runs []paperclip.AgentRun
|
||||||
err error
|
issues map[string]string
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
type runLogMsg struct {
|
type runLogMsg struct {
|
||||||
@@ -58,15 +60,24 @@ type agentsModel struct {
|
|||||||
notice string
|
notice string
|
||||||
readAt time.Time
|
readAt time.Time
|
||||||
|
|
||||||
tab int
|
tab int
|
||||||
cursor int
|
rosterAt int
|
||||||
scroll int
|
runAt int
|
||||||
showHelp bool
|
settingAt int
|
||||||
|
scroll int
|
||||||
|
showHelp bool
|
||||||
|
|
||||||
|
// runs are limited to this project by default; A shows every run in the
|
||||||
|
// company, which is a different question and a much longer list.
|
||||||
|
issues map[string]string // issue id -> identifier, for this project
|
||||||
|
showAll bool
|
||||||
|
editing string // the field being typed into, empty when not editing
|
||||||
|
editBuf string
|
||||||
|
|
||||||
width, height int
|
width, height int
|
||||||
}
|
}
|
||||||
|
|
||||||
var agentTabs = []string{"roster", "runs", "log"}
|
var agentTabs = []string{"roster", "runs", "log", "settings"}
|
||||||
|
|
||||||
func newAgentsModel(c *paperclip.Client, folder string) agentsModel {
|
func newAgentsModel(c *paperclip.Client, folder string) agentsModel {
|
||||||
return agentsModel{client: c, folder: folder}
|
return agentsModel{client: c, folder: folder}
|
||||||
@@ -83,6 +94,9 @@ func (m agentsModel) load() tea.Cmd {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return agentsMsg{err: err}
|
return agentsMsg{err: err}
|
||||||
}
|
}
|
||||||
|
// A stable order, so the cursor stays on the agent it was on after a
|
||||||
|
// reload rather than following whatever order the server returned.
|
||||||
|
sort.Slice(agents, func(i, j int) bool { return agents[i].Name < agents[j].Name })
|
||||||
names := map[string]string{}
|
names := map[string]string{}
|
||||||
for _, a := range agents {
|
for _, a := range agents {
|
||||||
names[a.ID] = a.Name
|
names[a.ID] = a.Name
|
||||||
@@ -93,17 +107,34 @@ func (m agentsModel) load() tea.Cmd {
|
|||||||
|
|
||||||
// loadRuns reads the company's runs. Called when the runs tab is opened and
|
// 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.
|
// when it is asked for, never on the timer: it is a megabyte and eight seconds.
|
||||||
func (m agentsModel) loadRuns() tea.Cmd {
|
func (m agentsModel) loadRuns(showAll bool) tea.Cmd {
|
||||||
c := m.client
|
c, folder := m.client, m.folder
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
|
issues, _ := c.Issues()
|
||||||
|
proj := ""
|
||||||
|
if cfg, err := readHarnessFile(folder); err == nil {
|
||||||
|
proj = cfg["paperclip.project"]
|
||||||
|
}
|
||||||
|
mine := map[string]string{}
|
||||||
|
for _, i := range issues {
|
||||||
|
if i.ProjectID == proj {
|
||||||
|
mine[i.ID] = i.Identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
runs, err := c.CompanyRuns()
|
runs, err := c.CompanyRuns()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return runsMsg{err: err}
|
return runsMsg{err: err}
|
||||||
}
|
}
|
||||||
if len(runs) > 200 {
|
filtered := make([]paperclip.AgentRun, 0, len(runs))
|
||||||
runs = runs[:200]
|
for _, r := range runs {
|
||||||
|
if showAll || mine[r.Snapshot.IssueID] != "" {
|
||||||
|
filtered = append(filtered, r)
|
||||||
|
}
|
||||||
|
if len(filtered) >= 400 {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return runsMsg{runs: runs}
|
return runsMsg{runs: filtered, issues: mine}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,6 +163,35 @@ func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Batch(m.load(), tickEvery(5*time.Second))
|
return m, tea.Batch(m.load(), tickEvery(5*time.Second))
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if m.editing != "" {
|
||||||
|
switch msg.Type {
|
||||||
|
case tea.KeyEsc:
|
||||||
|
m.editing = ""
|
||||||
|
m.editBuf = ""
|
||||||
|
return m, nil
|
||||||
|
case tea.KeyEnter:
|
||||||
|
field, value, agent := m.editing, m.editBuf, m.chosenAgent()
|
||||||
|
m.editing = ""
|
||||||
|
m.editBuf = ""
|
||||||
|
if agent == nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, m.saveSetting(agent.ID, field, value)
|
||||||
|
case tea.KeyBackspace:
|
||||||
|
r := []rune(m.editBuf)
|
||||||
|
if len(r) > 0 {
|
||||||
|
m.editBuf = string(r[:len(r)-1])
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case tea.KeySpace:
|
||||||
|
m.editBuf += " "
|
||||||
|
return m, nil
|
||||||
|
case tea.KeyRunes:
|
||||||
|
m.editBuf += string(msg.Runes)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
if m.showHelp {
|
if m.showHelp {
|
||||||
m.showHelp = false
|
m.showHelp = false
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -145,11 +205,16 @@ func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case "r":
|
case "r":
|
||||||
switch m.tab {
|
switch m.tab {
|
||||||
case 1:
|
case 1:
|
||||||
return m, m.loadRuns()
|
return m, m.loadRuns(m.showAll)
|
||||||
case 2:
|
case 2:
|
||||||
return m, m.loadLog(m.logRunID)
|
return m, m.loadLog(m.logRunID)
|
||||||
|
case 3:
|
||||||
|
return m, m.load()
|
||||||
}
|
}
|
||||||
return m, m.load()
|
return m, m.load()
|
||||||
|
case "A":
|
||||||
|
m.showAll = !m.showAll
|
||||||
|
return m, m.loadRuns(m.showAll)
|
||||||
case "R":
|
case "R":
|
||||||
if err := startRebuild(m.folder); err != nil {
|
if err := startRebuild(m.folder); err != nil {
|
||||||
m.err = err
|
m.err = err
|
||||||
@@ -167,24 +232,45 @@ func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.tab++
|
m.tab++
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
if m.tab == 1 && len(m.runs) == 0 {
|
if m.tab == 1 && len(m.runs) == 0 {
|
||||||
return m, m.loadRuns()
|
return m, m.loadRuns(m.showAll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
m.scroll--
|
return m.move(-1), nil
|
||||||
return m, nil
|
|
||||||
case "down", "j":
|
case "down", "j":
|
||||||
m.scroll++
|
return m.move(1), nil
|
||||||
return m, nil
|
|
||||||
case "g":
|
case "g":
|
||||||
m.scroll = 0
|
if m.tab == 2 {
|
||||||
|
m.scroll = 0
|
||||||
|
} else {
|
||||||
|
m.moveTo(0)
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "G":
|
case "G":
|
||||||
m.scroll = 1 << 30
|
if m.tab == 2 {
|
||||||
|
m.scroll = 1 << 30
|
||||||
|
} else {
|
||||||
|
m.moveTo(m.listLen() - 1)
|
||||||
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "enter":
|
case "enter":
|
||||||
if m.tab == 1 {
|
switch m.tab {
|
||||||
|
case 3:
|
||||||
|
if m.settingAt >= 0 && m.settingAt < len(settingFields) {
|
||||||
|
f := settingFields[m.settingAt]
|
||||||
|
if f.key == "heartbeat" {
|
||||||
|
agent := m.chosenAgent()
|
||||||
|
if agent == nil {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, m.saveSetting(agent.ID, "heartbeat", "toggle")
|
||||||
|
}
|
||||||
|
m.editing = f.key
|
||||||
|
m.editBuf = m.currentAgentSetting(f.key)
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
case 1:
|
||||||
if r := m.chosenRun(); r != nil {
|
if r := m.chosenRun(); r != nil {
|
||||||
m.logRunID = r.ID
|
m.logRunID = r.ID
|
||||||
m.tab = 2
|
m.tab = 2
|
||||||
@@ -239,6 +325,8 @@ func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case runsMsg:
|
case runsMsg:
|
||||||
if msg.runs != nil {
|
if msg.runs != nil {
|
||||||
m.runs = msg.runs
|
m.runs = msg.runs
|
||||||
|
m.issues = msg.issues
|
||||||
|
m.runAt = min(m.runAt, max(0, len(m.runs)-1))
|
||||||
m.readAt = time.Now()
|
m.readAt = time.Now()
|
||||||
}
|
}
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
@@ -265,29 +353,240 @@ func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// chosenRun is the run the roster cursor is on, in the runs tab.
|
// listLen is how many things the current tab can select. The log tab scrolls
|
||||||
|
// instead of selecting, so it has none.
|
||||||
|
func (m agentsModel) listLen() int {
|
||||||
|
switch m.tab {
|
||||||
|
case 0:
|
||||||
|
return len(m.agents)
|
||||||
|
case 1:
|
||||||
|
return len(m.runs)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// move keeps one cursor per tab, so switching tabs does not forget where you
|
||||||
|
// were, and keeps the list scrolled to wherever the cursor is.
|
||||||
|
func (m agentsModel) move(delta int) agentsModel {
|
||||||
|
if m.tab == 2 {
|
||||||
|
m.scroll += delta
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
switch m.tab {
|
||||||
|
case 0:
|
||||||
|
m.rosterAt = max(0, min(m.rosterAt+delta, len(m.agents)-1))
|
||||||
|
case 1:
|
||||||
|
m.runAt = max(0, min(m.runAt+delta, len(m.runs)-1))
|
||||||
|
case 3:
|
||||||
|
m.settingAt = max(0, min(m.settingAt+delta, len(settingFields)-1))
|
||||||
|
}
|
||||||
|
return m.follow()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m agentsModel) moveTo(i int) agentsModel {
|
||||||
|
if m.tab == 0 && len(m.agents) > 0 {
|
||||||
|
m.rosterAt = max(0, min(i, len(m.agents)-1))
|
||||||
|
}
|
||||||
|
if m.tab == 1 && len(m.runs) > 0 {
|
||||||
|
m.runAt = max(0, min(i, len(m.runs)-1))
|
||||||
|
}
|
||||||
|
return m.follow()
|
||||||
|
}
|
||||||
|
|
||||||
|
// follow scrolls so the selected row is on screen, with two rows of context.
|
||||||
|
func (m agentsModel) follow() agentsModel {
|
||||||
|
var at int
|
||||||
|
switch m.tab {
|
||||||
|
case 0:
|
||||||
|
at = m.rosterAt
|
||||||
|
case 1:
|
||||||
|
at = m.runAt
|
||||||
|
default:
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
visible := max(3, m.height-7)
|
||||||
|
if at < m.scroll+2 {
|
||||||
|
m.scroll = max(0, at-2)
|
||||||
|
}
|
||||||
|
if at > m.scroll+visible-3 {
|
||||||
|
m.scroll = at - visible + 3
|
||||||
|
}
|
||||||
|
if m.scroll < 0 {
|
||||||
|
m.scroll = 0
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveSetting writes one field of one agent to Paperclip, merging into the
|
||||||
|
// blocks it belongs to so nothing else is lost.
|
||||||
|
func (m agentsModel) saveSetting(agentID, field, value string) tea.Cmd {
|
||||||
|
c := m.client
|
||||||
|
return func() tea.Msg {
|
||||||
|
full, err := c.AgentRaw(agentID)
|
||||||
|
if err != nil {
|
||||||
|
return errMsg{err}
|
||||||
|
}
|
||||||
|
body := map[string]any{}
|
||||||
|
switch field {
|
||||||
|
case "role":
|
||||||
|
body["role"] = value
|
||||||
|
case "title":
|
||||||
|
body["title"] = value
|
||||||
|
case "model":
|
||||||
|
cfg, _ := full["adapterConfig"].(map[string]any)
|
||||||
|
merged := map[string]any{}
|
||||||
|
for k, v := range cfg {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
merged["model"] = value
|
||||||
|
body["adapterConfig"] = merged
|
||||||
|
case "budget":
|
||||||
|
var n int
|
||||||
|
if _, err := fmt.Sscanf(value, "%d", &n); err != nil || n < 0 {
|
||||||
|
return errMsg{fmt.Errorf("a budget is a whole number of cents, not %q", value)}
|
||||||
|
}
|
||||||
|
body["budgetMonthlyCents"] = n
|
||||||
|
case "heartbeat":
|
||||||
|
rc, _ := full["runtimeConfig"].(map[string]any)
|
||||||
|
merged := map[string]any{}
|
||||||
|
for k, v := range rc {
|
||||||
|
merged[k] = v
|
||||||
|
}
|
||||||
|
hb, _ := merged["heartbeat"].(map[string]any)
|
||||||
|
next := map[string]any{}
|
||||||
|
for k, v := range hb {
|
||||||
|
next[k] = v
|
||||||
|
}
|
||||||
|
on, _ := next["enabled"].(bool)
|
||||||
|
if value == "toggle" {
|
||||||
|
next["enabled"] = !on
|
||||||
|
} else {
|
||||||
|
next["enabled"] = value == "true"
|
||||||
|
}
|
||||||
|
merged["heartbeat"] = next
|
||||||
|
body["runtimeConfig"] = merged
|
||||||
|
}
|
||||||
|
if err := c.UpdateAgent(agentID, body); err != nil {
|
||||||
|
return errMsg{err}
|
||||||
|
}
|
||||||
|
agents, err := c.Agents()
|
||||||
|
if err != nil {
|
||||||
|
return errMsg{err}
|
||||||
|
}
|
||||||
|
names := map[string]string{}
|
||||||
|
for _, a := range agents {
|
||||||
|
names[a.ID] = a.Name
|
||||||
|
}
|
||||||
|
return agentsMsg{agents: agents, names: names}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// settingLines is the settings tab: one field per line, the chosen one marked.
|
||||||
|
func (m agentsModel) settingLines(width int) []string {
|
||||||
|
agent := m.chosenAgent()
|
||||||
|
if agent == nil {
|
||||||
|
return []string{"", mutedStyle2().Render("no agent chosen")}
|
||||||
|
}
|
||||||
|
out := []string{"", dimStyle().Render(" " + agent.Name + " · press enter to change a field")}
|
||||||
|
for i, f := range settingFields {
|
||||||
|
value := m.currentAgentSetting(f.key)
|
||||||
|
if value == "" {
|
||||||
|
value = "—"
|
||||||
|
}
|
||||||
|
if m.editing == f.key {
|
||||||
|
out = append(out, " ▸ "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Accent)).Bold(true).Render(f.label)+
|
||||||
|
": "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Warning)).Render(m.editBuf+"▌"))
|
||||||
|
out = append(out, " "+dimStyle().Render("enter saves, esc cancels · "+f.hint))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text))
|
||||||
|
mark := " "
|
||||||
|
if i == m.settingAt {
|
||||||
|
mark = " ▸ "
|
||||||
|
style = selectedStyle2()
|
||||||
|
value = f.label + ": " + value
|
||||||
|
}
|
||||||
|
out = append(out, mark+style.Render(fmt.Sprintf("%-14s %s", f.label, value)))
|
||||||
|
}
|
||||||
|
out = append(out, "")
|
||||||
|
out = append(out, dimStyle().Render(" a model change applies to that agent's next run, never to one in flight."))
|
||||||
|
out = append(out, dimStyle().Render(" p pause · P resume · w wake: they act on the agent chosen here."))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// chosenRun is the run the cursor is on, in the runs tab.
|
||||||
func (m agentsModel) chosenRun() *paperclip.AgentRun {
|
func (m agentsModel) chosenRun() *paperclip.AgentRun {
|
||||||
if m.cursor >= 0 && m.cursor < len(m.runs) {
|
if m.runAt >= 0 && m.runAt < len(m.runs) {
|
||||||
return &m.runs[m.cursor]
|
return &m.runs[m.runAt]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// chosenAgent is the agent whose name matches the chosen run, or the first one.
|
// chosenAgent is the agent whose name matches the chosen run, or the first one.
|
||||||
func (m agentsModel) chosenAgent() *paperclip.Agent {
|
func (m agentsModel) chosenAgent() *paperclip.Agent {
|
||||||
if r := m.chosenRun(); r != nil {
|
// On the roster the cursor picks the agent. On the runs tab it is the agent
|
||||||
for i := range m.agents {
|
// who ran the chosen run. On settings it is the roster's choice.
|
||||||
if m.agents[i].ID == r.AgentID {
|
if m.tab == 1 {
|
||||||
return &m.agents[i]
|
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 {
|
if m.rosterAt >= 0 && m.rosterAt < len(m.agents) {
|
||||||
return &m.agents[0]
|
return &m.agents[m.rosterAt]
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// settingField is one editable thing about an agent. Only fields Paperclip
|
||||||
|
// accepts on a PATCH are here, and each one sends only itself.
|
||||||
|
type settingField struct {
|
||||||
|
key string
|
||||||
|
label string
|
||||||
|
hint string
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingFields = []settingField{
|
||||||
|
{"role", "role", "what this agent is for"},
|
||||||
|
{"title", "job title", "the name a person sees"},
|
||||||
|
{"model", "model", "the one used for its next run"},
|
||||||
|
{"budget", "monthly budget", "in cents"},
|
||||||
|
{"heartbeat", "heartbeat", "whether it wakes on a timer"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m agentsModel) currentAgentSetting(key string) string {
|
||||||
|
a := m.chosenAgent()
|
||||||
|
if a == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
full, err := m.client.AgentRaw(a.ID)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "role":
|
||||||
|
return fmt.Sprint(full["role"])
|
||||||
|
case "title":
|
||||||
|
return fmt.Sprint(full["title"])
|
||||||
|
case "model":
|
||||||
|
if cfg, ok := full["adapterConfig"].(map[string]any); ok {
|
||||||
|
return fmt.Sprint(cfg["model"])
|
||||||
|
}
|
||||||
|
case "budget":
|
||||||
|
return fmt.Sprint(int(full["budgetMonthlyCents"].(float64)))
|
||||||
|
case "heartbeat":
|
||||||
|
if rc, ok := full["runtimeConfig"].(map[string]any); ok {
|
||||||
|
if hb, ok := rc["heartbeat"].(map[string]any); ok {
|
||||||
|
return fmt.Sprintf("%v", hb["enabled"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (m agentsModel) View() string {
|
func (m agentsModel) View() string {
|
||||||
if m.width == 0 || m.height == 0 {
|
if m.width == 0 || m.height == 0 {
|
||||||
return "reading…"
|
return "reading…"
|
||||||
@@ -321,6 +620,8 @@ func (m agentsModel) View() string {
|
|||||||
body = m.rosterLines(m.width - 4)
|
body = m.rosterLines(m.width - 4)
|
||||||
case 1:
|
case 1:
|
||||||
body = m.runLines(m.width - 4)
|
body = m.runLines(m.width - 4)
|
||||||
|
case 3:
|
||||||
|
body = m.settingLines(m.width - 4)
|
||||||
default:
|
default:
|
||||||
body = m.logLines(m.width - 4)
|
body = m.logLines(m.width - 4)
|
||||||
}
|
}
|
||||||
@@ -362,7 +663,7 @@ func (m agentsModel) View() string {
|
|||||||
|
|
||||||
func (m agentsModel) rosterLines(width int) []string {
|
func (m agentsModel) rosterLines(width int) []string {
|
||||||
out := []string{"", dimStyle().Render(fmt.Sprintf(" %-16s %-10s %-18s %8s %8s", "agent", "state", "working on", "spent", "budget"))}
|
out := []string{"", dimStyle().Render(fmt.Sprintf(" %-16s %-10s %-18s %8s %8s", "agent", "state", "working on", "spent", "budget"))}
|
||||||
for _, a := range m.agents {
|
for i, a := range m.agents {
|
||||||
mark := icons.Todo
|
mark := icons.Todo
|
||||||
if a.Status == "idle" {
|
if a.Status == "idle" {
|
||||||
mark = icons.Done
|
mark = icons.Done
|
||||||
@@ -375,8 +676,13 @@ func (m agentsModel) rosterLines(width int) []string {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out = append(out, fmt.Sprintf(" %s %-16s %-10s %-18s %6d c %6d c",
|
line := fmt.Sprintf(" %s %-16s %-10s %-18s %6d c %6d c",
|
||||||
mark, a.Name, a.Status, truncateVisible(working, 18), a.SpentCents, a.BudgetCents))
|
mark, a.Name, a.Status, truncateVisible(working, 18), a.SpentCents, a.BudgetCents)
|
||||||
|
if i == m.rosterAt {
|
||||||
|
line = "▸" + line[1:]
|
||||||
|
line = selectedStyle2().Render(padTo(line, width))
|
||||||
|
}
|
||||||
|
out = append(out, line)
|
||||||
}
|
}
|
||||||
out = append(out, "")
|
out = append(out, "")
|
||||||
out = append(out, dimStyle().Render(" c means cents. Budget is set in Paperclip; change it there, not here."))
|
out = append(out, dimStyle().Render(" c means cents. Budget is set in Paperclip; change it there, not here."))
|
||||||
@@ -384,8 +690,11 @@ func (m agentsModel) rosterLines(width int) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m agentsModel) taskLabel(r paperclip.AgentRun) string {
|
func (m agentsModel) taskLabel(r paperclip.AgentRun) string {
|
||||||
if r.Snapshot.IssueID != "" {
|
if id := r.Snapshot.IssueID; id != "" {
|
||||||
return short(r.Snapshot.IssueID)
|
if name, ok := m.issues[id]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return short(id)
|
||||||
}
|
}
|
||||||
if r.Snapshot.TaskKey != "" {
|
if r.Snapshot.TaskKey != "" {
|
||||||
return short(r.Snapshot.TaskKey)
|
return short(r.Snapshot.TaskKey)
|
||||||
@@ -399,7 +708,7 @@ func (m agentsModel) runLines(width int) []string {
|
|||||||
}
|
}
|
||||||
out := []string{"", dimStyle().Render(fmt.Sprintf(" %-10s %-11s %-9s %-12s %-8s %s",
|
out := []string{"", dimStyle().Render(fmt.Sprintf(" %-10s %-11s %-9s %-12s %-8s %s",
|
||||||
"when", "agent", "state", "woken by", "took", "task"))}
|
"when", "agent", "state", "woken by", "took", "task"))}
|
||||||
for _, r := range m.runs {
|
for i, r := range m.runs {
|
||||||
when := "-"
|
when := "-"
|
||||||
if t, err := time.Parse(time.RFC3339, r.StartedAt); err == nil {
|
if t, err := time.Parse(time.RFC3339, r.StartedAt); err == nil {
|
||||||
when = t.Local().Format("15:04:05")
|
when = t.Local().Format("15:04:05")
|
||||||
@@ -423,6 +732,13 @@ func (m agentsModel) runLines(width int) []string {
|
|||||||
case "failed":
|
case "failed":
|
||||||
colour = active.Error
|
colour = active.Error
|
||||||
}
|
}
|
||||||
|
line := fmt.Sprintf(" %-10s %-11s %-9s %-12s %-8s %s",
|
||||||
|
when, truncateVisible(m.names[r.AgentID], 11),
|
||||||
|
fmt.Sprintf("%-9s", r.Status), r.Source, took, m.taskLabel(r))
|
||||||
|
if i == m.runAt {
|
||||||
|
out = append(out, selectedStyle2().Render(padTo("▸"+line[1:], width)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
out = append(out, fmt.Sprintf(" %-10s %-11s %-9s %-12s %-8s %s",
|
out = append(out, fmt.Sprintf(" %-10s %-11s %-9s %-12s %-8s %s",
|
||||||
when, truncateVisible(m.names[r.AgentID], 11),
|
when, truncateVisible(m.names[r.AgentID], 11),
|
||||||
lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(fmt.Sprintf("%-9s", r.Status)),
|
lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(fmt.Sprintf("%-9s", r.Status)),
|
||||||
|
|||||||
@@ -441,3 +441,20 @@ func (c *Client) WakeAgent(agentID string) error {
|
|||||||
return c.do("POST", "/api/agents/"+agentID+"/wakeup",
|
return c.do("POST", "/api/agents/"+agentID+"/wakeup",
|
||||||
map[string]any{"source": "on_demand", "triggerDetail": "manual", "forceFreshSession": false}, nil)
|
map[string]any{"source": "on_demand", "triggerDetail": "manual", "forceFreshSession": false}, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAgent changes an agent's own record. Paperclip owns it, so this is a
|
||||||
|
// PATCH with only the fields that changed; a field left out is not touched.
|
||||||
|
func (c *Client) UpdateAgent(agentID string, body map[string]any) error {
|
||||||
|
if !c.UsingUserKey() {
|
||||||
|
return errors.New("refusing to write with the agent key: set PAPERCLIP_BOARD_KEY")
|
||||||
|
}
|
||||||
|
return c.do("PATCH", "/api/agents/"+agentID, body, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AgentRaw reads one agent's full record, including the configuration blocks,
|
||||||
|
// so an edit can change one field and leave the rest exactly as it was.
|
||||||
|
func (c *Client) AgentRaw(agentID string) (map[string]any, error) {
|
||||||
|
var out map[string]any
|
||||||
|
err := c.do("GET", "/api/agents/"+agentID, nil, &out)
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user