From c78b5082d2d2535360b17433e746a4349c2d128a Mon Sep 17 00:00:00 2001 From: Ine Maria Nilssen Aanonsen Date: Wed, 11 Mar 2026 14:54:14 +0100 Subject: [PATCH 1/5] TASK SPRINT PLANNING 2 --- cmd/pm/tasks/sprintPlanning.go | 51 +++++++++++++++++++++++++- internal/commands/issues/completion.go | 2 +- internal/commands/issues/create.go | 2 +- internal/commands/issues/list.go | 2 +- internal/commands/issues/update.go | 2 +- internal/models/beads.go | 11 +++--- pkg/tui/components/issue_list.go | 7 +++- pkg/tui/views/dashboard/operations.go | 6 +++ pkg/tui/views/kanban/operations.go | 5 +++ pkg/web/components/issue.templ | 5 ++- pkg/web/components/issue_detail.templ | 2 + pkg/web/handler/issues.go | 4 +- pkg/web/routes/boardview.templ | 4 ++ 13 files changed, 88 insertions(+), 15 deletions(-) diff --git a/cmd/pm/tasks/sprintPlanning.go b/cmd/pm/tasks/sprintPlanning.go index 572fa41..9d5a5fd 100644 --- a/cmd/pm/tasks/sprintPlanning.go +++ b/cmd/pm/tasks/sprintPlanning.go @@ -48,6 +48,7 @@ func (t *SprintPlanningTask) Questions(interfaceType InterfaceType) Questions { return BaseQuestions(interfaceType).With( huh.NewGroup( huh.NewSelect[int](). + Key("sprint-planning-selected-issues"). Title("How many issues did you select for the sprint?"). Options( huh.NewOption("2-3 issues", 1), @@ -58,6 +59,10 @@ func (t *SprintPlanningTask) Questions(interfaceType InterfaceType) Questions { ) } +func (t *SprintPlanningTask) QuestionnaireKeys(_ InterfaceType) []string { + return []string{"task_completed", "task_difficulty", "sprint-planning-selected-issues"} +} + func (t *SprintPlanningTask) Setup(ctx context.Context) error { if err := ClearIssues(t.app); err != nil { return err @@ -116,6 +121,50 @@ func (t *SprintPlanningTask) Setup(ctx context.Context) error { func (t *SprintPlanningTask) Validate(ctx context.Context) ValidationFeedback { expect := check.NewExpector() - return expect.Complete() + issues, err := FetchIssues(ctx, t.app, t.setupIssue) + if err != nil { + return expect.ValidationFeedback + } + if len(issues) == 0 { + expect.Fail("No backlog issues found to plan a sprint with.") + return expect.ValidationFeedback + } + + // Sort by priority ascending (0 is highest priority). + sorted := make([]*models.Issue, len(issues)) + copy(sorted, issues) + for i := 0; i < len(sorted); i++ { + for j := i + 1; j < len(sorted); j++ { + if sorted[j].Priority < sorted[i].Priority { + sorted[i], sorted[j] = sorted[j], sorted[i] + } + } + } + + topN := 5 + if len(sorted) < topN { + topN = len(sorted) + } + top := sorted[:topN] + + var plannedCount int + var readyToSprintCount int + for _, issue := range top { + if issue.Status == models.StatusReadyToSprint || + issue.Status == models.StatusInProgress || + issue.Status == models.StatusClosed { + plannedCount++ + } + if issue.Status == models.StatusReadyToSprint { + readyToSprintCount++ + } + } + + expect.Assert(plannedCount >= 3, + "Expected at least 3 of the 5 highest-priority issues to be moved into 'ready_to_sprint', 'in_progress', or 'closed' for the sprint.") + expect.Assert(readyToSprintCount >= 1, + "Expected at least one of the highest-priority issues to be marked as 'ready_to_sprint' to indicate it is planned for the sprint.") + + return expect.Complete() } diff --git a/internal/commands/issues/completion.go b/internal/commands/issues/completion.go index 0a532e1..19c8a35 100644 --- a/internal/commands/issues/completion.go +++ b/internal/commands/issues/completion.go @@ -11,7 +11,7 @@ import ( // Variables for completion options and functions. var ( typeOptions = []string{"bug", "feature", "task", "chore"} - statusOptions = []string{"open", "closed", "in_progress"} + statusOptions = []string{"open", "closed", "in_progress", "ready_to_sprint"} priorityRange = []string{"0", "1", "2", "3", "4"} ) diff --git a/internal/commands/issues/create.go b/internal/commands/issues/create.go index 2deafed..238e295 100644 --- a/internal/commands/issues/create.go +++ b/internal/commands/issues/create.go @@ -112,7 +112,7 @@ func runCreateInteractive() error { func init() { //CreateCmd.Flags().BoolVarP(&createFlags.interactive, "interactive", "i", false, "Create issue interactively") CreateCmd.Flags().StringVarP(&createFlags.description, "desc", "d", "", "Issue description") - CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress)") + CreateCmd.Flags().StringVarP(&createFlags.status, "status", "s", "open", "Issue status(open, closed, in_progress, ready_to_sprint)") CreateCmd.Flags().StringVarP(&createFlags.issueType, "type", "t", "task", "Issue type(bug, feature, task)") CreateCmd.Flags().IntVarP(&createFlags.priority, "priority", "p", 0, "Issue priority(0-4)") CreateCmd.Flags().StringVarP(&createFlags.assignee, "assignee", "a", "", "Issue assignee") diff --git a/internal/commands/issues/list.go b/internal/commands/issues/list.go index f61d869..e9eae24 100644 --- a/internal/commands/issues/list.go +++ b/internal/commands/issues/list.go @@ -76,7 +76,7 @@ func runGetIssuesCmd(cmd *cobra.Command, args []string) error { func init() { ListCmd.Flags().StringVar(&listFlags.title, "title", "", "Filter issues by title") ListCmd.Flags().StringVarP(&listFlags.description, "desc", "d", "", "Filter issues by description") - ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress)") + ListCmd.Flags().StringVarP(&listFlags.status, "status", "s", "", "Filter issues by status (open, closed, in_progress, ready_to_sprint)") ListCmd.Flags().StringVarP(&listFlags.issueType, "type", "t", "", "Filter issues by type (bug, feature, task)") ListCmd.Flags().IntVarP(&listFlags.priority, "priority", "p", 0, "Filter issues by priority (0-4)") ListCmd.Flags().StringVarP(&listFlags.assignee, "assignee", "a", "", "Filter issues by assignee") diff --git a/internal/commands/issues/update.go b/internal/commands/issues/update.go index f1dac02..29a6b29 100644 --- a/internal/commands/issues/update.go +++ b/internal/commands/issues/update.go @@ -58,7 +58,7 @@ func runUpdateCmd(cmd *cobra.Command, args []string) error { func init() { UpdateCmd.Flags().StringVar(&updateFlags.title, "title", "", "New issue title") UpdateCmd.Flags().StringVarP(&updateFlags.description, "desc", "d", "", "New issue description") - UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress)") + UpdateCmd.Flags().StringVarP(&updateFlags.status, "status", "s", "", "New issue status(open, closed, in_progress, ready_to_sprint)") UpdateCmd.Flags().StringVarP(&updateFlags.issueType, "type", "t", "", "New issue type(bug, feature, task)") UpdateCmd.Flags().IntVarP(&updateFlags.priority, "priority", "p", 0, "New issue priority(0-4)") UpdateCmd.Flags().StringVarP(&updateFlags.assignee, "assignee", "a", "", "New issue assignee") diff --git a/internal/models/beads.go b/internal/models/beads.go index a25d480..ff7730a 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -33,11 +33,12 @@ type ( // Status constants const ( - StatusOpen = beads.StatusOpen - StatusInProgress = beads.StatusInProgress - StatusBlocked = beads.StatusBlocked - StatusDeferred = beads.StatusDeferred - StatusClosed = beads.StatusClosed + StatusOpen = beads.StatusOpen + StatusInProgress = beads.StatusInProgress + StatusBlocked = beads.StatusBlocked + StatusDeferred = beads.StatusDeferred + StatusClosed = beads.StatusClosed + StatusReadyToSprint Status = "ready_to_sprint" ) // IssueType constants diff --git a/pkg/tui/components/issue_list.go b/pkg/tui/components/issue_list.go index 1667e34..2f237d4 100644 --- a/pkg/tui/components/issue_list.go +++ b/pkg/tui/components/issue_list.go @@ -213,7 +213,9 @@ func OpenAndInProgressOnly(issues []*models.Issue) []*models.Issue { // used to display open & in-progress issues in the first window in the dashboard out := make([]*models.Issue, 0, len(issues)) for _, issue := range issues { - if issue.Status == models.StatusOpen || issue.Status == models.StatusInProgress { + if issue.Status == models.StatusOpen || + issue.Status == models.StatusInProgress || + issue.Status == models.StatusReadyToSprint { out = append(out, issue) } } @@ -237,7 +239,8 @@ func ClosedOnly(issues []*models.Issue) []*models.Issue { func StatusOnly(issues []*models.Issue, status models.Status) []*models.Issue { out := make([]*models.Issue, 0, len(issues)) for _, issue := range issues { - if issue.Status == status { + if issue.Status == status || + (status == models.StatusOpen && issue.Status == models.StatusReadyToSprint) { out = append(out, issue) } } diff --git a/pkg/tui/views/dashboard/operations.go b/pkg/tui/views/dashboard/operations.go index 305f24f..2ad7524 100644 --- a/pkg/tui/views/dashboard/operations.go +++ b/pkg/tui/views/dashboard/operations.go @@ -343,6 +343,12 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.choosingStatus = false m.statusIssueID = "" return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) + case "r": + m.logAction("tui selected issue status ready_to_sprint") + issueID := m.statusIssueID + m.choosingStatus = false + m.statusIssueID = "" + return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) case "c": m.logAction("tui selected issue status closed") issueID := m.statusIssueID diff --git a/pkg/tui/views/kanban/operations.go b/pkg/tui/views/kanban/operations.go index 03a6beb..8a0e040 100644 --- a/pkg/tui/views/kanban/operations.go +++ b/pkg/tui/views/kanban/operations.go @@ -244,6 +244,11 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.choosingStatus = false m.statusIssueID = "" return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusInProgress)) + case "r": + issueID := m.statusIssueID + m.choosingStatus = false + m.statusIssueID = "" + return m, issues.UpdateIssueStatusCmd(m.app, issueID, string(models.StatusReadyToSprint)) case "c": issueID := m.statusIssueID m.choosingStatus = false diff --git a/pkg/web/components/issue.templ b/pkg/web/components/issue.templ index 894bdbd..faedc3c 100644 --- a/pkg/web/components/issue.templ +++ b/pkg/web/components/issue.templ @@ -57,6 +57,7 @@ templ IssueForm(props IssueFormProps) { Options: []base.SelectOption{ {Label: "Open", Value: "open", Selected: props.Status == "open"}, {Label: "In Progress", Value: "in_progress", Selected: props.Status == "in_progress"}, + {Label: "Ready to sprint", Value: "ready_to_sprint", Selected: props.Status == "ready_to_sprint"}, {Label: "Closed", Value: "closed", Selected: props.Status == "closed"}, }, Size: "md", @@ -159,7 +160,9 @@ templ IssueRows(issues []*models.Issue) { hx-push-url="true" >{ issue.Title } - { string(issue.Status) } + + @StatusBadge(issue.Status) + { string(issue.IssueType) } { fmt.Sprintf("P%d", issue.Priority) } diff --git a/pkg/web/components/issue_detail.templ b/pkg/web/components/issue_detail.templ index f8b811a..04f101a 100644 --- a/pkg/web/components/issue_detail.templ +++ b/pkg/web/components/issue_detail.templ @@ -60,6 +60,8 @@ templ StatusBadge(status models.Status) { Open case "in_progress": In Progress + case "ready_to_sprint": + Ready to sprint case "closed": Closed case "blocked": diff --git a/pkg/web/handler/issues.go b/pkg/web/handler/issues.go index 3306c7a..aad3f63 100644 --- a/pkg/web/handler/issues.go +++ b/pkg/web/handler/issues.go @@ -17,7 +17,7 @@ const commentsKey = "comments" type IssueForm struct { Title string `form:"title" validate:"required,max=255"` Description string `form:"description" validate:"max=2000"` - Status models.Status `form:"status" validate:"required,oneof=open in_progress closed"` + Status models.Status `form:"status" validate:"required,oneof=open in_progress ready_to_sprint closed"` IssueType models.IssueType `form:"issue_type" validate:"required,oneof=task bug feature chore"` Priority int `form:"priority" validate:"gte=0,lte=4"` } @@ -25,7 +25,7 @@ type IssueForm struct { type UpdateIssueForm struct { Title *string `form:"title" validate:"omitempty,max=255"` Description *string `form:"description" validate:"omitempty,max=2000"` - Status *models.Status `form:"status" validate:"omitempty,oneof=open in_progress closed"` + Status *models.Status `form:"status" validate:"omitempty,oneof=open in_progress ready_to_sprint closed"` CloseReason *string `form:"close_reason" validate:"omitempty,max=2000"` IssueType *models.IssueType `form:"issue_type" validate:"omitempty,oneof=task bug feature chore"` Priority *int `form:"priority" validate:"omitempty,gte=0,lte=4"` diff --git a/pkg/web/routes/boardview.templ b/pkg/web/routes/boardview.templ index 13927dc..e926309 100644 --- a/pkg/web/routes/boardview.templ +++ b/pkg/web/routes/boardview.templ @@ -62,6 +62,7 @@ templ BoardColumns(issues []*models.Issue) { {{ openIssues := []*models.Issue{} inProgressIssues := []*models.Issue{} + readyToSprintIssues := []*models.Issue{} closedIssues := []*models.Issue{} for _, issue := range issues { @@ -70,6 +71,8 @@ templ BoardColumns(issues []*models.Issue) { openIssues = append(openIssues, issue) case "in_progress": inProgressIssues = append(inProgressIssues, issue) + case "ready_to_sprint": + readyToSprintIssues = append(readyToSprintIssues, issue) case "closed": closedIssues = append(closedIssues, issue) } @@ -78,6 +81,7 @@ templ BoardColumns(issues []*models.Issue) {
@BoardColumn("Open", "open", openIssues, "badge-info") @BoardColumn("In Progress", "in_progress", inProgressIssues, "badge-warning") + @BoardColumn("Ready to sprint", "ready_to_sprint", readyToSprintIssues, "badge-primary") @BoardColumn("Closed", "closed", closedIssues, "badge-success")
} From e9a6d1e9af04d0a88b3e90079c8874b0d1db933c Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 11 Mar 2026 15:13:00 +0100 Subject: [PATCH 2/5] add ready_to_sprint status to beads --- internal/storage/beads.go | 2 + pkg/web/components/issue_detail_templ.go | 49 +++++++++++++----------- pkg/web/components/issue_templ.go | 42 +++++++++----------- pkg/web/routes/boardview_templ.go | 27 ++++++++----- 4 files changed, 65 insertions(+), 55 deletions(-) diff --git a/internal/storage/beads.go b/internal/storage/beads.go index 3d9ecf9..294da1f 100644 --- a/internal/storage/beads.go +++ b/internal/storage/beads.go @@ -21,6 +21,8 @@ func NewBeadsIssueStorage(ctx context.Context, storage beads.Storage, prefix str } } + storage.SetConfig(ctx, "status.custom", "ready_to_sprint") + return &BeadsService{ Storage: storage, }, nil diff --git a/pkg/web/components/issue_detail_templ.go b/pkg/web/components/issue_detail_templ.go index 45b2592..c405ee5 100644 --- a/pkg/web/components/issue_detail_templ.go +++ b/pkg/web/components/issue_detail_templ.go @@ -211,36 +211,41 @@ func StatusBadge(status models.Status) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + case "ready_to_sprint": + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Ready to sprint") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } case "closed": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Closed") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Closed") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "blocked": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Blocked") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Blocked") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "deferred": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Deferred") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "Deferred") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(string(status)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 70, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 72, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -272,45 +277,45 @@ func TypeBadge(issueType models.IssueType) templ.Component { ctx = templ.ClearChildren(ctx) switch issueType { case "bug": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Bug") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Bug") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "feature": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Feature") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Feature") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "task": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Task") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Task") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "chore": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Chore") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Chore") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case "epic": - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Epic") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "Epic") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(string(issueType)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 87, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 89, Col: 65} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -342,45 +347,45 @@ func PriorityBadge(priority int) templ.Component { ctx = templ.ClearChildren(ctx) switch priority { case 0: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Irrelevant") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Irrelevant") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 1: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Low") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Low") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 2: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "Normal") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "Normal") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 3: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "High") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "High") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } case 4: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "Critical") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Critical") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } default: - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", priority)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 104, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue_detail.templ`, Line: 106, Col: 74} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/components/issue_templ.go b/pkg/web/components/issue_templ.go index d055a8e..1e57c18 100644 --- a/pkg/web/components/issue_templ.go +++ b/pkg/web/components/issue_templ.go @@ -180,6 +180,7 @@ func IssueForm(props IssueFormProps) templ.Component { Options: []base.SelectOption{ {Label: "Open", Value: "open", Selected: props.Status == "open"}, {Label: "In Progress", Value: "in_progress", Selected: props.Status == "in_progress"}, + {Label: "Ready to sprint", Value: "ready_to_sprint", Selected: props.Status == "ready_to_sprint"}, {Label: "Closed", Value: "closed", Selected: props.Status == "closed"}, }, Size: "md", @@ -248,7 +249,7 @@ func IssueForm(props IssueFormProps) templ.Component { var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(props.DeleteAction) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 107, Col: 31} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 108, Col: 31} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -348,7 +349,7 @@ func IssueRows(issues []*models.Issue) templ.Component { var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 147, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 148, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -361,7 +362,7 @@ func IssueRows(issues []*models.Issue) templ.Component { var templ_7745c5c3_Var12 string templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 151, Col: 15} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 152, Col: 15} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { @@ -374,7 +375,7 @@ func IssueRows(issues []*models.Issue) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 156, Col: 35} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 157, Col: 35} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -387,7 +388,7 @@ func IssueRows(issues []*models.Issue) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 160, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 161, Col: 18} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -397,12 +398,7 @@ func IssueRows(issues []*models.Issue) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 string - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.Status)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 162, Col: 54} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + templ_7745c5c3_Err = StatusBadge(issue.Status).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -410,12 +406,12 @@ func IssueRows(issues []*models.Issue) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.IssueType)) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(string(issue.IssueType)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 163, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 166, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -423,12 +419,12 @@ func IssueRows(issues []*models.Issue) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", issue.Priority)) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("P%d", issue.Priority)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 164, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 167, Col: 68} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -436,12 +432,12 @@ func IssueRows(issues []*models.Issue) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + issue.ID) + var templ_7745c5c3_Var17 string + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 169, Col: 38} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/components/issue.templ`, Line: 172, Col: 38} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/pkg/web/routes/boardview_templ.go b/pkg/web/routes/boardview_templ.go index 406eedf..a7322a5 100644 --- a/pkg/web/routes/boardview_templ.go +++ b/pkg/web/routes/boardview_templ.go @@ -140,6 +140,7 @@ func BoardColumns(issues []*models.Issue) templ.Component { ctx = templ.ClearChildren(ctx) openIssues := []*models.Issue{} inProgressIssues := []*models.Issue{} + readyToSprintIssues := []*models.Issue{} closedIssues := []*models.Issue{} for _, issue := range issues { @@ -148,6 +149,8 @@ func BoardColumns(issues []*models.Issue) templ.Component { openIssues = append(openIssues, issue) case "in_progress": inProgressIssues = append(inProgressIssues, issue) + case "ready_to_sprint": + readyToSprintIssues = append(readyToSprintIssues, issue) case "closed": closedIssues = append(closedIssues, issue) } @@ -164,6 +167,10 @@ func BoardColumns(issues []*models.Issue) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } + templ_7745c5c3_Err = BoardColumn("Ready to sprint", "ready_to_sprint", readyToSprintIssues, "badge-primary").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } templ_7745c5c3_Err = BoardColumn("Closed", "closed", closedIssues, "badge-success").Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err @@ -204,7 +211,7 @@ func BoardColumn(title string, status string, issues []*models.Issue, badgeClass var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 86, Col: 77} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 90, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -217,7 +224,7 @@ func BoardColumn(title string, status string, issues []*models.Issue, badgeClass var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 88, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 92, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -252,7 +259,7 @@ func BoardColumn(title string, status string, issues []*models.Issue, badgeClass var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(issues))) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 89, Col: 71} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 93, Col: 71} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -265,7 +272,7 @@ func BoardColumn(title string, status string, issues []*models.Issue, badgeClass var templ_7745c5c3_Var11 string templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(status) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 91, Col: 110} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 95, Col: 110} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -323,7 +330,7 @@ func BoardCard(issue *models.Issue) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 106, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 110, Col: 26} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -336,7 +343,7 @@ func BoardCard(issue *models.Issue) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 110, Col: 80} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 114, Col: 80} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -349,7 +356,7 @@ func BoardCard(issue *models.Issue) templ.Component { var templ_7745c5c3_Var15 string templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + issue.ID + "?from=board") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 114, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 118, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -362,7 +369,7 @@ func BoardCard(issue *models.Issue) templ.Component { var templ_7745c5c3_Var16 string templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs("/issues/" + issue.ID + "/edit?from=board") if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 126, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 130, Col: 57} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { @@ -375,7 +382,7 @@ func BoardCard(issue *models.Issue) templ.Component { var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(issue.ID) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 138, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 142, Col: 65} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -405,7 +412,7 @@ func BoardCard(issue *models.Issue) templ.Component { var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Description) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 143, Col: 81} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `pkg/web/routes/boardview.templ`, Line: 147, Col: 81} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { From 0d0ba236a349e255abb14759461437d8356587fd Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 11 Mar 2026 15:31:08 +0100 Subject: [PATCH 3/5] add suggestions to repl --- pkg/repl/suggestions.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/repl/suggestions.go b/pkg/repl/suggestions.go index 07e36a6..8785944 100644 --- a/pkg/repl/suggestions.go +++ b/pkg/repl/suggestions.go @@ -79,6 +79,7 @@ var statusValues = []prompt.Suggest{ {Text: "open", Description: "Open status"}, {Text: "closed", Description: "Closed status"}, {Text: "in_progress", Description: "In progress status"}, + {Text: "ready_to_sprint", Description: "Ready to sprint status"}, } // typeValues is a list of prompt suggestions for issue types From 85e0bf698dd15b834596868586788588534e0f67 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 11 Mar 2026 15:31:29 +0100 Subject: [PATCH 4/5] show in ready_to_sprint in tui --- pkg/tui/components/modals.go | 3 +-- pkg/tui/views/dashboard/view.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/tui/components/modals.go b/pkg/tui/components/modals.go index b41e3bb..8eaf2d2 100644 --- a/pkg/tui/components/modals.go +++ b/pkg/tui/components/modals.go @@ -90,7 +90,7 @@ func RenderChooseStatus(width, height int, issueID string) string { } statusContent := lipgloss.JoinVertical(lipgloss.Left, styles.LabelStyle.Render("Change status for "+issueID+":"), - lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress c = closed"), + lipgloss.NewStyle().Foreground(styles.FaintText).Render("o = open i = in_progress c = closed ready_to_sprint = r"), lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), ) statusBoxWidth := modalBoxWidth(50, width) @@ -234,4 +234,3 @@ func RenderFooter(width int, helpBar *HelpBar, feedback models.ValidationFeedbac helpBar.SetWidth(helpWidth) return lipgloss.JoinHorizontal(lipgloss.Left, helpBar.View(), feedbackStatus) } - diff --git a/pkg/tui/views/dashboard/view.go b/pkg/tui/views/dashboard/view.go index b395f51..f32ac91 100644 --- a/pkg/tui/views/dashboard/view.go +++ b/pkg/tui/views/dashboard/view.go @@ -135,7 +135,7 @@ func (m *Model) View() string { 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("o = open i = in_progress c = closed ready_to_sprint = r"), lipgloss.NewStyle().Foreground(styles.FaintText).Render("Esc = cancel"), ) statusBoxWidth := min(50, m.width-4) From dc48ddf0e06a0ca3af6327c7d6fa6d66d28623d2 Mon Sep 17 00:00:00 2001 From: Robin Olsen Date: Wed, 11 Mar 2026 15:31:47 +0100 Subject: [PATCH 5/5] print issue list in cli better --- internal/models/beads.go | 45 +++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/internal/models/beads.go b/internal/models/beads.go index ff7730a..624ed6d 100644 --- a/internal/models/beads.go +++ b/internal/models/beads.go @@ -3,7 +3,9 @@ package models import ( "fmt" "os" + "strings" "text/tabwriter" + "unicode" "charm.land/lipgloss/v2" "github.com/muesli/reflow/truncate" @@ -33,11 +35,11 @@ type ( // Status constants const ( - StatusOpen = beads.StatusOpen - StatusInProgress = beads.StatusInProgress - StatusBlocked = beads.StatusBlocked - StatusDeferred = beads.StatusDeferred - StatusClosed = beads.StatusClosed + StatusOpen = beads.StatusOpen + StatusInProgress = beads.StatusInProgress + StatusBlocked = beads.StatusBlocked + StatusDeferred = beads.StatusDeferred + StatusClosed = beads.StatusClosed StatusReadyToSprint Status = "ready_to_sprint" ) @@ -114,20 +116,39 @@ func IssuesPtrToIssues(issuePtr []*Issue) []Issue { return issues } +func sanitizeCell(s string) string { + s = strings.Map(func(r rune) rune { + switch r { + case '\n', '\r', '\t': + return ' ' + default: + return r + } + }, s) + + return strings.Join(strings.FieldsFunc(s, unicode.IsSpace), " ") +} + func FormatIssueRow(issue Issue) string { + id := truncate.String(sanitizeCell(issue.ID), 10) + title := truncate.StringWithTail(sanitizeCell(issue.Title), 35, "...") + description := truncate.StringWithTail(sanitizeCell(issue.Description), 40, "...") + status := sanitizeCell(string(issue.Status)) + issueType := sanitizeCell(string(issue.IssueType)) + return fmt.Sprintf( "%s\t%s\t%s\t%s\t%s\t%d", - truncate.String(issue.ID, 10), - truncate.StringWithTail(issue.Title, 25, "..."), - truncate.StringWithTail(issue.Description, 40, "..."), - issue.Status, - issue.IssueType, + id, + title, + description, + status, + issueType, issue.Priority, ) } func PrintIssues(issues []Issue) { - w := tabwriter.NewWriter(os.Stdout, 8, 10, 5, ' ', 0) + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tTITLE\tDESCRIPTION\tSTATUS\tTYPE\tPRIORITY") @@ -135,5 +156,5 @@ func PrintIssues(issues []Issue) { fmt.Fprintln(w, FormatIssueRow(issue)) } - w.Flush() + _ = w.Flush() }