Revert "Every workspace split into real panes: 19 panes across 7 workspaces, one program per panel"

This reverts commit 32fa2752af.
This commit is contained in:
2026-11-10 16:40:14 +11:00
parent 32fa2752af
commit 851a15be74
19 changed files with 58 additions and 972 deletions

View File

@@ -49,8 +49,6 @@ type boardModel struct {
activity []paperclip.Activity
needsTab int
panel string
scroll int
zones *zone.Manager
filter string
typing bool
@@ -66,10 +64,10 @@ const (
focusCount
)
func newBoardModel(c *paperclip.Client, proj paperclip.Project, folder, panel string) boardModel {
func newBoardModel(c *paperclip.Client, proj paperclip.Project, folder string) boardModel {
// 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, panel: panel, loading: true, zones: zone.New()}
return boardModel{client: c, project: proj, folder: folder, loading: true, zones: zone.New()}
}
func (m boardModel) Init() tea.Cmd {
@@ -230,28 +228,14 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, tea.Quit
case "up", "k":
if m.panel == "detail" || m.panel == "needs" {
m.scroll--
return m, nil
}
if m.cursor > 0 {
m.cursor--
if m.panel != "" {
_ = rememberTask(m.folder, m.visible()[m.cursor].Issue.Identifier)
}
return m, m.loadDetail(m.selectedID())
}
return m, nil
case "down", "j":
if m.panel == "detail" || m.panel == "needs" {
m.scroll++
return m, nil
}
if m.cursor < len(m.visible())-1 {
m.cursor++
if m.panel != "" {
_ = rememberTask(m.folder, m.visible()[m.cursor].Issue.Identifier)
}
return m, m.loadDetail(m.selectedID())
}
return m, nil
@@ -280,12 +264,12 @@ func (m boardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil
case "[", "h":
if (m.panel == "needs" || m.focus == focusNeeds) && m.needsTab > 0 {
if m.focus == focusNeeds && m.needsTab > 0 {
m.needsTab--
}
return m, nil
case "]", "l":
if (m.panel == "needs" || m.focus == focusNeeds) && m.needsTab < 1 {
if m.focus == focusNeeds && m.needsTab < 1 {
m.needsTab++
}
return m, nil
@@ -339,9 +323,6 @@ func (m boardModel) View() string {
m.width, m.height))
}
if m.panel != "" {
return m.singleView()
}
if m.showHelp {
return helpOverlay("keys on this panel", [][2]string{
{"↑ ↓ j k", "choose a task"},
@@ -414,64 +395,6 @@ func (m boardModel) View() string {
return m.zones.Scan(strings.Join([]string{header, page, footer}, "\n"))
}
// singleView is one panel filling the pane, for when the Board workspace is
// split into real panes: tasks, the chosen task, and what needs a person.
func (m boardModel) singleView() string {
width := m.width
height := m.height
var body []string
title := "tasks"
tabs := []string(nil)
active := 0
footer := ""
switch m.panel {
case "detail":
title = "the chosen task"
body = m.summaryLines(width - 3)
footer = hintLine([]string{
keyHint("r", "read again"),
keyHint("R", "rebuild"),
keyHint("?", "keys"),
}, width)
case "needs":
title = "needs you"
tabs = []string{"decisions", "activity"}
active = m.needsTab
if m.needsTab == 0 {
body = m.needsLines(width-3, height-3)
} else {
body = m.activityLines(width-3, height-3)
}
footer = hintLine([]string{
keyHint("[ ]", "switch tab"),
keyHint("r", "read again"),
keyHint("R", "rebuild"),
}, width)
default:
body = m.treeLines(width - 3)
footer = hintLine([]string{
keyHint("↑↓ j k", "choose"),
keyHint("enter", "show everywhere"),
keyHint("a", "assign"),
keyHint("u", "unblock"),
keyHint("/", "filter"),
keyHint("?", "keys"),
}, width)
}
if m.showHelp {
return helpOverlay(title, [][2]string{
{"↑ ↓ j k", "choose a task"},
{"enter", "show it in every workspace"},
{"a / u", "assign / unblock"},
{"/", "filter"}, {"[ ]", "switch tab"},
{"r / R", "read again / rebuild"}, {"q", "leave"},
}, width, height)
}
return singlePanel(title, tabs, active, body, width, height, footer, m.scroll)
}
func (m boardModel) headerLine() string {
left := panelTitleStyle().Render("Board") + mutedStyle2().Render(" · "+m.project.Name)
state := "reading…"

View File

@@ -289,28 +289,3 @@ func helpOverlay(title string, entries [][2]string, w, h int) string {
p := panel{title: title, width: boxW, height: boxH, focused: true}
return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, p.render(lines))
}
// singlePanel draws one panel filling the whole pane: a title, optional tabs,
// a scrolling body, and one line of keys. It is what a pane shows when a
// workspace has been split into real panes instead of one program drawing
// several panels inside itself.
func singlePanel(title string, tabs []string, active int, body []string, w, h int, footer string, scroll int) string {
if w < 12 || h < 4 {
return fmt.Sprintf("this pane is %d × %d; it needs 12 × 4", w, h)
}
p := panel{title: title, tabs: tabs, active: active, width: w, height: h - 1, focused: true}
visible := p.bodyHeight()
maxScroll := max(0, len(body)-visible)
if scroll > maxScroll {
scroll = maxScroll
}
if scroll < 0 {
scroll = 0
}
from := scroll
to := min(len(body), from+visible)
if to < from {
to = from
}
return p.render(body[from:to]) + "\n" + truncateVisible(footer, w)
}

View File

@@ -20,7 +20,6 @@ const defaultAgent = "403de1f7-645c-4d35-bc7e-19b01d77bacc"
func main() {
screen := flag.String("screen", "board", "which screen to run: board, work, reply, new, project, repair")
panelName := flag.String("panel", "", "run one panel of that screen, for a workspace split into real panes")
folder := flag.String("folder", "", "project folder, for the board screen (defaults to the current directory)")
light := flag.String("theme", "", "colour variant: dark, light, or empty to follow the terminal")
ascii := flag.Bool("ascii", false, "plain characters instead of Nerd Font icons")
@@ -53,21 +52,21 @@ func main() {
if dir == "" {
dir, _ = os.Getwd()
}
m = newBoardModel(client, proj, dir, *panelName)
m = newBoardModel(client, proj, dir)
case "project":
dir := *folder
if dir == "" {
dir, _ = os.Getwd()
}
m = newProjectModel(client, dir, *panelName)
m = newProjectModel(client, dir)
case "repair":
dir := *folder
if dir == "" {
dir, _ = os.Getwd()
}
m = newRepairModel(client, dir, *panelName)
m = newRepairModel(client, dir)
case "work":
dir := *folder
@@ -78,7 +77,7 @@ func main() {
if issue == "" {
issue = watchedTask(dir)
}
m = newWorkModel(client, dir, issue, *panelName)
m = newWorkModel(client, dir, issue)
case "tree":
proj, err := client.ResolveProject(*project)
@@ -101,7 +100,7 @@ func main() {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
m = newTaskForm(client, proj, *agent, *panelName)
m = newTaskForm(client, proj, *agent)
case "log":
task, err := resolveIssue(client, *issue)
@@ -125,7 +124,7 @@ func main() {
}
id = task.ID
}
m = newReplyModel(client, dir, id, *panelName)
m = newReplyModel(client, dir, id)
default:
fmt.Fprintf(os.Stderr, "unknown screen %q: use board, work, reply, new, project or repair\n", *screen)

View File

@@ -27,7 +27,6 @@ type createdMsg struct {
}
type newTaskModel struct {
panel string
client *paperclip.Client
proj paperclip.Project
agent string
@@ -62,8 +61,8 @@ var fieldHints = []string{
"yes, to hand it to the configured agent after the blocker is set",
}
func newTaskForm(c *paperclip.Client, proj paperclip.Project, agentID, panel string) newTaskModel {
m := newTaskModel{client: c, proj: proj, agent: agentID, panel: panel}
func newTaskForm(c *paperclip.Client, proj paperclip.Project, agentID string) newTaskModel {
m := newTaskModel{client: c, proj: proj, agent: agentID}
for range fieldNames {
ti := textinput.New()
ti.Placeholder = ""
@@ -241,39 +240,7 @@ func (m newTaskModel) guidanceLines(width int) []string {
return out
}
// singleView is one panel filling the pane: the form, the guidance, or what
// will be sent. The form keeps the keyboard, because its letters are text.
func (m newTaskModel) singleView() string {
title := "new task"
var body []string
footer := ""
switch m.panel {
case "guidance":
title = "what a task needs"
body = m.guidanceLines(m.width - 3)
footer = hintLine([]string{keyHint("r", "nothing to read here"), keyHint("R", "rebuild")}, m.width)
case "preview":
title = "what will be sent"
body = m.previewLines(m.width - 3)
footer = hintLine([]string{keyHint("tab", "back to the form"), keyHint("R", "rebuild")}, m.width)
default:
body = m.formLines(m.width - 3)
footer = hintLine([]string{
keyHint("tab", "next field"),
keyHint("enter", "next, and create on the last"),
keyHint("esc", "cancel"),
}, m.width)
}
return singlePanel(title, nil, 0, body, m.width, m.height, footer, 0)
}
func (m newTaskModel) View() string {
if m.panel != "" {
if m.width == 0 {
return "reading…"
}
return m.singleView()
}
if m.width == 0 || m.height == 0 {
return "reading…"
}

View File

@@ -35,7 +35,6 @@ type taskCost struct {
}
type projectModel struct {
panel string
client *paperclip.Client
folder string
@@ -56,8 +55,8 @@ type projectModel struct {
var projectTabs = []string{"facts", "costs", "settings"}
func newProjectModel(c *paperclip.Client, folder, panel string) projectModel {
return projectModel{client: c, folder: folder, panel: panel}
func newProjectModel(c *paperclip.Client, folder string) projectModel {
return projectModel{client: c, folder: folder}
}
func (m projectModel) Init() tea.Cmd {
@@ -194,46 +193,7 @@ func (m projectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
// singleView is one panel filling the pane: facts, costs or settings.
func (m projectModel) singleView() string {
titles := map[string]string{"facts": "the project", "costs": "what it has cost", "settings": "settings and limits"}
title := titles[m.panel]
if title == "" {
title = "the project"
}
var body []string
switch m.panel {
case "costs":
body = m.costLines()
case "settings":
body = m.settingLines()
default:
body = m.factLines()
}
if m.err != nil {
body = append([]string{lipgloss.NewStyle().Foreground(lipgloss.Color(active.Error)).Render("error: " + m.err.Error()), ""}, body...)
}
footer := hintLine([]string{
keyHint("↑↓ j k", "scroll"),
keyHint("r", "read again"),
keyHint("R", "rebuild"),
keyHint("?", "keys"),
}, m.width)
if m.showHelp {
return helpOverlay(title, [][2]string{
{"↑ ↓ j k", "scroll"}, {"r / R", "read again / rebuild"}, {"q", "leave"},
}, m.width, m.height)
}
return singlePanel(title, nil, 0, body, m.width, m.height, footer, m.scroll)
}
func (m projectModel) View() string {
if m.panel != "" {
if m.width == 0 {
return "reading…"
}
return m.singleView()
}
if m.width == 0 || m.height == 0 {
return "reading…"
}

View File

@@ -40,7 +40,6 @@ type layoutRow struct {
}
type repairModel struct {
panel string
client *paperclip.Client
folder string
@@ -59,8 +58,8 @@ type repairModel struct {
var repairTabs = []string{"health", "layout", "rebuild log"}
func newRepairModel(c *paperclip.Client, folder, panel string) repairModel {
return repairModel{client: c, folder: folder, panel: panel}
func newRepairModel(c *paperclip.Client, folder string) repairModel {
return repairModel{client: c, folder: folder}
}
func (m repairModel) Init() tea.Cmd {
@@ -255,45 +254,7 @@ func (m repairModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
// singleView is one panel filling the pane: health, layout or the rebuild log.
func (m repairModel) singleView() string {
titles := map[string]string{"health": "checks", "layout": "the layout as TUIOS has it", "log": "the last rebuild"}
title := titles[m.panel]
if title == "" {
title = "checks"
}
var body []string
switch m.panel {
case "layout":
body = m.layoutLines()
case "log":
body = m.logLines()
default:
body = m.healthLines()
}
footer := hintLine([]string{
keyHint("↑↓ j k", "scroll"),
keyHint("R", "rebuild every pane"),
keyHint("B", "add missing panes"),
keyHint("r", "check again"),
keyHint("?", "keys"),
}, m.width)
if m.showHelp {
return helpOverlay(title, [][2]string{
{"R", "rebuild every pane"}, {"B", "add only the missing panes"},
{"↑ ↓ j k", "scroll"}, {"r", "check again"}, {"q", "leave"},
}, m.width, m.height)
}
return singlePanel(title, nil, 0, body, m.width, m.height, footer, m.scroll)
}
func (m repairModel) View() string {
if m.panel != "" {
if m.width == 0 {
return "checking…"
}
return m.singleView()
}
if m.width == 0 || m.height == 0 {
return "reading…"
}

View File

@@ -28,7 +28,6 @@ type replyThreadMsg struct {
type replySentMsg struct{ task paperclip.Issue }
type replyModel struct {
panel string
client *paperclip.Client
folder string
issue string
@@ -51,9 +50,8 @@ type replyModel struct {
height int
}
func newReplyModel(c *paperclip.Client, folder, issueID, panel string) replyModel {
return replyModel{client: c, folder: folder, issue: issueID, explicit: issueID != "",
panel: panel, inBox: panel == "reply"}
func newReplyModel(c *paperclip.Client, folder, issueID string) replyModel {
return replyModel{client: c, folder: folder, issue: issueID, explicit: issueID != ""}
}
func (m replyModel) Init() tea.Cmd {
@@ -229,62 +227,7 @@ func (m replyModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
// singleView is one panel filling the pane: the task, the thread, or the box.
func (m replyModel) singleView() string {
title := "thread"
var body []string
footer := ""
switch m.panel {
case "task":
title = "the task"
body = m.headerLines(m.width - 3)
footer = hintLine([]string{keyHint("r", "read again"), keyHint("R", "rebuild")}, m.width)
case "reply":
title = "reply · " + m.statusNote()
body = m.composerLines(m.width-3, m.height-3)
footer = hintLine([]string{
keyHint("enter", "send"),
keyHint("alt+enter", "new line"),
keyHint("s", "status"),
}, m.width)
if m.notice != "" {
footer = lipgloss.NewStyle().Foreground(lipgloss.Color(active.Success)).Render(m.notice)
}
if m.err != nil {
footer = lipgloss.NewStyle().Foreground(lipgloss.Color(active.Error)).Render(truncateVisible("error: "+m.err.Error(), m.width))
}
default:
body = m.threadLines(m.width - 3)
footer = hintLine([]string{
keyHint("↑↓ j k", "scroll"),
keyHint("r", "read again"),
keyHint("R", "rebuild"),
keyHint("?", "keys"),
}, m.width)
}
if m.showHelp {
return helpOverlay(title, [][2]string{
{"enter", "write a reply"}, {"s", "status to send with it"},
{"↑ ↓ j k", "scroll"}, {"r / R", "read again / rebuild"},
}, m.width, m.height)
}
return singlePanel(title, nil, 0, body, m.width, m.height, footer, m.scroll)
}
func (m replyModel) statusNote() string {
if m.next == "" {
return "leaves the status alone"
}
return "also sets the status to " + m.next
}
func (m replyModel) View() string {
if m.panel != "" {
if m.width == 0 {
return "reading…"
}
return m.singleView()
}
if m.width == 0 || m.height == 0 {
return "reading…"
}

View File

@@ -29,7 +29,6 @@ type workMsg struct {
}
type workModel struct {
panel string
client *paperclip.Client
folder string
issue string
@@ -53,8 +52,8 @@ type workModel struct {
var workTabs = []string{"runs", "change", "facts"}
func newWorkModel(c *paperclip.Client, folder, issueID, panel string) workModel {
return workModel{client: c, folder: folder, issue: issueID, explicit: issueID != "", panel: panel}
func newWorkModel(c *paperclip.Client, folder, issueID string) workModel {
return workModel{client: c, folder: folder, issue: issueID, explicit: issueID != ""}
}
func (m workModel) Init() tea.Cmd {
@@ -188,49 +187,7 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
// singleView is one panel filling the pane: runs, change or facts.
func (m workModel) singleView() string {
titles := map[string]string{"runs": "runs and activity", "change": "the change", "facts": "worktree facts"}
title, ok := titles[m.panel]
if !ok {
title = "runs and activity"
}
var body []string
switch m.panel {
case "change":
body = m.diffLines(m.width - 3)
case "facts":
body = m.factsLines(m.width - 3)
default:
body = m.runsLines(m.width - 3)
}
head := m.task.Identifier
if head == "" {
head = "no task chosen"
}
footer := hintLine([]string{
keyHint("↑↓ j k", "scroll"),
keyHint("g G", "top / bottom"),
keyHint("r", "read again"),
keyHint("R", "rebuild"),
keyHint("?", "keys"),
}, m.width)
if m.showHelp {
return helpOverlay(title, [][2]string{
{"↑ ↓ j k", "scroll"}, {"g / G", "top / bottom"},
{"r / R", "read again / rebuild"}, {"q", "leave"},
}, m.width, m.height)
}
return singlePanel(title+" · "+head, nil, 0, body, m.width, m.height, footer, m.scroll)
}
func (m workModel) View() string {
if m.panel != "" {
if m.width == 0 {
return "reading…"
}
return m.singleView()
}
if m.width == 0 || m.height == 0 {
return "reading…"
}