ccmanager 3.9.0 → 3.11.0
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 +159 -44
- package/dist/components/App.test.js +96 -5
- package/dist/components/Dashboard.d.ts +12 -0
- package/dist/components/Dashboard.js +443 -0
- package/dist/components/Dashboard.test.js +348 -0
- 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/services/globalSessionOrchestrator.d.ts +1 -0
- package/dist/services/globalSessionOrchestrator.js +3 -0
- package/dist/services/projectManager.d.ts +7 -1
- package/dist/services/projectManager.js +26 -10
- package/dist/services/sessionManager.d.ts +3 -2
- package/dist/services/sessionManager.js +37 -40
- package/dist/services/sessionManager.test.js +38 -0
- package/dist/services/worktreeNameGenerator.d.ts +8 -0
- package/dist/services/worktreeNameGenerator.js +184 -0
- package/dist/services/worktreeNameGenerator.test.js +35 -0
- package/dist/utils/presetPrompt.d.ts +11 -0
- package/dist/utils/presetPrompt.js +71 -0
- package/dist/utils/presetPrompt.test.d.ts +1 -0
- package/dist/utils/presetPrompt.test.js +167 -0
- package/dist/utils/worktreeUtils.d.ts +1 -2
- package/package.json +6 -6
- package/dist/components/ProjectList.d.ts +0 -10
- package/dist/components/ProjectList.js +0 -233
- package/dist/components/ProjectList.recent-projects.test.js +0 -193
- package/dist/components/ProjectList.test.js +0 -620
- /package/dist/components/{ProjectList.recent-projects.test.d.ts → Dashboard.test.d.ts} +0 -0
- /package/dist/{components/ProjectList.test.d.ts → services/worktreeNameGenerator.test.d.ts} +0 -0
package/dist/components/App.js
CHANGED
|
@@ -3,7 +3,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|
|
3
3
|
import { useApp, Box, Text } from 'ink';
|
|
4
4
|
import { Effect } from 'effect';
|
|
5
5
|
import Menu from './Menu.js';
|
|
6
|
-
import
|
|
6
|
+
import Dashboard from './Dashboard.js';
|
|
7
7
|
import Session from './Session.js';
|
|
8
8
|
import NewWorktree from './NewWorktree.js';
|
|
9
9
|
import DeleteWorktree from './DeleteWorktree.js';
|
|
@@ -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,10 +53,10 @@ 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 = 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') {
|
|
@@ -86,6 +89,33 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
86
89
|
callback();
|
|
87
90
|
}, 10); // Small delay to ensure screen clear is processed
|
|
88
91
|
}, []);
|
|
92
|
+
const navigateToSession = useCallback((session) => {
|
|
93
|
+
clearScreen();
|
|
94
|
+
setView('clearing');
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
setActiveSession(session);
|
|
97
|
+
setView('session');
|
|
98
|
+
}, 10);
|
|
99
|
+
}, []);
|
|
100
|
+
const startSessionForWorktree = useCallback(async (worktree, options) => {
|
|
101
|
+
let session = sessionManager.getSession(worktree.path);
|
|
102
|
+
if (!session) {
|
|
103
|
+
if (!options?.presetId && configReader.getSelectPresetOnStart()) {
|
|
104
|
+
setSelectedWorktree(worktree);
|
|
105
|
+
navigateWithClear('preset-selector');
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
setView(options?.presetId ? 'creating-session-preset' : 'creating-session');
|
|
109
|
+
const result = await createSessionWithEffect(worktree.path, options?.presetId, options?.initialPrompt);
|
|
110
|
+
if (!result.success) {
|
|
111
|
+
setError(result.errorMessage);
|
|
112
|
+
navigateWithClear('menu');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
session = result.session;
|
|
116
|
+
}
|
|
117
|
+
navigateToSession(session);
|
|
118
|
+
}, [sessionManager, navigateWithClear, navigateToSession]);
|
|
89
119
|
useEffect(() => {
|
|
90
120
|
// Listen for session exits to return to menu automatically
|
|
91
121
|
const handleSessionExit = (session) => {
|
|
@@ -115,6 +145,26 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
115
145
|
// Don't destroy sessions on unmount - they persist in memory
|
|
116
146
|
};
|
|
117
147
|
}, [sessionManager, multiProject, selectedProject, navigateWithClear]);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (view !== 'menu' || !pendingMenuSessionLaunch) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let cancelled = false;
|
|
153
|
+
void (async () => {
|
|
154
|
+
if (cancelled) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const launchRequest = pendingMenuSessionLaunch;
|
|
158
|
+
setPendingMenuSessionLaunch(null);
|
|
159
|
+
await startSessionForWorktree(launchRequest.worktree, {
|
|
160
|
+
presetId: launchRequest.presetId,
|
|
161
|
+
initialPrompt: launchRequest.initialPrompt,
|
|
162
|
+
});
|
|
163
|
+
})();
|
|
164
|
+
return () => {
|
|
165
|
+
cancelled = true;
|
|
166
|
+
};
|
|
167
|
+
}, [view, pendingMenuSessionLaunch, startSessionForWorktree]);
|
|
118
168
|
// Helper function to parse ambiguous branch error and create AmbiguousBranchError
|
|
119
169
|
const parseAmbiguousBranchError = (errorMessage) => {
|
|
120
170
|
const pattern = /Ambiguous branch '(.+?)' found in multiple remotes: (.+?)\. Please specify which remote to use\./;
|
|
@@ -141,6 +191,20 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
141
191
|
// Helper function to handle worktree creation results
|
|
142
192
|
const handleWorktreeCreationResult = (result, creationData) => {
|
|
143
193
|
if (result.success) {
|
|
194
|
+
if (creationData.presetId && creationData.initialPrompt) {
|
|
195
|
+
setPendingMenuSessionLaunch({
|
|
196
|
+
worktree: {
|
|
197
|
+
path: creationData.path,
|
|
198
|
+
branch: creationData.branch,
|
|
199
|
+
isMainWorktree: false,
|
|
200
|
+
hasSession: false,
|
|
201
|
+
},
|
|
202
|
+
presetId: creationData.presetId,
|
|
203
|
+
initialPrompt: creationData.initialPrompt,
|
|
204
|
+
});
|
|
205
|
+
handleReturnToMenu();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
144
208
|
handleReturnToMenu();
|
|
145
209
|
return;
|
|
146
210
|
}
|
|
@@ -207,28 +271,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
207
271
|
}
|
|
208
272
|
return;
|
|
209
273
|
}
|
|
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');
|
|
274
|
+
await startSessionForWorktree(worktree);
|
|
232
275
|
};
|
|
233
276
|
const handlePresetSelected = async (presetId) => {
|
|
234
277
|
if (!selectedWorktree)
|
|
@@ -244,8 +287,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
244
287
|
return;
|
|
245
288
|
}
|
|
246
289
|
// Success case
|
|
247
|
-
|
|
248
|
-
navigateWithClear('session');
|
|
290
|
+
navigateToSession(result.session);
|
|
249
291
|
setSelectedWorktree(null);
|
|
250
292
|
};
|
|
251
293
|
const handlePresetSelectorCancel = () => {
|
|
@@ -267,22 +309,69 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
267
309
|
// Ink's useInput in Menu will reconfigure stdin automatically
|
|
268
310
|
});
|
|
269
311
|
};
|
|
270
|
-
const handleCreateWorktree = async (
|
|
312
|
+
const handleCreateWorktree = async (request) => {
|
|
313
|
+
setError(null);
|
|
314
|
+
let branch = request.creationMode === 'manual' ? request.branch : '';
|
|
315
|
+
let targetPath = request.path;
|
|
316
|
+
if (request.creationMode === 'prompt') {
|
|
317
|
+
setLoadingContext({
|
|
318
|
+
copySessionData: request.copySessionData,
|
|
319
|
+
isPromptFlow: true,
|
|
320
|
+
stage: 'naming',
|
|
321
|
+
});
|
|
322
|
+
setView('creating-worktree');
|
|
323
|
+
const allBranches = await Effect.runPromise(Effect.either(worktreeService.getAllBranchesEffect()));
|
|
324
|
+
const existingBranches = allBranches._tag === 'Right' ? allBranches.right : [];
|
|
325
|
+
const generatedBranch = await Effect.runPromise(Effect.either(worktreeNameGenerator.generateBranchNameEffect(request.initialPrompt, request.baseBranch, existingBranches)));
|
|
326
|
+
if (generatedBranch._tag === 'Left') {
|
|
327
|
+
setError(formatErrorMessage(generatedBranch.left));
|
|
328
|
+
setView('new-worktree');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
branch = generatedBranch.right;
|
|
332
|
+
if (request.autoDirectoryPattern) {
|
|
333
|
+
targetPath = generateWorktreeDirectory(request.projectPath, branch, request.autoDirectoryPattern);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
271
336
|
// Set loading context before showing loading view
|
|
272
|
-
setLoadingContext({
|
|
337
|
+
setLoadingContext({
|
|
338
|
+
copySessionData: request.copySessionData,
|
|
339
|
+
isPromptFlow: request.creationMode === 'prompt',
|
|
340
|
+
stage: 'creating',
|
|
341
|
+
});
|
|
273
342
|
setView('creating-worktree');
|
|
274
|
-
setError(null);
|
|
275
343
|
// Create the worktree using Effect
|
|
276
|
-
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(
|
|
344
|
+
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(targetPath, branch, request.baseBranch, request.copySessionData, request.copyClaudeDirectory)));
|
|
277
345
|
// Transform Effect result to legacy format for handleWorktreeCreationResult
|
|
278
346
|
if (result._tag === 'Left') {
|
|
279
347
|
// Handle error using pattern matching on _tag
|
|
280
348
|
const errorMessage = formatErrorMessage(result.left);
|
|
281
|
-
handleWorktreeCreationResult({ success: false, error: errorMessage }, {
|
|
349
|
+
handleWorktreeCreationResult({ success: false, error: errorMessage }, {
|
|
350
|
+
path: targetPath,
|
|
351
|
+
branch,
|
|
352
|
+
baseBranch: request.baseBranch,
|
|
353
|
+
copySessionData: request.copySessionData,
|
|
354
|
+
copyClaudeDirectory: request.copyClaudeDirectory,
|
|
355
|
+
presetId: request.creationMode === 'prompt' ? request.presetId : undefined,
|
|
356
|
+
initialPrompt: request.creationMode === 'prompt'
|
|
357
|
+
? request.initialPrompt
|
|
358
|
+
: undefined,
|
|
359
|
+
});
|
|
282
360
|
}
|
|
283
361
|
else {
|
|
284
362
|
// Success case
|
|
285
|
-
|
|
363
|
+
const createdWorktree = result.right;
|
|
364
|
+
handleWorktreeCreationResult({ success: true }, {
|
|
365
|
+
path: createdWorktree.path,
|
|
366
|
+
branch: createdWorktree.branch || branch,
|
|
367
|
+
baseBranch: request.baseBranch,
|
|
368
|
+
copySessionData: request.copySessionData,
|
|
369
|
+
copyClaudeDirectory: request.copyClaudeDirectory,
|
|
370
|
+
presetId: request.creationMode === 'prompt' ? request.presetId : undefined,
|
|
371
|
+
initialPrompt: request.creationMode === 'prompt'
|
|
372
|
+
? request.initialPrompt
|
|
373
|
+
: undefined,
|
|
374
|
+
});
|
|
286
375
|
}
|
|
287
376
|
};
|
|
288
377
|
const handleCancelNewWorktree = () => {
|
|
@@ -296,7 +385,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
296
385
|
setPendingWorktreeCreation(null);
|
|
297
386
|
// Retry worktree creation with the resolved base branch
|
|
298
387
|
// Set loading context before showing loading view
|
|
299
|
-
setLoadingContext({
|
|
388
|
+
setLoadingContext({
|
|
389
|
+
copySessionData: creationData.copySessionData,
|
|
390
|
+
isPromptFlow: Boolean(creationData.presetId && creationData.initialPrompt),
|
|
391
|
+
stage: 'creating',
|
|
392
|
+
});
|
|
300
393
|
setView('creating-worktree');
|
|
301
394
|
setError(null);
|
|
302
395
|
const result = await Effect.runPromise(Effect.either(worktreeService.createWorktreeEffect(creationData.path, creationData.branch, selectedRemoteRef, // Use the selected remote reference
|
|
@@ -308,8 +401,15 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
308
401
|
setView('new-worktree');
|
|
309
402
|
}
|
|
310
403
|
else {
|
|
311
|
-
|
|
312
|
-
|
|
404
|
+
handleWorktreeCreationResult({ success: true }, {
|
|
405
|
+
path: creationData.path,
|
|
406
|
+
branch: creationData.branch,
|
|
407
|
+
baseBranch: selectedRemoteRef,
|
|
408
|
+
copySessionData: creationData.copySessionData,
|
|
409
|
+
copyClaudeDirectory: creationData.copyClaudeDirectory,
|
|
410
|
+
presetId: creationData.presetId,
|
|
411
|
+
initialPrompt: creationData.initialPrompt,
|
|
412
|
+
});
|
|
313
413
|
}
|
|
314
414
|
};
|
|
315
415
|
const handleRemoteBranchSelectorCancel = () => {
|
|
@@ -363,6 +463,15 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
363
463
|
projectManager.addRecentProject(project);
|
|
364
464
|
navigateWithClear('menu');
|
|
365
465
|
};
|
|
466
|
+
const handleSelectSessionFromDashboard = (session, project) => {
|
|
467
|
+
// Set the correct session manager for this project
|
|
468
|
+
const projectSessionManager = globalSessionOrchestrator.getManagerForProject(project.path);
|
|
469
|
+
setSessionManager(projectSessionManager);
|
|
470
|
+
setWorktreeService(new WorktreeService(project.path));
|
|
471
|
+
// Don't set selectedProject so session exit returns to Dashboard
|
|
472
|
+
setActiveSession(session);
|
|
473
|
+
navigateWithClear('session');
|
|
474
|
+
};
|
|
366
475
|
const handleBackToProjectList = () => {
|
|
367
476
|
// Sessions persist in their project-specific managers
|
|
368
477
|
setSelectedProject(null);
|
|
@@ -378,7 +487,7 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
378
487
|
if (!projectsDir) {
|
|
379
488
|
return (_jsx(Box, { children: _jsxs(Text, { color: "red", children: ["Error: ", MULTI_PROJECT_ERRORS.NO_PROJECTS_DIR] }) }));
|
|
380
489
|
}
|
|
381
|
-
return (_jsx(
|
|
490
|
+
return (_jsx(Dashboard, { projectsDir: projectsDir, onSelectSession: handleSelectSessionFromDashboard, onSelectProject: handleSelectProject, error: error, onDismissError: () => setError(null), version: version }));
|
|
382
491
|
}
|
|
383
492
|
if (view === 'menu') {
|
|
384
493
|
return (_jsx(Menu, { sessionManager: sessionManager, worktreeService: worktreeService, onSelectWorktree: handleSelectWorktree, onSelectRecentProject: handleSelectProject, error: error, onDismissError: () => setError(null), projectName: selectedProject?.name, multiProject: multiProject, version: version }, menuKey));
|
|
@@ -391,9 +500,13 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
391
500
|
}
|
|
392
501
|
if (view === 'creating-worktree') {
|
|
393
502
|
// Compose message based on loading context
|
|
394
|
-
const message = loadingContext.
|
|
395
|
-
?
|
|
396
|
-
|
|
503
|
+
const message = loadingContext.isPromptFlow
|
|
504
|
+
? loadingContext.stage === 'naming'
|
|
505
|
+
? 'Generating branch name with Claude...'
|
|
506
|
+
: 'Creating worktree from generated branch name...'
|
|
507
|
+
: loadingContext.copySessionData
|
|
508
|
+
? 'Creating worktree and copying session data...'
|
|
509
|
+
: 'Creating worktree...';
|
|
397
510
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(LoadingSpinner, { message: message, color: "cyan" }) }));
|
|
398
511
|
}
|
|
399
512
|
if (view === 'delete-worktree') {
|
|
@@ -432,9 +545,11 @@ const App = ({ devcontainerConfig, multiProject, version, }) => {
|
|
|
432
545
|
if (view === 'creating-session-preset') {
|
|
433
546
|
// Always display preset-specific message
|
|
434
547
|
// Devcontainer operations take >5 seconds, so indicate extended duration
|
|
435
|
-
const message =
|
|
436
|
-
? 'Creating session with preset
|
|
437
|
-
:
|
|
548
|
+
const message = loadingContext.isPromptFlow
|
|
549
|
+
? 'Creating session with preset and prompt...'
|
|
550
|
+
: devcontainerConfig
|
|
551
|
+
? 'Creating session with preset (this may take a moment)...'
|
|
552
|
+
: 'Creating session with preset...';
|
|
438
553
|
// Use yellow color for devcontainer, cyan for standard
|
|
439
554
|
const color = devcontainerConfig ? 'yellow' : 'cyan';
|
|
440
555
|
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');
|
|
@@ -54,6 +57,8 @@ vi.mock('../services/globalSessionOrchestrator.js', () => ({
|
|
|
54
57
|
globalSessionOrchestrator: {
|
|
55
58
|
getManagerForProject: getManagerForProjectMock,
|
|
56
59
|
destroyAllSessions: vi.fn(),
|
|
60
|
+
getProjectPaths: vi.fn(() => []),
|
|
61
|
+
getProjectSessions: vi.fn(() => []),
|
|
57
62
|
},
|
|
58
63
|
}));
|
|
59
64
|
vi.mock('../services/projectManager.js', () => ({
|
|
@@ -62,16 +67,20 @@ vi.mock('../services/projectManager.js', () => ({
|
|
|
62
67
|
vi.mock('../services/config/configReader.js', () => ({
|
|
63
68
|
configReader: configReaderMock,
|
|
64
69
|
}));
|
|
70
|
+
vi.mock('../services/worktreeNameGenerator.js', () => ({
|
|
71
|
+
worktreeNameGenerator: worktreeNameGeneratorMock,
|
|
72
|
+
}));
|
|
65
73
|
vi.mock('../services/worktreeService.js', () => ({
|
|
66
74
|
WorktreeService: vi.fn(function () {
|
|
67
75
|
return {
|
|
68
76
|
createWorktreeEffect: (...args) => createWorktreeEffectMock(...args),
|
|
69
77
|
deleteWorktreeEffect: (...args) => deleteWorktreeEffectMock(...args),
|
|
78
|
+
getAllBranchesEffect: () => Effect.succeed([]),
|
|
70
79
|
};
|
|
71
80
|
}),
|
|
72
81
|
}));
|
|
73
82
|
vi.mock('./Menu.js', createInkMock('Menu View', props => (menuProps = props)));
|
|
74
|
-
vi.mock('./
|
|
83
|
+
vi.mock('./Dashboard.js', createInkMock('Dashboard View', () => { }));
|
|
75
84
|
vi.mock('./NewWorktree.js', createInkMock('New Worktree View', props => {
|
|
76
85
|
newWorktreeProps = props;
|
|
77
86
|
}));
|
|
@@ -116,13 +125,20 @@ beforeEach(() => {
|
|
|
116
125
|
sessionProps = undefined;
|
|
117
126
|
createWorktreeEffectMock.mockReset();
|
|
118
127
|
deleteWorktreeEffectMock.mockReset();
|
|
119
|
-
createWorktreeEffectMock.mockImplementation(() => Effect.succeed(
|
|
128
|
+
createWorktreeEffectMock.mockImplementation((path, branch) => Effect.succeed({
|
|
129
|
+
path,
|
|
130
|
+
branch,
|
|
131
|
+
isMainWorktree: false,
|
|
132
|
+
hasSession: false,
|
|
133
|
+
}));
|
|
120
134
|
deleteWorktreeEffectMock.mockImplementation(() => Effect.succeed(undefined));
|
|
121
135
|
sessionManagers.length = 0;
|
|
122
136
|
getManagerForProjectMock.mockClear();
|
|
123
137
|
configReaderMock.getSelectPresetOnStart.mockReset();
|
|
124
138
|
configReaderMock.getSelectPresetOnStart.mockReturnValue(false);
|
|
125
139
|
projectManagerMock.addRecentProject.mockReset();
|
|
140
|
+
worktreeNameGeneratorMock.generateBranchNameEffect.mockReset();
|
|
141
|
+
worktreeNameGeneratorMock.generateBranchNameEffect.mockImplementation(() => Effect.succeed('fix/trim-worktree-name'));
|
|
126
142
|
});
|
|
127
143
|
afterEach(() => {
|
|
128
144
|
delete process.env[ENV_VARS.MULTI_PROJECT_ROOT];
|
|
@@ -139,7 +155,7 @@ describe('App component view state', () => {
|
|
|
139
155
|
process.env[ENV_VARS.MULTI_PROJECT_ROOT] = '/tmp/projects';
|
|
140
156
|
const { lastFrame, unmount } = render(_jsx(App, { multiProject: true, version: "test" }));
|
|
141
157
|
await flush();
|
|
142
|
-
expect(lastFrame()).toContain('
|
|
158
|
+
expect(lastFrame()).toContain('Dashboard View');
|
|
143
159
|
unmount();
|
|
144
160
|
if (original !== undefined) {
|
|
145
161
|
process.env[ENV_VARS.MULTI_PROJECT_ROOT] = original;
|
|
@@ -151,7 +167,12 @@ describe('App component loading state machine', () => {
|
|
|
151
167
|
let resolveWorktree;
|
|
152
168
|
createWorktreeEffectMock.mockImplementation(() => Effect.tryPromise({
|
|
153
169
|
try: () => new Promise(resolve => {
|
|
154
|
-
resolveWorktree = resolve
|
|
170
|
+
resolveWorktree = () => resolve({
|
|
171
|
+
path: '/tmp/test',
|
|
172
|
+
branch: 'feature',
|
|
173
|
+
isMainWorktree: false,
|
|
174
|
+
hasSession: false,
|
|
175
|
+
});
|
|
155
176
|
}),
|
|
156
177
|
catch: (error) => error,
|
|
157
178
|
}));
|
|
@@ -166,7 +187,14 @@ describe('App component loading state machine', () => {
|
|
|
166
187
|
}));
|
|
167
188
|
await waitForCondition(() => Boolean(newWorktreeProps));
|
|
168
189
|
const newWorktree = newWorktreeProps;
|
|
169
|
-
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
|
+
}));
|
|
170
198
|
await flush();
|
|
171
199
|
expect(lastFrame()).toContain('Creating worktree and copying session data...');
|
|
172
200
|
resolveWorktree?.();
|
|
@@ -176,6 +204,69 @@ describe('App component loading state machine', () => {
|
|
|
176
204
|
expect(lastFrame()).toContain('Menu View');
|
|
177
205
|
unmount();
|
|
178
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
|
+
});
|
|
179
270
|
it('displays branch deletion message while deleting worktrees', async () => {
|
|
180
271
|
let resolveDelete;
|
|
181
272
|
deleteWorktreeEffectMock.mockImplementation(() => Effect.tryPromise({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { GitProject, Session as ISession } from '../types/index.js';
|
|
3
|
+
interface DashboardProps {
|
|
4
|
+
projectsDir: string;
|
|
5
|
+
onSelectSession: (session: ISession, project: GitProject) => void;
|
|
6
|
+
onSelectProject: (project: GitProject) => void;
|
|
7
|
+
error: string | null;
|
|
8
|
+
onDismissError: () => void;
|
|
9
|
+
version: string;
|
|
10
|
+
}
|
|
11
|
+
declare const Dashboard: React.FC<DashboardProps>;
|
|
12
|
+
export default Dashboard;
|