Cleanup pane, Pi session tools, review from the UI, project registry, needs panel acts
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
type: context
|
||||||
|
id: context.paperclip-harness.paperclip-harness-tui-key-hint-footer-convention-and-planned
|
||||||
|
title: paperclip-harness TUI key-hint footer convention and planned work
|
||||||
|
status: active
|
||||||
|
source_of_truth: false
|
||||||
|
freshness: current
|
||||||
|
last_reviewed: 2026-11-11
|
||||||
|
tags:
|
||||||
|
- pack/paperclip-harness
|
||||||
|
- agent-memory/context
|
||||||
|
- paperclip-harness
|
||||||
|
- tui
|
||||||
|
- key-hints
|
||||||
|
- docs/NEXT.md
|
||||||
|
- conventions
|
||||||
|
---
|
||||||
|
|
||||||
|
# paperclip-harness TUI key-hint footer convention and planned work
|
||||||
|
|
||||||
|
Repo/project: paperclip-harness (Go TUI + bin/ scripts, docs/).
|
||||||
|
|
||||||
|
Convention (established this turn, commit 0476afc "Short comma-separated key hints everywhere; write down what is left to build"): every TUI pane footer is a single short, comma-separated hint line built from keyHint()/hintLine() helpers (helpers live around the pane view code, e.g. lines ~521-529 for the Work pane). Hints must be brief and fit m.width; they are truncated with an ellipsis when too long.
|
||||||
|
|
||||||
|
Current footers by pane:
|
||||||
|
- Work: "n p task, [ ] tab, ↑↓ scroll, enter log, o pick, m merge, t tools, r refresh, R rebuild"
|
||||||
|
- Board: "↑↓ move, enter open, a assign, u unblock, t tools, / filter, ? keys, tab panel, r refresh"
|
||||||
|
- Agents: "[ ] tab, enter open, p P w pause/resume/wake, r refresh, ? keys"
|
||||||
|
- Project: "[ ] tab, ↑↓ or j k scroll, r refresh, R rebuild, q quit"
|
||||||
|
- Repair: "[ ] tab, R rebuild, ↑↓ or j k scroll, r recheck, q quit"
|
||||||
|
- Reply: "enter reply, s status, ↑↓ scroll, r refresh, R rebuild, q quit"
|
||||||
|
- New task: "tab field, enter next/create, ctrl+n tab, esc cancel"
|
||||||
|
|
||||||
|
Planned work is tracked in docs/NEXT.md, ordered by usefulness: (1) Repair>cleanup (session/pane killer with tick-to-kill, refuses attached sessions and paperclip-harness, never touches ~/.pi/paperclips); (2) Pi session browser (t opens ~/.pi/paperclips in Yazi; pi --resume); (3) Project>projects registry of adopted projects (id, folder, host, session) with switch key, projects on .27 shown as 'on .27'; (4) Work>review starting bin/review <task> --post in background with log-following tab like merge; (5) old office scaffold (settings.toml, per-role files, verify.sh, page notes, fill only missing); (6) Board>needs acting on a card (unblock/assign/open in Reply); (7) spend chart in Project>overview in Go next to runs sparkline; (8) per-agent memory read-only row (engram vs headroom); (9) MOUSE-1 triage only.
|
||||||
|
|
||||||
|
Unproven areas noted: office opening a project on another host (--host) never exercised; mouse-click on a task row (zones wired, untested); R from a pane not at expected size; agent behavior with an open blocker.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Confidence: 78%
|
||||||
|
- Reason: Concrete, evidence-backed footer convention plus the durable roadmap content of docs/NEXT.md; useful for future work in this repo.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
|
||||||
|
|
||||||
|
## Related learned memories <!-- same-turn-2026-11-11 02:40:13 -->
|
||||||
|
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness-tui-key-hint-footer-convention-and-planned|paperclip-harness TUI key-hint footer convention and planned work]]
|
||||||
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness task state and session inventory]]
|
||||||
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-pause-2026-11-11-02-40-13|Rich discovery: pause 2026-11-11 02:40:13]]
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness|pause]]
|
||||||
@@ -380,3 +380,64 @@ Committed as `049e25a`. The eight workspaces are rebuilt; `Work>work` will open
|
|||||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-11-01-58-59|Rich discovery: tmp 2026-11-11 01:58:59]]
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-11-01-58-59|Rich discovery: tmp 2026-11-11 01:58:59]]
|
||||||
- context: [[packs/paperclip-harness/20-context/tmp|tmp]]
|
- context: [[packs/paperclip-harness/20-context/tmp|tmp]]
|
||||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|tmp repository structure and configuration patterns]]
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|tmp repository structure and configuration patterns]]
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update (2026-11-11 02:40:13)
|
||||||
|
|
||||||
|
## Rich discovery update (2026-11-11 02:40:13)
|
||||||
|
|
||||||
|
Footers are now short and comma-separated, with `n p` in:
|
||||||
|
|
||||||
|
```
|
||||||
|
Work n p task, [ ] tab, ↑↓ scroll, enter log, o pick, m merge, t tools, r refresh, R rebuild
|
||||||
|
Board ↑↓ move, enter open, a assign, u unblock, t tools, / filter, ? keys, tab panel, r refresh
|
||||||
|
Agents [ ] tab, enter open, p P w pause/resume/wake, r refresh, ? keys
|
||||||
|
Repair [ ] tab, R rebuild, ↑↓ scroll, r recheck, q quit
|
||||||
|
Reply enter reply, s status, ↑↓ scroll, r refresh, R rebuild, q quit
|
||||||
|
New task tab field, enter next/create, ctrl+n tab, esc cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is left — `docs/NEXT.md`
|
||||||
|
|
||||||
|
In order of usefulness:
|
||||||
|
|
||||||
|
1. **`Repair>cleanup`** — the pane you asked for: sessions with pane count and age, kill only what you tick, refuse attached ones and refuse `paperclip-harness`, and never touch `~/.pi/paperclips` (that is your history). Still 37 sessions, 21 of them from the closed project's tests; nothing killed.
|
||||||
|
2. **A Pi session browser** — cheapest first: a `t` entry opening `~/.pi/paperclips` in Yazi, and one running `pi --resume`. A transcript tab later only if browsing files feels like a detour.
|
||||||
|
3. **`Project>projects`** — a registry of adopted projects (id, folder, host, session) with a switch key. A project on `.27` shows as `on .27`, not offered.
|
||||||
|
4. **Start a review from `Work>review`** — `v` runs `bin/review <task> --post` in the background, tab follows the log, exactly as merge does now.
|
||||||
|
5. **The old `office` scaffold** — `settings.toml`, a file per role, `verify.sh`, page notes, writing only what is missing. The cheapest remaining gap in the project layer.
|
||||||
|
6. **`Board>needs` acting on a card** — unblock, assign, open in Reply, from the card itself.
|
||||||
|
7. **Spend chart** in `Project>overview`, in Go, next to the runs sparkline.
|
||||||
|
8. **Memory per agent** — read-only row showing whether an agent uses engram or headroom, before any switch.
|
||||||
|
9. **MOUSE-1** — key present, software absent; triage only, never a gate.
|
||||||
|
|
||||||
|
## Unproven, worth a test
|
||||||
|
|
||||||
|
- opening a project **on another machine** (`bin/office --host`) — written, never exercised;
|
||||||
|
- **clicking** a task row with the mouse — zones wired, nobody has clicked;
|
||||||
|
- `R` from a pane that is not at its expected size;
|
||||||
|
- a task whose blocker is still **open** — does the agent stay away until it closes?
|
||||||
|
|
||||||
|
## Paperclip tasks in this project
|
||||||
|
|
||||||
|
Thirteen tasks, **one open**: `AUD-162`, a leftover test task in `todo` from the first day. Everything else is `done` — including the four you created today (`AUD-168`, `AUD-169`, `AUD-170`, `AUD-171`).
|
||||||
|
|
||||||
|
Committed as `0476afc`; the eight workspaces were rebuilt and are running. My suggestion for the next session: build `Repair>cleanup` first, since you have a concrete pile of junk to clear and it is a read-then-confirm operation with no risk to the Pi history.
|
||||||
|
|
||||||
|
## Source paths observed
|
||||||
|
|
||||||
|
- `pause/resume/wake`
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Confidence: 84%
|
||||||
|
- Reason: Update component/repository context with detailed discovered structure and configuration
|
||||||
|
|
||||||
|
## Related learned memories <!-- same-turn-2026-11-11 02:40:13 -->
|
||||||
|
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness-tui-key-hint-footer-convention-and-planned|paperclip-harness TUI key-hint footer convention and planned work]]
|
||||||
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness task state and session inventory]]
|
||||||
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-pause-2026-11-11-02-40-13|Rich discovery: pause 2026-11-11 02:40:13]]
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness|pause]]
|
||||||
|
|||||||
@@ -2554,3 +2554,120 @@ Committed as `f949345`; `docs/RUNS-AND-MERGE.md` now carries the explanation of
|
|||||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-each-of-those-was-a-real-defect-not-misunders|Rich discovery: each-of-those-was-a-real-defect-not-misunderstanding-on-your 2026-11-11 02:28:06]]
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-each-of-those-was-a-real-defect-not-misunders|Rich discovery: each-of-those-was-a-real-defect-not-misunderstanding-on-your 2026-11-11 02:28:06]]
|
||||||
- context: [[packs/paperclip-harness/20-context/each-of-those-was-a-real-defect-not-misunderstanding-on-your|each-of-those-was-a-real-defect-not-misunderstanding-on-your]]
|
- context: [[packs/paperclip-harness/20-context/each-of-those-was-a-real-defect-not-misunderstanding-on-your|each-of-those-was-a-real-defect-not-misunderstanding-on-your]]
|
||||||
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|each-of-those-was-a-real-defect-not-misunderstanding-on-your repository structure and configuration patterns]]
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|each-of-those-was-a-real-defect-not-misunderstanding-on-your repository structure and configuration patterns]]
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update (2026-11-11 02:40:13)
|
||||||
|
|
||||||
|
As of this turn in the paperclip-harness repo: 13 tasks exist in the project, only 1 open — AUD-162 'Tree render check' in todo, a leftover test task from the first day. All others done, including AUD-168, AUD-169, AUD-170, AUD-171 created earlier the same day.
|
||||||
|
|
||||||
|
Environment caveat: `~/.pi/paperclips` holds 37 sessions, 21 of them from a closed project's tests. This path is Pi history and must never be touched by cleanup tooling.
|
||||||
|
|
||||||
|
Build/validation: `go build` returned build=0 after the footer change; commit 0476afc; the eight workspaces were rebuilt and are running.
|
||||||
|
|
||||||
|
## Detailed observations from completed turn
|
||||||
|
|
||||||
|
Footers are now short and comma-separated, with `n p` in:
|
||||||
|
|
||||||
|
```
|
||||||
|
Work n p task, [ ] tab, ↑↓ scroll, enter log, o pick, m merge, t tools, r refresh, R rebuild
|
||||||
|
Board ↑↓ move, enter open, a assign, u unblock, t tools, / filter, ? keys, tab panel, r refresh
|
||||||
|
Agents [ ] tab, enter open, p P w pause/resume/wake, r refresh, ? keys
|
||||||
|
Repair [ ] tab, R rebuild, ↑↓ scroll, r recheck, q quit
|
||||||
|
Reply enter reply, s status, ↑↓ scroll, r refresh, R rebuild, q quit
|
||||||
|
New task tab field, enter next/create, ctrl+n tab, esc cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is left — `docs/NEXT.md`
|
||||||
|
|
||||||
|
In order of usefulness:
|
||||||
|
|
||||||
|
1. **`Repair>cleanup`** — the pane you asked for: sessions with pane count and age, kill only what you tick, refuse attached ones and refuse `paperclip-harness`, and never touch `~/.pi/paperclips` (that is your history). Still 37 sessions, 21 of them from the closed project's tests; nothing killed.
|
||||||
|
2. **A Pi session browser** — cheapest first: a `t` entry opening `~/.pi/paperclips` in Yazi, and one running `pi --resume`. A transcript tab later only if browsing files feels like a detour.
|
||||||
|
3. **`Project>projects`** — a registry of adopted projects (id, folder, host, session) with a switch key. A project on `.27` shows as `on .27`, not offered.
|
||||||
|
4. **Start a review from `Work>review`** — `v` runs `bin/review <task> --post` in the background, tab follows the log, exactly as merge does now.
|
||||||
|
5. **The old `office` scaffold** — `settings.toml`, a file per role, `verify.sh`, page notes, writing only what is missing. The cheapest remaining gap in the project layer.
|
||||||
|
6. **`Board>needs` acting on a card** — unblock, assign, open in Reply, from the card itself.
|
||||||
|
7. **Spend chart** in `Project>overview`, in Go, next to the runs sparkline.
|
||||||
|
8. **Memory per agent** — read-only row showing whether an agent uses engram or headroom, before any switch.
|
||||||
|
9. **MOUSE-1** — key present, software absent; triage only, never a gate.
|
||||||
|
|
||||||
|
## Unproven, worth a test
|
||||||
|
|
||||||
|
- opening a project **on another machine** (`bin/office --host`) — written, never exercised;
|
||||||
|
- **clicking** a task row with the mouse — zones wired, nobody has clicked;
|
||||||
|
- `R` from a pane that is not at its expected size;
|
||||||
|
- a task whose blocker is still **open** — does the agent stay away until it closes?
|
||||||
|
|
||||||
|
## Paperclip tasks in this project
|
||||||
|
|
||||||
|
Thirteen tasks, **one open**: `AUD-162`, a leftover test task in `todo` from the first day. Everything else is `done` — including the four you created today (`AUD-168`, `AUD-169`, `AUD-170`, `AUD-171`).
|
||||||
|
|
||||||
|
Committed as `0476afc`; the eight workspaces were rebuilt and are running. My suggestion for the next session: build `Repair>cleanup` first, since you have a concrete pile of junk to clear and it is a read-then-confirm operation with no risk to the Pi history.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Confidence: 80%
|
||||||
|
- Reason: Factual counts, task state, and the never-touch path caveat are durable operational facts.; enriched with final-answer detail because persistence should be reusable
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update (2026-11-11 02:40:13)
|
||||||
|
|
||||||
|
## Structural/configuration observations (2026-11-11 02:40:13)
|
||||||
|
|
||||||
|
Footers are now short and comma-separated, with `n p` in:
|
||||||
|
|
||||||
|
```
|
||||||
|
Work n p task, [ ] tab, ↑↓ scroll, enter log, o pick, m merge, t tools, r refresh, R rebuild
|
||||||
|
Board ↑↓ move, enter open, a assign, u unblock, t tools, / filter, ? keys, tab panel, r refresh
|
||||||
|
Agents [ ] tab, enter open, p P w pause/resume/wake, r refresh, ? keys
|
||||||
|
Repair [ ] tab, R rebuild, ↑↓ scroll, r recheck, q quit
|
||||||
|
Reply enter reply, s status, ↑↓ scroll, r refresh, R rebuild, q quit
|
||||||
|
New task tab field, enter next/create, ctrl+n tab, esc cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is left — `docs/NEXT.md`
|
||||||
|
|
||||||
|
In order of usefulness:
|
||||||
|
|
||||||
|
1. **`Repair>cleanup`** — the pane you asked for: sessions with pane count and age, kill only what you tick, refuse attached ones and refuse `paperclip-harness`, and never touch `~/.pi/paperclips` (that is your history). Still 37 sessions, 21 of them from the closed project's tests; nothing killed.
|
||||||
|
2. **A Pi session browser** — cheapest first: a `t` entry opening `~/.pi/paperclips` in Yazi, and one running `pi --resume`. A transcript tab later only if browsing files feels like a detour.
|
||||||
|
3. **`Project>projects`** — a registry of adopted projects (id, folder, host, session) with a switch key. A project on `.27` shows as `on .27`, not offered.
|
||||||
|
4. **Start a review from `Work>review`** — `v` runs `bin/review <task> --post` in the background, tab follows the log, exactly as merge does now.
|
||||||
|
5. **The old `office` scaffold** — `settings.toml`, a file per role, `verify.sh`, page notes, writing only what is missing. The cheapest remaining gap in the project layer.
|
||||||
|
6. **`Board>needs` acting on a card** — unblock, assign, open in Reply, from the card itself.
|
||||||
|
7. **Spend chart** in `Project>overview`, in Go, next to the runs sparkline.
|
||||||
|
8. **Memory per agent** — read-only row showing whether an agent uses engram or headroom, before any switch.
|
||||||
|
9. **MOUSE-1** — key present, software absent; triage only, never a gate.
|
||||||
|
|
||||||
|
## Unproven, worth a test
|
||||||
|
|
||||||
|
- opening a project **on another machine** (`bin/office --host`) — written, never exercised;
|
||||||
|
- **clicking** a task row with the mouse — zones wired, nobody has clicked;
|
||||||
|
- `R` from a pane that is not at its expected size;
|
||||||
|
- a task whose blocker is still **open** — does the agent stay away until it closes?
|
||||||
|
|
||||||
|
## Paperclip tasks in this project
|
||||||
|
|
||||||
|
Thirteen tasks, **one open**: `AUD-162`, a leftover test task in `todo` from the first day. Everything else is `done` — including the four you created today (`AUD-168`, `AUD-169`, `AUD-170`, `AUD-171`).
|
||||||
|
|
||||||
|
Committed as `0476afc`; the eight workspaces were rebuilt and are running. My suggestion for the next session: build `Repair>cleanup` first, since you have a concrete pile of junk to clear and it is a read-then-confirm operation with no risk to the Pi history.
|
||||||
|
|
||||||
|
## Source paths observed
|
||||||
|
|
||||||
|
- `pause/resume/wake`
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
- Confidence: 82%
|
||||||
|
- Reason: Captured factual repo structure/configuration patterns from rich discovery
|
||||||
|
|
||||||
|
## Related learned memories <!-- same-turn-2026-11-11 02:40:13 -->
|
||||||
|
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness-tui-key-hint-footer-convention-and-planned|paperclip-harness TUI key-hint footer convention and planned work]]
|
||||||
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness task state and session inventory]]
|
||||||
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-pause-2026-11-11-02-40-13|Rich discovery: pause 2026-11-11 02:40:13]]
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness|pause]]
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
type: session
|
||||||
|
id: session.paperclip-harness.rich-discovery-pause-2026-11-11-02-40-13
|
||||||
|
title: Rich discovery: pause 2026-11-11 02:40:13
|
||||||
|
status: active
|
||||||
|
source_of_truth: false
|
||||||
|
freshness: current
|
||||||
|
last_reviewed: 2026-11-11
|
||||||
|
tags:
|
||||||
|
- pack/paperclip-harness
|
||||||
|
- agent-memory/session
|
||||||
|
- autolearn
|
||||||
|
- rich-discovery
|
||||||
|
- repo/pause
|
||||||
|
---
|
||||||
|
|
||||||
|
# Rich discovery: pause 2026-11-11 02:40:13
|
||||||
|
|
||||||
|
Sanitized rich discovery snapshot captured automatically from the completed turn.
|
||||||
|
|
||||||
|
|
||||||
|
## Source paths observed
|
||||||
|
|
||||||
|
- `pause/resume/wake`
|
||||||
|
|
||||||
|
## Final answer snapshot
|
||||||
|
|
||||||
|
Footers are now short and comma-separated, with `n p` in:
|
||||||
|
|
||||||
|
```
|
||||||
|
Work n p task, [ ] tab, ↑↓ scroll, enter log, o pick, m merge, t tools, r refresh, R rebuild
|
||||||
|
Board ↑↓ move, enter open, a assign, u unblock, t tools, / filter, ? keys, tab panel, r refresh
|
||||||
|
Agents [ ] tab, enter open, p P w pause/resume/wake, r refresh, ? keys
|
||||||
|
Repair [ ] tab, R rebuild, ↑↓ scroll, r recheck, q quit
|
||||||
|
Reply enter reply, s status, ↑↓ scroll, r refresh, R rebuild, q quit
|
||||||
|
New task tab field, enter next/create, ctrl+n tab, esc cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
## What is left — `docs/NEXT.md`
|
||||||
|
|
||||||
|
In order of usefulness:
|
||||||
|
|
||||||
|
1. **`Repair>cleanup`** — the pane you asked for: sessions with pane count and age, kill only what you tick, refuse attached ones and refuse `paperclip-harness`, and never touch `~/.pi/paperclips` (that is your history). Still 37 sessions, 21 of them from the closed project's tests; nothing killed.
|
||||||
|
2. **A Pi session browser** — cheapest first: a `t` entry opening `~/.pi/paperclips` in Yazi, and one running `pi --resume`. A transcript tab later only if browsing files feels like a detour.
|
||||||
|
3. **`Project>projects`** — a registry of adopted projects (id, folder, host, session) with a switch key. A project on `.27` shows as `on .27`, not offered.
|
||||||
|
4. **Start a review from `Work>review`** — `v` runs `bin/review <task> --post` in the background, tab follows the log, exactly as merge does now.
|
||||||
|
5. **The old `office` scaffold** — `settings.toml`, a file per role, `verify.sh`, page notes, writing only what is missing. The cheapest remaining gap in the project layer.
|
||||||
|
6. **`Board>needs` acting on a card** — unblock, assign, open in Reply, from the card itself.
|
||||||
|
7. **Spend chart** in `Project>overview`, in Go, next to the runs sparkline.
|
||||||
|
8. **Memory per agent** — read-only row showing whether an agent uses engram or headroom, before any switch.
|
||||||
|
9. **MOUSE-1** — key present, software absent; triage only, never a gate.
|
||||||
|
|
||||||
|
## Unproven, worth a test
|
||||||
|
|
||||||
|
- opening a project **on another machine** (`bin/office --host`) — written, never exercised;
|
||||||
|
- **clicking** a task row with the mouse — zones wired, nobody has clicked;
|
||||||
|
- `R` from a pane that is not at its expected size;
|
||||||
|
- a task whose blocker is still **open** — does the agent stay away until it closes?
|
||||||
|
|
||||||
|
## Paperclip tasks in this project
|
||||||
|
|
||||||
|
Thirteen tasks, **one open**: `AUD-162`, a leftover test task in `todo` from the first day. Everything else is `done` — including the four you created today (`AUD-168`, `AUD-169`, `AUD-170`, `AUD-171`).
|
||||||
|
|
||||||
|
Committed as `0476afc`; the eight workspaces were rebuilt and are running. My suggestion for the next session: build `Repair>cleanup` first, since you have a concrete pile of junk to clear and it is a read-then-confirm operation with no risk to the Pi history.
|
||||||
|
|
||||||
|
## 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-11 02:40:13 -->
|
||||||
|
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness-tui-key-hint-footer-convention-and-planned|paperclip-harness TUI key-hint footer convention and planned work]]
|
||||||
|
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|paperclip-harness task state and session inventory]]
|
||||||
|
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-pause-2026-11-11-02-40-13|Rich discovery: pause 2026-11-11 02:40:13]]
|
||||||
|
- context: [[packs/paperclip-harness/20-context/paperclip-harness|pause]]
|
||||||
BIN
bin/harness
BIN
bin/harness
Binary file not shown.
Binary file not shown.
@@ -114,3 +114,54 @@ def watch_task(cfg):
|
|||||||
def set_watch(root, cfg, identifier):
|
def set_watch(root, cfg, identifier):
|
||||||
cfg["watching"] = identifier
|
cfg["watching"] = identifier
|
||||||
save(root, cfg)
|
save(root, cfg)
|
||||||
|
|
||||||
|
|
||||||
|
# --- the registry of adopted projects -------------------------------------
|
||||||
|
#
|
||||||
|
# One project is one folder on one machine. Paperclip owns the project record;
|
||||||
|
# what it cannot know is where the folder is and which session shows it, so that
|
||||||
|
# is kept here, beside the person's other configuration.
|
||||||
|
|
||||||
|
REGISTRY = os.path.join(os.path.expanduser("~"), ".config", "paperclip-harness", "projects.toml")
|
||||||
|
|
||||||
|
|
||||||
|
def register(cfg, name):
|
||||||
|
"""Record an adopted project so it can be listed and switched to later."""
|
||||||
|
os.makedirs(os.path.dirname(REGISTRY), exist_ok=True)
|
||||||
|
rows = {}
|
||||||
|
if os.path.isfile(REGISTRY):
|
||||||
|
with open(REGISTRY, "rb") as f:
|
||||||
|
try:
|
||||||
|
rows = tomllib.load(f).get("projects", {})
|
||||||
|
except Exception:
|
||||||
|
rows = {}
|
||||||
|
key = cfg["paperclip"]["project"]
|
||||||
|
rows[key] = {
|
||||||
|
"name": name,
|
||||||
|
"folder": cfg["machine"]["folder"],
|
||||||
|
"host": cfg["machine"]["host"],
|
||||||
|
"session": cfg["session"],
|
||||||
|
}
|
||||||
|
lines = ["# Adopted projects: one folder, one machine, one session each.",
|
||||||
|
"# Written by bin/office. Paperclip owns the project itself; this is only where it lives.", ""]
|
||||||
|
for k, v in rows.items():
|
||||||
|
lines.append("[projects.%r]" % k)
|
||||||
|
lines.append('name = "%s"' % v["name"])
|
||||||
|
lines.append('folder = "%s"' % v["folder"])
|
||||||
|
lines.append('host = "%s"' % v["host"])
|
||||||
|
lines.append('session = "%s"' % v["session"])
|
||||||
|
lines.append("")
|
||||||
|
with open(REGISTRY, "w") as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
def registered():
|
||||||
|
"""Every adopted project, as a list of dicts."""
|
||||||
|
if not os.path.isfile(REGISTRY):
|
||||||
|
return []
|
||||||
|
with open(REGISTRY, "rb") as f:
|
||||||
|
try:
|
||||||
|
data = tomllib.load(f)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
return [dict(v, id=k) for k, v in data.get("projects", {}).items()]
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ def main():
|
|||||||
pj.save(folder, cfg)
|
pj.save(folder, cfg)
|
||||||
h.say("wrote %s" % pj.path(folder))
|
h.say("wrote %s" % pj.path(folder))
|
||||||
|
|
||||||
|
pj.register(cfg, proj["name"])
|
||||||
|
h.say("recorded in %s" % pj.REGISTRY)
|
||||||
|
|
||||||
# The pages are built by this machine, from this folder, in this session.
|
# The pages are built by this machine, from this folder, in this session.
|
||||||
code = os.spawnv(
|
code = os.spawnv(
|
||||||
os.P_WAIT,
|
os.P_WAIT,
|
||||||
|
|||||||
11
docs/NEXT.md
11
docs/NEXT.md
@@ -3,6 +3,17 @@
|
|||||||
Written 2026-11-11. The interface has eight workspaces and 23 panels; the work below is what is
|
Written 2026-11-11. The interface has eight workspaces and 23 panels; the work below is what is
|
||||||
still missing or unproven. Order is by usefulness, not by size.
|
still missing or unproven. Order is by usefulness, not by size.
|
||||||
|
|
||||||
|
## Done since this list was written
|
||||||
|
|
||||||
|
- **`Repair>cleanup`** — session list with pane count and last use, `space` ticks, `x` closes
|
||||||
|
after asking, protected and attached sessions refused, and a line on screen saying that
|
||||||
|
`~/.pi/paperclips` is not touched.
|
||||||
|
- **`t` → `pi sessions` (Yazi) and `t` → `pi resume`** — the two cheapest ways into the history.
|
||||||
|
- **`v` in `Work>review`** — starts `bin/review <task> --post` and the tab follows its log.
|
||||||
|
- **`Project>projects`** — the registry `bin/office` writes, listed with folder, machine and
|
||||||
|
session; a project on another machine is shown and not offered.
|
||||||
|
- **`Board>needs` → `enter`** now also switches to the Reply workspace.
|
||||||
|
|
||||||
## 1. Repair>cleanup — the pane you asked for
|
## 1. Repair>cleanup — the pane you asked for
|
||||||
|
|
||||||
A fourth tab in `Repair>repair` that lists TUIOS sessions with their pane count and last-active
|
A fourth tab in `Repair>repair` that lists TUIOS sessions with their pane count and last-active
|
||||||
|
|||||||
@@ -381,13 +381,13 @@ func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "[", "h":
|
case "(", "[", "h":
|
||||||
if m.tab > 0 {
|
if m.tab > 0 {
|
||||||
m.tab--
|
m.tab--
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "]", "l", "tab":
|
case ")", "]", "l", "tab":
|
||||||
if m.tab < len(agentTabs)-1 {
|
if m.tab < len(agentTabs)-1 {
|
||||||
m.tab++
|
m.tab++
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
@@ -853,7 +853,7 @@ func (m agentsModel) View() string {
|
|||||||
to := min(len(body), from+visible)
|
to := min(len(body), from+visible)
|
||||||
|
|
||||||
hints := []string{
|
hints := []string{
|
||||||
keyHint("[ ]", "tab"),
|
keyHint("( )", "tab"),
|
||||||
keyHint("enter", "open"),
|
keyHint("enter", "open"),
|
||||||
keyHint("p P w", "pause/resume/wake"),
|
keyHint("p P w", "pause/resume/wake"),
|
||||||
keyHint("r", "refresh"),
|
keyHint("r", "refresh"),
|
||||||
@@ -861,7 +861,7 @@ func (m agentsModel) View() string {
|
|||||||
}
|
}
|
||||||
if m.tab == 4 || m.tab == 5 {
|
if m.tab == 4 || m.tab == 5 {
|
||||||
hints = []string{
|
hints = []string{
|
||||||
keyHint("[ ]", "tab"),
|
keyHint("( )", "tab"),
|
||||||
keyHint("space", "toggle"),
|
keyHint("space", "toggle"),
|
||||||
keyHint("↑↓", "move"),
|
keyHint("↑↓", "move"),
|
||||||
keyHint("r", "refresh"),
|
keyHint("r", "refresh"),
|
||||||
|
|||||||
10
tui/board.go
10
tui/board.go
@@ -293,9 +293,15 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
m.notice = "showing " + r.Issue.Identifier + " in every workspace"
|
m.notice = "showing " + r.Issue.Identifier + " in every workspace"
|
||||||
|
// On the needs panel, enter also takes you where the answer is.
|
||||||
|
if m.focus == focusNeeds {
|
||||||
|
if err := switchWorkspace("Reply"); err != nil {
|
||||||
|
m.err = err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "[", "h":
|
case "(", "[", "h":
|
||||||
if m.focus == focusNeeds && m.needsTab > 0 {
|
if m.focus == focusNeeds && m.needsTab > 0 {
|
||||||
m.needsTab--
|
m.needsTab--
|
||||||
}
|
}
|
||||||
@@ -485,7 +491,7 @@ func (m boardModel) footerLine() string {
|
|||||||
default:
|
default:
|
||||||
return hintLine([]string{
|
return hintLine([]string{
|
||||||
keyHint("tab", "panel"),
|
keyHint("tab", "panel"),
|
||||||
keyHint("[ ]", "tab"),
|
keyHint("( )", "tab"),
|
||||||
keyHint("enter", "show this task everywhere"),
|
keyHint("enter", "show this task everywhere"),
|
||||||
keyHint("r", "refresh"),
|
keyHint("r", "refresh"),
|
||||||
keyHint("q", "quit"),
|
keyHint("q", "quit"),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ type projectModel struct {
|
|||||||
width, height int
|
width, height int
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectTabs = []string{"overview", "facts", "costs", "settings"}
|
var projectTabs = []string{"overview", "facts", "costs", "settings", "projects"}
|
||||||
|
|
||||||
func newProjectModel(c *paperclip.Client, folder string) projectModel {
|
func newProjectModel(c *paperclip.Client, folder string) projectModel {
|
||||||
return projectModel{client: c, folder: folder}
|
return projectModel{client: c, folder: folder}
|
||||||
@@ -162,13 +163,13 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "[", "h":
|
case "(", "[", "h":
|
||||||
if m.tab > 0 {
|
if m.tab > 0 {
|
||||||
m.tab--
|
m.tab--
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "]", "l", "tab":
|
case ")", "]", "l", "tab":
|
||||||
if m.tab < len(projectTabs)-1 {
|
if m.tab < len(projectTabs)-1 {
|
||||||
m.tab++
|
m.tab++
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
@@ -234,6 +235,8 @@ func (m projectModel) View() string {
|
|||||||
body = m.factLines()
|
body = m.factLines()
|
||||||
case 2:
|
case 2:
|
||||||
body = m.costLines()
|
body = m.costLines()
|
||||||
|
case 4:
|
||||||
|
body = m.projectListLines(m.width - 4)
|
||||||
default:
|
default:
|
||||||
body = m.settingLines()
|
body = m.settingLines()
|
||||||
}
|
}
|
||||||
@@ -254,7 +257,7 @@ func (m projectModel) View() string {
|
|||||||
to := min(len(body), from+visible)
|
to := min(len(body), from+visible)
|
||||||
|
|
||||||
footer := hintLine([]string{
|
footer := hintLine([]string{
|
||||||
keyHint("[ ]", "tab"),
|
keyHint("( )", "tab"),
|
||||||
keyHint("↑↓", "scroll"),
|
keyHint("↑↓", "scroll"),
|
||||||
keyHint("r", "refresh"),
|
keyHint("r", "refresh"),
|
||||||
keyHint("R", "rebuild"),
|
keyHint("R", "rebuild"),
|
||||||
@@ -433,3 +436,70 @@ func (m projectModel) verifyCommand() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _ = exec.Command
|
var _ = exec.Command
|
||||||
|
|
||||||
|
// registryRow is one adopted project, from the file bin/office writes.
|
||||||
|
type registryRow struct {
|
||||||
|
name, folder, host, session, id string
|
||||||
|
}
|
||||||
|
|
||||||
|
// readRegistry lists adopted projects. It is the only thing this program keeps
|
||||||
|
// besides the project file: where a folder is, and on which machine.
|
||||||
|
func readRegistry() []registryRow {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
path := filepath.Join(home, ".config", "paperclip-harness", "projects.toml")
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var rows []registryRow
|
||||||
|
cur := map[string]string{}
|
||||||
|
id := ""
|
||||||
|
flush := func() {
|
||||||
|
if id != "" && cur["folder"] != "" {
|
||||||
|
rows = append(rows, registryRow{
|
||||||
|
name: cur["name"], folder: cur["folder"], host: cur["host"],
|
||||||
|
session: cur["session"], id: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
cur = map[string]string{}
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(b), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "[projects.") {
|
||||||
|
flush()
|
||||||
|
id = strings.Trim(strings.TrimPrefix(line, "[projects."), "]'\"")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cur[strings.TrimSpace(parts[0])] = strings.Trim(strings.TrimSpace(parts[1]), "\"'")
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// projectListLines shows every adopted project and where it lives.
|
||||||
|
func (m projectModel) projectListLines(width int) []string {
|
||||||
|
rows := readRegistry()
|
||||||
|
out := []string{"", dimStyle().Render(fmt.Sprintf(" %-26s %-12s %-14s %s", "project", "machine", "session", "folder"))}
|
||||||
|
me, _ := os.Hostname()
|
||||||
|
for _, r := range rows {
|
||||||
|
here := ""
|
||||||
|
if r.host == me || r.host == strings.Split(me, ".")[0] {
|
||||||
|
here = " (this machine)"
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf(" %-26s %-12s %-14s %s%s",
|
||||||
|
truncateVisible(r.name, 26), r.host, truncateVisible(r.session, 14),
|
||||||
|
truncateVisible(r.folder, max(10, width-60)), here)
|
||||||
|
out = append(out, line)
|
||||||
|
}
|
||||||
|
if len(rows) == 0 {
|
||||||
|
out = append(out, " "+mutedStyle2().Render("none adopted yet: bin/office <folder> adds one"))
|
||||||
|
}
|
||||||
|
out = append(out, "")
|
||||||
|
out = append(out, dimStyle().Render(" a project on another machine can be listed here but not opened from this one."))
|
||||||
|
out = append(out, dimStyle().Render(" to open one: attach to its session, or run bin/office <folder> there."))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -140,3 +140,52 @@ func mergeLogTail(lines int) []string {
|
|||||||
start := max(0, len(all)-lines)
|
start := max(0, len(all)-lines)
|
||||||
return all[start:]
|
return all[start:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reviewLogPath is where a review started from the interface writes its output.
|
||||||
|
func reviewLogPath() string {
|
||||||
|
return filepath.Join(os.TempDir(), "paperclip-harness-review.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
// startReview runs bin/review for one task and returns at once. The Review tab
|
||||||
|
// follows the log, so a review takes no keypress more than starting it.
|
||||||
|
func startReview(folder, identifier string) error {
|
||||||
|
review := filepath.Join(exeDir(), "review")
|
||||||
|
if _, err := os.Stat(review); err != nil {
|
||||||
|
return fmt.Errorf("no bin/review next to the program (%s)", review)
|
||||||
|
}
|
||||||
|
log, err := os.Create(reviewLogPath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(log, "review of %s started %s\n", identifier, time.Now().Format("15:04:05"))
|
||||||
|
|
||||||
|
cmd := exec.Command(review, identifier, "--post")
|
||||||
|
cmd.Dir = folder
|
||||||
|
cmd.Stdout = log
|
||||||
|
cmd.Stderr = log
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
log.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
err := cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(log, "\nreview stopped: %v\n", err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(log, "\nreview finished %s\n", time.Now().Format("15:04:05"))
|
||||||
|
}
|
||||||
|
log.Close()
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reviewLogTail(lines int) []string {
|
||||||
|
b, err := os.ReadFile(reviewLogPath())
|
||||||
|
if err != nil {
|
||||||
|
return []string{"", " no review has been run from this screen yet", "", " press v to review this task"}
|
||||||
|
}
|
||||||
|
all := splitLines(string(b))
|
||||||
|
start := max(0, len(all)-lines)
|
||||||
|
return all[start:]
|
||||||
|
}
|
||||||
|
|||||||
157
tui/repair.go
157
tui/repair.go
@@ -7,10 +7,12 @@ package main
|
|||||||
// pass, and a screen that says otherwise is how an afternoon disappears.
|
// pass, and a screen that says otherwise is how an afternoon disappears.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -27,12 +29,22 @@ type checkResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type repairMsg struct {
|
type repairMsg struct {
|
||||||
|
sessions []sessionRow
|
||||||
checks []checkResult
|
checks []checkResult
|
||||||
workspaces []layoutRow
|
workspaces []layoutRow
|
||||||
runs int
|
runs int
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sessionRow is one TUIOS session as the cleanup tab sees it.
|
||||||
|
type sessionRow struct {
|
||||||
|
name string
|
||||||
|
panes int
|
||||||
|
lastUsed time.Time
|
||||||
|
attached bool
|
||||||
|
protected bool // this project's own session, or nothing to clean
|
||||||
|
}
|
||||||
|
|
||||||
type layoutRow struct {
|
type layoutRow struct {
|
||||||
number int
|
number int
|
||||||
name string
|
name string
|
||||||
@@ -54,9 +66,15 @@ type repairModel struct {
|
|||||||
|
|
||||||
showHelp bool
|
showHelp bool
|
||||||
width, height int
|
width, height int
|
||||||
|
|
||||||
|
sessions []sessionRow
|
||||||
|
sessAt int
|
||||||
|
ticked map[string]bool
|
||||||
|
confirmKill bool
|
||||||
|
killNote string
|
||||||
}
|
}
|
||||||
|
|
||||||
var repairTabs = []string{"health", "layout", "rebuild log"}
|
var repairTabs = []string{"health", "layout", "rebuild log", "cleanup"}
|
||||||
|
|
||||||
func newRepairModel(c *paperclip.Client, folder string) repairModel {
|
func newRepairModel(c *paperclip.Client, folder string) repairModel {
|
||||||
return repairModel{client: c, folder: folder}
|
return repairModel{client: c, folder: folder}
|
||||||
@@ -120,7 +138,8 @@ func (m repairModel) load() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ws, layoutErr := layoutRows(session)
|
ws, layoutErr := layoutRows(session)
|
||||||
res := repairMsg{checks: checks, workspaces: ws}
|
sess := listSessions(session)
|
||||||
|
res := repairMsg{checks: checks, workspaces: ws, sessions: sess}
|
||||||
if layoutErr != nil {
|
if layoutErr != nil {
|
||||||
res.err = layoutErr
|
res.err = layoutErr
|
||||||
}
|
}
|
||||||
@@ -197,6 +216,30 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, tea.Batch(m.load(), tickEvery(8*time.Second), tea.WindowSize())
|
return m, tea.Batch(m.load(), tickEvery(8*time.Second), tea.WindowSize())
|
||||||
|
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
|
if m.confirmKill {
|
||||||
|
switch msg.String() {
|
||||||
|
case "y":
|
||||||
|
m.confirmKill = false
|
||||||
|
killed := 0
|
||||||
|
for _, row := range m.sessions {
|
||||||
|
if !m.ticked[row.name] || row.protected || row.attached {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out, err := exec.Command("tuios", "kill-session", row.name).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
m.err = fmt.Errorf("could not kill %q: %v %s", row.name, err, strings.TrimSpace(string(out)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
killed++
|
||||||
|
}
|
||||||
|
m.ticked = map[string]bool{}
|
||||||
|
m.killNote = fmt.Sprintf("%d session(s) closed", killed)
|
||||||
|
return m, m.load()
|
||||||
|
case "esc", "n", "q":
|
||||||
|
m.confirmKill = false
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
if m.showHelp {
|
if m.showHelp {
|
||||||
m.showHelp = false
|
m.showHelp = false
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -226,14 +269,33 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.err = nil
|
m.err = nil
|
||||||
m.tab = 1
|
m.tab = 1
|
||||||
return m, m.load()
|
return m, m.load()
|
||||||
case "[", "h":
|
case "(", "[", "h":
|
||||||
m.tab = (m.tab + len(repairTabs) - 1) % len(repairTabs)
|
m.tab = (m.tab + len(repairTabs) - 1) % len(repairTabs)
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
return m, nil
|
return m, nil
|
||||||
case "]", "l", "tab":
|
case ")", "]", "l", "tab":
|
||||||
m.tab = (m.tab + 1) % len(repairTabs)
|
m.tab = (m.tab + 1) % len(repairTabs)
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
return m, nil
|
return m, nil
|
||||||
|
case " ":
|
||||||
|
if m.tab == 3 && len(m.sessions) > 0 {
|
||||||
|
row := m.sessions[max(0, min(m.sessAt, len(m.sessions)-1))]
|
||||||
|
if row.protected || row.attached {
|
||||||
|
m.killNote = row.name + " is attached or is this project's session; it is not touched"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if m.ticked == nil {
|
||||||
|
m.ticked = map[string]bool{}
|
||||||
|
}
|
||||||
|
m.ticked[row.name] = !m.ticked[row.name]
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
case "x":
|
||||||
|
if m.tab == 3 && len(m.ticked) > 0 {
|
||||||
|
m.confirmKill = true
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case "up", "k":
|
case "up", "k":
|
||||||
m.scroll--
|
m.scroll--
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -245,6 +307,9 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case repairMsg:
|
case repairMsg:
|
||||||
|
if msg.sessions != nil {
|
||||||
|
m.sessions = msg.sessions
|
||||||
|
}
|
||||||
m.checks = msg.checks
|
m.checks = msg.checks
|
||||||
m.workspaces = msg.workspaces
|
m.workspaces = msg.workspaces
|
||||||
m.err = msg.err
|
m.err = msg.err
|
||||||
@@ -287,6 +352,8 @@ func (m repairModel) View() string {
|
|||||||
body = m.layoutLines()
|
body = m.layoutLines()
|
||||||
case 2:
|
case 2:
|
||||||
body = m.logLines()
|
body = m.logLines()
|
||||||
|
case 3:
|
||||||
|
body = m.cleanupLines(m.width - 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := panel{title: "checks", width: m.width, height: m.height - 2, tabs: repairTabs, active: m.tab, focused: true}
|
p := panel{title: "checks", width: m.width, height: m.height - 2, tabs: repairTabs, active: m.tab, focused: true}
|
||||||
@@ -302,7 +369,7 @@ func (m repairModel) View() string {
|
|||||||
to := min(len(body), from+visible)
|
to := min(len(body), from+visible)
|
||||||
|
|
||||||
footer := hintLine([]string{
|
footer := hintLine([]string{
|
||||||
keyHint("[ ]", "tab"),
|
keyHint("( )", "tab"),
|
||||||
keyHint("R", "rebuild"),
|
keyHint("R", "rebuild"),
|
||||||
keyHint("↑↓", "scroll"),
|
keyHint("↑↓", "scroll"),
|
||||||
keyHint("r", "recheck"),
|
keyHint("r", "recheck"),
|
||||||
@@ -378,3 +445,83 @@ func (m repairModel) layoutLines() []string {
|
|||||||
out = append(out, dimStyle().Render(" so every panel is found by name, never by number."))
|
out = append(out, dimStyle().Render(" so every panel is found by name, never by number."))
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// listSessions reads every TUIOS session on this machine, newest activity first.
|
||||||
|
// It never reads or touches the Pi conversation files: those are separate and
|
||||||
|
// are the person's history.
|
||||||
|
func listSessions(current string) []sessionRow {
|
||||||
|
out, err := exec.Command("tuios", "ls", "--all-hosts", "--json").Output()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var doc struct {
|
||||||
|
Hosts []struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Sessions []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Windows int `json:"window_count"`
|
||||||
|
LastActive int64 `json:"last_active"`
|
||||||
|
Attached bool `json:"attached"`
|
||||||
|
} `json:"sessions"`
|
||||||
|
} `json:"hosts"`
|
||||||
|
}
|
||||||
|
if json.Unmarshal(out, &doc) != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var rows []sessionRow
|
||||||
|
for _, host := range doc.Hosts {
|
||||||
|
for _, s := range host.Sessions {
|
||||||
|
// A session on another machine is listed but never touched from here.
|
||||||
|
row := sessionRow{
|
||||||
|
name: s.Name,
|
||||||
|
panes: s.Windows,
|
||||||
|
lastUsed: time.Unix(s.LastActive, 0),
|
||||||
|
attached: s.Attached || host.Host != "local",
|
||||||
|
}
|
||||||
|
if s.Name == current {
|
||||||
|
row.protected = true
|
||||||
|
}
|
||||||
|
rows = append(rows, row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(rows, func(i, j int) bool { return rows[i].lastUsed.After(rows[j].lastUsed) })
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupLines lists TUIOS sessions, marks what is protected, and lets a person
|
||||||
|
// tick the ones to close. It never touches ~/.pi/paperclips: closing a TUIOS
|
||||||
|
// session ends panes, and the Pi conversations are files that stay.
|
||||||
|
func (m repairModel) cleanupLines(width int) []string {
|
||||||
|
out := []string{"", dimStyle().Render(fmt.Sprintf(" %-34s %6s %-12s %s", "session", "panes", "last used", "state"))}
|
||||||
|
for i, r := range m.sessions {
|
||||||
|
state := ""
|
||||||
|
switch {
|
||||||
|
case r.protected:
|
||||||
|
state = "this project's session"
|
||||||
|
case r.attached:
|
||||||
|
state = "attached, or on another machine"
|
||||||
|
case m.ticked[r.name]:
|
||||||
|
state = "ticked for closing"
|
||||||
|
}
|
||||||
|
line := fmt.Sprintf(" %-34s %6d %-12s %s", truncateVisible(r.name, 34), r.panes,
|
||||||
|
r.lastUsed.Format("2 Jan 15:04"), state)
|
||||||
|
mark := " "
|
||||||
|
if m.ticked[r.name] {
|
||||||
|
mark = "▸ "
|
||||||
|
}
|
||||||
|
if i == m.sessAt {
|
||||||
|
out = append(out, selectedStyle2().Render(padTo(mark+line, width)))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, mark+line)
|
||||||
|
}
|
||||||
|
out = append(out, "")
|
||||||
|
if m.killNote != "" {
|
||||||
|
out = append(out, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Warning)).Render(m.killNote))
|
||||||
|
out = append(out, "")
|
||||||
|
}
|
||||||
|
out = append(out, dimStyle().Render(" space ticks a session, x closes the ticked ones after asking."))
|
||||||
|
out = append(out, dimStyle().Render(" closing a TUIOS session ends its panes. It does not delete anything in"))
|
||||||
|
out = append(out, dimStyle().Render(" ~/.pi/paperclips, where every Pi conversation is kept."))
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ var tools = []tool{
|
|||||||
// A review that finds nothing exits in a second, which looks like a broken
|
// A review that finds nothing exits in a second, which looks like a broken
|
||||||
// pane. It is given a last line so the output can be read before closing.
|
// pane. It is given a last line so the output can be read before closing.
|
||||||
{"ocr", "ocr review --color never; printf '\\npress enter to close\\n'; read -r _", "the review of the current change"},
|
{"ocr", "ocr review --color never; printf '\\npress enter to close\\n'; read -r _", "the review of the current change"},
|
||||||
|
{"pi sessions", "yazi ~/.pi/paperclips", "every Pi conversation Paperclip has recorded"},
|
||||||
|
{"pi resume", "pi --resume", "Pi's own list of sessions; pick one and read it"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// currentWorkspace finds the workspace this program is drawn in. TUIOS knows;
|
// currentWorkspace finds the workspace this program is drawn in. TUIOS knows;
|
||||||
@@ -185,3 +187,31 @@ func fieldNumber(obj, name string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// switchWorkspace shows a workspace by name. It is how one pane can hand you to
|
||||||
|
// another without any message passing: the name is the address.
|
||||||
|
func switchWorkspace(name string) error {
|
||||||
|
session, _, err := currentWorkspace()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out, err := exec.Command("tuios", "list-workspaces", "-s", session, "--json").Output()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var doc struct {
|
||||||
|
Workspaces []struct {
|
||||||
|
Workspace int `json:"workspace"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"workspaces"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(out, &doc); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, w := range doc.Workspaces {
|
||||||
|
if w.Name == name {
|
||||||
|
return exec.Command("tuios", "select-workspace", strconv.Itoa(w.Workspace), "-s", session).Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no workspace called %q", name)
|
||||||
|
}
|
||||||
|
|||||||
37
tui/work.go
37
tui/work.go
@@ -294,6 +294,18 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return pickTasksMsg{issues: mine}
|
return pickTasksMsg{issues: mine}
|
||||||
}
|
}
|
||||||
|
case "v":
|
||||||
|
// A review is cheap to start and posts its verdict on the task, so
|
||||||
|
// the record lives with the work.
|
||||||
|
if m.task.Identifier == "" {
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
if err := startReview(m.folder, m.task.Identifier); err != nil {
|
||||||
|
m.err = err
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.tab = 3
|
||||||
|
return m, m.load()
|
||||||
case "m":
|
case "m":
|
||||||
// Merging is a decision, so it asks first and shows what it will do.
|
// Merging is a decision, so it asks first and shows what it will do.
|
||||||
if m.task.Identifier == "" {
|
if m.task.Identifier == "" {
|
||||||
@@ -314,13 +326,13 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
return m, tea.Quit
|
return m, tea.Quit
|
||||||
case "[", "h":
|
case "(", "[", "h":
|
||||||
if m.tab > 0 {
|
if m.tab > 0 {
|
||||||
m.tab--
|
m.tab--
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case "]", "l", "tab":
|
case ")", "]", "l", "tab":
|
||||||
if m.tab < len(workTabs)-1 {
|
if m.tab < len(workTabs)-1 {
|
||||||
m.tab++
|
m.tab++
|
||||||
m.scroll = 0
|
m.scroll = 0
|
||||||
@@ -519,11 +531,12 @@ func (m workModel) View() string {
|
|||||||
|
|
||||||
footer := hintLine([]string{
|
footer := hintLine([]string{
|
||||||
keyHint("n p", "task"),
|
keyHint("n p", "task"),
|
||||||
keyHint("[ ]", "tab"),
|
keyHint("( )", "tab"),
|
||||||
keyHint("↑↓", "scroll"),
|
keyHint("↑↓", "scroll"),
|
||||||
keyHint("enter", "log"),
|
keyHint("enter", "log"),
|
||||||
keyHint("o", "pick"),
|
keyHint("o", "pick"),
|
||||||
keyHint("m", "merge"),
|
keyHint("m", "merge"),
|
||||||
|
keyHint("v", "review"),
|
||||||
keyHint("t", "tools"),
|
keyHint("t", "tools"),
|
||||||
keyHint("r", "refresh"),
|
keyHint("r", "refresh"),
|
||||||
keyHint("R", "rebuild"),
|
keyHint("R", "rebuild"),
|
||||||
@@ -639,6 +652,13 @@ func (m workModel) diffLines(width int) []string {
|
|||||||
// written as a comment, so the record lives with the work and survives us.
|
// written as a comment, so the record lives with the work and survives us.
|
||||||
func (m workModel) reviewLines(width int) []string {
|
func (m workModel) reviewLines(width int) []string {
|
||||||
out := []string{"", dimStyle().Render(" verdicts recorded on this task")}
|
out := []string{"", dimStyle().Render(" verdicts recorded on this task")}
|
||||||
|
if rl := reviewLogTailFor(m.task.Identifier); len(rl) > 0 {
|
||||||
|
out = append(out, "")
|
||||||
|
out = append(out, dimStyle().Render(" the last review started from here:"))
|
||||||
|
for _, ln := range rl {
|
||||||
|
out = append(out, " "+mutedStyle2().Render(truncateVisible(ln, width-2)))
|
||||||
|
}
|
||||||
|
}
|
||||||
if m.task.Identifier == "" {
|
if m.task.Identifier == "" {
|
||||||
return append(out, mutedStyle2().Render(" no task chosen"))
|
return append(out, mutedStyle2().Render(" no task chosen"))
|
||||||
}
|
}
|
||||||
@@ -776,3 +796,14 @@ func firstLineWith(lines []string, marker string) string {
|
|||||||
}
|
}
|
||||||
return "none"
|
return "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reviewLogTailFor returns the last review log, but only when it was for this
|
||||||
|
// task: another task's review is not evidence about this one.
|
||||||
|
func reviewLogTailFor(identifier string) []string {
|
||||||
|
tail := reviewLogTail(8)
|
||||||
|
joined := strings.Join(tail, "\n")
|
||||||
|
if !strings.Contains(joined, identifier) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tail
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user