From 501c2a71e58e8fa249edbae7e9ce98a292b49d52 Mon Sep 17 00:00:00 2001 From: viljarb Date: Sat, 14 Feb 2026 15:29:08 +0100 Subject: [PATCH] update description and status, and replicating help message from lazybeads --- .beads/issues.jsonl | 2 +- pkg/tui/views/dashboard/help_bar.go | 3 +- pkg/tui/views/dashboard/issue_list.go | 1 - pkg/tui/views/dashboard/keys.go | 30 +++++-- pkg/tui/views/dashboard/model.go | 38 +++++++-- pkg/tui/views/dashboard/update.go | 111 +++++++++++++++++++++++++- pkg/tui/views/dashboard/view.go | 29 +++++++ 7 files changed, 197 insertions(+), 17 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index 6448261..7077f7c 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -1 +1 @@ -{"id":"LazyPM-LPM-32-dko","title":"231","status":"open","priority":2,"issue_type":"feature","owner":"viljarb@tutanota.com","created_at":"2026-02-13T12:35:13.465758325+01:00","created_by":"vb","updated_at":"2026-02-13T12:35:13.465758325+01:00"} +{"id":"LazyPM-LPM-32-dko","title":"231","status":"open","priority":2,"issue_type":"chore","owner":"viljarb@tutanota.com","created_at":"2026-02-13T12:35:13.465758325+01:00","created_by":"vb","updated_at":"2026-02-14T11:40:22.72004373+01:00"} diff --git a/pkg/tui/views/dashboard/help_bar.go b/pkg/tui/views/dashboard/help_bar.go index 7c85500..fee67e6 100644 --- a/pkg/tui/views/dashboard/help_bar.go +++ b/pkg/tui/views/dashboard/help_bar.go @@ -34,7 +34,7 @@ func (h HelpBar) shortHelp() string { styles.HighlightKey("↑/k") + " up", styles.HighlightKey("↓/j") + " down", styles.HighlightKey("a") + " add", - styles.HighlightKey("e") + " edit", + styles.HighlightKey("e/d/s") + " edit", styles.HighlightKey("x") + " delete", styles.HighlightKey("q") + " quit", styles.HighlightKey("?") + " help", @@ -71,6 +71,7 @@ func (h HelpBar) fullHelp() string { renderRow("↑/k", "up", "enter", "view issue"), renderRow("↓/j", "down", "b", "back to list"), renderRow("a", "add issue", "e", "edit title"), + renderRow("d", "edit desc", "s", "change status"), renderRow("x", "delete issue", "?", "help"), renderRow("q", "quit", "", ""), } diff --git a/pkg/tui/views/dashboard/issue_list.go b/pkg/tui/views/dashboard/issue_list.go index 9730e95..11cdf08 100644 --- a/pkg/tui/views/dashboard/issue_list.go +++ b/pkg/tui/views/dashboard/issue_list.go @@ -201,7 +201,6 @@ func (l *IssueList) SetIssues(issues []models.Issue) tea.Cmd { return l.list.SetItems(listIssues) } -// SelectIssueID sets the list selection to the issue with the given ID, if present. func (l *IssueList) SelectIssueID(issueID string) { items := l.list.Items() for i := 0; i < len(items); i++ { diff --git a/pkg/tui/views/dashboard/keys.go b/pkg/tui/views/dashboard/keys.go index 3dd21f6..0cb780e 100644 --- a/pkg/tui/views/dashboard/keys.go +++ b/pkg/tui/views/dashboard/keys.go @@ -12,9 +12,11 @@ type DashboardKeyMap struct { BackToList key.Binding ScrollUp key.Binding ScrollDown key.Binding - EditTitle key.Binding - AddIssue key.Binding - DeleteIssue key.Binding + EditTitle key.Binding + EditDescription key.Binding + ChangeStatus key.Binding + AddIssue key.Binding + DeleteIssue key.Binding } var defaultDashboardKeyMap = DashboardKeyMap{ @@ -46,6 +48,14 @@ var defaultDashboardKeyMap = DashboardKeyMap{ key.WithKeys("e"), key.WithHelp("e", "edit title"), ), + EditDescription: key.NewBinding( + key.WithKeys("d"), + key.WithHelp("d", "edit description"), + ), + ChangeStatus: key.NewBinding( + key.WithKeys("s"), + key.WithHelp("s", "change status"), + ), AddIssue: key.NewBinding( key.WithKeys("a"), key.WithHelp("a", "add issue"), @@ -72,16 +82,24 @@ func (d *Model) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { d.issueDetail.ScrollUp(1) case d.IsFocusedOnDetail() && key.Matches(msg, d.keyMap.ScrollDown): d.issueDetail.ScrollDown(1) - case !d.editingTitle && !d.creatingIssue && key.Matches(msg, d.keyMap.EditTitle): - // just to check if an issue is selected before trying to edit + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && key.Matches(msg, d.keyMap.EditTitle): if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startEditTitle(selected) cmd = d.titleInput.Focus() } + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && key.Matches(msg, d.keyMap.EditDescription): + if selected := d.issueList.SelectedItem(); selected.ID != "" { + d.startEditDescription(selected) + cmd = d.descriptionInput.Focus() + } + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && key.Matches(msg, d.keyMap.ChangeStatus): + if selected := d.issueList.SelectedItem(); selected.ID != "" { + d.startChooseStatus(selected) + } case !d.editingTitle && !d.creatingIssue && key.Matches(msg, d.keyMap.AddIssue): d.startCreateIssue() cmd = d.createTitleInput.Focus() - case !d.editingTitle && !d.creatingIssue && !d.confirmingDelete && key.Matches(msg, d.keyMap.DeleteIssue): + case !d.editingTitle && !d.creatingIssue && !d.editingDescription && !d.choosingStatus && !d.confirmingDelete && key.Matches(msg, d.keyMap.DeleteIssue): if selected := d.issueList.SelectedItem(); selected.ID != "" { d.startConfirmDelete(selected.ID, d.issueList.Index()) } diff --git a/pkg/tui/views/dashboard/model.go b/pkg/tui/views/dashboard/model.go index 5664a3f..1b2f9dd 100644 --- a/pkg/tui/views/dashboard/model.go +++ b/pkg/tui/views/dashboard/model.go @@ -3,6 +3,7 @@ package dashboard import ( "github.com/LazyBachelor/LazyPM/internal/service" "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/textarea" tea "github.com/charmbracelet/bubbletea" ) @@ -17,15 +18,22 @@ type Model struct { height int focusedPane int // 0 = list, 1 = detail - editingTitle bool // should be true while we are editing a title - titleInput textinput.Model - editingIssueID string - creatingIssue bool // should be true while we are creating a new issue - createTitleInput textinput.Model + editingTitle bool // true while we are editing a title + titleInput textinput.Model + editingIssueID string - confirmingDelete bool + editingDescription bool // true while editing a description + descriptionInput textarea.Model + editingDescIssueID string + creatingIssue bool // true while creating a new issue + createTitleInput textinput.Model + + confirmingDelete bool // true while confirming a delete deleteConfirmID string deleteConfirmIndex int + + choosingStatus bool + statusIssueID string } func NewDashboard(svc *service.Services) *Model { @@ -52,6 +60,12 @@ func NewDashboard(svc *service.Services) *Model { createTi.CharLimit = 256 m.createTitleInput = createTi + descTa := textarea.New() + descTa.Placeholder = "Issue description..." + descTa.SetWidth(56) + descTa.SetHeight(8) + m.descriptionInput = descTa + if selected := m.issueList.SelectedItem(); selected.ID != "" { m.issueDetail.SetIssue(selected.Issue) } @@ -66,6 +80,13 @@ func (m *Model) startEditTitle(selected ListIssue) { m.titleInput.CursorEnd() } +func (m *Model) startEditDescription(selected ListIssue) { + m.editingDescription = true + m.editingDescIssueID = selected.ID + m.descriptionInput.SetValue(selected.Issue.Description) + m.descriptionInput.CursorEnd() +} + func (m *Model) startCreateIssue() { m.creatingIssue = true m.createTitleInput.SetValue("") @@ -78,6 +99,11 @@ func (m *Model) startConfirmDelete(issueID string, index int) { m.deleteConfirmIndex = index } +func (m *Model) startChooseStatus(selected ListIssue) { + m.choosingStatus = true + m.statusIssueID = selected.ID +} + func (m *Model) Init() tea.Cmd { return nil } diff --git a/pkg/tui/views/dashboard/update.go b/pkg/tui/views/dashboard/update.go index 22b1f51..9dc9d3c 100644 --- a/pkg/tui/views/dashboard/update.go +++ b/pkg/tui/views/dashboard/update.go @@ -14,6 +14,16 @@ type issueTitleUpdatedMsg struct { Err error } +type issueDescriptionUpdatedMsg struct { + IssueID string + Err error +} + +type issueStatusUpdatedMsg struct { + IssueID string + Err error +} + type selectIssueMsg struct { IssueID string } @@ -37,6 +47,22 @@ func updateIssueTitleCmd(svc *service.Services, issueID, newTitle string) tea.Cm } } +func updateIssueDescriptionCmd(svc *service.Services, issueID, newDescription string) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"description": newDescription} + err := svc.Beads.UpdateIssue(context.Background(), issueID, updates, "tui") + return issueDescriptionUpdatedMsg{IssueID: issueID, Err: err} + } +} + +func updateIssueStatusCmd(svc *service.Services, issueID, status string) tea.Cmd { + return func() tea.Msg { + updates := map[string]interface{}{"status": status} + err := svc.Beads.UpdateIssue(context.Background(), issueID, updates, "tui") + return issueStatusUpdatedMsg{IssueID: issueID, Err: err} + } +} + func createIssueCmd(svc *service.Services, title string) tea.Cmd { return func() tea.Msg { issue := &models.Issue{ @@ -65,7 +91,26 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg.Err != nil { return m, nil } - // reload issues and keep selection on the updated issue + issues, err := m.svc.Beads.AllIssues(context.Background()) + if err != nil { + return m, nil + } + setItemsCmd := m.issueList.SetIssues(issues) + for _, issue := range issues { + if issue.ID == msg.IssueID { + m.issueDetail.SetIssue(issue) + break + } + } + return m, tea.Sequence(setItemsCmd, func() tea.Msg { return selectIssueMsg{IssueID: msg.IssueID} }) + + case issueDescriptionUpdatedMsg: + m.editingDescription = false + m.editingDescIssueID = "" + m.descriptionInput.Blur() + if msg.Err != nil { + return m, nil + } issues, err := m.svc.Beads.AllIssues(context.Background()) if err != nil { return m, nil @@ -80,6 +125,25 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // after list is updated, select the edited issue again return m, tea.Sequence(setItemsCmd, func() tea.Msg { return selectIssueMsg{IssueID: msg.IssueID} }) + case issueStatusUpdatedMsg: + m.choosingStatus = false + m.statusIssueID = "" + if msg.Err != nil { + return m, nil + } + issues, err := m.svc.Beads.AllIssues(context.Background()) + if err != nil { + return m, nil + } + setItemsCmd := m.issueList.SetIssues(issues) + for _, issue := range issues { + if issue.ID == msg.IssueID { + m.issueDetail.SetIssue(issue) + break + } + } + return m, tea.Sequence(setItemsCmd, func() tea.Msg { return selectIssueMsg{IssueID: msg.IssueID} }) + case selectIssueMsg: m.issueList.SelectIssueID(msg.IssueID) return m, nil @@ -138,6 +202,30 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } + if m.choosingStatus { + switch msg.String() { + case "o": + issueID := m.statusIssueID + m.choosingStatus = false + m.statusIssueID = "" + return m, updateIssueStatusCmd(m.svc, issueID, "open") + case "i": + issueID := m.statusIssueID + m.choosingStatus = false + m.statusIssueID = "" + return m, updateIssueStatusCmd(m.svc, issueID, "in_progress") + case "c": + issueID := m.statusIssueID + m.choosingStatus = false + m.statusIssueID = "" + return m, updateIssueStatusCmd(m.svc, issueID, "closed") + case "esc": + m.choosingStatus = false + m.statusIssueID = "" + return m, nil + } + } + if m.creatingIssue { if msg.String() == "enter" { title := m.createTitleInput.Value() @@ -159,7 +247,6 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.editingTitle { if msg.String() == "enter" { newTitle := m.titleInput.Value() - // dont update if its empty or same as the original if newTitle != "" { return m, updateIssueTitleCmd(m.svc, m.editingIssueID, newTitle) } @@ -175,6 +262,26 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } + if m.editingDescription { + if msg.String() == "ctrl+s" { + issueID := m.editingDescIssueID + newDesc := m.descriptionInput.Value() + m.editingDescription = false + m.editingDescIssueID = "" + m.descriptionInput.Blur() + return m, updateIssueDescriptionCmd(m.svc, issueID, newDesc) + } + if msg.String() == "esc" { + m.editingDescription = false + m.editingDescIssueID = "" + m.descriptionInput.Blur() + return m, nil + } + var cmd tea.Cmd + m.descriptionInput, cmd = m.descriptionInput.Update(msg) + return m, cmd + } + if m.issueList.FilterState() == list.Filtering { cmd, _ := m.issueList.Update(msg) return m, cmd diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index 96132c1..c17f380 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -49,6 +49,21 @@ func (m *Model) View() string { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) } + if m.editingDescription { + editBoxWidth := min(60, m.width-4) + m.descriptionInput.SetWidth(editBoxWidth - 2) + m.descriptionInput.SetHeight(10) + editContent := lipgloss.JoinVertical(lipgloss.Left, + styles.LabelStyle.Render("Edit description (Ctrl+S to save, Esc to cancel):"), + m.descriptionInput.View(), + ) + editBox := styles.ContainerStyle. + Width(editBoxWidth). + BorderForeground(styles.PrimaryBorder). + Render(editContent) + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, editBox) + } + if m.creatingIssue { createBoxWidth := min(60, m.width-4) m.createTitleInput.Width = createBoxWidth - 2 @@ -76,5 +91,19 @@ func (m *Model) View() string { return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, confirmBox) } + if m.choosingStatus { + statusContent := lipgloss.JoinVertical(lipgloss.Left, + styles.LabelStyle.Render("Change status for "+m.statusIssueID+":"), + lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress c = closed"), + lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), + ) + statusBoxWidth := min(50, m.width-4) + statusBox := styles.ContainerStyle. + Width(statusBoxWidth). + BorderForeground(styles.PrimaryBorder). + Render(statusContent) + return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, statusBox) + } + return mainView }