ccmanager 3.10.0 → 3.11.1
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 +154 -45
- package/dist/components/App.test.js +92 -3
- package/dist/components/Menu.recent-projects.test.js +19 -19
- package/dist/components/NewWorktree.d.ts +20 -1
- package/dist/components/NewWorktree.js +103 -56
- package/dist/components/NewWorktree.test.js +17 -4
- package/dist/components/TextInputWrapper.d.ts +0 -3
- package/dist/components/TextInputWrapper.js +120 -11
- package/dist/services/sessionManager.d.ts +3 -2
- package/dist/services/sessionManager.js +37 -40
- package/dist/services/sessionManager.test.js +26 -0
- package/dist/services/worktreeNameGenerator.d.ts +8 -0
- package/dist/services/worktreeNameGenerator.js +192 -0
- package/dist/services/worktreeNameGenerator.test.d.ts +1 -0
- package/dist/services/worktreeNameGenerator.test.js +35 -0
- package/dist/utils/presetPrompt.d.ts +11 -0
- package/dist/utils/presetPrompt.js +73 -0
- package/dist/utils/presetPrompt.test.d.ts +1 -0
- package/dist/utils/presetPrompt.test.js +155 -0
- package/package.json +6 -6
package/dist/components/App.js
CHANGED
|
@@ -14,11 +14,13 @@ import RemoteBranchSelector from './RemoteBranchSelector.js';
|
|
|
14
14
|
import LoadingSpinner from './LoadingSpinner.js';
|
|
15
15
|
import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
|
|
16
16
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
17
|
+
import { worktreeNameGenerator } from '../services/worktreeNameGenerator.js';
|
|
17
18
|
import { AmbiguousBranchError, } from '../types/index.js';
|
|
18
19
|
import { configReader } from '../services/config/configReader.js';
|
|
19
20
|
import { ENV_VARS } from '../constants/env.js';
|
|
20
21
|
import { MULTI_PROJECT_ERRORS } from '../constants/error.js';
|
|
21
22
|
import { projectManager } from '../services/projectManager.js';
|
|
23
|
+
import { generateWorktreeDirectory } from '../utils/worktreeUtils.js';
|
|
22
24
|
const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
23
25
|
const { exit } = useApp();
|
|
24
26
|
const [view, setView] = useState(multiProject ? 'project-list' : 'menu');
|
|
@@ -30,6 +32,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
30
32
|
const [selectedWorktree, setSelectedWorktree] = useState(null); // Store selected worktree for preset selection
|
|
31
33
|
const [selectedProject, setSelectedProject] = useState(null); // Store selected project in multi-project mode
|
|
32
34
|
const [configScope, setConfigScope] = useState('global'); // Store config scope for configuration view
|
|
35
|
+
const [pendingMenuSessionLaunch, setPendingMenuSessionLaunch] = useState(null);
|
|
33
36
|
// State for remote branch disambiguation
|
|
34
37
|
const [pendingWorktreeCreation, setPendingWorktreeCreation] = useState(null);
|
|
35
38
|
// State for loading context - track flags for message composition
|
|
@@ -50,26 +53,24 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
50
53
|
}
|
|
51
54
|
};
|
|
52
55
|
// Helper function to create session with Effect-based error handling
|
|
53
|
-
const createSessionWithEffect = async (worktreePath, presetId) => {
|
|
56
|
+
const createSessionWithEffect = useCallback(async (worktreePath, presetId, initialPrompt) => {
|
|
54
57
|
const sessionEffect = devcontainerConfig
|
|
55
|
-
? sessionManager.createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId)
|
|
56
|
-
: sessionManager.createSessionWithPresetEffect(worktreePath, presetId);
|
|
58
|
+
? sessionManager.createSessionWithDevcontainerEffect(worktreePath, devcontainerConfig, presetId, initialPrompt)
|
|
59
|
+
: sessionManager.createSessionWithPresetEffect(worktreePath, presetId, initialPrompt);
|
|
57
60
|
// Execute the Effect and handle both success and failure cases
|
|
58
61
|
const result = await Effect.runPromise(Effect.either(sessionEffect));
|
|
59
62
|
if (result._tag === 'Left') {
|
|
60
|
-
// Handle error using pattern matching on _tag
|
|
61
63
|
const errorMessage = formatErrorMessage(result.left);
|
|
62
64
|
return {
|
|
63
65
|
success: false,
|
|
64
66
|
errorMessage: `Failed to create session: ${errorMessage}`,
|
|
65
67
|
};
|
|
66
68
|
}
|
|
67
|
-
// Success case - extract session from Right
|
|
68
69
|
return {
|
|
69
70
|
success: true,
|
|
70
71
|
session: result.right,
|
|
71
72
|
};
|
|
72
|
-
};
|
|
73
|
+
}, [sessionManager, devcontainerConfig]);
|
|
73
74
|
// Helper function to clear terminal screen
|
|
74
75
|
const clearScreen = () => {
|
|
75
76
|
if (process.stdout.isTTY) {
|
|
@@ -86,6 +87,38 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
86
87
|
callback();
|
|
87
88
|
}, 10); // Small delay to ensure screen clear is processed
|
|
88
89
|
}, []);
|
|
90
|
+
const navigateToSession = useCallback((session) => {
|
|
91
|
+
clearScreen();
|
|
92
|
+
setView('clearing');
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
setActiveSession(session);
|
|
95
|
+
setView('session');
|
|
96
|
+
}, 10);
|
|
97
|
+
}, []);
|
|
98
|
+
const startSessionForWorktree = useCallback(async (worktree, options) => {
|
|
99
|
+
let session = sessionManager.getSession(worktree.path);
|
|
100
|
+
if (!session) {
|
|
101
|
+
if (!options?.presetId && configReader.getSelectPresetOnStart()) {
|
|
102
|
+
setSelectedWorktree(worktree);
|
|
103
|
+
navigateWithClear('preset-selector');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
setView(options?.presetId ? 'creating-session-preset' : 'creating-session');
|
|
107
|
+
const result = await createSessionWithEffect(worktree.path, options?.presetId, options?.initialPrompt);
|
|
108
|
+
if (!result.success) {
|
|
109
|
+
setError(result.errorMessage);
|
|
110
|
+
navigateWithClear('menu');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
session = result.session;
|
|
114
|
+
}
|
|
115
|
+
navigateToSession(session);
|
|
116
|
+
}, [
|
|
117
|
+
sessionManager,
|
|
118
|
+
navigateWithClear,
|
|
119
|
+
navigateToSession,
|
|
120
|
+
createSessionWithEffect,
|
|
121
|
+
]);
|
|
89
122
|
useEffect(() => {
|
|
90
123
|
// Listen for session exits to return to menu automatically
|
|
91
124
|
const handleSessionExit = (session) => {
|
|
@@ -115,6 +148,26 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
115
148
|
// Don't destroy sessions on unmount - they persist in memory
|
|
116
149
|
};
|
|
117
150
|
}, [sessionManager, multiProject, selectedProject, navigateWithClear]);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (view !== 'menu' || !pendingMenuSessionLaunch) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let cancelled = false;
|
|
156
|
+
void (async () => {
|
|
157
|
+
if (cancelled) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const launchRequest = pendingMenuSessionLaunch;
|
|
161
|
+
setPendingMenuSessionLaunch(null);
|
|
162
|
+
await startSessionForWorktree(launchRequest.worktree, {
|
|
163
|
+
presetId: launchRequest.presetId,
|
|
164
|
+
initialPrompt: launchRequest.initialPrompt,
|
|
165
|
+
});
|
|
166
|
+
})();
|
|
167
|
+
return () => {
|
|
168
|
+
cancelled = true;
|
|
169
|
+
};
|
|
170
|
+
}, [view, pendingMenuSessionLaunch, startSessionForWorktree]);
|
|
118
171
|
// Helper function to parse ambiguous branch error and create AmbiguousBranchError
|
|
119
172
|
const parseAmbiguousBranchError = (errorMessage) => {
|
|
120
173
|
const pattern = /Ambiguous branch '(.+?)' found in multiple remotes: (.+?)\. Please specify which remote to use\./;
|
|
@@ -141,6 +194,20 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
141
194
|
// Helper function to handle worktree creation results
|
|
142
195
|
const handleWorktreeCreationResult = (result, creationData) => {
|
|
143
196
|
if (result.success) {
|
|
197
|
+
if (creationData.presetId && creationData.initialPrompt) {
|
|
198
|
+
setPendingMenuSessionLaunch({
|
|
199
|
+
worktree: {
|
|
200
|
+
path: creationData.path,
|
|
201
|
+
branch: creationData.branch,
|
|
202
|
+
isMainWorktree: false,
|
|
203
|
+
hasSession: false,
|
|
204
|
+
},
|
|
205
|
+
presetId: creationData.presetId,
|
|
206
|
+
initialPrompt: creationData.initialPrompt,
|
|
207
|
+
});
|
|
208
|
+
handleReturnToMenu();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
144
211
|
handleReturnToMenu();
|
|
145
212
|
return;
|
|
146
213
|
}
|
|
@@ -207,28 +274,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
207
274
|
}
|
|
208
275
|
return;
|
|
209
276
|
}
|
|
210
|
-
|
|
211
|
-
let session = sessionManager.getSession(worktree.path);
|
|
212
|
-
if (!session) {
|
|
213
|
-
// Check if we should show preset selector
|
|
214
|
-
if (configReader.getSelectPresetOnStart()) {
|
|
215
|
-
setSelectedWorktree(worktree);
|
|
216
|
-
navigateWithClear('preset-selector');
|
|
217
|
-
return;
|
|
218
|
-
}
|
|
219
|
-
// Set loading state before async operation
|
|
220
|
-
setView('creating-session');
|
|
221
|
-
// Use Effect-based session creation with default preset
|
|
222
|
-
const result = await createSessionWithEffect(worktree.path);
|
|
223
|
-
if (!result.success) {
|
|
224
|
-
setError(result.errorMessage);
|
|
225
|
-
navigateWithClear('menu');
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
session = result.session;
|
|
229
|
-
}
|
|
230
|
-
setActiveSession(session);
|
|
231
|
-
navigateWithClear('session');
|
|
277
|
+
await startSessionForWorktree(worktree);
|
|
232
278
|
};
|
|
233
279
|
const handlePresetSelected = async (presetId) => {
|
|
234
280
|
if (!selectedWorktree)
|
|
@@ -244,8 +290,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
244
290
|
return;
|
|
245
291
|
}
|
|
246
292
|
// Success case
|
|
247
|
-
|
|
248
|
-
navigateWithClear('session');
|
|
293
|
+
navigateToSession(result.session);
|
|
249
294
|
setSelectedWorktree(null);
|
|
250
295
|
};
|
|
251
296
|
const handlePresetSelectorCancel = () => {
|
|
@@ -267,22 +312,69 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
267
312
|
// Ink's useInput in Menu will reconfigure stdin automatically
|
|
268
313
|
});
|
|
269
314
|
};
|
|
270
|
-
const handleCreateWorktree = async (
|
|
315
|
+
const handleCreateWorktree = async (request) => {
|
|
316
|
+
setError(null);
|
|
317
|
+
let branch = request.creationMode === 'manual' ? request.branch : '';
|
|
318
|
+
let targetPath = request.path;
|
|
319
|
+
if (request.creationMode === 'prompt') {
|
|
320
|
+
setLoadingContext({
|
|
321
|
+
copySessionData: request.copySessionData,
|
|
322
|
+
isPromptFlow: true,
|
|
323
|
+
stage: 'naming',
|
|
324
|
+
});
|
|
325
|
+
setView('creating-worktree');
|
|
326
|
+
const allBranches = await Effect.runPromise(Effect.either(worktreeService.getAllBranchesEffect()));
|
|
327
|
+
const existingBranches = allBranches._tag === 'Right' ? allBranches.right : [];
|
|
328
|
+
const generatedBranch = await Effect.runPromise(Effect.either(worktreeNameGenerator.generateBranchNameEffect(request.initialPrompt, request.baseBranch, existingBranches)));
|
|
329
|
+
if (generatedBranch._tag === 'Left') {
|
|
330
|
+
setError(formatErrorMessage(generatedBranch.left));
|
|
331
|
+
setView('new-worktree');
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
branch = generatedBranch.right;
|
|
335
|
+
if (request.autoDirectoryPattern) {
|
|
336
|
+
targetPath = generateWorktreeDirectory(request.projectPath, branch, request.autoDirectoryPattern);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
271
339
|
// Set loading context before showing loading view
|
|
272
|
-
setLoadingContext({
|
|
340
|
+
setLoadingContext({
|
|
341
|
+
copySessionData: request.copySessionData,
|
|
342
|
+
isPromptFlow: request.creationMode === 'prompt',
|
|
343
|
+
stage: 'creating',
|
|
344
|
+
});
|
|
273
345
|
setView('creating-worktree');
|
|
274
|
-
setError(null);
|
|
275
346
|
// Create the worktree using Effect
|
|
276
|
-
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(
|
|
347
|
+
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(targetPath, branch, request.baseBranch, request.copySessionData, request.copyClaudeDirectory)));
|
|
277
348
|
// Transform Effect result to legacy format for handleWorktreeCreationResult
|
|
278
349
|
if (result._tag === 'Left') {
|
|
279
350
|
// Handle error using pattern matching on _tag
|
|
280
351
|
const errorMessage = formatErrorMessage(result.left);
|
|
281
|
-
handleWorktreeCreationResult({ success: false, error: errorMessage }, {
|
|
352
|
+
handleWorktreeCreationResult({ success: false, error: errorMessage }, {
|
|
353
|
+
path: targetPath,
|
|
354
|
+
branch,
|
|
355
|
+
baseBranch: request.baseBranch,
|
|
356
|
+
copySessionData: request.copySessionData,
|
|
357
|
+
copyClaudeDirectory: request.copyClaudeDirectory,
|
|
358
|
+
presetId: request.creationMode === 'prompt' ? request.presetId : undefined,
|
|
359
|
+
initialPrompt: request.creationMode === 'prompt'
|
|
360
|
+
? request.initialPrompt
|
|
361
|
+
: undefined,
|
|
362
|
+
});
|
|
282
363
|
}
|
|
283
364
|
else {
|
|
284
365
|
// Success case
|
|
285
|
-
|
|
366
|
+
const createdWorktree = result.right;
|
|
367
|
+
handleWorktreeCreationResult({ success: true }, {
|
|
368
|
+
path: createdWorktree.path,
|
|
369
|
+
branch: createdWorktree.branch || branch,
|
|
370
|
+
baseBranch: request.baseBranch,
|
|
371
|
+
copySessionData: request.copySessionData,
|
|
372
|
+
copyClaudeDirectory: request.copyClaudeDirectory,
|
|
373
|
+
presetId: request.creationMode === 'prompt' ? request.presetId : undefined,
|
|
374
|
+
initialPrompt: request.creationMode === 'prompt'
|
|
375
|
+
? request.initialPrompt
|
|
376
|
+
: undefined,
|
|
377
|
+
});
|
|
286
378
|
}
|
|
287
379
|
};
|
|
288
380
|
const handleCancelNewWorktree = () => {
|
|
@@ -296,7 +388,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
296
388
|
setPendingWorktreeCreation(null);
|
|
297
389
|
// Retry worktree creation with the resolved base branch
|
|
298
390
|
// Set loading context before showing loading view
|
|
299
|
-
setLoadingContext({
|
|
391
|
+
setLoadingContext({
|
|
392
|
+
copySessionData: creationData.copySessionData,
|
|
393
|
+
isPromptFlow: Boolean(creationData.presetId && creationData.initialPrompt),
|
|
394
|
+
stage: 'creating',
|
|
395
|
+
});
|
|
300
396
|
setView('creating-worktree');
|
|
301
397
|
setError(null);
|
|
302
398
|
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(creationData.path, creationData.branch, selectedRemoteRef, // Use the selected remote reference
|
|
@@ -308,8 +404,15 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
308
404
|
setView('new-worktree');
|
|
309
405
|
}
|
|
310
406
|
else {
|
|
311
|
-
|
|
312
|
-
|
|
407
|
+
handleWorktreeCreationResult({ success: true }, {
|
|
408
|
+
path: creationData.path,
|
|
409
|
+
branch: creationData.branch,
|
|
410
|
+
baseBranch: selectedRemoteRef,
|
|
411
|
+
copySessionData: creationData.copySessionData,
|
|
412
|
+
copyClaudeDirectory: creationData.copyClaudeDirectory,
|
|
413
|
+
presetId: creationData.presetId,
|
|
414
|
+
initialPrompt: creationData.initialPrompt,
|
|
415
|
+
});
|
|
313
416
|
}
|
|
314
417
|
};
|
|
315
418
|
const handleRemoteBranchSelectorCancel = () => {
|
|
@@ -400,9 +503,13 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
400
503
|
}
|
|
401
504
|
if (view === 'creating-worktree') {
|
|
402
505
|
// Compose message based on loading context
|
|
403
|
-
const message = loadingContext.
|
|
404
|
-
?
|
|
405
|
-
|
|
506
|
+
const message = loadingContext.isPromptFlow
|
|
507
|
+
? loadingContext.stage === 'naming'
|
|
508
|
+
? 'Generating branch name with Claude...'
|
|
509
|
+
: 'Creating worktree from generated branch name...'
|
|
510
|
+
: loadingContext.copySessionData
|
|
511
|
+
? 'Creating worktree and copying session data...'
|
|
512
|
+
: 'Creating worktree...';
|
|
406
513
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(LoadingSpinner, { message: message, color: "cyan" }) }));
|
|
407
514
|
}
|
|
408
515
|
if (view === 'delete-worktree') {
|
|
@@ -441,9 +548,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
441
548
|
if (view === 'creating-session-preset') {
|
|
442
549
|
// Always display preset-specific message
|
|
443
550
|
// Devcontainer operations take >5 seconds, so indicate extended duration
|
|
444
|
-
const message =
|
|
445
|
-
? 'Creating session with preset
|
|
446
|
-
:
|
|
551
|
+
const message = loadingContext.isPromptFlow
|
|
552
|
+
? 'Creating session with preset and prompt...'
|
|
553
|
+
: devcontainerConfig
|
|
554
|
+
? 'Creating session with preset (this may take a moment)...'
|
|
555
|
+
: 'Creating session with preset...';
|
|
447
556
|
// Use yellow color for devcontainer, cyan for standard
|
|
448
557
|
const color = devcontainerConfig ? 'yellow' : 'cyan';
|
|
449
558
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(LoadingSpinner, { message: message, color: color }) }));
|
|
@@ -33,6 +33,9 @@ const configReaderMock = {
|
|
|
33
33
|
const projectManagerMock = {
|
|
34
34
|
addRecentProject: vi.fn(),
|
|
35
35
|
};
|
|
36
|
+
const worktreeNameGeneratorMock = {
|
|
37
|
+
generateBranchNameEffect: vi.fn(() => Effect.succeed('fix/trim-worktree-name')),
|
|
38
|
+
};
|
|
36
39
|
function createInkMock(label, onRender) {
|
|
37
40
|
return async () => {
|
|
38
41
|
const ReactActual = await vi.importActual('react');
|
|
@@ -64,11 +67,15 @@ vi.mock('../services/projectManager.js', () => ({
|
|
|
64
67
|
vi.mock('../services/config/configReader.js', () => ({
|
|
65
68
|
configReader: configReaderMock,
|
|
66
69
|
}));
|
|
70
|
+
vi.mock('../services/worktreeNameGenerator.js', () => ({
|
|
71
|
+
worktreeNameGenerator: worktreeNameGeneratorMock,
|
|
72
|
+
}));
|
|
67
73
|
vi.mock('../services/worktreeService.js', () => ({
|
|
68
74
|
WorktreeService: vi.fn(function () {
|
|
69
75
|
return {
|
|
70
76
|
createWorktreeEffect: (...args) => createWorktreeEffectMock(...args),
|
|
71
77
|
deleteWorktreeEffect: (...args) => deleteWorktreeEffectMock(...args),
|
|
78
|
+
getAllBranchesEffect: () => Effect.succeed([]),
|
|
72
79
|
};
|
|
73
80
|
}),
|
|
74
81
|
}));
|
|
@@ -118,13 +125,20 @@ beforeEach(() => {
|
|
|
118
125
|
sessionProps = undefined;
|
|
119
126
|
createWorktreeEffectMock.mockReset();
|
|
120
127
|
deleteWorktreeEffectMock.mockReset();
|
|
121
|
-
createWorktreeEffectMock.mockImplementation(() => Effect.succeed(
|
|
128
|
+
createWorktreeEffectMock.mockImplementation((path, branch) => Effect.succeed({
|
|
129
|
+
path,
|
|
130
|
+
branch,
|
|
131
|
+
isMainWorktree: false,
|
|
132
|
+
hasSession: false,
|
|
133
|
+
}));
|
|
122
134
|
deleteWorktreeEffectMock.mockImplementation(() => Effect.succeed(undefined));
|
|
123
135
|
sessionManagers.length = 0;
|
|
124
136
|
getManagerForProjectMock.mockClear();
|
|
125
137
|
configReaderMock.getSelectPresetOnStart.mockReset();
|
|
126
138
|
configReaderMock.getSelectPresetOnStart.mockReturnValue(false);
|
|
127
139
|
projectManagerMock.addRecentProject.mockReset();
|
|
140
|
+
worktreeNameGeneratorMock.generateBranchNameEffect.mockReset();
|
|
141
|
+
worktreeNameGeneratorMock.generateBranchNameEffect.mockImplementation(() => Effect.succeed('fix/trim-worktree-name'));
|
|
128
142
|
});
|
|
129
143
|
afterEach(() => {
|
|
130
144
|
delete process.env[ENV_VARS.MULTI_PROJECT_ROOT];
|
|
@@ -153,7 +167,12 @@ describe('App component loading state machine', () => {
|
|
|
153
167
|
let resolveWorktree;
|
|
154
168
|
createWorktreeEffectMock.mockImplementation(() => Effect.tryPromise({
|
|
155
169
|
try: () => new Promise(resolve => {
|
|
156
|
-
resolveWorktree = resolve
|
|
170
|
+
resolveWorktree = () => resolve({
|
|
171
|
+
path: '/tmp/test',
|
|
172
|
+
branch: 'feature',
|
|
173
|
+
isMainWorktree: false,
|
|
174
|
+
hasSession: false,
|
|
175
|
+
});
|
|
157
176
|
}),
|
|
158
177
|
catch: (error) => error,
|
|
159
178
|
}));
|
|
@@ -168,7 +187,14 @@ describe('App component loading state machine', () => {
|
|
|
168
187
|
}));
|
|
169
188
|
await waitForCondition(() => Boolean(newWorktreeProps));
|
|
170
189
|
const newWorktree = newWorktreeProps;
|
|
171
|
-
const createPromise = Promise.resolve(newWorktree.onComplete(
|
|
190
|
+
const createPromise = Promise.resolve(newWorktree.onComplete({
|
|
191
|
+
creationMode: 'manual',
|
|
192
|
+
path: '/tmp/test',
|
|
193
|
+
branch: 'feature',
|
|
194
|
+
baseBranch: 'main',
|
|
195
|
+
copySessionData: true,
|
|
196
|
+
copyClaudeDirectory: false,
|
|
197
|
+
}));
|
|
172
198
|
await flush();
|
|
173
199
|
expect(lastFrame()).toContain('Creating worktree and copying session data...');
|
|
174
200
|
resolveWorktree?.();
|
|
@@ -178,6 +204,69 @@ describe('App component loading state machine', () => {
|
|
|
178
204
|
expect(lastFrame()).toContain('Menu View');
|
|
179
205
|
unmount();
|
|
180
206
|
});
|
|
207
|
+
it('auto-starts the prompt-first session with the created worktree path', async () => {
|
|
208
|
+
const { lastFrame, unmount } = render(_jsx(App, { version: "test" }));
|
|
209
|
+
await waitForCondition(() => Boolean(menuProps));
|
|
210
|
+
expect(sessionManagers).toHaveLength(1);
|
|
211
|
+
const sessionManager = sessionManagers[0];
|
|
212
|
+
const menu = menuProps;
|
|
213
|
+
await Promise.resolve(menu.onSelectWorktree({
|
|
214
|
+
path: '',
|
|
215
|
+
branch: '',
|
|
216
|
+
isMainWorktree: false,
|
|
217
|
+
hasSession: false,
|
|
218
|
+
}));
|
|
219
|
+
await waitForCondition(() => Boolean(newWorktreeProps));
|
|
220
|
+
await Promise.resolve(newWorktreeProps.onComplete({
|
|
221
|
+
creationMode: 'prompt',
|
|
222
|
+
path: '/tmp/project',
|
|
223
|
+
projectPath: '/tmp/project',
|
|
224
|
+
autoDirectoryPattern: '../{branch}',
|
|
225
|
+
baseBranch: 'main',
|
|
226
|
+
presetId: 'claude',
|
|
227
|
+
initialPrompt: 'trim worktree name output',
|
|
228
|
+
copySessionData: false,
|
|
229
|
+
copyClaudeDirectory: false,
|
|
230
|
+
}));
|
|
231
|
+
const createdPath = createWorktreeEffectMock.mock.calls[0]?.[0];
|
|
232
|
+
await waitForCondition(() => sessionManager.createSessionWithPresetEffect.mock.calls.length > 0, 200);
|
|
233
|
+
await waitForCondition(() => lastFrame()?.includes('Session View') ?? false);
|
|
234
|
+
expect(sessionManager.createSessionWithPresetEffect).toHaveBeenCalledWith(createdPath, 'claude', 'trim worktree name output');
|
|
235
|
+
expect(sessionProps?.session).toEqual(mockSession);
|
|
236
|
+
unmount();
|
|
237
|
+
});
|
|
238
|
+
it('uses the created worktree path when auto-starting a prompt-first session', async () => {
|
|
239
|
+
createWorktreeEffectMock.mockImplementation((_path, branch) => Effect.succeed({
|
|
240
|
+
path: '/tmp/resolved-worktree',
|
|
241
|
+
branch,
|
|
242
|
+
isMainWorktree: false,
|
|
243
|
+
hasSession: false,
|
|
244
|
+
}));
|
|
245
|
+
const { unmount } = render(_jsx(App, { version: "test" }));
|
|
246
|
+
await waitForCondition(() => Boolean(menuProps));
|
|
247
|
+
const sessionManager = sessionManagers[0];
|
|
248
|
+
await Promise.resolve(menuProps.onSelectWorktree({
|
|
249
|
+
path: '',
|
|
250
|
+
branch: '',
|
|
251
|
+
isMainWorktree: false,
|
|
252
|
+
hasSession: false,
|
|
253
|
+
}));
|
|
254
|
+
await waitForCondition(() => Boolean(newWorktreeProps));
|
|
255
|
+
await Promise.resolve(newWorktreeProps.onComplete({
|
|
256
|
+
creationMode: 'prompt',
|
|
257
|
+
path: '../relative-worktree',
|
|
258
|
+
projectPath: '/tmp/project',
|
|
259
|
+
autoDirectoryPattern: '../{branch}',
|
|
260
|
+
baseBranch: 'main',
|
|
261
|
+
presetId: 'claude',
|
|
262
|
+
initialPrompt: 'trim worktree name output',
|
|
263
|
+
copySessionData: false,
|
|
264
|
+
copyClaudeDirectory: false,
|
|
265
|
+
}));
|
|
266
|
+
await waitForCondition(() => sessionManager.createSessionWithPresetEffect.mock.calls.length > 0, 200);
|
|
267
|
+
expect(sessionManager.createSessionWithPresetEffect).toHaveBeenCalledWith('/tmp/resolved-worktree', 'claude', 'trim worktree name output');
|
|
268
|
+
unmount();
|
|
269
|
+
});
|
|
181
270
|
it('displays branch deletion message while deleting worktrees', async () => {
|
|
182
271
|
let resolveDelete;
|
|
183
272
|
deleteWorktreeEffectMock.mockImplementation(() => Effect.tryPromise({
|
|
@@ -132,12 +132,12 @@ describe('Menu - Recent Projects', () => {
|
|
|
132
132
|
const { lastFrame, rerender } = render(_jsx(Menu, { sessionManager: mockSessionManager, worktreeService: mockWorktreeService, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true, version: "test" }));
|
|
133
133
|
// Force a rerender to ensure all effects have run
|
|
134
134
|
rerender(_jsx(Menu, { sessionManager: mockSessionManager, worktreeService: mockWorktreeService, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true, version: "test" }));
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
await vi.waitFor(() => {
|
|
136
|
+
const output = lastFrame();
|
|
137
|
+
expect(output).toContain('─ Recent ─');
|
|
138
|
+
expect(output).toContain('Project 1');
|
|
139
|
+
expect(output).toContain('Project 2');
|
|
140
|
+
});
|
|
141
141
|
});
|
|
142
142
|
it('should not show recent projects section when no recent projects', () => {
|
|
143
143
|
vi.mocked(projectManager.getRecentProjects).mockReturnValue([]);
|
|
@@ -164,12 +164,12 @@ describe('Menu - Recent Projects', () => {
|
|
|
164
164
|
});
|
|
165
165
|
vi.spyOn(SessionManager, 'formatSessionCounts').mockReturnValue('');
|
|
166
166
|
const { lastFrame } = render(_jsx(Menu, { sessionManager: mockSessionManager, worktreeService: mockWorktreeService, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true, version: "test" }));
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
167
|
+
await vi.waitFor(() => {
|
|
168
|
+
const output = lastFrame();
|
|
169
|
+
expect(output).toContain('─ Recent ─');
|
|
170
|
+
expect(output).toContain('Project 0');
|
|
171
|
+
expect(output).toContain('Project 4');
|
|
172
|
+
});
|
|
173
173
|
});
|
|
174
174
|
it('should filter out current project from recent projects', async () => {
|
|
175
175
|
// Setup the initial recent projects
|
|
@@ -186,13 +186,13 @@ describe('Menu - Recent Projects', () => {
|
|
|
186
186
|
const { lastFrame, rerender } = render(_jsx(Menu, { sessionManager: mockSessionManager, worktreeService: worktreeServiceWithGitRoot, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true, version: "test" }));
|
|
187
187
|
// Force a rerender to ensure all effects have run
|
|
188
188
|
rerender(_jsx(Menu, { sessionManager: mockSessionManager, worktreeService: worktreeServiceWithGitRoot, onSelectWorktree: vi.fn(), onSelectRecentProject: vi.fn(), multiProject: true, version: "test" }));
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
189
|
+
await vi.waitFor(() => {
|
|
190
|
+
const output = lastFrame();
|
|
191
|
+
expect(output).toContain('─ Recent ─');
|
|
192
|
+
expect(output).not.toContain('Current Project');
|
|
193
|
+
expect(output).toContain('Project 1');
|
|
194
|
+
expect(output).toContain('Project 2');
|
|
195
|
+
});
|
|
196
196
|
});
|
|
197
197
|
it('should hide recent projects section when all projects are filtered out', () => {
|
|
198
198
|
vi.mocked(projectManager.getRecentProjects).mockReturnValue([
|
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
interface NewWorktreeProps {
|
|
3
3
|
projectPath?: string;
|
|
4
|
-
onComplete: (
|
|
4
|
+
onComplete: (request: NewWorktreeRequest) => void;
|
|
5
5
|
onCancel: () => void;
|
|
6
6
|
}
|
|
7
|
+
export type NewWorktreeRequest = {
|
|
8
|
+
creationMode: 'manual';
|
|
9
|
+
path: string;
|
|
10
|
+
branch: string;
|
|
11
|
+
baseBranch: string;
|
|
12
|
+
copySessionData: boolean;
|
|
13
|
+
copyClaudeDirectory: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
creationMode: 'prompt';
|
|
16
|
+
path: string;
|
|
17
|
+
projectPath: string;
|
|
18
|
+
autoDirectoryPattern?: string;
|
|
19
|
+
baseBranch: string;
|
|
20
|
+
presetId: string;
|
|
21
|
+
initialPrompt: string;
|
|
22
|
+
copySessionData: boolean;
|
|
23
|
+
copyClaudeDirectory: boolean;
|
|
24
|
+
branch?: never;
|
|
25
|
+
};
|
|
7
26
|
declare const NewWorktree: React.FC<NewWorktreeProps>;
|
|
8
27
|
export default NewWorktree;
|