Work>work chooses its own task and merges from the interface; panes find their tools; shell follows the project folder

This commit is contained in:
Sam
2026-11-11 12:02:12 +11:00
parent 678c497064
commit 7198c34e42
13 changed files with 441 additions and 15 deletions

View File

@@ -19,6 +19,11 @@ import (
"paperclip-harness/tui/paperclip"
)
type pickTasksMsg struct {
issues []paperclip.Issue
err error
}
type workMsg struct {
task paperclip.Issue
runs []paperclip.Run
@@ -46,13 +51,19 @@ type workModel struct {
scroll int
tools bool
toolAt int
readAt time.Time
// choosing a task here, so the screen does not depend on the Board
picking bool
pickList []paperclip.Issue
pickNames map[string]string
pickAt int
readAt time.Time
showHelp bool
width, height int
}
var workTabs = []string{"runs", "change", "facts", "review"}
var workTabs = []string{"runs", "change", "facts", "review", "merge"}
func newWorkModel(c *paperclip.Client, folder, issueID string) workModel {
return workModel{client: c, folder: folder, issue: issueID, explicit: issueID != ""}
@@ -125,6 +136,34 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(m.load(), tickEvery(5*time.Second), tea.WindowSize())
case tea.KeyMsg:
if m.picking {
switch msg.String() {
case "esc", "q", "o":
m.picking = false
return m, nil
case "up", "k":
m.pickAt = max(0, m.pickAt-1)
return m, nil
case "down", "j":
m.pickAt = min(len(m.pickList)-1, m.pickAt+1)
return m, nil
case "enter":
if m.pickAt < len(m.pickList) {
chosen := m.pickList[m.pickAt]
m.picking = false
if err := rememberTask(m.folder, chosen.Identifier); err != nil {
m.err = err
return m, nil
}
m.explicit = true
m.issue = chosen.ID
m.task = chosen
return m, m.load()
}
return m, nil
}
return m, nil
}
if m.tools {
switch msg.String() {
case "esc", "q", "t":
@@ -160,6 +199,39 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "t":
m.tools = true
return m, nil
case "o":
c := m.client
folder := m.folder
return m, func() tea.Msg {
cfg, err := readHarnessFile(folder)
if err != nil {
return errMsg{err}
}
issues, err := c.Issues()
if err != nil {
return errMsg{err}
}
var mine []paperclip.Issue
for _, i := range issues {
if i.ProjectID == cfg["paperclip.project"] {
mine = append(mine, i)
}
}
return pickTasksMsg{issues: mine}
}
case "m":
if m.task.Identifier == "" {
return m, nil
}
// The merge runs on its own and writes what it does to a log this
// tab reads, so nobody has to type a command or leave the screen.
if err := startMerge(m.folder, m.task.Identifier); err != nil {
m.err = err
return m, nil
}
m.tab = 4
m.scroll = 1 << 30
return m, m.load()
case "?":
m.showHelp = true
return m, nil
@@ -201,6 +273,15 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
case pickTasksMsg:
m.pickList = msg.issues
m.pickAt = 0
m.picking = true
if msg.err != nil {
m.err = msg.err
}
return m, nil
case workMsg:
m.task = msg.task
m.runs = msg.runs
@@ -259,10 +340,23 @@ func (m workModel) View() string {
body = m.diffLines(m.width - 4)
case 3:
body = m.reviewLines(m.width - 4)
case 4:
body = m.mergeLines(m.width - 4)
default:
body = m.factsLines(m.width - 4)
}
if m.picking {
entries := make([][2]string, 0, len(m.pickList))
for i, is := range m.pickList {
mark := " "
if i == m.pickAt {
mark = "▸ "
}
entries = append(entries, [2]string{mark + is.Identifier, is.Status + " " + is.Title})
}
return helpOverlay("choose a task to look at", entries, m.width, m.height)
}
if m.tools {
entries := make([][2]string, 0, len(tools))
for i, t := range tools {
@@ -296,6 +390,9 @@ func (m workModel) View() string {
keyHint("[ ]", "switch tab"),
keyHint("↑↓ or j k", "scroll"),
keyHint("t", "open a tool pane"),
keyHint("o", "choose a task"),
keyHint("m", "merge this task"),
keyHint("]", "tabs"),
keyHint("r", "read again"),
keyHint("R", "rebuild every pane"),
keyHint("q", "leave"),
@@ -441,6 +538,24 @@ func (m workModel) reviewLines(width int) []string {
return out
}
// mergeLines shows the last merge this screen started, read from the log the
// merge writes. Pressing m again is safe: the merge takes a lock.
func (m workModel) mergeLines(width int) []string {
out := []string{"", dimStyle().Render(" " + mergeLogPath()), ""}
for _, ln := range mergeLogTail(max(6, m.height-6)) {
colour := active.Text
switch {
case strings.Contains(ln, "refused"), strings.Contains(ln, "stopped"):
colour = active.Error
case strings.Contains(ln, "merged into"), strings.Contains(ln, "finished"):
colour = active.Success
}
out = append(out, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(truncateVisible(ln, width-2)))
}
out = append(out, "", dimStyle().Render(" m runs the checks and merges this task; the log above is what it did"))
return out
}
func (m workModel) factsLines(width int) []string {
var out []string
out = append(out, "")