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.
Files changed (31) hide show
  1. package/dist/components/App.js +159 -44
  2. package/dist/components/App.test.js +96 -5
  3. package/dist/components/Dashboard.d.ts +12 -0
  4. package/dist/components/Dashboard.js +443 -0
  5. package/dist/components/Dashboard.test.js +348 -0
  6. package/dist/components/Menu.recent-projects.test.js +19 -19
  7. package/dist/components/NewWorktree.d.ts +20 -1
  8. package/dist/components/NewWorktree.js +103 -56
  9. package/dist/components/NewWorktree.test.js +17 -4
  10. package/dist/services/globalSessionOrchestrator.d.ts +1 -0
  11. package/dist/services/globalSessionOrchestrator.js +3 -0
  12. package/dist/services/projectManager.d.ts +7 -1
  13. package/dist/services/projectManager.js +26 -10
  14. package/dist/services/sessionManager.d.ts +3 -2
  15. package/dist/services/sessionManager.js +37 -40
  16. package/dist/services/sessionManager.test.js +38 -0
  17. package/dist/services/worktreeNameGenerator.d.ts +8 -0
  18. package/dist/services/worktreeNameGenerator.js +184 -0
  19. package/dist/services/worktreeNameGenerator.test.js +35 -0
  20. package/dist/utils/presetPrompt.d.ts +11 -0
  21. package/dist/utils/presetPrompt.js +71 -0
  22. package/dist/utils/presetPrompt.test.d.ts +1 -0
  23. package/dist/utils/presetPrompt.test.js +167 -0
  24. package/dist/utils/worktreeUtils.d.ts +1 -2
  25. package/package.json +6 -6
  26. package/dist/components/ProjectList.d.ts +0 -10
  27. package/dist/components/ProjectList.js +0 -233
  28. package/dist/components/ProjectList.recent-projects.test.js +0 -193
  29. package/dist/components/ProjectList.test.js +0 -620
  30. /package/dist/components/{ProjectList.recent-projects.test.d.ts → Dashboard.test.d.ts} +0 -0
  31. /package/dist/{components/ProjectList.test.d.ts → services/worktreeNameGenerator.test.d.ts} +0 -0
@@ -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 ProjectList from './ProjectList.js';
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
- // 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');
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
- setActiveSession(result.session);
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 (path, branch, baseBranch, copySessionData, copyClaudeDirectory) => {
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({ copySessionData });
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(path, branch, baseBranch, copySessionData, copyClaudeDirectory)));
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 }, { path, branch, baseBranch, copySessionData, copyClaudeDirectory });
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
- handleWorktreeCreationResult({ success: true }, { path, branch, baseBranch, copySessionData, copyClaudeDirectory });
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({ copySessionData: creationData.copySessionData });
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
- // Success - return to menu
312
- handleReturnToMenu();
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(ProjectList, { projectsDir: projectsDir, onSelectProject: handleSelectProject, error: error, onDismissError: () => setError(null) }));
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.copySessionData
395
- ? 'Creating worktree and copying session data...'
396
- : 'Creating worktree...';
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 = devcontainerConfig
436
- ? 'Creating session with preset (this may take a moment)...'
437
- : 'Creating session with preset...';
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('./ProjectList.js', createInkMock('Project List View', () => { }));
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(undefined));
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('Project List View');
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('/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
+ }));
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;