ccmanager 4.1.20 → 4.1.22
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.
- package/dist/components/Dashboard.js +1 -0
- package/dist/components/Menu.js +4 -5
- package/dist/utils/filterByQuery.d.ts +11 -0
- package/dist/utils/filterByQuery.js +16 -0
- package/dist/utils/filterByQuery.test.d.ts +1 -0
- package/dist/utils/filterByQuery.test.js +81 -0
- package/dist/utils/worktreeUtils.d.ts +25 -0
- package/dist/utils/worktreeUtils.js +61 -7
- package/dist/utils/worktreeUtils.test.js +108 -2
- package/package.json +6 -6
|
@@ -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,
|
package/dist/components/Menu.js
CHANGED
|
@@ -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 {
|
|
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
|
|
128
|
-
|
|
129
|
-
const
|
|
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;
|
|
@@ -24,7 +25,31 @@ export interface SessionItem {
|
|
|
24
25
|
*/
|
|
25
26
|
export declare function formatRelativeDate(date: Date): string;
|
|
26
27
|
export declare function truncateString(str: string, maxLength: number): string;
|
|
28
|
+
/**
|
|
29
|
+
* Sanitize a branch name into the form used for a worktree directory name:
|
|
30
|
+
* slashes become dashes, characters outside [a-zA-Z0-9-_.] are stripped,
|
|
31
|
+
* leading/trailing dashes are removed, and the result is lowercased.
|
|
32
|
+
*
|
|
33
|
+
* This is the single source of truth for that mapping. It is reused both to
|
|
34
|
+
* generate worktree directories (see {@link generateWorktreeDirectory}) and to
|
|
35
|
+
* decide whether an existing directory name already conveys the branch name
|
|
36
|
+
* (see {@link formatWorktreeDirectorySuffix}), so a directory generated from a
|
|
37
|
+
* branch is reliably recognized as a match.
|
|
38
|
+
*/
|
|
39
|
+
export declare function sanitizeNameForDirectory(value: string): string;
|
|
27
40
|
export declare function generateWorktreeDirectory(projectPath: string, branchName: string, pattern?: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Returns the worktree directory basename to show alongside the branch name,
|
|
43
|
+
* or an empty string when it would be redundant (main worktree, detached,
|
|
44
|
+
* or the directory name matches the branch).
|
|
45
|
+
*
|
|
46
|
+
* The visible suffix is truncated; the raw basename is also returned so
|
|
47
|
+
* callers that want to make it searchable can include it untruncated.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatWorktreeDirectorySuffix(wt: Worktree, fullBranchName: string): {
|
|
50
|
+
displaySuffix: string;
|
|
51
|
+
rawName: string;
|
|
52
|
+
};
|
|
28
53
|
export declare function extractBranchParts(branchName: string): {
|
|
29
54
|
prefix?: string;
|
|
30
55
|
name: string;
|
|
@@ -5,6 +5,7 @@ import { getStatusDisplay } from '../constants/statusIcons.js';
|
|
|
5
5
|
import { formatGitFileChanges, formatGitAheadBehind, formatParentBranch, } from './gitStatus.js';
|
|
6
6
|
// Constants
|
|
7
7
|
const MAX_BRANCH_NAME_LENGTH = 70; // Maximum characters for branch name display
|
|
8
|
+
const MAX_WORKTREE_DIR_NAME_LENGTH = 30; // Maximum characters for the worktree directory name suffix
|
|
8
9
|
const MIN_COLUMN_PADDING = 2; // Minimum spaces between columns
|
|
9
10
|
/**
|
|
10
11
|
* Format a date as a relative time string (e.g., "2h ago", "3d ago").
|
|
@@ -64,6 +65,24 @@ function getGitRepositoryName(projectPath) {
|
|
|
64
65
|
return path.basename(projectPath);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Sanitize a branch name into the form used for a worktree directory name:
|
|
70
|
+
* slashes become dashes, characters outside [a-zA-Z0-9-_.] are stripped,
|
|
71
|
+
* leading/trailing dashes are removed, and the result is lowercased.
|
|
72
|
+
*
|
|
73
|
+
* This is the single source of truth for that mapping. It is reused both to
|
|
74
|
+
* generate worktree directories (see {@link generateWorktreeDirectory}) and to
|
|
75
|
+
* decide whether an existing directory name already conveys the branch name
|
|
76
|
+
* (see {@link formatWorktreeDirectorySuffix}), so a directory generated from a
|
|
77
|
+
* branch is reliably recognized as a match.
|
|
78
|
+
*/
|
|
79
|
+
export function sanitizeNameForDirectory(value) {
|
|
80
|
+
return value
|
|
81
|
+
.replace(/\//g, '-') // Replace forward slashes with dashes
|
|
82
|
+
.replace(/[^a-zA-Z0-9-_.]+/g, '') // Remove special characters except dash, dot, underscore
|
|
83
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
|
|
84
|
+
.toLowerCase(); // Convert to lowercase for consistency
|
|
85
|
+
}
|
|
67
86
|
export function generateWorktreeDirectory(projectPath, branchName, pattern) {
|
|
68
87
|
// Default pattern if not specified
|
|
69
88
|
const defaultPattern = '../{branch}';
|
|
@@ -74,12 +93,7 @@ export function generateWorktreeDirectory(projectPath, branchName, pattern) {
|
|
|
74
93
|
switch (name) {
|
|
75
94
|
case 'branch':
|
|
76
95
|
case 'branch-name':
|
|
77
|
-
|
|
78
|
-
sanitizedBranch ??= branchName
|
|
79
|
-
.replace(/\//g, '-') // Replace forward slashes with dashes
|
|
80
|
-
.replace(/[^a-zA-Z0-9-_.]+/g, '') // Remove special characters except dash, dot, underscore
|
|
81
|
-
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
|
|
82
|
-
.toLowerCase(); // Convert to lowercase for consistency
|
|
96
|
+
sanitizedBranch ??= sanitizeNameForDirectory(branchName);
|
|
83
97
|
return sanitizedBranch;
|
|
84
98
|
case 'project':
|
|
85
99
|
projectName ??= getGitRepositoryName(projectPath);
|
|
@@ -91,6 +105,40 @@ export function generateWorktreeDirectory(projectPath, branchName, pattern) {
|
|
|
91
105
|
// Ensure the path is relative to the repository root
|
|
92
106
|
return path.normalize(directory);
|
|
93
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Returns the worktree directory basename to show alongside the branch name,
|
|
110
|
+
* or an empty string when it would be redundant (main worktree, detached,
|
|
111
|
+
* or the directory name matches the branch).
|
|
112
|
+
*
|
|
113
|
+
* The visible suffix is truncated; the raw basename is also returned so
|
|
114
|
+
* callers that want to make it searchable can include it untruncated.
|
|
115
|
+
*/
|
|
116
|
+
export function formatWorktreeDirectorySuffix(wt, fullBranchName) {
|
|
117
|
+
if (wt.isMainWorktree)
|
|
118
|
+
return { displaySuffix: '', rawName: '' };
|
|
119
|
+
if (!wt.branch)
|
|
120
|
+
return { displaySuffix: '', rawName: '' };
|
|
121
|
+
if (!wt.path)
|
|
122
|
+
return { displaySuffix: '', rawName: '' };
|
|
123
|
+
const dirName = path.basename(wt.path);
|
|
124
|
+
if (!dirName)
|
|
125
|
+
return { displaySuffix: '', rawName: '' };
|
|
126
|
+
const normalizedDir = sanitizeNameForDirectory(dirName);
|
|
127
|
+
if (!normalizedDir)
|
|
128
|
+
return { displaySuffix: '', rawName: '' };
|
|
129
|
+
const normalizedBranch = sanitizeNameForDirectory(fullBranchName);
|
|
130
|
+
if (normalizedDir === normalizedBranch) {
|
|
131
|
+
return { displaySuffix: '', rawName: '' };
|
|
132
|
+
}
|
|
133
|
+
// Also suppress when the directory matches just the tail segment of the
|
|
134
|
+
// branch (e.g. branch "feature/foo" vs directory "foo").
|
|
135
|
+
const tail = extractBranchParts(fullBranchName).name;
|
|
136
|
+
if (tail && sanitizeNameForDirectory(tail) === normalizedDir) {
|
|
137
|
+
return { displaySuffix: '', rawName: '' };
|
|
138
|
+
}
|
|
139
|
+
const visible = truncateString(dirName, MAX_WORKTREE_DIR_NAME_LENGTH);
|
|
140
|
+
return { displaySuffix: ` @ ${visible}`, rawName: dirName };
|
|
141
|
+
}
|
|
94
142
|
export function extractBranchParts(branchName) {
|
|
95
143
|
const parts = branchName.split('/');
|
|
96
144
|
if (parts.length > 1) {
|
|
@@ -165,7 +213,12 @@ function buildSessionItem(wt, session, sessionSuffix) {
|
|
|
165
213
|
: 'detached';
|
|
166
214
|
const branchName = truncateString(fullBranchName, MAX_BRANCH_NAME_LENGTH);
|
|
167
215
|
const isMain = wt.isMainWorktree ? ' (main)' : '';
|
|
168
|
-
const
|
|
216
|
+
const { displaySuffix: dirSuffix, rawName: rawDirName } = formatWorktreeDirectorySuffix(wt, fullBranchName);
|
|
217
|
+
const baseLabel = `${branchName}${dirSuffix}${isMain}${sessionSuffix}${status}`;
|
|
218
|
+
// Use the full (untruncated) branch name so search still matches the tail
|
|
219
|
+
// of long branch names; status icons are excluded so they don't match.
|
|
220
|
+
const rawDirForSearch = rawDirName ? ` @ ${rawDirName}` : '';
|
|
221
|
+
const searchableName = `${fullBranchName}${rawDirForSearch}${isMain}${sessionSuffix}`;
|
|
169
222
|
const { fileChanges, aheadBehind, parentBranch, error } = gitStatusColumns(wt, fullBranchName);
|
|
170
223
|
const lastCommitDate = wt.lastCommitDate
|
|
171
224
|
? `\x1b[90m${formatRelativeDate(wt.lastCommitDate)}\x1b[0m`
|
|
@@ -174,6 +227,7 @@ function buildSessionItem(wt, session, sessionSuffix) {
|
|
|
174
227
|
worktree: wt,
|
|
175
228
|
session,
|
|
176
229
|
baseLabel,
|
|
230
|
+
searchableName,
|
|
177
231
|
fileChanges,
|
|
178
232
|
aheadBehind,
|
|
179
233
|
parentBranch,
|
|
@@ -113,8 +113,10 @@ describe('truncateString', () => {
|
|
|
113
113
|
});
|
|
114
114
|
});
|
|
115
115
|
describe('prepareSessionItems', () => {
|
|
116
|
+
// The directory basename here matches the sanitized branch tail so the
|
|
117
|
+
// worktree-directory suffix is suppressed in the basic baseLabel tests.
|
|
116
118
|
const mockWorktree = {
|
|
117
|
-
path: '/path/to/
|
|
119
|
+
path: '/path/to/test-branch',
|
|
118
120
|
branch: 'feature/test-branch',
|
|
119
121
|
isMainWorktree: false,
|
|
120
122
|
hasSession: false,
|
|
@@ -122,7 +124,7 @@ describe('prepareSessionItems', () => {
|
|
|
122
124
|
// Simplified mock
|
|
123
125
|
const mockSession = {
|
|
124
126
|
id: 'test-session',
|
|
125
|
-
worktreePath: '/path/to/
|
|
127
|
+
worktreePath: '/path/to/test-branch',
|
|
126
128
|
sessionNumber: 1,
|
|
127
129
|
command: 'claude',
|
|
128
130
|
fallbackArgs: undefined,
|
|
@@ -167,12 +169,115 @@ describe('prepareSessionItems', () => {
|
|
|
167
169
|
const items = prepareSessionItems([longBranch], []);
|
|
168
170
|
expect(items[0]?.baseLabel.length).toBeLessThanOrEqual(80); // 70 + status + default
|
|
169
171
|
});
|
|
172
|
+
describe('worktree directory suffix', () => {
|
|
173
|
+
it('shows the directory name when it differs from the branch', () => {
|
|
174
|
+
const wt = {
|
|
175
|
+
path: '/repos/myproj/worktrees/login-api',
|
|
176
|
+
branch: 'feature/login',
|
|
177
|
+
isMainWorktree: false,
|
|
178
|
+
hasSession: false,
|
|
179
|
+
};
|
|
180
|
+
const items = prepareSessionItems([wt], []);
|
|
181
|
+
expect(items[0]?.baseLabel).toBe('feature/login @ login-api');
|
|
182
|
+
});
|
|
183
|
+
it('hides the directory name when the directory equals the branch tail', () => {
|
|
184
|
+
const wt = {
|
|
185
|
+
path: '/repos/myproj/worktrees/foo',
|
|
186
|
+
branch: 'feature/foo',
|
|
187
|
+
isMainWorktree: false,
|
|
188
|
+
hasSession: false,
|
|
189
|
+
};
|
|
190
|
+
const items = prepareSessionItems([wt], []);
|
|
191
|
+
expect(items[0]?.baseLabel).toBe('feature/foo');
|
|
192
|
+
});
|
|
193
|
+
it('hides the directory name when it matches the sanitized full branch', () => {
|
|
194
|
+
const wt = {
|
|
195
|
+
path: '/repos/myproj/worktrees/feature-foo',
|
|
196
|
+
branch: 'feature/foo',
|
|
197
|
+
isMainWorktree: false,
|
|
198
|
+
hasSession: false,
|
|
199
|
+
};
|
|
200
|
+
const items = prepareSessionItems([wt], []);
|
|
201
|
+
expect(items[0]?.baseLabel).toBe('feature/foo');
|
|
202
|
+
});
|
|
203
|
+
it('hides the directory name for the main worktree', () => {
|
|
204
|
+
const wt = {
|
|
205
|
+
path: '/repos/myproj',
|
|
206
|
+
branch: 'main',
|
|
207
|
+
isMainWorktree: true,
|
|
208
|
+
hasSession: false,
|
|
209
|
+
};
|
|
210
|
+
const items = prepareSessionItems([wt], []);
|
|
211
|
+
expect(items[0]?.baseLabel).toBe('main (main)');
|
|
212
|
+
});
|
|
213
|
+
it('truncates long directory names in the displayed label', () => {
|
|
214
|
+
const longDir = '/repos/myproj/worktrees/this-is-a-very-long-directory-name-that-should-be-truncated';
|
|
215
|
+
const wt = {
|
|
216
|
+
path: longDir,
|
|
217
|
+
branch: 'feature/short',
|
|
218
|
+
isMainWorktree: false,
|
|
219
|
+
hasSession: false,
|
|
220
|
+
};
|
|
221
|
+
const items = prepareSessionItems([wt], []);
|
|
222
|
+
// "feature/short @ " (16 chars) + up to MAX_WORKTREE_DIR_NAME_LENGTH (30)
|
|
223
|
+
const after = items[0]?.baseLabel.split(' @ ')[1] ?? '';
|
|
224
|
+
expect(after.length).toBeLessThanOrEqual(30);
|
|
225
|
+
expect(after.endsWith('...')).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
it('keeps the untruncated directory basename in searchableName', () => {
|
|
228
|
+
const longDir = '/repos/myproj/worktrees/this-is-a-very-long-directory-name-that-should-be-truncated';
|
|
229
|
+
const wt = {
|
|
230
|
+
path: longDir,
|
|
231
|
+
branch: 'feature/short',
|
|
232
|
+
isMainWorktree: false,
|
|
233
|
+
hasSession: false,
|
|
234
|
+
};
|
|
235
|
+
const items = prepareSessionItems([wt], []);
|
|
236
|
+
expect(items[0]?.searchableName).toContain('this-is-a-very-long-directory-name-that-should-be-truncated');
|
|
237
|
+
});
|
|
238
|
+
it('places the directory suffix before session and status markers', () => {
|
|
239
|
+
const wt = {
|
|
240
|
+
path: '/repos/myproj/worktrees/foo-api',
|
|
241
|
+
branch: 'feature/foo',
|
|
242
|
+
isMainWorktree: false,
|
|
243
|
+
hasSession: false,
|
|
244
|
+
};
|
|
245
|
+
const items = prepareSessionItems([wt], [
|
|
246
|
+
{
|
|
247
|
+
...mockSession,
|
|
248
|
+
worktreePath: '/repos/myproj/worktrees/foo-api',
|
|
249
|
+
sessionName: 'lab',
|
|
250
|
+
},
|
|
251
|
+
]);
|
|
252
|
+
// Order must be: branch, dir suffix, (no main), session suffix, status.
|
|
253
|
+
expect(items[0]?.baseLabel).toMatch(/^feature\/foo @ foo-api: lab \[.*Idle.*\]$/);
|
|
254
|
+
});
|
|
255
|
+
it('does not break column alignment when a dir suffix is appended', () => {
|
|
256
|
+
const items = prepareSessionItems([
|
|
257
|
+
{
|
|
258
|
+
path: '/repos/myproj/worktrees/foo-api',
|
|
259
|
+
branch: 'feature/foo',
|
|
260
|
+
isMainWorktree: false,
|
|
261
|
+
hasSession: false,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
path: '/repos/myproj',
|
|
265
|
+
branch: 'main',
|
|
266
|
+
isMainWorktree: true,
|
|
267
|
+
hasSession: false,
|
|
268
|
+
},
|
|
269
|
+
], []);
|
|
270
|
+
expect(items[0]?.lengths.base).toBe(items[0]?.baseLabel.length);
|
|
271
|
+
expect(items[1]?.lengths.base).toBe(items[1]?.baseLabel.length);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
170
274
|
});
|
|
171
275
|
describe('column alignment', () => {
|
|
172
276
|
const mockItems = [
|
|
173
277
|
{
|
|
174
278
|
worktree: {},
|
|
175
279
|
baseLabel: 'feature/test-branch',
|
|
280
|
+
searchableName: 'feature/test-branch',
|
|
176
281
|
fileChanges: '\x1b[32m+10\x1b[0m \x1b[31m-5\x1b[0m',
|
|
177
282
|
aheadBehind: '\x1b[33m↑2 ↓3\x1b[0m',
|
|
178
283
|
parentBranch: '',
|
|
@@ -188,6 +293,7 @@ describe('column alignment', () => {
|
|
|
188
293
|
{
|
|
189
294
|
worktree: {},
|
|
190
295
|
baseLabel: 'main',
|
|
296
|
+
searchableName: 'main',
|
|
191
297
|
fileChanges: '\x1b[32m+2\x1b[0m \x1b[31m-1\x1b[0m',
|
|
192
298
|
aheadBehind: '\x1b[33m↑1\x1b[0m',
|
|
193
299
|
parentBranch: '',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccmanager",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.22",
|
|
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.
|
|
45
|
-
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.
|
|
46
|
-
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.
|
|
47
|
-
"@kodaikabasawa/ccmanager-linux-x64": "4.1.
|
|
48
|
-
"@kodaikabasawa/ccmanager-win32-x64": "4.1.
|
|
44
|
+
"@kodaikabasawa/ccmanager-darwin-arm64": "4.1.22",
|
|
45
|
+
"@kodaikabasawa/ccmanager-darwin-x64": "4.1.22",
|
|
46
|
+
"@kodaikabasawa/ccmanager-linux-arm64": "4.1.22",
|
|
47
|
+
"@kodaikabasawa/ccmanager-linux-x64": "4.1.22",
|
|
48
|
+
"@kodaikabasawa/ccmanager-win32-x64": "4.1.22"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@eslint/js": "^9.28.0",
|