diff --git a/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness-tui-key-hint-footer-convention-and-planned.md b/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness-tui-key-hint-footer-convention-and-planned.md new file mode 100644 index 0000000..2b71e08 --- /dev/null +++ b/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness-tui-key-hint-footer-convention-and-planned.md @@ -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 --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 + +- 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]] diff --git a/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md b/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md index 3abbd7b..b50f593 100644 --- a/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md +++ b/.pi/memory-vault/packs/paperclip-harness/20-context/paperclip-harness.md @@ -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]] - 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]] + + +--- + +## 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 --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 + +- 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]] diff --git a/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md b/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md index 1a426ef..b45e84b 100644 --- a/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md +++ b/.pi/memory-vault/packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns.md @@ -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]] - 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]] + + +--- + +## 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 --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 --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 + +- 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]] diff --git a/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-pause-2026-11-11-02-40-13.md b/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-pause-2026-11-11-02-40-13.md new file mode 100644 index 0000000..1ff7fbc --- /dev/null +++ b/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-pause-2026-11-11-02-40-13.md @@ -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 --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 + +- 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]] diff --git a/bin/harness b/bin/harness index 7f97ece..b57e722 100755 Binary files a/bin/harness and b/bin/harness differ diff --git a/bin/lib/__pycache__/project.cpython-314.pyc b/bin/lib/__pycache__/project.cpython-314.pyc index f8add43..00ee76e 100644 Binary files a/bin/lib/__pycache__/project.cpython-314.pyc and b/bin/lib/__pycache__/project.cpython-314.pyc differ diff --git a/bin/lib/project.py b/bin/lib/project.py index c35bfc6..72faaa0 100644 --- a/bin/lib/project.py +++ b/bin/lib/project.py @@ -114,3 +114,54 @@ def watch_task(cfg): def set_watch(root, cfg, identifier): cfg["watching"] = identifier 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()] diff --git a/bin/office b/bin/office index c00642e..4b79fb0 100755 --- a/bin/office +++ b/bin/office @@ -98,6 +98,9 @@ def main(): pj.save(folder, cfg) 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. code = os.spawnv( os.P_WAIT, diff --git a/docs/NEXT.md b/docs/NEXT.md index 921ed52..9d7bdc1 100644 --- a/docs/NEXT.md +++ b/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 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 --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 A fourth tab in `Repair>repair` that lists TUIOS sessions with their pane count and last-active diff --git a/tui/agents.go b/tui/agents.go index 0fbaed7..4bf3dea 100644 --- a/tui/agents.go +++ b/tui/agents.go @@ -381,13 +381,13 @@ func (m agentsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } return m, tea.Quit - case "[", "h": + case "(", "[", "h": if m.tab > 0 { m.tab-- m.scroll = 0 } return m, nil - case "]", "l", "tab": + case ")", "]", "l", "tab": if m.tab < len(agentTabs)-1 { m.tab++ m.scroll = 0 @@ -853,7 +853,7 @@ func (m agentsModel) View() string { to := min(len(body), from+visible) hints := []string{ - keyHint("[ ]", "tab"), + keyHint("( )", "tab"), keyHint("enter", "open"), keyHint("p P w", "pause/resume/wake"), keyHint("r", "refresh"), @@ -861,7 +861,7 @@ func (m agentsModel) View() string { } if m.tab == 4 || m.tab == 5 { hints = []string{ - keyHint("[ ]", "tab"), + keyHint("( )", "tab"), keyHint("space", "toggle"), keyHint("↑↓", "move"), keyHint("r", "refresh"), diff --git a/tui/board.go b/tui/board.go index 2cc40cc..ca2f408 100644 --- a/tui/board.go +++ b/tui/board.go @@ -293,9 +293,15 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } 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 - case "[", "h": + case "(", "[", "h": if m.focus == focusNeeds && m.needsTab > 0 { m.needsTab-- } @@ -485,7 +491,7 @@ func (m boardModel) footerLine() string { default: return hintLine([]string{ keyHint("tab", "panel"), - keyHint("[ ]", "tab"), + keyHint("( )", "tab"), keyHint("enter", "show this task everywhere"), keyHint("r", "refresh"), keyHint("q", "quit"), diff --git a/tui/project.go b/tui/project.go index d8fe234..797d695 100644 --- a/tui/project.go +++ b/tui/project.go @@ -9,6 +9,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strings" "time" @@ -55,7 +56,7 @@ type projectModel struct { 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 { 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, tea.Quit - case "[", "h": + case "(", "[", "h": if m.tab > 0 { m.tab-- m.scroll = 0 } return m, nil - case "]", "l", "tab": + case ")", "]", "l", "tab": if m.tab < len(projectTabs)-1 { m.tab++ m.scroll = 0 @@ -234,6 +235,8 @@ func (m projectModel) View() string { body = m.factLines() case 2: body = m.costLines() + case 4: + body = m.projectListLines(m.width - 4) default: body = m.settingLines() } @@ -254,7 +257,7 @@ func (m projectModel) View() string { to := min(len(body), from+visible) footer := hintLine([]string{ - keyHint("[ ]", "tab"), + keyHint("( )", "tab"), keyHint("↑↓", "scroll"), keyHint("r", "refresh"), keyHint("R", "rebuild"), @@ -433,3 +436,70 @@ func (m projectModel) verifyCommand() string { } 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 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 there.")) + return out +} diff --git a/tui/rebuild.go b/tui/rebuild.go index da32172..64b6d1f 100644 --- a/tui/rebuild.go +++ b/tui/rebuild.go @@ -140,3 +140,52 @@ func mergeLogTail(lines int) []string { start := max(0, len(all)-lines) 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:] +} diff --git a/tui/repair.go b/tui/repair.go index c36864d..a1264c3 100644 --- a/tui/repair.go +++ b/tui/repair.go @@ -7,10 +7,12 @@ package main // pass, and a screen that says otherwise is how an afternoon disappears. import ( + "encoding/json" "fmt" "os" "os/exec" "path/filepath" + "sort" "strings" "time" @@ -27,12 +29,22 @@ type checkResult struct { } type repairMsg struct { + sessions []sessionRow checks []checkResult workspaces []layoutRow runs int 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 { number int name string @@ -54,9 +66,15 @@ type repairModel struct { showHelp bool 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 { return repairModel{client: c, folder: folder} @@ -120,7 +138,8 @@ func (m repairModel) load() tea.Cmd { } ws, layoutErr := layoutRows(session) - res := repairMsg{checks: checks, workspaces: ws} + sess := listSessions(session) + res := repairMsg{checks: checks, workspaces: ws, sessions: sess} if layoutErr != nil { 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()) 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 { m.showHelp = false return m, nil @@ -226,14 +269,33 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.err = nil m.tab = 1 return m, m.load() - case "[", "h": + case "(", "[", "h": m.tab = (m.tab + len(repairTabs) - 1) % len(repairTabs) m.scroll = 0 return m, nil - case "]", "l", "tab": + case ")", "]", "l", "tab": m.tab = (m.tab + 1) % len(repairTabs) m.scroll = 0 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": m.scroll-- return m, nil @@ -245,6 +307,9 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case repairMsg: + if msg.sessions != nil { + m.sessions = msg.sessions + } m.checks = msg.checks m.workspaces = msg.workspaces m.err = msg.err @@ -287,6 +352,8 @@ func (m repairModel) View() string { body = m.layoutLines() case 2: 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} @@ -302,7 +369,7 @@ func (m repairModel) View() string { to := min(len(body), from+visible) footer := hintLine([]string{ - keyHint("[ ]", "tab"), + keyHint("( )", "tab"), keyHint("R", "rebuild"), keyHint("↑↓", "scroll"), 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.")) 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 +} diff --git a/tui/toolpane.go b/tui/toolpane.go index 7b47dcc..37d0d06 100644 --- a/tui/toolpane.go +++ b/tui/toolpane.go @@ -39,6 +39,8 @@ var tools = []tool{ // 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. {"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; @@ -185,3 +187,31 @@ func fieldNumber(obj, name string) string { } 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) +} diff --git a/tui/work.go b/tui/work.go index 07b5ea1..53d0399 100644 --- a/tui/work.go +++ b/tui/work.go @@ -294,6 +294,18 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } 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": // Merging is a decision, so it asks first and shows what it will do. if m.task.Identifier == "" { @@ -314,13 +326,13 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } return m, tea.Quit - case "[", "h": + case "(", "[", "h": if m.tab > 0 { m.tab-- m.scroll = 0 } return m, nil - case "]", "l", "tab": + case ")", "]", "l", "tab": if m.tab < len(workTabs)-1 { m.tab++ m.scroll = 0 @@ -519,11 +531,12 @@ func (m workModel) View() string { footer := hintLine([]string{ keyHint("n p", "task"), - keyHint("[ ]", "tab"), + keyHint("( )", "tab"), keyHint("↑↓", "scroll"), keyHint("enter", "log"), keyHint("o", "pick"), keyHint("m", "merge"), + keyHint("v", "review"), keyHint("t", "tools"), keyHint("r", "refresh"), 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. func (m workModel) reviewLines(width int) []string { 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 == "" { return append(out, mutedStyle2().Render(" no task chosen")) } @@ -776,3 +796,14 @@ func firstLineWith(lines []string, marker string) string { } 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 +}