ccmanager 4.1.19 → 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.
- package/dist/components/App.js +23 -42
- package/dist/components/Dashboard.js +1 -0
- package/dist/components/Menu.js +4 -5
- package/dist/services/worktreeService.d.ts +3 -2
- package/dist/services/worktreeService.js +20 -2
- package/dist/services/worktreeService.test.js +115 -0
- package/dist/types/index.d.ts +7 -5
- package/dist/types/index.js +1 -0
- 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 +1 -0
- package/dist/utils/worktreeUtils.js +4 -0
- package/dist/utils/worktreeUtils.test.js +2 -0
- package/package.json +6 -6
package/dist/components/App.js
CHANGED
|
@@ -18,7 +18,6 @@ import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator
|
|
|
18
18
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
19
19
|
import { worktreeNameGenerator, generateFallbackBranchName, } from '../services/worktreeNameGenerator.js';
|
|
20
20
|
import { logger } from '../utils/logger.js';
|
|
21
|
-
import { AmbiguousBranchError, } from '../types/index.js';
|
|
22
21
|
import { configReader } from '../services/config/configReader.js';
|
|
23
22
|
import { ENV_VARS } from '../constants/env.js';
|
|
24
23
|
import { MULTI_PROJECT_ERRORS } from '../constants/error.js';
|
|
@@ -217,29 +216,6 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
217
216
|
cancelled = true;
|
|
218
217
|
};
|
|
219
218
|
}, [view, pendingMenuSessionLaunch, startSessionForWorktree]);
|
|
220
|
-
// Helper function to parse ambiguous branch error and create AmbiguousBranchError
|
|
221
|
-
const parseAmbiguousBranchError = (errorMessage) => {
|
|
222
|
-
const pattern = /Ambiguous branch '(.+?)' found in multiple remotes: (.+?)\. Please specify which remote to use\./;
|
|
223
|
-
const match = errorMessage.match(pattern);
|
|
224
|
-
if (!match) {
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const branchName = match[1];
|
|
228
|
-
const remoteRefsText = match[2];
|
|
229
|
-
const remoteRefs = remoteRefsText.split(', ');
|
|
230
|
-
// Parse remote refs into RemoteBranchMatch objects
|
|
231
|
-
const matches = remoteRefs.map(fullRef => {
|
|
232
|
-
const parts = fullRef.split('/');
|
|
233
|
-
const remote = parts[0];
|
|
234
|
-
const branch = parts.slice(1).join('/');
|
|
235
|
-
return {
|
|
236
|
-
remote,
|
|
237
|
-
branch,
|
|
238
|
-
fullRef,
|
|
239
|
-
};
|
|
240
|
-
});
|
|
241
|
-
return new AmbiguousBranchError(branchName, matches);
|
|
242
|
-
};
|
|
243
219
|
// Helper function to handle worktree creation results
|
|
244
220
|
const handleWorktreeCreationResult = (result, creationData) => {
|
|
245
221
|
if (result.success) {
|
|
@@ -266,21 +242,8 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
266
242
|
handleReturnToMenu();
|
|
267
243
|
return;
|
|
268
244
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (ambiguousError) {
|
|
272
|
-
// Handle ambiguous branch error
|
|
273
|
-
setPendingWorktreeCreation({
|
|
274
|
-
...creationData,
|
|
275
|
-
ambiguousError,
|
|
276
|
-
});
|
|
277
|
-
navigateWithClear('remote-branch-selector');
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
// Handle regular error
|
|
281
|
-
setError(errorMessage);
|
|
282
|
-
setView('new-worktree');
|
|
283
|
-
}
|
|
245
|
+
setError(result.error || 'Failed to create worktree');
|
|
246
|
+
setView('new-worktree');
|
|
284
247
|
};
|
|
285
248
|
const handleMenuAction = async (action) => {
|
|
286
249
|
switch (action.type) {
|
|
@@ -409,9 +372,23 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
409
372
|
setView('creating-worktree');
|
|
410
373
|
// Create the worktree using Effect
|
|
411
374
|
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(targetPath, branch, request.baseBranch, request.copySessionData, request.copyClaudeDirectory)));
|
|
412
|
-
// Transform Effect result to legacy format for handleWorktreeCreationResult
|
|
413
375
|
if (result._tag === 'Left') {
|
|
414
|
-
|
|
376
|
+
if (result.left._tag === 'AmbiguousBranchError') {
|
|
377
|
+
setPendingWorktreeCreation({
|
|
378
|
+
path: targetPath,
|
|
379
|
+
branch,
|
|
380
|
+
baseBranch: request.baseBranch,
|
|
381
|
+
copySessionData: request.copySessionData,
|
|
382
|
+
copyClaudeDirectory: request.copyClaudeDirectory,
|
|
383
|
+
presetId: request.creationMode === 'prompt' ? request.presetId : undefined,
|
|
384
|
+
initialPrompt: request.creationMode === 'prompt'
|
|
385
|
+
? request.initialPrompt
|
|
386
|
+
: undefined,
|
|
387
|
+
ambiguousError: result.left,
|
|
388
|
+
});
|
|
389
|
+
navigateWithClear('remote-branch-selector');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
415
392
|
const errorMessage = formatPreCreationHookError(result.left);
|
|
416
393
|
if (result.left._tag === 'ProcessError') {
|
|
417
394
|
setError(null);
|
|
@@ -473,7 +450,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
473
450
|
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(creationData.path, creationData.branch, selectedRemoteRef, // Use the selected remote reference
|
|
474
451
|
creationData.copySessionData, creationData.copyClaudeDirectory)));
|
|
475
452
|
if (result._tag === 'Left') {
|
|
476
|
-
|
|
453
|
+
if (result.left._tag === 'AmbiguousBranchError') {
|
|
454
|
+
setError(result.left.message);
|
|
455
|
+
setView('new-worktree');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
477
458
|
const errorMessage = formatPreCreationHookError(result.left);
|
|
478
459
|
if (result.left._tag === 'ProcessError') {
|
|
479
460
|
setError(null);
|
|
@@ -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,5 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
|
-
import { Worktree, CreateWorktreeResult, MergeConfig } from '../types/index.js';
|
|
2
|
+
import { Worktree, CreateWorktreeResult, AmbiguousBranchError, MergeConfig } from '../types/index.js';
|
|
3
3
|
import { GitError, FileSystemError, ProcessError } from '../types/errors.js';
|
|
4
4
|
/**
|
|
5
5
|
* WorktreeService - Git worktree management with Effect-based error handling
|
|
@@ -53,6 +53,7 @@ export declare class WorktreeService {
|
|
|
53
53
|
* @throws {AmbiguousBranchError} When branch exists in multiple remotes
|
|
54
54
|
*/
|
|
55
55
|
private resolveBranchReference;
|
|
56
|
+
private resolveBranchReferenceEffect;
|
|
56
57
|
/**
|
|
57
58
|
* SYNCHRONOUS HELPER: Gets all git remotes for this repository.
|
|
58
59
|
*
|
|
@@ -323,7 +324,7 @@ export declare class WorktreeService {
|
|
|
323
324
|
* @throws {GitError} When git worktree add command fails
|
|
324
325
|
* @throws {FileSystemError} When session data copy fails
|
|
325
326
|
*/
|
|
326
|
-
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Effect.Effect<CreateWorktreeResult, GitError | FileSystemError | ProcessError, never>;
|
|
327
|
+
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Effect.Effect<CreateWorktreeResult, GitError | FileSystemError | ProcessError | AmbiguousBranchError, never>;
|
|
327
328
|
/**
|
|
328
329
|
* Effect-based deleteWorktree operation
|
|
329
330
|
* May fail with GitError
|
|
@@ -162,6 +162,18 @@ export class WorktreeService {
|
|
|
162
162
|
return branchName;
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
+
resolveBranchReferenceEffect(branchName) {
|
|
166
|
+
return Effect.try({
|
|
167
|
+
try: () => this.resolveBranchReference(branchName),
|
|
168
|
+
catch: error => {
|
|
169
|
+
if (error instanceof AmbiguousBranchError)
|
|
170
|
+
return error;
|
|
171
|
+
// resolveBranchReference only re-throws AmbiguousBranchError; all
|
|
172
|
+
// other errors are swallowed internally. This path is unreachable.
|
|
173
|
+
throw error;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
}
|
|
165
177
|
/**
|
|
166
178
|
* SYNCHRONOUS HELPER: Gets all git remotes for this repository.
|
|
167
179
|
*
|
|
@@ -812,12 +824,18 @@ export class WorktreeService {
|
|
|
812
824
|
if (localBranchExists) {
|
|
813
825
|
command = `git worktree add "${resolvedPath}" "${branch}"`;
|
|
814
826
|
}
|
|
827
|
+
else if (baseBranch.endsWith(`/${branch}`)) {
|
|
828
|
+
// baseBranch is already a remote-tracking ref for branch
|
|
829
|
+
// (e.g. "origin/feature/x" after the user resolved an ambiguity).
|
|
830
|
+
// Use it directly to avoid re-triggering AmbiguousBranchError.
|
|
831
|
+
command = `git worktree add -b "${branch}" "${resolvedPath}" "${baseBranch}"`;
|
|
832
|
+
}
|
|
815
833
|
else {
|
|
816
|
-
const resolvedRef = self.
|
|
834
|
+
const resolvedRef = yield* self.resolveBranchReferenceEffect(branch);
|
|
817
835
|
const isRemoteBranch = resolvedRef !== branch;
|
|
818
836
|
const startPoint = isRemoteBranch
|
|
819
837
|
? resolvedRef
|
|
820
|
-
: self.
|
|
838
|
+
: yield* self.resolveBranchReferenceEffect(baseBranch);
|
|
821
839
|
command = `git worktree add -b "${branch}" "${resolvedPath}" "${startPoint}"`;
|
|
822
840
|
}
|
|
823
841
|
// Execute the worktree creation command
|
|
@@ -5,6 +5,7 @@ import { existsSync, statSync } from 'fs';
|
|
|
5
5
|
import { configReader } from './config/configReader.js';
|
|
6
6
|
import { Effect } from 'effect';
|
|
7
7
|
import { GitError, ProcessError } from '../types/errors.js';
|
|
8
|
+
import { AmbiguousBranchError } from '../types/index.js';
|
|
8
9
|
// Mock child_process module
|
|
9
10
|
vi.mock('child_process');
|
|
10
11
|
// Mock fs module
|
|
@@ -369,6 +370,61 @@ origin/feature/test
|
|
|
369
370
|
expect(result).toBe('foo/bar-xyz');
|
|
370
371
|
});
|
|
371
372
|
});
|
|
373
|
+
describe('resolveBranchReferenceEffect', () => {
|
|
374
|
+
it('should succeed with remote ref when single remote has the branch', async () => {
|
|
375
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
376
|
+
if (typeof cmd === 'string') {
|
|
377
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/foo/bar-xyz')) {
|
|
378
|
+
throw new Error('Local branch not found');
|
|
379
|
+
}
|
|
380
|
+
if (cmd === 'git remote') {
|
|
381
|
+
return 'origin\nupstream\n';
|
|
382
|
+
}
|
|
383
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/foo/bar-xyz')) {
|
|
384
|
+
return '';
|
|
385
|
+
}
|
|
386
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/upstream/foo/bar-xyz')) {
|
|
387
|
+
throw new Error('Remote branch not found in upstream');
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
391
|
+
});
|
|
392
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
393
|
+
const effect = service.resolveBranchReferenceEffect('foo/bar-xyz');
|
|
394
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
395
|
+
expect(result._tag).toBe('Right');
|
|
396
|
+
if (result._tag === 'Right') {
|
|
397
|
+
expect(result.right).toBe('origin/foo/bar-xyz');
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
it('should fail with AmbiguousBranchError (not Die) when multiple remotes have the branch', async () => {
|
|
401
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
402
|
+
if (typeof cmd === 'string') {
|
|
403
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/foo/bar-xyz')) {
|
|
404
|
+
throw new Error('Local branch not found');
|
|
405
|
+
}
|
|
406
|
+
if (cmd === 'git remote') {
|
|
407
|
+
return 'origin\nupstream\n';
|
|
408
|
+
}
|
|
409
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/origin/foo/bar-xyz') ||
|
|
410
|
+
cmd.includes('show-ref --verify --quiet refs/remotes/upstream/foo/bar-xyz')) {
|
|
411
|
+
return '';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
415
|
+
});
|
|
416
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
417
|
+
const effect = service.resolveBranchReferenceEffect('foo/bar-xyz');
|
|
418
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
419
|
+
expect(result._tag).toBe('Left');
|
|
420
|
+
if (result._tag === 'Left') {
|
|
421
|
+
expect(result.left).toBeInstanceOf(AmbiguousBranchError);
|
|
422
|
+
expect(result.left._tag).toBe('AmbiguousBranchError');
|
|
423
|
+
expect(result.left.branchName).toBe('foo/bar-xyz');
|
|
424
|
+
expect(result.left.matches).toHaveLength(2);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
});
|
|
372
428
|
describe('hasClaudeDirectoryInBranchEffect', () => {
|
|
373
429
|
it('should return Effect with true when .claude directory exists in branch worktree', async () => {
|
|
374
430
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
@@ -695,6 +751,65 @@ branch refs/heads/feature
|
|
|
695
751
|
expect(worktreeAddCmd).toContain('-b "feature/remote-only"');
|
|
696
752
|
expect(worktreeAddCmd).toContain('"origin/feature/remote-only"');
|
|
697
753
|
});
|
|
754
|
+
it('should return Effect Left with AmbiguousBranchError when branch exists in multiple remotes', async () => {
|
|
755
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
756
|
+
if (typeof cmd === 'string') {
|
|
757
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
758
|
+
return '/fake/path/.git\n';
|
|
759
|
+
}
|
|
760
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
761
|
+
throw new Error('Branch not found');
|
|
762
|
+
}
|
|
763
|
+
if (cmd === 'git remote') {
|
|
764
|
+
return 'origin\nkbwo-fork\n';
|
|
765
|
+
}
|
|
766
|
+
if (cmd.includes('show-ref --verify --quiet refs/remotes/')) {
|
|
767
|
+
return ''; // Both remotes have the branch
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
throw new Error('Command not mocked: ' + cmd);
|
|
771
|
+
});
|
|
772
|
+
const effect = service.createWorktreeEffect('/path/to/worktree', 'feature/feed-mention', 'main');
|
|
773
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
774
|
+
expect(result._tag).toBe('Left');
|
|
775
|
+
if (result._tag === 'Left') {
|
|
776
|
+
expect(result.left).toBeInstanceOf(AmbiguousBranchError);
|
|
777
|
+
expect(result.left._tag).toBe('AmbiguousBranchError');
|
|
778
|
+
expect(result.left.branchName).toBe('feature/feed-mention');
|
|
779
|
+
expect(result.left.matches).toHaveLength(2);
|
|
780
|
+
expect(result.left.matches.map(m => m.remote)).toEqual(['origin', 'kbwo-fork']);
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
it('should succeed when baseBranch is already a resolved remote ref for branch (retry after disambiguation)', async () => {
|
|
784
|
+
const executedCommands = [];
|
|
785
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
786
|
+
if (typeof cmd === 'string') {
|
|
787
|
+
executedCommands.push(cmd);
|
|
788
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
789
|
+
return '/fake/path/.git\n';
|
|
790
|
+
}
|
|
791
|
+
// No local branch
|
|
792
|
+
if (cmd.includes('show-ref --verify --quiet refs/heads/')) {
|
|
793
|
+
throw new Error('Branch not found');
|
|
794
|
+
}
|
|
795
|
+
if (cmd.includes('git worktree add')) {
|
|
796
|
+
return '';
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return '';
|
|
800
|
+
});
|
|
801
|
+
// Simulate the retry call: user selected "origin/feature/feed-mention",
|
|
802
|
+
// which is passed as baseBranch.
|
|
803
|
+
const effect = service.createWorktreeEffect('/path/to/worktree', 'feature/feed-mention', 'origin/feature/feed-mention');
|
|
804
|
+
const result = await Effect.runPromise(Effect.either(effect));
|
|
805
|
+
expect(result._tag).toBe('Right');
|
|
806
|
+
// Should use baseBranch directly as startPoint without calling show-ref
|
|
807
|
+
const worktreeAddCmd = executedCommands.find(c => c.includes('git worktree add'));
|
|
808
|
+
expect(worktreeAddCmd).toContain('-b "feature/feed-mention"');
|
|
809
|
+
expect(worktreeAddCmd).toContain('"origin/feature/feed-mention"');
|
|
810
|
+
// show-ref for remotes must NOT be called (no re-resolution)
|
|
811
|
+
expect(executedCommands.some(c => c.includes('show-ref') && c.includes('refs/remotes/'))).toBe(false);
|
|
812
|
+
});
|
|
698
813
|
it('should return Effect that fails with GitError on git command failure', async () => {
|
|
699
814
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
700
815
|
if (typeof cmd === 'string') {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import type { SerializeAddon } from '@xterm/addon-serialize';
|
|
|
4
4
|
import { GitStatus } from '../utils/gitStatus.js';
|
|
5
5
|
import { Mutex, SessionStateData } from '../utils/mutex.js';
|
|
6
6
|
import type { StateDetector } from '../services/stateDetector/types.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type { Effect } from 'effect';
|
|
8
|
+
import type { GitError, FileSystemError, ProcessError } from './errors.js';
|
|
8
9
|
export type Terminal = InstanceType<typeof pkg.Terminal>;
|
|
9
10
|
export type SessionState = 'idle' | 'busy' | 'waiting_input' | 'pending_auto_approval';
|
|
10
11
|
export type StateDetectionStrategy = 'claude' | 'gemini' | 'codex' | 'cursor' | 'github-copilot' | 'cline' | 'opencode' | 'kimi';
|
|
@@ -249,16 +250,17 @@ export interface RemoteBranchMatch {
|
|
|
249
250
|
fullRef: string;
|
|
250
251
|
}
|
|
251
252
|
export declare class AmbiguousBranchError extends Error {
|
|
253
|
+
readonly _tag: "AmbiguousBranchError";
|
|
252
254
|
branchName: string;
|
|
253
255
|
matches: RemoteBranchMatch[];
|
|
254
256
|
constructor(branchName: string, matches: RemoteBranchMatch[]);
|
|
255
257
|
}
|
|
256
258
|
export interface IWorktreeService {
|
|
257
|
-
getWorktreesEffect():
|
|
259
|
+
getWorktreesEffect(): Effect.Effect<Worktree[], GitError, never>;
|
|
258
260
|
getGitRootPath(): string;
|
|
259
|
-
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean):
|
|
261
|
+
createWorktreeEffect(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Effect.Effect<CreateWorktreeResult, GitError | FileSystemError | ProcessError | AmbiguousBranchError, never>;
|
|
260
262
|
deleteWorktreeEffect(worktreePath: string, options?: {
|
|
261
263
|
deleteBranch?: boolean;
|
|
262
|
-
}):
|
|
263
|
-
mergeWorktreeEffect(sourceBranch: string, targetBranch: string, operation?: 'merge' | 'rebase', mergeConfig?: MergeConfig):
|
|
264
|
+
}): Effect.Effect<void, GitError, never>;
|
|
265
|
+
mergeWorktreeEffect(sourceBranch: string, targetBranch: string, operation?: 'merge' | 'rebase', mergeConfig?: MergeConfig): Effect.Effect<void, GitError, never>;
|
|
264
266
|
}
|
package/dist/types/index.js
CHANGED
|
@@ -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
|
+
});
|
|
@@ -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.
|
|
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.
|
|
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.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",
|