ccmanager 4.1.20 → 4.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -245,6 +245,7 @@ const Dashboard = ({ projectsDir, onSelectSession, onSelectProject, onSessionAct
245
245
  worktree: wt,
246
246
  session: entry.session,
247
247
  baseLabel,
248
+ searchableName: `${entry.projectName} :: ${fullBranchName}${isMain}`,
248
249
  fileChanges,
249
250
  aheadBehind,
250
251
  parentBranch,
@@ -10,7 +10,7 @@ import { prepareSessionItems, calculateColumnPositions, assembleSessionLabel, }
10
10
  import { projectManager } from '../services/projectManager.js';
11
11
  import { useSearchMode } from '../hooks/useSearchMode.js';
12
12
  import { useDynamicLimit } from '../hooks/useDynamicLimit.js';
13
- import { filterWorktreesByQuery } from '../utils/filterByQuery.js';
13
+ import { filterSessionItemsByQuery } from '../utils/filterByQuery.js';
14
14
  import SearchableList from './SearchableList.js';
15
15
  import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
16
16
  import { configReader } from '../services/config/configReader.js';
@@ -124,10 +124,9 @@ const Menu = ({ sessionManager, worktreeService, onMenuAction, onSelectRecentPro
124
124
  sortByLastSession: worktreeConfig.sortByLastSession,
125
125
  });
126
126
  const columnPositions = calculateColumnPositions(items);
127
- // Filter worktrees based on search query
128
- const filteredWorktrees = filterWorktreesByQuery(items.map(item => item.worktree), searchQuery);
129
- const filteredWorktreeSet = new Set(filteredWorktrees);
130
- const filteredItems = items.filter(item => filteredWorktreeSet.has(item.worktree));
127
+ // Filter session items based on search query, matching the name shown in
128
+ // the menu (branch name, " (main)", and session name) plus the path.
129
+ const filteredItems = filterSessionItemsByQuery(items, searchQuery);
131
130
  // Build menu items with proper alignment
132
131
  const menuItems = filteredItems.map((item, index) => {
133
132
  const baseLabel = assembleSessionLabel(item, columnPositions);
@@ -1,5 +1,16 @@
1
1
  import { Worktree } from '../types/index.js';
2
+ import { SessionItem } from './worktreeUtils.js';
2
3
  /**
3
4
  * Filter worktrees by matching search query against branch name and path.
4
5
  */
5
6
  export declare function filterWorktreesByQuery(worktrees: Worktree[], query: string): Worktree[];
7
+ /**
8
+ * Filter session items by matching the search query against the name shown in
9
+ * the menu (branch name, " (main)" indicator, and session name) and the
10
+ * worktree path. Status icons and git status columns are not matched.
11
+ *
12
+ * Filtering happens per session item (not per worktree) so that a query can
13
+ * match an individual session name within a worktree that has multiple
14
+ * sessions.
15
+ */
16
+ export declare function filterSessionItemsByQuery(items: SessionItem[], query: string): SessionItem[];
@@ -11,3 +11,19 @@ export function filterWorktreesByQuery(worktrees, query) {
11
11
  worktree.path.toLowerCase().includes(searchLower));
12
12
  });
13
13
  }
14
+ /**
15
+ * Filter session items by matching the search query against the name shown in
16
+ * the menu (branch name, " (main)" indicator, and session name) and the
17
+ * worktree path. Status icons and git status columns are not matched.
18
+ *
19
+ * Filtering happens per session item (not per worktree) so that a query can
20
+ * match an individual session name within a worktree that has multiple
21
+ * sessions.
22
+ */
23
+ export function filterSessionItemsByQuery(items, query) {
24
+ if (!query)
25
+ return items;
26
+ const searchLower = query.toLowerCase();
27
+ return items.filter(item => item.searchableName.toLowerCase().includes(searchLower) ||
28
+ item.worktree.path.toLowerCase().includes(searchLower));
29
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,81 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { filterWorktreesByQuery, filterSessionItemsByQuery, } from './filterByQuery.js';
3
+ const makeItem = (searchableName, path) => ({
4
+ worktree: {
5
+ path,
6
+ isMainWorktree: false,
7
+ hasSession: false,
8
+ },
9
+ baseLabel: searchableName,
10
+ searchableName,
11
+ fileChanges: '',
12
+ aheadBehind: '',
13
+ parentBranch: '',
14
+ lastCommitDate: '',
15
+ lengths: {
16
+ base: 0,
17
+ fileChanges: 0,
18
+ aheadBehind: 0,
19
+ parentBranch: 0,
20
+ lastCommitDate: 0,
21
+ },
22
+ });
23
+ describe('filterWorktreesByQuery', () => {
24
+ const worktrees = [
25
+ {
26
+ path: '/repo/feature-a',
27
+ branch: 'feature/a',
28
+ isMainWorktree: false,
29
+ hasSession: false,
30
+ },
31
+ {
32
+ path: '/repo/main',
33
+ branch: 'main',
34
+ isMainWorktree: true,
35
+ hasSession: false,
36
+ },
37
+ ];
38
+ it('returns all worktrees when query is empty', () => {
39
+ expect(filterWorktreesByQuery(worktrees, '')).toEqual(worktrees);
40
+ });
41
+ it('matches branch name case-insensitively', () => {
42
+ const result = filterWorktreesByQuery(worktrees, 'FEATURE');
43
+ expect(result).toHaveLength(1);
44
+ expect(result[0]?.branch).toBe('feature/a');
45
+ });
46
+ it('matches path', () => {
47
+ const result = filterWorktreesByQuery(worktrees, '/repo/main');
48
+ expect(result).toHaveLength(1);
49
+ expect(result[0]?.path).toBe('/repo/main');
50
+ });
51
+ });
52
+ describe('filterSessionItemsByQuery', () => {
53
+ const items = [
54
+ makeItem('feature/a', '/repo/feature-a'),
55
+ makeItem('main (main)', '/repo/main'),
56
+ makeItem('feature/b: my-session', '/repo/feature-b'),
57
+ makeItem('feature/b: other', '/repo/feature-b'),
58
+ ];
59
+ it('returns all items when query is empty', () => {
60
+ expect(filterSessionItemsByQuery(items, '')).toEqual(items);
61
+ });
62
+ it('matches the session name within a worktree', () => {
63
+ const result = filterSessionItemsByQuery(items, 'my-session');
64
+ expect(result).toHaveLength(1);
65
+ expect(result[0]?.searchableName).toBe('feature/b: my-session');
66
+ });
67
+ it('matches the (main) indicator', () => {
68
+ const result = filterSessionItemsByQuery(items, '(main)');
69
+ expect(result).toHaveLength(1);
70
+ expect(result[0]?.searchableName).toBe('main (main)');
71
+ });
72
+ it('matches branch name case-insensitively', () => {
73
+ const result = filterSessionItemsByQuery(items, 'FEATURE/A');
74
+ expect(result).toHaveLength(1);
75
+ expect(result[0]?.searchableName).toBe('feature/a');
76
+ });
77
+ it('matches path', () => {
78
+ const result = filterSessionItemsByQuery(items, '/repo/feature-b');
79
+ expect(result).toHaveLength(2);
80
+ });
81
+ });
@@ -6,6 +6,7 @@ export interface SessionItem {
6
6
  worktree: Worktree;
7
7
  session?: Session;
8
8
  baseLabel: string;
9
+ searchableName: string;
9
10
  fileChanges: string;
10
11
  aheadBehind: string;
11
12
  parentBranch: string;
@@ -166,6 +166,9 @@ function buildSessionItem(wt, session, sessionSuffix) {
166
166
  const branchName = truncateString(fullBranchName, MAX_BRANCH_NAME_LENGTH);
167
167
  const isMain = wt.isMainWorktree ? ' (main)' : '';
168
168
  const baseLabel = `${branchName}${isMain}${sessionSuffix}${status}`;
169
+ // Use the full (untruncated) branch name so search still matches the tail
170
+ // of long branch names; status icons are excluded so they don't match.
171
+ const searchableName = `${fullBranchName}${isMain}${sessionSuffix}`;
169
172
  const { fileChanges, aheadBehind, parentBranch, error } = gitStatusColumns(wt, fullBranchName);
170
173
  const lastCommitDate = wt.lastCommitDate
171
174
  ? `\x1b[90m${formatRelativeDate(wt.lastCommitDate)}\x1b[0m`
@@ -174,6 +177,7 @@ function buildSessionItem(wt, session, sessionSuffix) {
174
177
  worktree: wt,
175
178
  session,
176
179
  baseLabel,
180
+ searchableName,
177
181
  fileChanges,
178
182
  aheadBehind,
179
183
  parentBranch,
@@ -173,6 +173,7 @@ describe('column alignment', () => {
173
173
  {
174
174
  worktree: {},
175
175
  baseLabel: 'feature/test-branch',
176
+ searchableName: 'feature/test-branch',
176
177
  fileChanges: '\x1b[32m+10\x1b[0m \x1b[31m-5\x1b[0m',
177
178
  aheadBehind: '\x1b[33m↑2 ↓3\x1b[0m',
178
179
  parentBranch: '',
@@ -188,6 +189,7 @@ describe('column alignment', () => {
188
189
  {
189
190
  worktree: {},
190
191
  baseLabel: 'main',
192
+ searchableName: 'main',
191
193
  fileChanges: '\x1b[32m+2\x1b[0m \x1b[31m-1\x1b[0m',
192
194
  aheadBehind: '\x1b[33m↑1\x1b[0m',
193
195
  parentBranch: '',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "4.1.20",
3
+ "version": "4.1.21",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -41,11 +41,11 @@
41
41
  "bin"
42
42
  ],
43
43
  "optionalDependencies": {
44
- "@kodaikabasawa/ccmanager-darwin-arm64": "4.1.20",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "4.1.20",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "4.1.20",
47
- "@kodaikabasawa/ccmanager-linux-x64": "4.1.20",
48
- "@kodaikabasawa/ccmanager-win32-x64": "4.1.20"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "4.1.21",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "4.1.21",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "4.1.21",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "4.1.21",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "4.1.21"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",