diff --git a/.pi/memory-vault/packs/paperclip-harness/20-context/tmp.md b/.pi/memory-vault/packs/paperclip-harness/20-context/tmp.md index abb2c43..3068d1c 100644 --- a/.pi/memory-vault/packs/paperclip-harness/20-context/tmp.md +++ b/.pi/memory-vault/packs/paperclip-harness/20-context/tmp.md @@ -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 + +- 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]] 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 f6d78c2..7ab3711 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 @@ -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 + +- 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]] diff --git a/.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan.md b/.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan.md new file mode 100644 index 0000000..75aa9e4 --- /dev/null +++ b/.pi/memory-vault/packs/paperclip-harness/80-sessions/paperclip-harness-tui-rebuild-refresh-keys-author-repair-pan.md @@ -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 `, `board: (session paperclip-harness, folder /tmp/pc-sandbox-2026-11-10)`, `watching: AUD-162`, then `closed ` 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 + +- 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]] diff --git a/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-34-34.md b/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-34-34.md new file mode 100644 index 0000000..c4a491b --- /dev/null +++ b/.pi/memory-vault/packs/paperclip-harness/80-sessions/rich-discovery-tmp-2026-11-10-03-34-34.md @@ -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 + +- 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]] diff --git a/README.md b/README.md index be9a99d..dbfe559 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/harness b/bin/harness index f6a6700..31dd5ec 100755 Binary files a/bin/harness and b/bin/harness differ diff --git a/docs/UI-LAYOUT.md b/docs/UI-LAYOUT.md index 4655ba3..b4cb14c 100644 --- a/docs/UI-LAYOUT.md +++ b/docs/UI-LAYOUT.md @@ -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 | diff --git a/tui/board.go b/tui/board.go index 4df2887..b42c8cb 100644 --- a/tui/board.go +++ b/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. diff --git a/tui/frame.go b/tui/frame.go index 05ea3d5..f9a72f6 100644 --- a/tui/frame.go +++ b/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)) +} diff --git a/tui/project.go b/tui/project.go index 8141863..cec71cf 100644 --- a/tui/project.go +++ b/tui/project.go @@ -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) diff --git a/tui/repair.go b/tui/repair.go index 0a885bc..db27490 100644 --- a/tui/repair.go +++ b/tui/repair.go @@ -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() { diff --git a/tui/reply.go b/tui/reply.go index d1d9972..dd6f9d4 100644 --- a/tui/reply.go +++ b/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)). diff --git a/tui/work.go b/tui/work.go index e64859e..a6398b1 100644 --- a/tui/work.go +++ b/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)