R rebuilds every pane detached; r refreshes data; Author gains guidance and preview; Repair gains the rebuild log

This commit is contained in:
Sam
2026-11-10 14:34:14 +11:00
parent 351cb35e8e
commit 972d4c0184
15 changed files with 683 additions and 23 deletions

View File

@@ -20,10 +20,10 @@ import (
)
type createdMsg struct {
issue paperclip.Issue
blocked string
issue paperclip.Issue
blocked string
assigned bool
err error
err error
}
type newTaskModel struct {
@@ -37,9 +37,12 @@ type newTaskModel struct {
busy bool
result string
tab int
width, height int
}
var authorTabs = []string{"form", "guidance", "preview"}
const (
fieldTitle = iota
fieldBody
@@ -127,7 +130,19 @@ func (m newTaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyMsg:
switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc:
case tea.KeyCtrlC:
return m, tea.Quit
case tea.KeyCtrlN:
m.tab = (m.tab + 1) % len(authorTabs)
return m, nil
case tea.KeyCtrlP:
m.tab = (m.tab + len(authorTabs) - 1) % len(authorTabs)
return m, nil
case tea.KeyEsc:
if m.tab != 0 {
m.tab = 0
return m, m.inputs[m.focus].Focus()
}
return m, tea.Quit
case tea.KeyTab, tea.KeyDown:
m.inputs[m.focus].Blur()
@@ -177,6 +192,54 @@ func (m newTaskModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, cmd
}
// descriptionPreview shows exactly what the agent will be given: the same
// text that submit() puts in the task's description, built the same way.
func (m newTaskModel) descriptionPreview() string {
body := strings.TrimSpace(m.inputs[fieldBody].Value())
verify := strings.TrimSpace(m.inputs[fieldVerify].Value())
blocker := strings.TrimSpace(m.inputs[fieldBlocker].Value())
out := body
if verify != "" {
out += "\n\nDone when this command succeeds:\n\n " + verify
}
if blocker != "" {
out += "\n\nMust finish first: " + blocker
}
if strings.TrimSpace(out) == "" {
return "nothing written yet"
}
return out
}
func (m newTaskModel) previewLines(width int) []string {
out := []string{"", dimStyle().Render(" the task's description, exactly as the agent receives it"), ""}
for _, para := range strings.Split(m.descriptionPreview(), "\n") {
for _, ln := range wrap(para, max(8, width-2)) {
out = append(out, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(ln))
}
if para == "" {
out = append(out, "")
}
}
return out
}
func (m newTaskModel) guidanceLines(width int) []string {
out := []string{"", groupHeading("what a task needs", 4, width-2), ""}
add := func(k, v string) {
out = append(out, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Accent)).Render(k))
for _, ln := range wrap(v, max(8, width-4)) {
out = append(out, " "+mutedStyle2().Render(ln))
}
out = append(out, "")
}
add("one thing", "A task that asks for two things cannot be judged, only argued about.")
add("a command", "The verify command decides when the work is finished. The agent does not.")
add("a boundary", "Say which files may change. An unbounded task is how a review becomes an autopsy.")
add("a blocker", "If it must wait, name the task it waits for. A sentence in the description is not a blocker.")
return out
}
func (m newTaskModel) View() string {
if m.width == 0 || m.height == 0 {
return "reading…"
@@ -187,7 +250,47 @@ func (m newTaskModel) View() string {
}
head := titleStyle.Render("Author") + mutedStyle2().Render(" · new task in "+m.proj.Name)
bodyHeight := m.height - 2
formLines := m.formLines(m.width - 4)
footer := hintLine([]string{
keyHint("tab", "next field"),
keyHint("enter", "next, and create on the last"),
keyHint("ctrl+n", "form, guidance, preview"),
keyHint("esc", "cancel"),
}, m.width)
// Wide screens: the form on the left, what to write and what will be sent on
// the right. Narrow ones: one panel, switched with the tabs.
if m.width >= 120 {
formW := max(50, m.width*58/100)
sideW := m.width - formW - 1
topH := bodyHeight / 2
bottomH := bodyHeight - topH
form := panel{title: "new task", width: formW, height: bodyHeight, focused: true}
guide := panel{title: "what a task needs", width: sideW, height: topH}
prev := panel{title: "what will be sent", width: sideW, height: bottomH}
page := lipgloss.JoinHorizontal(lipgloss.Top,
form.render(formLines), " ",
lipgloss.JoinVertical(lipgloss.Left,
guide.render(m.guidanceLines(sideW-3)),
prev.render(m.previewLines(sideW-3))))
return strings.Join([]string{head, page, footer}, "\n")
}
p := panel{title: "new task", width: m.width, height: bodyHeight, tabs: authorTabs, active: m.tab, focused: true}
lines := formLines
switch m.tab {
case 1:
lines = m.guidanceLines(m.width - 3)
case 2:
lines = m.previewLines(m.width - 3)
}
return strings.Join([]string{head, p.render(lines), footer}, "\n")
}
func (m newTaskModel) formLines(width int) []string {
var body []string
body = append(body, "")
for i, in := range m.inputs {
@@ -201,25 +304,15 @@ func (m newTaskModel) View() string {
body = append(body, " "+mutedStyle2().Render(label))
}
body = append(body, " "+in.View())
hint := fieldHints[i]
if i == m.focus {
body = append(body, " "+dimStyle().Render(hint))
body = append(body, " "+dimStyle().Render(fieldHints[i]))
}
body = append(body, "")
}
if m.err != nil {
body = append(body, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Error)).Render("error: "+m.err.Error()))
} else if m.busy {
body = append(body, " "+mutedStyle2().Render("writing to Paperclip…"))
}
p := panel{title: "new task", width: m.width, height: m.height - 2, focused: true}
footer := hintLine([]string{
keyHint("tab", "next field"),
keyHint("enter", "next, and create on the last"),
keyHint("esc", "cancel"),
}, m.width)
return strings.Join([]string{head, p.render(body), footer}, "\n")
return body
}