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.
@@ -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
- // Get or create session for this worktree
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
- setActiveSession(result.session);
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 (path, branch, baseBranch, copySessionData, copyClaudeDirectory) => {
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({ copySessionData });
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(path, branch, baseBranch, copySessionData, copyClaudeDirectory)));
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 }, { path, branch, baseBranch, copySessionData, copyClaudeDirectory });
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
- handleWorktreeCreationResult({ success: true }, { path, branch, baseBranch, copySessionData, copyClaudeDirectory });
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({ copySessionData: creationData.copySessionData });
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
- // Success - return to menu
312
- handleReturnToMenu();
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.copySessionData
404
- ? 'Creating worktree and copying session data...'
405
- : 'Creating worktree...';
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 = devcontainerConfig
445
- ? 'Creating session with preset (this may take a moment)...'
446
- : 'Creating session with preset...';
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(undefined));
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('/tmp/test', 'feature', 'main', true, false));
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
- // Wait for Effect to execute
136
- await new Promise(resolve => setTimeout(resolve, 100));
137
- const output = lastFrame();
138
- expect(output).toContain(' Recent ─');
139
- expect(output).toContain('Project 1');
140
- expect(output).toContain('Project 2');
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
- // Wait for Effect to execute
168
- await new Promise(resolve => setTimeout(resolve, 100));
169
- const output = lastFrame();
170
- expect(output).toContain(' Recent ─');
171
- expect(output).toContain('Project 0');
172
- expect(output).toContain('Project 4');
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
- // Wait for the state to update and component to re-render
190
- await new Promise(resolve => setTimeout(resolve, 50));
191
- const output = lastFrame();
192
- expect(output).toContain(' Recent ─');
193
- expect(output).not.toContain('Current Project');
194
- expect(output).toContain('Project 1');
195
- expect(output).toContain('Project 2');
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: (path: string, branch: string, baseBranch: string, copySessionData: boolean, copyClaudeDirectory: boolean) => void;
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;