Help on every panel, task filter, unblock key, mouse zones, safe add-only rebuild
This commit is contained in:
@@ -232,3 +232,87 @@ Also still true: `bin/board` builds five workspaces (shell, board, work, reply,
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-19-00|Rich discovery: tmp 2026-11-10 03:19:00]]
|
||||
- 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-10 03:34:34)
|
||||
|
||||
## Rich discovery update (2026-11-10 03:34:34)
|
||||
|
||||
Done. `R` rebuilds everything and I proved it by pressing it.
|
||||
|
||||
## Rebuild and refresh, both real
|
||||
|
||||
```
|
||||
r read the data again in this panel. Nothing closes, nothing moves.
|
||||
R rebuild every workspace and pane from the project file.
|
||||
```
|
||||
|
||||
`R` is on the board, work, conversation, project and repair screens (not the new-task form, where letters are text). It runs `bin/board --replace` **detached in its own session**, because the panel you press it in is one of the panes being replaced — a rebuild that dies with its caller is not a rebuild.
|
||||
|
||||
Pressed `R` in the board pane and read the result:
|
||||
|
||||
```
|
||||
rebuild started 2026-11-10 14:33:47
|
||||
closed shell
|
||||
closed board
|
||||
closed work
|
||||
closed reply
|
||||
closed new
|
||||
closed project
|
||||
closed repair
|
||||
shell created workspace 2
|
||||
board created workspace 3
|
||||
...
|
||||
```
|
||||
|
||||
Afterwards, exactly seven panes, one per workspace, all of them drawing:
|
||||
|
||||
```
|
||||
ws 1 shell ws 2 board ws 3 work ws 4 reply
|
||||
ws 5 new ws 6 project ws 7 repair
|
||||
```
|
||||
|
||||
The log is `/tmp/paperclip-harness-rebuild.log` and the Repair workspace has a third tab, **rebuild log**, that shows it — so the record of a repair outlives the panes it replaced.
|
||||
|
||||
## Missing panels added
|
||||
|
||||
- **Author** now has three: the form, *what a task needs* (one thing, a command, a boundary, a blocker — each explaining why), and *what will be sent*, which renders the exact description the agent receives, verify command and blocker included. At 177 columns the form sits left with the other two stacked right; narrower, they are tabs under `ctrl+n`.
|
||||
- **Repair** now has health, layout and rebuild log.
|
||||
- Conversation's task header is a line rather than a panel, deliberately: a panel for one line wastes the border.
|
||||
|
||||
## Where the layout stands
|
||||
|
||||
```
|
||||
1 shell 2 board 3 work 4 reply 5 new 6 project 7 repair
|
||||
```
|
||||
|
||||
One pane each, named, rebuilt by name, adopted if present. `bin/board --replace` rebuilds them all; `bin/board` alone adds whatever is missing without touching what is there.
|
||||
|
||||
Screenshots refreshed: `/tmp/mockups/{board,work,reply,new,project,repair}-live.png`.
|
||||
|
||||
Committed as `972d4c0`. Ready for you to poke at: the four things I would test first are `R` on the repair screen, the conversation follow when you pick a task on the board, the costs tab against what Paperclip reports, and whether the icons render now that the font is in.
|
||||
|
||||
## Source paths observed
|
||||
|
||||
- `home/sam/paseo/projects/paperclip-harness/tui/rebuild.go`
|
||||
- `paperclip-harness/tui/paperclip`
|
||||
- `tmp/mockups/board-live.png`
|
||||
- `tmp/mockups/new-live.png`
|
||||
- `tmp/mockups/project-live.png`
|
||||
- `tmp/mockups/repair-live.png`
|
||||
- `tmp/mockups/reply-live.png`
|
||||
- `tmp/mockups/work-live.png`
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 84%
|
||||
- Reason: Update component/repository context with detailed discovered structure and configuration
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-10 03:34:34 -->
|
||||
|
||||
- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan|paperclip-harness TUI: Rebuild/Refresh keys + Author/Repair panel expansion]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-34-34|Rich discovery: tmp 2026-11-10 03:34:34]]
|
||||
- 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]]
|
||||
|
||||
@@ -567,3 +567,87 @@ Committed as `351cb35`. Next, when you are back: tell me which of the six you wa
|
||||
- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-six-workspace-build-screens-and-bug-fi|paperclip-harness TUI: six-workspace build, screens, and bug fixes (351cb35)]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-what-you-will-see-when-you-open-it-2026-11-10|Rich discovery: what-you-will-see-when-you-open-it 2026-11-10 03:29:19]]
|
||||
- context: [[packs/paperclip-harness/20-context/paperclip-harness|what-you-will-see-when-you-open-it]]
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Update (2026-11-10 03:34:34)
|
||||
|
||||
## Structural/configuration observations (2026-11-10 03:34:34)
|
||||
|
||||
Done. `R` rebuilds everything and I proved it by pressing it.
|
||||
|
||||
## Rebuild and refresh, both real
|
||||
|
||||
```
|
||||
r read the data again in this panel. Nothing closes, nothing moves.
|
||||
R rebuild every workspace and pane from the project file.
|
||||
```
|
||||
|
||||
`R` is on the board, work, conversation, project and repair screens (not the new-task form, where letters are text). It runs `bin/board --replace` **detached in its own session**, because the panel you press it in is one of the panes being replaced — a rebuild that dies with its caller is not a rebuild.
|
||||
|
||||
Pressed `R` in the board pane and read the result:
|
||||
|
||||
```
|
||||
rebuild started 2026-11-10 14:33:47
|
||||
closed shell
|
||||
closed board
|
||||
closed work
|
||||
closed reply
|
||||
closed new
|
||||
closed project
|
||||
closed repair
|
||||
shell created workspace 2
|
||||
board created workspace 3
|
||||
...
|
||||
```
|
||||
|
||||
Afterwards, exactly seven panes, one per workspace, all of them drawing:
|
||||
|
||||
```
|
||||
ws 1 shell ws 2 board ws 3 work ws 4 reply
|
||||
ws 5 new ws 6 project ws 7 repair
|
||||
```
|
||||
|
||||
The log is `/tmp/paperclip-harness-rebuild.log` and the Repair workspace has a third tab, **rebuild log**, that shows it — so the record of a repair outlives the panes it replaced.
|
||||
|
||||
## Missing panels added
|
||||
|
||||
- **Author** now has three: the form, *what a task needs* (one thing, a command, a boundary, a blocker — each explaining why), and *what will be sent*, which renders the exact description the agent receives, verify command and blocker included. At 177 columns the form sits left with the other two stacked right; narrower, they are tabs under `ctrl+n`.
|
||||
- **Repair** now has health, layout and rebuild log.
|
||||
- Conversation's task header is a line rather than a panel, deliberately: a panel for one line wastes the border.
|
||||
|
||||
## Where the layout stands
|
||||
|
||||
```
|
||||
1 shell 2 board 3 work 4 reply 5 new 6 project 7 repair
|
||||
```
|
||||
|
||||
One pane each, named, rebuilt by name, adopted if present. `bin/board --replace` rebuilds them all; `bin/board` alone adds whatever is missing without touching what is there.
|
||||
|
||||
Screenshots refreshed: `/tmp/mockups/{board,work,reply,new,project,repair}-live.png`.
|
||||
|
||||
Committed as `972d4c0`. Ready for you to poke at: the four things I would test first are `R` on the repair screen, the conversation follow when you pick a task on the board, the costs tab against what Paperclip reports, and whether the icons render now that the font is in.
|
||||
|
||||
## Source paths observed
|
||||
|
||||
- `home/sam/paseo/projects/paperclip-harness/tui/rebuild.go`
|
||||
- `paperclip-harness/tui/paperclip`
|
||||
- `tmp/mockups/board-live.png`
|
||||
- `tmp/mockups/new-live.png`
|
||||
- `tmp/mockups/project-live.png`
|
||||
- `tmp/mockups/repair-live.png`
|
||||
- `tmp/mockups/reply-live.png`
|
||||
- `tmp/mockups/work-live.png`
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 82%
|
||||
- Reason: Captured factual repo structure/configuration patterns from rich discovery
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-10 03:34:34 -->
|
||||
|
||||
- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan|paperclip-harness TUI: Rebuild/Refresh keys + Author/Repair panel expansion]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-34-34|Rich discovery: tmp 2026-11-10 03:34:34]]
|
||||
- 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]]
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
type: session
|
||||
id: session.paperclip-harness.paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan
|
||||
title: paperclip-harness TUI: Rebuild/Refresh keys + Author/Repair panel expansion
|
||||
status: active
|
||||
source_of_truth: false
|
||||
freshness: current
|
||||
last_reviewed: 2026-11-10
|
||||
tags:
|
||||
- pack/paperclip-harness
|
||||
- agent-memory/session
|
||||
- paperclip-harness
|
||||
- tui
|
||||
- go
|
||||
- bubbles-tea
|
||||
- keybindings
|
||||
- rebuild
|
||||
- layout
|
||||
- author
|
||||
---
|
||||
|
||||
# paperclip-harness TUI: Rebuild/Refresh keys + Author/Repair panel expansion
|
||||
|
||||
Repository: paperclip-harness, TUI component in `tui/` (Go, bubbles/tea).
|
||||
|
||||
## Key bindings established
|
||||
- `r` — refresh/re-read data in the current panel only (nothing closes or moves).
|
||||
- `R` — full rebuild of every workspace/pane from the project file.
|
||||
- `R` is bound on board, work, conversation, project, and repair screens — NOT on the new-task form (letters are text there).
|
||||
|
||||
## Rebuild implementation detail
|
||||
`R` shells out to `bin/board --replace`, run **detached in its own session**, because the pane you press `R` in is itself being replaced (a rebuild that dies with its caller is not a rebuild).
|
||||
- `bin/board --replace` rebuilds all panes; plain `bin/board` adds only missing panes without touching existing ones.
|
||||
- Panes are rebuilt **by name** and **adopted if already present**.
|
||||
- Rebuild log written to `/tmp/paperclip-harness-rebuild.log`.
|
||||
|
||||
## Verified behavior
|
||||
Sample rebuild log lines: `rebuild started <ts>`, `board: <uuid> (session paperclip-harness, folder /tmp/pc-sandbox-2026-11-10)`, `watching: AUD-162`, then `closed <name>` for all 7 panes and `created workspace N` for each. Post-rebuild: exactly 7 panes, all drawing, 177x53 each:
|
||||
```
|
||||
ws 1 shell ws 2 board ws 3 work ws 4 reply
|
||||
ws 5 new ws 6 project ws 7 repair
|
||||
```
|
||||
|
||||
## Layout (fixed order)
|
||||
`1 shell 2 board 3 work 4 reply 5 new 6 project 7 repair`
|
||||
|
||||
## Panel changes this turn
|
||||
- **Author** now has three sub-panels: the form; *what a task needs* (one thing, a command, a boundary, a blocker — each with a why); and *what will be sent* (renders the exact description the agent receives, incl. verify command and blocker). At 177 columns: form left, other two stacked right; narrower: tabs under `ctrl+n`.
|
||||
- **Repair** now has three tabs: health, layout, rebuild log.
|
||||
- Conversation task header is rendered as a **line, not a panel** (deliberately — a panel for one line wastes the border).
|
||||
|
||||
## Build/toolchain note
|
||||
Initial build failed in `newtask.go:141` with `undefined: tea.KeyCtrlBracketLeft` / `tea.KeyCtrlBracketRight` (those tea constants don't exist in the installed bubbles version); resolved and `build=0`. Tests: `ok paperclip-harness/tui 0.479s`.
|
||||
|
||||
## Artifacts
|
||||
- Commit `972d4c0`: "R rebuilds every pane detached; r refreshes data; Author gains guidance and preview; Repair gains the rebuild log".
|
||||
- Screenshots refreshed: `/tmp/mockups/{board,work,reply,new,project,repair}-live.png`.
|
||||
|
||||
## Suggested next tests
|
||||
`R` on the repair screen; conversation follow when picking a task on the board; costs tab vs what Paperclip reports; icon rendering now that the font is installed.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 85%
|
||||
- Reason: Rich multi-file discovery and implementation turn with concrete pane layout, key bindings, rebuild mechanics, file paths, and commit hash; durable for future agents working in this repo.
|
||||
|
||||
## Related
|
||||
|
||||
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-10 03:34:34 -->
|
||||
|
||||
- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan|paperclip-harness TUI: Rebuild/Refresh keys + Author/Repair panel expansion]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-34-34|Rich discovery: tmp 2026-11-10 03:34:34]]
|
||||
- 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]]
|
||||
@@ -0,0 +1,103 @@
|
||||
---
|
||||
type: session
|
||||
id: session.paperclip-harness.rich-discovery-tmp-2026-11-10-03-34-34
|
||||
title: Rich discovery: tmp 2026-11-10 03:34:34
|
||||
status: active
|
||||
source_of_truth: false
|
||||
freshness: current
|
||||
last_reviewed: 2026-11-10
|
||||
tags:
|
||||
- pack/paperclip-harness
|
||||
- agent-memory/session
|
||||
- autolearn
|
||||
- rich-discovery
|
||||
- repo/tmp
|
||||
---
|
||||
|
||||
# Rich discovery: tmp 2026-11-10 03:34:34
|
||||
|
||||
Sanitized rich discovery snapshot captured automatically from the completed turn.
|
||||
|
||||
|
||||
## Source paths observed
|
||||
|
||||
- `home/sam/paseo/projects/paperclip-harness/tui/rebuild.go`
|
||||
- `paperclip-harness/tui/paperclip`
|
||||
- `tmp/mockups/board-live.png`
|
||||
- `tmp/mockups/new-live.png`
|
||||
- `tmp/mockups/project-live.png`
|
||||
- `tmp/mockups/repair-live.png`
|
||||
- `tmp/mockups/reply-live.png`
|
||||
- `tmp/mockups/work-live.png`
|
||||
|
||||
## Final answer snapshot
|
||||
|
||||
Done. `R` rebuilds everything and I proved it by pressing it.
|
||||
|
||||
## Rebuild and refresh, both real
|
||||
|
||||
```
|
||||
r read the data again in this panel. Nothing closes, nothing moves.
|
||||
R rebuild every workspace and pane from the project file.
|
||||
```
|
||||
|
||||
`R` is on the board, work, conversation, project and repair screens (not the new-task form, where letters are text). It runs `bin/board --replace` **detached in its own session**, because the panel you press it in is one of the panes being replaced — a rebuild that dies with its caller is not a rebuild.
|
||||
|
||||
Pressed `R` in the board pane and read the result:
|
||||
|
||||
```
|
||||
rebuild started 2026-11-10 14:33:47
|
||||
closed shell
|
||||
closed board
|
||||
closed work
|
||||
closed reply
|
||||
closed new
|
||||
closed project
|
||||
closed repair
|
||||
shell created workspace 2
|
||||
board created workspace 3
|
||||
...
|
||||
```
|
||||
|
||||
Afterwards, exactly seven panes, one per workspace, all of them drawing:
|
||||
|
||||
```
|
||||
ws 1 shell ws 2 board ws 3 work ws 4 reply
|
||||
ws 5 new ws 6 project ws 7 repair
|
||||
```
|
||||
|
||||
The log is `/tmp/paperclip-harness-rebuild.log` and the Repair workspace has a third tab, **rebuild log**, that shows it — so the record of a repair outlives the panes it replaced.
|
||||
|
||||
## Missing panels added
|
||||
|
||||
- **Author** now has three: the form, *what a task needs* (one thing, a command, a boundary, a blocker — each explaining why), and *what will be sent*, which renders the exact description the agent receives, verify command and blocker included. At 177 columns the form sits left with the other two stacked right; narrower, they are tabs under `ctrl+n`.
|
||||
- **Repair** now has health, layout and rebuild log.
|
||||
- Conversation's task header is a line rather than a panel, deliberately: a panel for one line wastes the border.
|
||||
|
||||
## Where the layout stands
|
||||
|
||||
```
|
||||
1 shell 2 board 3 work 4 reply 5 new 6 project 7 repair
|
||||
```
|
||||
|
||||
One pane each, named, rebuilt by name, adopted if present. `bin/board --replace` rebuilds them all; `bin/board` alone adds whatever is missing without touching what is there.
|
||||
|
||||
Screenshots refreshed: `/tmp/mockups/{board,work,reply,new,project,repair}-live.png`.
|
||||
|
||||
Committed as `972d4c0`. Ready for you to poke at: the four things I would test first are `R` on the repair screen, the conversation follow when you pick a task on the board, the costs tab against what Paperclip reports, and whether the icons render now that the font is in.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Confidence: 86%
|
||||
- Reason: Preserve full sanitized discovery detail from a rich investigation turn
|
||||
|
||||
## Related
|
||||
|
||||
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
|
||||
|
||||
## Related learned memories <!-- same-turn-2026-11-10 03:34:34 -->
|
||||
|
||||
- session: [[packs/paperclip-harness/80-sessions/paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan|paperclip-harness TUI: Rebuild/Refresh keys + Author/Repair panel expansion]]
|
||||
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-34-34|Rich discovery: tmp 2026-11-10 03:34:34]]
|
||||
- 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]]
|
||||
@@ -70,6 +70,11 @@ Choosing a task on the board and pressing Enter writes it into the project's
|
||||
|
||||
- **`r`** — read the data again in this panel. Nothing closes, nothing moves, the
|
||||
layout is untouched. Use it when the numbers look stale.
|
||||
- **`B`** — on the Repair screen only: add whatever panes are missing without
|
||||
closing any, so it is safe to press from inside a pane.
|
||||
- **`?`** — the keys for the panel you are in, as an overlay.
|
||||
- **`/`** — filter the task list by identifier, title or assignee name.
|
||||
- **`u`** — unblock a task whose blocker has finished but which still says blocked.
|
||||
- **`R`** — rebuild everything: `bin/board --replace` closes each pane the layout
|
||||
owns and builds all seven again, in order. It runs detached in its own session,
|
||||
because the panel you pressed it in is one of the panes being replaced; the
|
||||
|
||||
BIN
bin/harness
BIN
bin/harness
Binary file not shown.
@@ -308,6 +308,10 @@ them. A bare Esc must stay free, because it closes a box.
|
||||
| Any panel | t | open a tool panel here: shell, Lazygit, Hunk, git, review |
|
||||
| Any panel | ? | show the keys for this panel |
|
||||
| Any panel | r | read again from Paperclip — data only, layout untouched |
|
||||
| Any panel | ? | show the keys for this panel |
|
||||
| Task tree | / | filter by identifier, title or assignee |
|
||||
| Task tree | u | unblock a task whose blocker has finished |
|
||||
| Repair | B | add only the panes that are missing |
|
||||
| Any panel | R | rebuild every workspace and pane from the project file |
|
||||
| Any panel | q | leave this panel's program, returning the pane to its shell |
|
||||
| Any panel | Esc | close the open overlay or leave the composer, never quit |
|
||||
|
||||
140
tui/board.go
140
tui/board.go
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
zone "github.com/lrstanley/bubblezone"
|
||||
|
||||
"paperclip-harness/tui/paperclip"
|
||||
)
|
||||
@@ -48,6 +49,11 @@ type boardModel struct {
|
||||
activity []paperclip.Activity
|
||||
needsTab int
|
||||
|
||||
zones *zone.Manager
|
||||
filter string
|
||||
typing bool
|
||||
showHelp bool
|
||||
|
||||
width, height int
|
||||
}
|
||||
|
||||
@@ -59,7 +65,9 @@ const (
|
||||
)
|
||||
|
||||
func newBoardModel(c *paperclip.Client, proj paperclip.Project, folder string) boardModel {
|
||||
return boardModel{client: c, project: proj, folder: folder, loading: true}
|
||||
// One click-zone manager for the whole program, created once. Wrapping each
|
||||
// panel separately drifts as soon as a pane is resized.
|
||||
return boardModel{client: c, project: proj, folder: folder, loading: true, zones: zone.New()}
|
||||
}
|
||||
|
||||
func (m boardModel) Init() tea.Cmd {
|
||||
@@ -94,16 +102,36 @@ func (m boardModel) loadDetail(id string) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// visible is the list after the filter. The cursor moves over this, so what is
|
||||
// highlighted is always what would be acted on.
|
||||
func (m boardModel) visible() []Row {
|
||||
if m.filter == "" {
|
||||
return m.rows
|
||||
}
|
||||
needle := strings.ToLower(m.filter)
|
||||
var out []Row
|
||||
for _, r := range m.rows {
|
||||
if strings.Contains(strings.ToLower(r.Issue.Identifier), needle) ||
|
||||
strings.Contains(strings.ToLower(r.Issue.Title), needle) ||
|
||||
strings.Contains(strings.ToLower(r.Who), needle) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m boardModel) selectedID() string {
|
||||
if m.cursor >= 0 && m.cursor < len(m.rows) {
|
||||
return m.rows[m.cursor].Issue.ID
|
||||
vis := m.visible()
|
||||
if m.cursor >= 0 && m.cursor < len(vis) {
|
||||
return vis[m.cursor].Issue.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m boardModel) selectedTask() *Row {
|
||||
if m.cursor >= 0 && m.cursor < len(m.rows) {
|
||||
return &m.rows[m.cursor]
|
||||
vis := m.visible()
|
||||
if m.cursor >= 0 && m.cursor < len(vis) {
|
||||
return &vis[m.cursor]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -117,10 +145,73 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case tickMsg:
|
||||
return m, tea.Batch(m.reload(), tickEvery(5*time.Second))
|
||||
|
||||
case tea.MouseMsg:
|
||||
if msg.Action == tea.MouseActionPress && msg.Button == tea.MouseButtonLeft {
|
||||
for i, r := range m.visible() {
|
||||
if z := m.zones.Get("row-" + r.Issue.ID); z != nil && z.InBounds(msg) {
|
||||
m.cursor = i
|
||||
return m, m.loadDetail(r.Issue.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.showHelp {
|
||||
m.showHelp = false
|
||||
return m, nil
|
||||
}
|
||||
if m.typing {
|
||||
switch msg.Type {
|
||||
case tea.KeyEsc:
|
||||
m.typing = false
|
||||
m.filter = ""
|
||||
return m, nil
|
||||
case tea.KeyEnter, tea.KeyTab:
|
||||
m.typing = false
|
||||
return m, nil
|
||||
case tea.KeyBackspace:
|
||||
r := []rune(m.filter)
|
||||
if len(r) > 0 {
|
||||
m.filter = string(r[:len(r)-1])
|
||||
}
|
||||
return m, nil
|
||||
case tea.KeySpace:
|
||||
m.filter += " "
|
||||
return m, nil
|
||||
case tea.KeyRunes:
|
||||
m.filter += string(msg.Runes)
|
||||
return m, nil
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
switch msg.String() {
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "?":
|
||||
m.showHelp = true
|
||||
return m, nil
|
||||
case "/":
|
||||
m.typing = true
|
||||
return m, nil
|
||||
case "u":
|
||||
// A task whose blocker finished but which still says blocked is the
|
||||
// one thing a person unblocks without reading anything else.
|
||||
if r := m.selectedTask(); r != nil && isReadyButBlocked(*r) {
|
||||
c, id := m.client, r.Issue.ID
|
||||
return m, func() tea.Msg {
|
||||
if _, err := c.SetStatus(id, "todo", "Unblocked: its blocker finished."); err != nil {
|
||||
return errMsg{err}
|
||||
}
|
||||
rows, err := loadRows(c, m.project)
|
||||
if err != nil {
|
||||
return errMsg{err}
|
||||
}
|
||||
return rowsMsg(rows)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
case "tab":
|
||||
m.focus = (m.focus + 1) % focusCount
|
||||
return m, nil
|
||||
@@ -143,7 +234,7 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
case "down", "j":
|
||||
if m.cursor < len(m.rows)-1 {
|
||||
if m.cursor < len(m.visible())-1 {
|
||||
m.cursor++
|
||||
return m, m.loadDetail(m.selectedID())
|
||||
}
|
||||
@@ -192,8 +283,8 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.loading = false
|
||||
m.readAt = time.Now()
|
||||
m.err = nil
|
||||
if m.cursor >= len(m.rows) {
|
||||
m.cursor = max(0, len(m.rows)-1)
|
||||
if m.cursor >= len(m.visible()) {
|
||||
m.cursor = max(0, len(m.visible())-1)
|
||||
}
|
||||
if id := m.selectedID(); id != "" && id != m.detailID {
|
||||
return m, m.loadDetail(id)
|
||||
@@ -232,6 +323,19 @@ func (m boardModel) View() string {
|
||||
m.width, m.height))
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
return helpOverlay("keys on this panel", [][2]string{
|
||||
{"↑ ↓ j k", "choose a task"},
|
||||
{"enter", "show that task in every workspace"},
|
||||
{"a", "assign it to the configured agent"},
|
||||
{"u", "unblock it, when its blocker has finished"},
|
||||
{"/", "filter the list by identifier, title or name"},
|
||||
{"tab", "move to the next panel"},
|
||||
{"r / R", "read again / rebuild every pane"},
|
||||
{"q", "leave this panel"},
|
||||
}, m.width, m.height)
|
||||
}
|
||||
|
||||
header := m.headerLine()
|
||||
footer := m.footerLine()
|
||||
bodyHeight := m.height - 2
|
||||
@@ -257,7 +361,7 @@ func (m boardModel) View() string {
|
||||
left.render(m.treeLines(treeW-3)), " ",
|
||||
mid.render(m.summaryLines(sumW-3)), " ",
|
||||
rightP.render(body))
|
||||
return strings.Join([]string{header, page, footer}, "\n")
|
||||
return m.zones.Scan(strings.Join([]string{header, page, footer}, "\n"))
|
||||
}
|
||||
|
||||
leftWidth := max(34, m.width*40/100)
|
||||
@@ -288,7 +392,7 @@ func (m boardModel) View() string {
|
||||
)
|
||||
|
||||
page := lipgloss.JoinHorizontal(lipgloss.Top, left, " ", right)
|
||||
return strings.Join([]string{header, page, footer}, "\n")
|
||||
return m.zones.Scan(strings.Join([]string{header, page, footer}, "\n"))
|
||||
}
|
||||
|
||||
func (m boardModel) headerLine() string {
|
||||
@@ -319,6 +423,9 @@ func (m boardModel) footerLine() string {
|
||||
keyHint("↑↓ or j k", "choose a task"),
|
||||
keyHint("enter", "show it everywhere"),
|
||||
keyHint("a", "assign"),
|
||||
keyHint("u", "unblock"),
|
||||
keyHint("/", "filter"),
|
||||
keyHint("?", "keys"),
|
||||
keyHint("tab", "next panel"),
|
||||
keyHint("r", "read again"),
|
||||
keyHint("R", "rebuild every pane"),
|
||||
@@ -345,13 +452,17 @@ func (m boardModel) footerLine() string {
|
||||
// treeLines draws the grouped task list.
|
||||
func (m boardModel) treeLines(width int) []string {
|
||||
var out []string
|
||||
if len(m.rows) == 0 && !m.loading {
|
||||
vis := m.visible()
|
||||
if len(vis) == 0 && !m.loading {
|
||||
if m.filter != "" {
|
||||
return []string{mutedStyle2().Render(" nothing matches " + m.filter)}
|
||||
}
|
||||
return []string{mutedStyle2().Render(" no tasks in this project")}
|
||||
}
|
||||
index := 0
|
||||
for _, g := range groupOrder {
|
||||
var group []Row
|
||||
for _, r := range m.rows {
|
||||
for _, r := range vis {
|
||||
if r.Group == g {
|
||||
group = append(group, r)
|
||||
}
|
||||
@@ -414,7 +525,10 @@ func (m boardModel) taskLine(r Row, index, width int) []string {
|
||||
if chosen {
|
||||
first, second = padTo(first, width), padTo(second, width)
|
||||
}
|
||||
return []string{firstStyle.Render(first), secondStyle.Render(second)}
|
||||
return []string{
|
||||
m.zones.Mark("row-"+r.Issue.ID, firstStyle.Render(first)),
|
||||
secondStyle.Render(second),
|
||||
}
|
||||
}
|
||||
|
||||
// shortStatus is the one word a row has room for.
|
||||
|
||||
17
tui/frame.go
17
tui/frame.go
@@ -272,3 +272,20 @@ func wrap(s string, w int) []string {
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// helpOverlay draws the keys for the panel that asked for it, centred over the
|
||||
// screen. It replaces the whole view rather than floating over it, so nothing
|
||||
// behind it can be misread as still active.
|
||||
func helpOverlay(title string, entries [][2]string, w, h int) string {
|
||||
boxW := min(64, max(30, w-8))
|
||||
boxH := min(len(entries)+4, max(8, h-4))
|
||||
lines := []string{""}
|
||||
for _, e := range entries {
|
||||
key := lipgloss.NewStyle().Foreground(lipgloss.Color(active.Accent)).Bold(true).
|
||||
Width(16).Render(e[0])
|
||||
lines = append(lines, " "+key+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(e[1]))
|
||||
}
|
||||
lines = append(lines, "", dimStyle().Render(" esc or ? closes this"))
|
||||
p := panel{title: title, width: boxW, height: boxH, focused: true}
|
||||
return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, p.render(lines))
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ type projectModel struct {
|
||||
tab int
|
||||
scroll int
|
||||
|
||||
showHelp bool
|
||||
width, height int
|
||||
}
|
||||
|
||||
@@ -140,7 +141,14 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(m.load(), tickEvery(10*time.Second))
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.showHelp {
|
||||
m.showHelp = false
|
||||
return m, nil
|
||||
}
|
||||
switch msg.String() {
|
||||
case "?":
|
||||
m.showHelp = true
|
||||
return m, nil
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "r":
|
||||
@@ -194,6 +202,17 @@ func (m projectModel) View() string {
|
||||
fmt.Sprintf("This workspace needs 80 × 24.\nThis panel is %d × %d.", m.width, m.height))
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
return helpOverlay("the project", [][2]string{
|
||||
{"[ ] h l", "facts, costs, settings"},
|
||||
{"↑ ↓ j k", "scroll"},
|
||||
{"r", "read again"},
|
||||
{"R", "rebuild every pane"},
|
||||
{"q", "leave"},
|
||||
{"?", "this list"},
|
||||
}, m.width, m.height)
|
||||
}
|
||||
|
||||
head := titleStyle.Render("Project") + mutedStyle2().Render(" · ")
|
||||
if m.project.Name != "" {
|
||||
head += lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(m.project.Name)
|
||||
|
||||
@@ -52,6 +52,7 @@ type repairModel struct {
|
||||
tab int
|
||||
scroll int
|
||||
|
||||
showHelp bool
|
||||
width, height int
|
||||
}
|
||||
|
||||
@@ -196,7 +197,14 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(m.load(), tickEvery(8*time.Second))
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.showHelp {
|
||||
m.showHelp = false
|
||||
return m, nil
|
||||
}
|
||||
switch msg.String() {
|
||||
case "?":
|
||||
m.showHelp = true
|
||||
return m, nil
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "r":
|
||||
@@ -207,6 +215,17 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
return m, tea.Quit
|
||||
case "B":
|
||||
// Add what is missing without replacing what is there. Safe to press
|
||||
// from inside a pane, because it never closes one.
|
||||
out, err := exec.Command(filepath.Join(exeDir(), "board"), "--folder", m.folder).CombinedOutput()
|
||||
if err != nil {
|
||||
m.err = fmt.Errorf("adding the missing panes failed: %v: %s", err, strings.TrimSpace(string(out)))
|
||||
return m, nil
|
||||
}
|
||||
m.err = nil
|
||||
m.tab = 1
|
||||
return m, m.load()
|
||||
case "[", "h":
|
||||
m.tab = (m.tab + len(repairTabs) - 1) % len(repairTabs)
|
||||
m.scroll = 0
|
||||
@@ -244,6 +263,18 @@ func (m repairModel) View() string {
|
||||
fmt.Sprintf("This workspace needs 80 × 24.\nThis panel is %d × %d.", m.width, m.height))
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
return helpOverlay("repair", [][2]string{
|
||||
{"[ ] h l", "health, layout, rebuild log"},
|
||||
{"R", "rebuild every pane"},
|
||||
{"B", "add only the missing panes"},
|
||||
{"↑ ↓ j k", "scroll"},
|
||||
{"r", "check again"},
|
||||
{"q", "leave"},
|
||||
{"?", "this list"},
|
||||
}, m.width, m.height)
|
||||
}
|
||||
|
||||
head := titleStyle.Render("Repair") + mutedStyle2().Render(" · layout and health")
|
||||
state := "read " + m.readAt.Format("15:04:05")
|
||||
if m.readAt.IsZero() {
|
||||
|
||||
20
tui/reply.go
20
tui/reply.go
@@ -42,9 +42,10 @@ type replyModel struct {
|
||||
err error
|
||||
readAt time.Time
|
||||
|
||||
scroll int
|
||||
width int
|
||||
height int
|
||||
showHelp bool
|
||||
scroll int
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func newReplyModel(c *paperclip.Client, folder, issueID string) replyModel {
|
||||
@@ -226,6 +227,19 @@ func (m replyModel) View() string {
|
||||
fmt.Sprintf("This workspace needs 80 × 24.\nThis panel is %d × %d.", m.width, m.height))
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
return helpOverlay("the conversation", [][2]string{
|
||||
{"enter", "write a reply"},
|
||||
{"alt+enter", "new line inside the box"},
|
||||
{"s", "choose the status to send with it"},
|
||||
{"↑ ↓ j k", "read the thread"},
|
||||
{"r", "read again"},
|
||||
{"R", "rebuild every pane"},
|
||||
{"q", "leave"},
|
||||
{"?", "this list"},
|
||||
}, m.width, m.height)
|
||||
}
|
||||
|
||||
head := titleStyle.Render("Conversation") + mutedStyle2().Render(" · ")
|
||||
if m.task.Identifier != "" {
|
||||
head += lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).
|
||||
|
||||
20
tui/work.go
20
tui/work.go
@@ -46,6 +46,7 @@ type workModel struct {
|
||||
scroll int
|
||||
readAt time.Time
|
||||
|
||||
showHelp bool
|
||||
width, height int
|
||||
}
|
||||
|
||||
@@ -122,7 +123,14 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(m.load(), tickEvery(5*time.Second))
|
||||
|
||||
case tea.KeyMsg:
|
||||
if m.showHelp {
|
||||
m.showHelp = false
|
||||
return m, nil
|
||||
}
|
||||
switch msg.String() {
|
||||
case "?":
|
||||
m.showHelp = true
|
||||
return m, nil
|
||||
case "q", "ctrl+c":
|
||||
return m, tea.Quit
|
||||
case "r":
|
||||
@@ -188,6 +196,18 @@ func (m workModel) View() string {
|
||||
fmt.Sprintf("This workspace needs 80 × 24.\nThis panel is %d × %d.", m.width, m.height))
|
||||
}
|
||||
|
||||
if m.showHelp {
|
||||
return helpOverlay("the work", [][2]string{
|
||||
{"[ ] h l", "switch between runs, change and facts"},
|
||||
{"↑ ↓ j k", "scroll"},
|
||||
{"g / G", "top / bottom"},
|
||||
{"r", "read again"},
|
||||
{"R", "rebuild every pane"},
|
||||
{"q", "leave"},
|
||||
{"?", "this list"},
|
||||
}, m.width, m.height)
|
||||
}
|
||||
|
||||
head := titleStyle.Render("Work") + mutedStyle2().Render(" · ")
|
||||
if m.task.Identifier != "" {
|
||||
head += lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(m.task.Identifier)
|
||||
|
||||
Reference in New Issue
Block a user