Files
LazyPM/pkg/repl/suggestions.go
Robin Olsen d900a5fcd3 Merge pull request #75 from LazyBachelor/LPM-140
LPM-140 The Sprinting Update
Adds sprint support across the web UI (board + issue detail), TUI kanban, CLI/REPL, and the Beads-backed storage layer.

Changes:

Introduces sprint entities in storage and exposes sprint operations via IssueService.
Updates web board to support backlog vs. sprint columns, sprint selection, and sprint creation routes.
Updates TUI kanban to show backlog + sprint columns and adds sprint selection/creation actions.
2026-03-23 19:26:04 +01:00

348 lines
11 KiB
Go

package repl
import (
"context"
"strings"
"github.com/LazyBachelor/LazyPM/internal/commands/issues"
"github.com/c-bata/go-prompt"
"github.com/muesli/reflow/truncate"
)
// rootSuggestions is a list of prompt suggestions for root-level commands.
var rootSuggestions = []prompt.Suggest{
{Text: "pm", Description: "Project Management System"},
{Text: "status", Description: "Show task status"},
{Text: "exit", Description: "Exit pm CLI"},
{Text: "help", Description: "Show help information"},
{Text: "title", Description: "Print the welcome title"},
{Text: "git", Description: "Version control system"},
}
// commandSuggestions is a list of prompt suggestions for PM commands.
var baseSuggestions = []prompt.Suggest{
{Text: "help", Description: "Show help information"},
{Text: "sprint", Description: "Manage sprints"},
{Text: "create", Description: "Create a new issue with title"},
{Text: "list", Description: "List all issues"},
{Text: "read", Description: "Read issue details by ID"},
{Text: "update", Description: "Update an existing issue by ID"},
{Text: "close", Description: "Close an issue by ID"},
{Text: "delete", Description: "Delete an issue by ID"},
{Text: "comment", Description: "Add a comment on an issue by ID"},
{Text: "comments", Description: "List comments on an issue by ID"},
{Text: "new", Description: "Alias for create command"},
{Text: "ls", Description: "Alias for list command"},
{Text: "search", Description: "Alias for list command"},
{Text: "describe", Description: "Alias for read command"},
{Text: "details", Description: "Alias for read command"},
{Text: "edit", Description: "Alias for update command"},
{Text: "rm", Description: "Alias for delete command"},
{Text: "del", Description: "Alias for delete command"},
{Text: "get", Description: "Alias for read command"},
}
// createFlags is a list of prompt suggestions for the create command flags.
var createFlags = []prompt.Suggest{
{Text: "--desc", Description: "Issue description"},
{Text: "--status", Description: "Issue status"},
{Text: "--type", Description: "Issue type"},
{Text: "--priority", Description: "Issue priority"},
{Text: "--assignee", Description: "Issue assignee"},
{Text: "-d", Description: "Issue description (short)"},
{Text: "-s", Description: "Issue status (short)"},
{Text: "-t", Description: "Issue type (short)"},
{Text: "-p", Description: "Issue priority (short)"},
{Text: "-a", Description: "Issue assignee (short)"},
}
// updateFlags is a list of prompt suggestions for the update command flags.
var updateFlags = []prompt.Suggest{
{Text: "--title", Description: "New issue title"},
{Text: "--desc", Description: "New issue description"},
{Text: "--status", Description: "New issue status"},
{Text: "--type", Description: "New issue type"},
{Text: "--priority", Description: "New issue priority"},
{Text: "--assignee", Description: "New issue assignee"},
{Text: "-t", Description: "New issue type (short)"},
{Text: "-p", Description: "New issue priority (short)"},
{Text: "-a", Description: "New issue assignee (short)"},
{Text: "-d", Description: "New issue description (short)"},
{Text: "-s", Description: "New issue status (short)"},
}
// listFlags is a list of prompt suggestions for the list command flags.
var listFlags = []prompt.Suggest{
{Text: "--title", Description: "Filter by title"},
{Text: "--desc", Description: "Filter by description"},
{Text: "--status", Description: "Filter by status"},
{Text: "--type", Description: "Filter by type"},
{Text: "--priority", Description: "Filter by priority"},
{Text: "--assignee", Description: "Filter by assignee"},
{Text: "--limit", Description: "Limit number of results"},
{Text: "-s", Description: "Filter by status (short)"},
{Text: "-t", Description: "Filter by type (short)"},
{Text: "-p", Description: "Filter by priority (short)"},
{Text: "-a", Description: "Filter by assignee (short)"},
{Text: "-d", Description: "Filter by description (short)"},
{Text: "-l", Description: "Limit number of results (short)"},
}
var deleteFlags = []prompt.Suggest{
{Text: "--yes", Description: "Confirm deletion without prompt"},
{Text: "--interactive", Description: "Select issues to delete interactively"},
}
var commentFlags = []prompt.Suggest{
{Text: "--message", Description: "Comment text"},
{Text: "--author", Description: "Author name for the comment"},
{Text: "-m", Description: "Comment text (short)"},
{Text: "-a", Description: "Author name (short)"},
}
// sprintSubcommands is a list of prompt suggestions for sprint subcommands.
var sprintSubcommands = []prompt.Suggest{
{Text: "list", Description: "List all sprints"},
{Text: "create", Description: "Create a new sprint"},
{Text: "issues", Description: "List issues in a sprint"},
{Text: "add", Description: "Add an issue to a sprint"},
{Text: "remove", Description: "Remove an issue from a sprint"},
{Text: "backlog", Description: "Show backlog issues"},
{Text: "delete", Description: "Delete a sprint"},
{Text: "ls", Description: "Alias for list subcommand"},
{Text: "new", Description: "Alias for create subcommand"},
{Text: "show", Description: "Alias for issues subcommand"},
{Text: "view", Description: "Alias for issues subcommand"},
{Text: "rm", Description: "Alias for remove subcommand"},
{Text: "del", Description: "Alias for delete subcommand"},
}
// statusValues is a list of prompt suggestions for status types
var statusValues = []prompt.Suggest{
{Text: "open", Description: "Open status"},
{Text: "closed", Description: "Closed status"},
{Text: "in_progress", Description: "In progress status"},
{Text: "blocked", Description: "Blocked status"},
}
// typeValues is a list of prompt suggestions for issue types
var typeValues = []prompt.Suggest{
{Text: "task", Description: "Task issue type"},
{Text: "bug", Description: "Bug issue type"},
{Text: "feature", Description: "Feature issue type"},
{Text: "chore", Description: "Chore issue type"},
}
// priorityValues is a list of prompt suggestions for issue priority levels
var priorityValues = []prompt.Suggest{
{Text: "0", Description: "Irrelevant"},
{Text: "1", Description: "Low priority"},
{Text: "2", Description: "Normal priority"},
{Text: "3", Description: "High priority"},
{Text: "4", Description: "Critical priority"},
}
// isIDCommand maps command names to a boolean indicating whether they expect an issue ID as an argument.
var isIDCommand = map[string]bool{
"describe": true,
"delete": true,
"del": true,
"rm": true,
"remove": true,
"get": true,
"read": true,
"close": true,
"update": true,
"edit": true,
"comment": true,
"comments": true,
"sprint-add": true,
"sprint-remove": true,
}
var commandFlags = map[string][]prompt.Suggest{
"create": createFlags,
"add": createFlags,
"update": updateFlags,
"edit": updateFlags,
"list": listFlags,
"ls": listFlags,
"search": listFlags,
"delete": deleteFlags,
"del": deleteFlags,
"rm": deleteFlags,
"remove": deleteFlags,
"comment": commentFlags,
"comments": nil, // no flags, just issue ID
"sprint": sprintSubcommands,
}
// commandSuggestions returns a list of prompt suggestions based on the current input words.
func commandSuggestions(words []string) []prompt.Suggest {
if len(words) == 0 {
return baseSuggestions
}
return filterByPrefix(baseSuggestions, words[0])
}
// flagSuggestions returns a list of prompt suggestions for command flags based on the current input.
func flagSuggestions(cmd string, words []string, text string) []prompt.Suggest {
lastWord, prevWord := parseWords(words, text)
if values := getFlagValues(prevWord); values != nil {
return filterByPrefix(values, lastWord)
}
if cmd == "sprint" {
switch len(words) {
case 1:
if strings.HasSuffix(text, " ") {
return sprintSubcommands
}
return nil
case 2:
subcommand := words[1]
if !strings.HasSuffix(text, " ") {
return filterByPrefix(sprintSubcommands, lastWord)
}
switch subcommand {
case "list", "ls", "create", "new", "backlog":
return nil
case "issues", "show", "view", "delete", "del":
return sprintNumSuggestions("", subcommand != "delete" && subcommand != "del")
case "add", "remove", "rm":
return issueIDSuggestions("", true)
default:
return filterByPrefix(sprintSubcommands, subcommand)
}
case 3:
subcommand := words[1]
switch subcommand {
case "issues", "show", "view", "delete", "del":
if !strings.HasSuffix(text, " ") {
return sprintNumSuggestions(lastWord, subcommand != "delete" && subcommand != "del")
}
return nil
case "add", "remove", "rm":
if !strings.HasSuffix(text, " ") {
return issueIDSuggestions(lastWord, true)
}
return sprintNumSuggestions("", true)
}
case 4:
subcommand := words[1]
switch subcommand {
case "add", "remove", "rm":
if !strings.HasSuffix(text, " ") {
return sprintNumSuggestions(lastWord, true)
}
}
}
return nil
}
flags := commandFlags[cmd]
if isIDCommand[cmd] {
if len(words) == 1 && strings.HasSuffix(text, " ") {
return issueIDSuggestions("", true)
}
if len(words) == 2 &&
!strings.HasSuffix(text, " ") &&
!strings.HasPrefix(words[1], "-") {
return issueIDSuggestions(words[1], true)
}
return filterByPrefix(flags, lastWord)
}
return filterByPrefix(flags, lastWord)
}
// issueIDSuggestions returns a list of prompt suggestions for issue IDs based on the current partial input.
func issueIDSuggestions(partial string, hasCommand bool) []prompt.Suggest {
// Only show suggestions if we've typed the command already
if !hasCommand {
return nil
}
issues, _ := issues.GetIssueCompletions(context.Background(), partial)
var suggestions []prompt.Suggest
for _, issue := range issues {
suggestions = append(suggestions, prompt.Suggest{
Text: issue.ID,
Description: truncate.String(issue.Title, 20),
})
}
return suggestions
}
// sprintNumSuggestions returns a list of prompt suggestions for sprint numbers.
func sprintNumSuggestions(partial string, includeBacklog bool) []prompt.Suggest {
suggestions := []prompt.Suggest{
{Text: "0", Description: "Backlog"},
{Text: "1", Description: "Sprint 1"},
{Text: "2", Description: "Sprint 2"},
{Text: "3", Description: "Sprint 3"},
{Text: "4", Description: "Sprint 4"},
{Text: "5", Description: "Sprint 5"},
}
if !includeBacklog {
suggestions = suggestions[1:]
}
return filterByPrefix(suggestions, partial)
}
// parseWords extracts the last and previous words from the input for flag suggestion logic.
func parseWords(words []string, text string) (lastWord, prevWord string) {
if len(words) > 0 && !strings.HasSuffix(text, " ") {
lastWord = words[len(words)-1]
if len(words) >= 2 {
prevWord = words[len(words)-2]
}
} else if len(words) >= 1 {
prevWord = words[len(words)-1]
}
return
}
// getFlagValues returns a list of prompt suggestions for flag values based on the given flag.
func getFlagValues(flag string) []prompt.Suggest {
switch flag {
case "-s", "--status":
return statusValues
case "-t", "--type":
return typeValues
case "-p", "--priority":
return priorityValues
}
return nil
}
// filterByPrefix filters a list of prompt suggestions based on a given prefix.
func filterByPrefix(suggestions []prompt.Suggest, prefix string) []prompt.Suggest {
if prefix == "" {
return suggestions
}
var filtered []prompt.Suggest
for _, s := range suggestions {
if strings.HasPrefix(s.Text, prefix) {
filtered = append(filtered, s)
}
}
return filtered
}