From 88b5423c64dc26ac1f89be745944e193d2720330 Mon Sep 17 00:00:00 2001 From: Bruno Bornsztein Date: Sat, 31 Jan 2026 08:51:13 -0600 Subject: [PATCH] feat(ui): add project-only filter with [ prefix in kanban Add support for filtering tasks by project only using the [ prefix. When typing [project in the kanban filter, it matches only against the project field, not the title or other text fields. - "[" alone shows all tasks with a project set - "[workflow" filters tasks where project matches "workflow" - "[ol" uses fuzzy matching to find projects like "offerlab" - "[project]" with trailing bracket also works Updated filter placeholder to indicate the available prefixes. Co-Authored-By: Claude Opus 4.5 --- internal/ui/app.go | 25 +++++++++++++++++++++++- internal/ui/app_test.go | 43 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/internal/ui/app.go b/internal/ui/app.go index a6baf256..1a066c04 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -420,7 +420,7 @@ func NewAppModel(database *db.DB, exec *executor.Executor, workingDir string) *A // Setup filter input filterInput := textinput.New() - filterInput.Placeholder = "Filter by project, type, or text..." + filterInput.Placeholder = "Filter text, #id, or [project..." filterInput.CharLimit = 50 // Get available executors for form filtering and warnings @@ -1459,9 +1459,32 @@ func (m *AppModel) applyFilter() { // Returns -1 if the task doesn't match, otherwise returns a positive score. // Higher scores indicate better matches. // This uses the same matching logic as the command palette (Ctrl+P) for consistency. +// +// Special prefixes: +// - "[project" filters only on the project field (matches the [project] display format) func scoreTaskForFilter(task *db.Task, query string) int { bestScore := -1 + // Check for project-only filter prefix "[" (matches display format [project]) + if strings.HasPrefix(query, "[") { + projectQuery := strings.TrimPrefix(query, "[") + // Also strip trailing ] if present (user typed full "[project]") + projectQuery = strings.TrimSuffix(projectQuery, "]") + if projectQuery == "" { + // Just "[" typed, show all tasks with a project + if task.Project != "" { + return 100 + } + return -1 + } + // Only match against project field + projectScore := fuzzyScore(task.Project, projectQuery) + if projectScore > 0 { + return projectScore + } + return -1 + } + // Check task ID (exact or prefix match gets high priority) idStr := fmt.Sprintf("%d", task.ID) if strings.HasPrefix(query, "#") { diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index abcaf9cd..bcdfa45b 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -461,6 +461,49 @@ func TestScoreTaskForFilter(t *testing.T) { wantMin: -1, wantMax: -1, }, + // Project-only filter tests (using [ prefix) + { + name: "project-only filter matches project", + task: &db.Task{ID: 1, Title: "Some task", Project: "workflow"}, + query: "[workflow", + wantMin: 100, + wantMax: 500, + }, + { + name: "project-only filter with fuzzy match", + task: &db.Task{ID: 1, Title: "Some task", Project: "offerlab"}, + query: "[ol", + wantMin: 100, + wantMax: 500, + }, + { + name: "project-only filter excludes title matches", + task: &db.Task{ID: 1, Title: "workflow improvements", Project: ""}, + query: "[workflow", + wantMin: -1, + wantMax: -1, + }, + { + name: "project-only filter with trailing bracket", + task: &db.Task{ID: 1, Title: "Task", Project: "influencekit"}, + query: "[influencekit]", + wantMin: 100, + wantMax: 700, // exact match gets high score + }, + { + name: "just bracket shows tasks with project", + task: &db.Task{ID: 1, Title: "Task", Project: "myproject"}, + query: "[", + wantMin: 100, + wantMax: 100, + }, + { + name: "just bracket hides tasks without project", + task: &db.Task{ID: 1, Title: "Task", Project: ""}, + query: "[", + wantMin: -1, + wantMax: -1, + }, } for _, tt := range tests {