Skip to content

Commit 793c238

Browse files
kbwoclaude
andauthored
feat: match menu search against displayed name (session name and (main)) (#309)
The "/" filter previously matched only the branch name and path, so a query could not select a row by its session name or the "(main)" indicator shown in the menu. Filtering also operated per worktree, which could not distinguish individual session names within a worktree that has multiple sessions. Add a searchableName field to SessionItem (branch + " (main)" + session name, built from the same pieces as the displayed label but without status icons or git status columns) and filter per session item against it plus the path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b2e7e2d commit 793c238

6 files changed

Lines changed: 134 additions & 10 deletions

File tree

‎src/components/Dashboard.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ const Dashboard: React.FC<DashboardProps> = ({
403403
worktree: wt,
404404
session: entry.session,
405405
baseLabel,
406+
searchableName: `${entry.projectName} :: ${fullBranchName}${isMain}`,
406407
fileChanges,
407408
aheadBehind,
408409
parentBranch,

‎src/components/Menu.tsx‎

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {projectManager} from '../services/projectManager.js';
2121
import {RecentProject} from '../types/index.js';
2222
import {useSearchMode} from '../hooks/useSearchMode.js';
2323
import {useDynamicLimit} from '../hooks/useDynamicLimit.js';
24-
import {filterWorktreesByQuery} from '../utils/filterByQuery.js';
24+
import {filterSessionItemsByQuery} from '../utils/filterByQuery.js';
2525
import SearchableList from './SearchableList.js';
2626
import {globalSessionOrchestrator} from '../services/globalSessionOrchestrator.js';
2727
import {configReader} from '../services/config/configReader.js';
@@ -212,15 +212,9 @@ const Menu: React.FC<MenuProps> = ({
212212
});
213213
const columnPositions = calculateColumnPositions(items);
214214

215-
// Filter worktrees based on search query
216-
const filteredWorktrees = filterWorktreesByQuery(
217-
items.map(item => item.worktree),
218-
searchQuery,
219-
);
220-
const filteredWorktreeSet = new Set(filteredWorktrees);
221-
const filteredItems = items.filter(item =>
222-
filteredWorktreeSet.has(item.worktree),
223-
);
215+
// Filter session items based on search query, matching the name shown in
216+
// the menu (branch name, " (main)", and session name) plus the path.
217+
const filteredItems = filterSessionItemsByQuery(items, searchQuery);
224218

225219
// Build menu items with proper alignment
226220
const menuItems: MenuItem[] = filteredItems.map(

‎src/utils/filterByQuery.test.ts‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {describe, it, expect} from 'vitest';
2+
import {
3+
filterWorktreesByQuery,
4+
filterSessionItemsByQuery,
5+
} from './filterByQuery.js';
6+
import {Worktree} from '../types/index.js';
7+
import {SessionItem} from './worktreeUtils.js';
8+
9+
const makeItem = (searchableName: string, path: string): SessionItem => ({
10+
worktree: {
11+
path,
12+
isMainWorktree: false,
13+
hasSession: false,
14+
} as Worktree,
15+
baseLabel: searchableName,
16+
searchableName,
17+
fileChanges: '',
18+
aheadBehind: '',
19+
parentBranch: '',
20+
lastCommitDate: '',
21+
lengths: {
22+
base: 0,
23+
fileChanges: 0,
24+
aheadBehind: 0,
25+
parentBranch: 0,
26+
lastCommitDate: 0,
27+
},
28+
});
29+
30+
describe('filterWorktreesByQuery', () => {
31+
const worktrees: Worktree[] = [
32+
{
33+
path: '/repo/feature-a',
34+
branch: 'feature/a',
35+
isMainWorktree: false,
36+
hasSession: false,
37+
},
38+
{
39+
path: '/repo/main',
40+
branch: 'main',
41+
isMainWorktree: true,
42+
hasSession: false,
43+
},
44+
];
45+
46+
it('returns all worktrees when query is empty', () => {
47+
expect(filterWorktreesByQuery(worktrees, '')).toEqual(worktrees);
48+
});
49+
50+
it('matches branch name case-insensitively', () => {
51+
const result = filterWorktreesByQuery(worktrees, 'FEATURE');
52+
expect(result).toHaveLength(1);
53+
expect(result[0]?.branch).toBe('feature/a');
54+
});
55+
56+
it('matches path', () => {
57+
const result = filterWorktreesByQuery(worktrees, '/repo/main');
58+
expect(result).toHaveLength(1);
59+
expect(result[0]?.path).toBe('/repo/main');
60+
});
61+
});
62+
63+
describe('filterSessionItemsByQuery', () => {
64+
const items: SessionItem[] = [
65+
makeItem('feature/a', '/repo/feature-a'),
66+
makeItem('main (main)', '/repo/main'),
67+
makeItem('feature/b: my-session', '/repo/feature-b'),
68+
makeItem('feature/b: other', '/repo/feature-b'),
69+
];
70+
71+
it('returns all items when query is empty', () => {
72+
expect(filterSessionItemsByQuery(items, '')).toEqual(items);
73+
});
74+
75+
it('matches the session name within a worktree', () => {
76+
const result = filterSessionItemsByQuery(items, 'my-session');
77+
expect(result).toHaveLength(1);
78+
expect(result[0]?.searchableName).toBe('feature/b: my-session');
79+
});
80+
81+
it('matches the (main) indicator', () => {
82+
const result = filterSessionItemsByQuery(items, '(main)');
83+
expect(result).toHaveLength(1);
84+
expect(result[0]?.searchableName).toBe('main (main)');
85+
});
86+
87+
it('matches branch name case-insensitively', () => {
88+
const result = filterSessionItemsByQuery(items, 'FEATURE/A');
89+
expect(result).toHaveLength(1);
90+
expect(result[0]?.searchableName).toBe('feature/a');
91+
});
92+
93+
it('matches path', () => {
94+
const result = filterSessionItemsByQuery(items, '/repo/feature-b');
95+
expect(result).toHaveLength(2);
96+
});
97+
});

‎src/utils/filterByQuery.ts‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Worktree} from '../types/index.js';
2+
import {SessionItem} from './worktreeUtils.js';
23

34
/**
45
* Filter worktrees by matching search query against branch name and path.
@@ -17,3 +18,25 @@ export function filterWorktreesByQuery(
1718
);
1819
});
1920
}
21+
22+
/**
23+
* Filter session items by matching the search query against the name shown in
24+
* the menu (branch name, " (main)" indicator, and session name) and the
25+
* worktree path. Status icons and git status columns are not matched.
26+
*
27+
* Filtering happens per session item (not per worktree) so that a query can
28+
* match an individual session name within a worktree that has multiple
29+
* sessions.
30+
*/
31+
export function filterSessionItemsByQuery(
32+
items: SessionItem[],
33+
query: string,
34+
): SessionItem[] {
35+
if (!query) return items;
36+
const searchLower = query.toLowerCase();
37+
return items.filter(
38+
item =>
39+
item.searchableName.toLowerCase().includes(searchLower) ||
40+
item.worktree.path.toLowerCase().includes(searchLower),
41+
);
42+
}

‎src/utils/worktreeUtils.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ describe('column alignment', () => {
290290
{
291291
worktree: {} as Worktree,
292292
baseLabel: 'feature/test-branch',
293+
searchableName: 'feature/test-branch',
293294
fileChanges: '\x1b[32m+10\x1b[0m \x1b[31m-5\x1b[0m',
294295
aheadBehind: '\x1b[33m↑2 ↓3\x1b[0m',
295296
parentBranch: '',
@@ -305,6 +306,7 @@ describe('column alignment', () => {
305306
{
306307
worktree: {} as Worktree,
307308
baseLabel: 'main',
309+
searchableName: 'main',
308310
fileChanges: '\x1b[32m+2\x1b[0m \x1b[31m-1\x1b[0m',
309311
aheadBehind: '\x1b[33m↑1\x1b[0m',
310312
parentBranch: '',

‎src/utils/worktreeUtils.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export interface SessionItem {
2020
worktree: Worktree;
2121
session?: Session;
2222
baseLabel: string;
23+
// Name portion shown in the menu (branch + " (main)" + session name),
24+
// without status icons or git status columns. Used for search matching.
25+
searchableName: string;
2326
fileChanges: string;
2427
aheadBehind: string;
2528
parentBranch: string;
@@ -237,6 +240,9 @@ function buildSessionItem(
237240
const branchName = truncateString(fullBranchName, MAX_BRANCH_NAME_LENGTH);
238241
const isMain = wt.isMainWorktree ? ' (main)' : '';
239242
const baseLabel = `${branchName}${isMain}${sessionSuffix}${status}`;
243+
// Use the full (untruncated) branch name so search still matches the tail
244+
// of long branch names; status icons are excluded so they don't match.
245+
const searchableName = `${fullBranchName}${isMain}${sessionSuffix}`;
240246
const {fileChanges, aheadBehind, parentBranch, error} = gitStatusColumns(
241247
wt,
242248
fullBranchName,
@@ -249,6 +255,7 @@ function buildSessionItem(
249255
worktree: wt,
250256
session,
251257
baseLabel,
258+
searchableName,
252259
fileChanges,
253260
aheadBehind,
254261
parentBranch,

0 commit comments

Comments
 (0)