feat(completion): optimize ID prefix filtering and add completions to more commands

Improvements to shell completions from PR #935:

1. Add IDPrefix field to IssueFilter for efficient database-level filtering
   - Queries are now filtered at SQL level instead of fetching all issues
   - Updated sqlite, transaction, and memory stores to support IDPrefix

2. Add ValidArgsFunction to additional commands:
   - dep (add, remove, list, tree)
   - comments, comment (add)
   - delete
   - graph
   - label (add, remove, list)
   - duplicate, supersede
   - audit
   - move
   - relate, unrelate
   - refile
   - gate (show, resolve, add-waiter)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Executed-By: beads/crew/dave
Rig: beads
Role: crew
This commit is contained in:
beads/crew/dave
2026-01-06 19:05:34 -08:00
committed by Steve Yegge
parent 025cdac962
commit 5dfb838d60
16 changed files with 59 additions and 11 deletions

View File

@@ -160,6 +160,9 @@ func init() {
auditLabelCmd.Flags().StringVar(&auditLabelValue, "label", "", `Label value (e.g. "good" or "bad")`) auditLabelCmd.Flags().StringVar(&auditLabelValue, "label", "", `Label value (e.g. "good" or "bad")`)
auditLabelCmd.Flags().StringVar(&auditLabelReason, "reason", "", "Reason for label") auditLabelCmd.Flags().StringVar(&auditLabelReason, "reason", "", "Reason for label")
// Issue ID completions
auditCmd.ValidArgsFunction = issueIDCompletion
auditCmd.AddCommand(auditRecordCmd) auditCmd.AddCommand(auditRecordCmd)
auditCmd.AddCommand(auditLabelCmd) auditCmd.AddCommand(auditLabelCmd)
rootCmd.AddCommand(auditCmd) rootCmd.AddCommand(auditCmd)

View File

@@ -223,6 +223,11 @@ func init() {
commentCmd.Flags().StringP("file", "f", "", "Read comment text from file") commentCmd.Flags().StringP("file", "f", "", "Read comment text from file")
commentCmd.Flags().StringP("author", "a", "", "Add author to comment") commentCmd.Flags().StringP("author", "a", "", "Add author to comment")
// Issue ID completions
commentsCmd.ValidArgsFunction = issueIDCompletion
commentsAddCmd.ValidArgsFunction = issueIDCompletion
commentCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(commentsCmd) rootCmd.AddCommand(commentsCmd)
rootCmd.AddCommand(commentCmd) rootCmd.AddCommand(commentCmd)
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@@ -51,8 +50,10 @@ func issueIDCompletion(cmd *cobra.Command, args []string, toComplete string) ([]
defer currentStore.Close() defer currentStore.Close()
} }
// Use SearchIssues with empty query and default filter to get all issues // Use SearchIssues with IDPrefix filter to efficiently query matching issues
filter := types.IssueFilter{} filter := types.IssueFilter{
IDPrefix: toComplete, // Filter at database level for better performance
}
issues, err := currentStore.SearchIssues(ctx, "", filter) issues, err := currentStore.SearchIssues(ctx, "", filter)
if err != nil { if err != nil {
// If we can't list issues, return empty completion // If we can't list issues, return empty completion
@@ -62,11 +63,6 @@ func issueIDCompletion(cmd *cobra.Command, args []string, toComplete string) ([]
// Build completion list // Build completion list
completions := make([]string, 0, len(issues)) completions := make([]string, 0, len(issues))
for _, issue := range issues { for _, issue := range issues {
// Filter based on what's already typed
if toComplete != "" && !strings.HasPrefix(issue.ID, toComplete) {
continue
}
// Format: ID\tTitle (shown during completion) // Format: ID\tTitle (shown during completion)
completions = append(completions, fmt.Sprintf("%s\t%s", issue.ID, issue.Title)) completions = append(completions, fmt.Sprintf("%s\t%s", issue.ID, issue.Title))
} }

View File

@@ -833,5 +833,6 @@ func init() {
deleteCmd.Flags().Bool("cascade", false, "Recursively delete all dependent issues") deleteCmd.Flags().Bool("cascade", false, "Recursively delete all dependent issues")
deleteCmd.Flags().Bool("hard", false, "Permanently delete (skip tombstone, cannot be recovered via sync)") deleteCmd.Flags().Bool("hard", false, "Permanently delete (skip tombstone, cannot be recovered via sync)")
deleteCmd.Flags().String("reason", "", "Reason for deletion (stored in tombstone for audit trail)") deleteCmd.Flags().String("reason", "", "Reason for deletion (stored in tombstone for audit trail)")
deleteCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(deleteCmd) rootCmd.AddCommand(deleteCmd)
} }

View File

@@ -1167,6 +1167,12 @@ func init() {
depListCmd.Flags().String("direction", "down", "Direction: 'down' (dependencies), 'up' (dependents)") depListCmd.Flags().String("direction", "down", "Direction: 'down' (dependencies), 'up' (dependents)")
depListCmd.Flags().StringP("type", "t", "", "Filter by dependency type (e.g., tracks, blocks, parent-child)") depListCmd.Flags().StringP("type", "t", "", "Filter by dependency type (e.g., tracks, blocks, parent-child)")
// Issue ID completions for dep subcommands
depAddCmd.ValidArgsFunction = issueIDCompletion
depRemoveCmd.ValidArgsFunction = issueIDCompletion
depListCmd.ValidArgsFunction = issueIDCompletion
depTreeCmd.ValidArgsFunction = issueIDCompletion
depCmd.AddCommand(depAddCmd) depCmd.AddCommand(depAddCmd)
depCmd.AddCommand(depRemoveCmd) depCmd.AddCommand(depRemoveCmd)
depCmd.AddCommand(depListCmd) depCmd.AddCommand(depListCmd)

View File

@@ -50,10 +50,12 @@ var (
func init() { func init() {
duplicateCmd.Flags().StringVar(&duplicateOf, "of", "", "Canonical issue ID (required)") duplicateCmd.Flags().StringVar(&duplicateOf, "of", "", "Canonical issue ID (required)")
_ = duplicateCmd.MarkFlagRequired("of") // Only fails if flag missing (caught in tests) _ = duplicateCmd.MarkFlagRequired("of") // Only fails if flag missing (caught in tests)
duplicateCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(duplicateCmd) rootCmd.AddCommand(duplicateCmd)
supersedeCmd.Flags().StringVar(&supersededWith, "with", "", "Replacement issue ID (required)") supersedeCmd.Flags().StringVar(&supersededWith, "with", "", "Replacement issue ID (required)")
_ = supersedeCmd.MarkFlagRequired("with") // Only fails if flag missing (caught in tests) _ = supersedeCmd.MarkFlagRequired("with") // Only fails if flag missing (caught in tests)
supersedeCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(supersedeCmd) rootCmd.AddCommand(supersedeCmd)
} }

View File

@@ -871,6 +871,11 @@ func init() {
gateCheckCmd.Flags().BoolP("escalate", "e", false, "Escalate failed/expired gates") gateCheckCmd.Flags().BoolP("escalate", "e", false, "Escalate failed/expired gates")
gateCheckCmd.Flags().IntP("limit", "l", 100, "Limit results (default 100)") gateCheckCmd.Flags().IntP("limit", "l", 100, "Limit results (default 100)")
// Issue ID completions
gateShowCmd.ValidArgsFunction = issueIDCompletion
gateResolveCmd.ValidArgsFunction = issueIDCompletion
gateAddWaiterCmd.ValidArgsFunction = issueIDCompletion
// Add subcommands // Add subcommands
gateCmd.AddCommand(gateListCmd) gateCmd.AddCommand(gateListCmd)
gateCmd.AddCommand(gateShowCmd) gateCmd.AddCommand(gateShowCmd)

View File

@@ -117,6 +117,7 @@ Colors indicate status:
} }
func init() { func init() {
graphCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(graphCmd) rootCmd.AddCommand(graphCmd)
} }

View File

@@ -314,6 +314,11 @@ var labelListAllCmd = &cobra.Command{
}, },
} }
func init() { func init() {
// Issue ID completions
labelAddCmd.ValidArgsFunction = issueIDCompletion
labelRemoveCmd.ValidArgsFunction = issueIDCompletion
labelListCmd.ValidArgsFunction = issueIDCompletion
labelCmd.AddCommand(labelAddCmd) labelCmd.AddCommand(labelAddCmd)
labelCmd.AddCommand(labelRemoveCmd) labelCmd.AddCommand(labelRemoveCmd)
labelCmd.AddCommand(labelListCmd) labelCmd.AddCommand(labelListCmd)

View File

@@ -276,5 +276,6 @@ func init() {
moveCmd.Flags().String("to", "", "Target rig or prefix (required)") moveCmd.Flags().String("to", "", "Target rig or prefix (required)")
moveCmd.Flags().Bool("keep-open", false, "Keep the source issue open (don't close it)") moveCmd.Flags().Bool("keep-open", false, "Keep the source issue open (don't close it)")
moveCmd.Flags().Bool("skip-deps", false, "Skip dependency remapping") moveCmd.Flags().Bool("skip-deps", false, "Skip dependency remapping")
moveCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(moveCmd) rootCmd.AddCommand(moveCmd)
} }

View File

@@ -160,5 +160,6 @@ Examples:
func init() { func init() {
refileCmd.Flags().Bool("keep-open", false, "Keep the source issue open (don't close it)") refileCmd.Flags().Bool("keep-open", false, "Keep the source issue open (don't close it)")
refileCmd.ValidArgsFunction = issueIDCompletion
rootCmd.AddCommand(refileCmd) rootCmd.AddCommand(refileCmd)
} }

View File

@@ -41,6 +41,10 @@ Example:
} }
func init() { func init() {
// Issue ID completions
relateCmd.ValidArgsFunction = issueIDCompletion
unrelateCmd.ValidArgsFunction = issueIDCompletion
// Add as subcommands of dep // Add as subcommands of dep
depCmd.AddCommand(relateCmd) depCmd.AddCommand(relateCmd)
depCmd.AddCommand(unrelateCmd) depCmd.AddCommand(unrelateCmd)

View File

@@ -620,6 +620,11 @@ func (m *MemoryStorage) SearchIssues(ctx context.Context, query string, filter t
} }
} }
// ID prefix filtering (for shell completion)
if filter.IDPrefix != "" && !strings.HasPrefix(issue.ID, filter.IDPrefix) {
continue
}
// Parent filtering (bd-yqhh): filter children by parent issue // Parent filtering (bd-yqhh): filter children by parent issue
if filter.ParentID != nil { if filter.ParentID != nil {
isChild := false isChild := false

View File

@@ -1836,6 +1836,12 @@ func (s *SQLiteStorage) SearchIssues(ctx context.Context, query string, filter t
whereClauses = append(whereClauses, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", "))) whereClauses = append(whereClauses, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", ")))
} }
// ID prefix filtering (for shell completion)
if filter.IDPrefix != "" {
whereClauses = append(whereClauses, "id LIKE ?")
args = append(args, filter.IDPrefix+"%")
}
// Wisp filtering // Wisp filtering
if filter.Ephemeral != nil { if filter.Ephemeral != nil {
if *filter.Ephemeral { if *filter.Ephemeral {

View File

@@ -1181,6 +1181,12 @@ func (t *sqliteTxStorage) SearchIssues(ctx context.Context, query string, filter
whereClauses = append(whereClauses, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", "))) whereClauses = append(whereClauses, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ", ")))
} }
// ID prefix filtering (for shell completion)
if filter.IDPrefix != "" {
whereClauses = append(whereClauses, "id LIKE ?")
args = append(args, filter.IDPrefix+"%")
}
// Wisp filtering // Wisp filtering
if filter.Ephemeral != nil { if filter.Ephemeral != nil {
if *filter.Ephemeral { if *filter.Ephemeral {

View File

@@ -758,7 +758,8 @@ type IssueFilter struct {
Labels []string // AND semantics: issue must have ALL these labels Labels []string // AND semantics: issue must have ALL these labels
LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels LabelsAny []string // OR semantics: issue must have AT LEAST ONE of these labels
TitleSearch string TitleSearch string
IDs []string // Filter by specific issue IDs IDs []string // Filter by specific issue IDs
IDPrefix string // Filter by ID prefix (for shell completion)
Limit int Limit int
// Pattern matching // Pattern matching