From 7ffb660d91a1c7a4977e714ad737f9b6fef33b10 Mon Sep 17 00:00:00 2001 From: Greg Priday Date: Mon, 16 Mar 2026 22:18:59 +1100 Subject: [PATCH] fix(worktree): fix sidebar search filtering for text and bare numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove redundant startsWith("#") guard in matchesFilters — parseExactNumber already handles both #123 and 123 - Gate alwaysShowActive/alwaysShowWaiting bypasses on !hasActiveQuery so text search actually filters the list - Replace ad-hoc worktreeMatchesQuery with parseExactNumber-based logic for consistent pinned worktree filtering - Apply same bypass fix to WorktreeOverviewModal for consistency - Add tests for bare-number matching by issueNumber, prNumber, text fallback rejection, and whitespace padding --- src/App.tsx | 16 ++++----- .../Worktree/WorktreeOverviewModal.tsx | 6 ++-- src/lib/__tests__/worktreeFilters.test.ts | 36 +++++++++++++++++++ src/lib/worktreeFilters.ts | 2 +- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 65b50132..51e7a277 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -126,6 +126,7 @@ import { type DerivedWorktreeMeta, type FilterState, } from "./lib/worktreeFilters"; +import { parseExactNumber } from "./lib/parseExactNumber"; import type { WorktreeState, PanelKind } from "./types"; import { actionService } from "./services/ActionService"; import { voiceRecordingService } from "./services/VoiceRecordingService"; @@ -281,12 +282,13 @@ function SidebarContent({ onOpenOverview }: SidebarContentProps) { hasCompletedAgent: false, }; const isActive = worktree.id === activeWorktreeId; + const hasActiveQuery = query.trim().length > 0; - if (alwaysShowActive && isActive) { + if (alwaysShowActive && isActive && !hasActiveQuery) { return true; } - if (alwaysShowWaiting && derived.hasWaitingAgent) { + if (alwaysShowWaiting && derived.hasWaitingAgent && !hasActiveQuery) { return true; } @@ -508,13 +510,11 @@ function SidebarContent({ onOpenOverview }: SidebarContentProps) { const hasFilters = hasActiveFilters(); const worktreeMatchesQuery = (w: WorktreeState) => { if (!query) return true; - if (scoreWorktree(w, query) > 0) return true; - const trimmed = query.trim(); - if (trimmed.startsWith("#")) { - const num = parseInt(trimmed.slice(1), 10); - if (num > 0 && (w.issueNumber === num || w.prNumber === num)) return true; + const exactNum = parseExactNumber(query); + if (exactNum !== null) { + return w.issueNumber === exactNum || w.prNumber === exactNum; } - return false; + return scoreWorktree(w, query) > 0; }; const mainMatchesQuery = mainWorktree && worktreeMatchesQuery(mainWorktree); const integrationMatchesQuery = integrationWorktree && worktreeMatchesQuery(integrationWorktree); diff --git a/src/components/Worktree/WorktreeOverviewModal.tsx b/src/components/Worktree/WorktreeOverviewModal.tsx index 44cc86f5..e2439422 100644 --- a/src/components/Worktree/WorktreeOverviewModal.tsx +++ b/src/components/Worktree/WorktreeOverviewModal.tsx @@ -174,18 +174,18 @@ export function WorktreeOverviewModal({ hasCompletedAgent: false, }; const isActive = worktree.id === activeWorktreeId; + const hasActiveQuery = query.trim().length > 0; // hideMainWorktree always takes precedence for the main worktree (user's explicit intent) if (hideMainWorktree && worktree.isMainWorktree) { return false; } - // Always show active worktree if setting is enabled - if (alwaysShowActive && isActive) { + if (alwaysShowActive && isActive && !hasActiveQuery) { return true; } - if (alwaysShowWaiting && derived.hasWaitingAgent) { + if (alwaysShowWaiting && derived.hasWaitingAgent && !hasActiveQuery) { return true; } diff --git a/src/lib/__tests__/worktreeFilters.test.ts b/src/lib/__tests__/worktreeFilters.test.ts index ee1117bb..52f525a7 100644 --- a/src/lib/__tests__/worktreeFilters.test.ts +++ b/src/lib/__tests__/worktreeFilters.test.ts @@ -477,6 +477,42 @@ describe("matchesFilters", () => { expect(matchesFilters(worktree, filters, meta, false)).toBe(true); }); + it("matches bare number by issueNumber", () => { + const worktree = createMockWorktree({ issueNumber: 123 }); + const filters = createEmptyFilters(); + filters.query = "123"; + const meta = createEmptyMeta(); + expect(matchesFilters(worktree, filters, meta, false)).toBe(true); + }); + + it("matches bare number by prNumber", () => { + const worktree = createMockWorktree({ prNumber: 456 }); + const filters = createEmptyFilters(); + filters.query = "456"; + const meta = createEmptyMeta(); + expect(matchesFilters(worktree, filters, meta, false)).toBe(true); + }); + + it("does not match bare number via text fallback when no issue/PR matches", () => { + const worktree = createMockWorktree({ + branch: "feature/issue-123-fix", + issueNumber: undefined, + prNumber: undefined, + }); + const filters = createEmptyFilters(); + filters.query = "123"; + const meta = createEmptyMeta(); + expect(matchesFilters(worktree, filters, meta, false)).toBe(false); + }); + + it("matches bare number with whitespace padding", () => { + const worktree = createMockWorktree({ issueNumber: 123 }); + const filters = createEmptyFilters(); + filters.query = " 123 "; + const meta = createEmptyMeta(); + expect(matchesFilters(worktree, filters, meta, false)).toBe(true); + }); + it("does not match #number when neither issueNumber nor prNumber match", () => { const worktree = createMockWorktree({ issueNumber: 100, prNumber: 200 }); const filters = createEmptyFilters(); diff --git a/src/lib/worktreeFilters.ts b/src/lib/worktreeFilters.ts index a5321268..60820343 100644 --- a/src/lib/worktreeFilters.ts +++ b/src/lib/worktreeFilters.ts @@ -130,7 +130,7 @@ export function matchesFilters( // Text search if (filters.query.length > 0) { const exactNum = parseExactNumber(filters.query); - if (exactNum !== null && filters.query.trim().startsWith("#")) { + if (exactNum !== null) { if (worktree.issueNumber !== exactNum && worktree.prNumber !== exactNum) { return false; }