Merge asks first; Work>work lists runs with a log tab; Shell says where everything is
This commit is contained in:
127
tui/work.go
127
tui/work.go
@@ -19,6 +19,12 @@ import (
|
||||
"paperclip-harness/tui/paperclip"
|
||||
)
|
||||
|
||||
type runLogMsg2 struct {
|
||||
runID string
|
||||
text string
|
||||
err error
|
||||
}
|
||||
|
||||
type pickTasksMsg struct {
|
||||
issues []paperclip.Issue
|
||||
err error
|
||||
@@ -46,11 +52,15 @@ type workModel struct {
|
||||
diff string
|
||||
diffErr error
|
||||
err error
|
||||
logRunID string
|
||||
logText string
|
||||
|
||||
tab int
|
||||
scroll int
|
||||
tools bool
|
||||
toolAt int
|
||||
tab int
|
||||
scroll int
|
||||
runAt int
|
||||
confirm bool
|
||||
tools bool
|
||||
toolAt int
|
||||
|
||||
// choosing a task here, so the screen does not depend on the Board
|
||||
picking bool
|
||||
@@ -63,7 +73,7 @@ type workModel struct {
|
||||
width, height int
|
||||
}
|
||||
|
||||
var workTabs = []string{"runs", "change", "facts", "review", "merge"}
|
||||
var workTabs = []string{"runs", "change", "facts", "review", "log", "merge"}
|
||||
|
||||
func newWorkModel(c *paperclip.Client, folder, issueID string) workModel {
|
||||
return workModel{client: c, folder: folder, issue: issueID, explicit: issueID != ""}
|
||||
@@ -73,6 +83,15 @@ func (m workModel) Init() tea.Cmd {
|
||||
return tea.Batch(m.load(), tickEvery(5*time.Second), tea.WindowSize())
|
||||
}
|
||||
|
||||
// loadRunLog reads one run's output for the log tab.
|
||||
func (m workModel) loadRunLog(runID string) tea.Cmd {
|
||||
c := m.client
|
||||
return func() tea.Msg {
|
||||
text, err := c.RunLog(runID, 8<<20)
|
||||
return runLogMsg2{runID: runID, text: text, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (m workModel) load() tea.Cmd {
|
||||
c, folder := m.client, m.folder
|
||||
fixed, explicit := m.issue, m.explicit
|
||||
@@ -136,6 +155,22 @@ 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.confirm {
|
||||
switch msg.String() {
|
||||
case "y":
|
||||
m.confirm = false
|
||||
if err := startMerge(m.folder, m.task.Identifier); err != nil {
|
||||
m.err = err
|
||||
return m, nil
|
||||
}
|
||||
m.tab = 5
|
||||
m.scroll = 1 << 30
|
||||
return m, m.load()
|
||||
case "esc", "n", "q":
|
||||
m.confirm = false
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
if m.picking {
|
||||
switch msg.String() {
|
||||
case "esc", "q", "o":
|
||||
@@ -220,18 +255,12 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return pickTasksMsg{issues: mine}
|
||||
}
|
||||
case "m":
|
||||
// Merging is a decision, so it asks first and shows what it will do.
|
||||
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()
|
||||
m.confirm = true
|
||||
return m, nil
|
||||
case "?":
|
||||
m.showHelp = true
|
||||
return m, nil
|
||||
@@ -258,9 +287,15 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
case "up", "k":
|
||||
if m.tab == 0 && m.runAt > 0 {
|
||||
m.runAt--
|
||||
}
|
||||
m.scroll--
|
||||
return m, nil
|
||||
case "down", "j":
|
||||
if m.tab == 0 && m.runAt < len(m.runs)-1 {
|
||||
m.runAt++
|
||||
}
|
||||
m.scroll++
|
||||
return m, nil
|
||||
case "g":
|
||||
@@ -269,10 +304,25 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "G":
|
||||
m.scroll = 1 << 30
|
||||
return m, nil
|
||||
case "enter":
|
||||
if m.tab == 0 && m.runAt < len(m.runs) {
|
||||
m.tab = 4
|
||||
m.scroll = 1 << 30
|
||||
return m, m.loadRunLog(m.runs[m.runAt].ID)
|
||||
}
|
||||
return m, nil
|
||||
case "esc":
|
||||
return m, nil
|
||||
}
|
||||
|
||||
case runLogMsg2:
|
||||
m.logRunID = msg.runID
|
||||
m.logText = msg.text
|
||||
if msg.err != nil {
|
||||
m.err = msg.err
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case pickTasksMsg:
|
||||
m.pickList = msg.issues
|
||||
m.pickAt = 0
|
||||
@@ -341,11 +391,27 @@ func (m workModel) View() string {
|
||||
case 3:
|
||||
body = m.reviewLines(m.width - 4)
|
||||
case 4:
|
||||
body = m.logLinesWork(m.width - 4)
|
||||
case 5:
|
||||
body = m.mergeLines(m.width - 4)
|
||||
default:
|
||||
body = m.factsLines(m.width - 4)
|
||||
}
|
||||
|
||||
if m.confirm {
|
||||
verify := "no verify command: the review decides"
|
||||
if cmd := verifyFromDescription(m.task.Description); cmd != "" {
|
||||
verify = "verify command: " + cmd
|
||||
}
|
||||
return helpOverlay("merge "+m.task.Identifier+"?", [][2]string{
|
||||
{"what happens", "rebase onto the base branch"},
|
||||
{"", verify},
|
||||
{"", "ocr reviews the change; a critical finding refuses the merge"},
|
||||
{"", "merge, then re-run the checks on the base branch"},
|
||||
{"y", "do it"},
|
||||
{"esc or n", "do nothing"},
|
||||
}, m.width, m.height)
|
||||
}
|
||||
if m.picking {
|
||||
entries := make([][2]string, 0, len(m.pickList))
|
||||
for i, is := range m.pickList {
|
||||
@@ -391,7 +457,7 @@ func (m workModel) View() string {
|
||||
keyHint("↑↓ or j k", "scroll"),
|
||||
keyHint("t", "open a tool pane"),
|
||||
keyHint("o", "choose a task"),
|
||||
keyHint("m", "merge this task"),
|
||||
keyHint("m", "merge this task (asks first)"),
|
||||
keyHint("]", "tabs"),
|
||||
keyHint("r", "read again"),
|
||||
keyHint("R", "rebuild every pane"),
|
||||
@@ -540,6 +606,37 @@ func (m workModel) reviewLines(width int) []string {
|
||||
|
||||
// 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.
|
||||
// logLinesWork shows the output of the run chosen on the runs tab.
|
||||
func (m workModel) logLinesWork(width int) []string {
|
||||
if m.logRunID == "" {
|
||||
return []string{"", mutedStyle2().Render(" choose a run on the runs tab, then press enter")}
|
||||
}
|
||||
if m.logText == "" {
|
||||
return []string{"", mutedStyle2().Render(" no output recorded for this run")}
|
||||
}
|
||||
out := []string{"", dimStyle().Render(" run " + short(m.logRunID)), ""}
|
||||
entries := decodeRunLog(m.logText)
|
||||
if len(entries) > 2000 {
|
||||
entries = entries[len(entries)-2000:]
|
||||
}
|
||||
for _, e := range entries {
|
||||
colour := active.Text
|
||||
switch e.stream {
|
||||
case "thinking":
|
||||
colour = active.Muted
|
||||
case "prompt":
|
||||
colour = active.Info
|
||||
case "tool":
|
||||
colour = active.Warning
|
||||
case "event":
|
||||
colour = active.Overlay
|
||||
}
|
||||
out = append(out, " "+dimStyle().Render(e.when)+" "+
|
||||
lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(truncateVisible(e.line, width-10)))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m workModel) mergeLines(width int) []string {
|
||||
out := []string{"", dimStyle().Render(" " + mergeLogPath()), ""}
|
||||
for _, ln := range mergeLogTail(max(6, m.height-6)) {
|
||||
|
||||
Reference in New Issue
Block a user