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
@@ -0,0 +1,348 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from 'ink-testing-library';
3
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
4
+ // Mock bunTerminal to avoid native module loading issues
5
+ vi.mock('../services/bunTerminal.js', () => ({
6
+ spawn: vi.fn(function () {
7
+ return null;
8
+ }),
9
+ }));
10
+ // Import the actual component code but skip the useInput hook
11
+ vi.mock('ink', async () => {
12
+ const actual = await vi.importActual('ink');
13
+ return {
14
+ ...actual,
15
+ useInput: vi.fn(),
16
+ };
17
+ });
18
+ // Mock SelectInput to render items as simple text
19
+ vi.mock('ink-select-input', async () => {
20
+ const React = await vi.importActual('react');
21
+ const { Text, Box } = await vi.importActual('ink');
22
+ return {
23
+ default: ({ items }) => {
24
+ return React.createElement(Box, { flexDirection: 'column' }, items.map((item, index) => React.createElement(Text, { key: index }, item.label)));
25
+ },
26
+ };
27
+ });
28
+ // Mock TextInputWrapper to render as simple text
29
+ vi.mock('./TextInputWrapper.js', async () => {
30
+ const React = await vi.importActual('react');
31
+ const { Text } = await vi.importActual('ink');
32
+ return {
33
+ default: ({ value, placeholder }) => {
34
+ return React.createElement(Text, {}, value || placeholder || '');
35
+ },
36
+ };
37
+ });
38
+ // Mock Effect for testing
39
+ vi.mock('effect', async () => {
40
+ const actual = await vi.importActual('effect');
41
+ return actual;
42
+ });
43
+ // Mock the projectManager
44
+ vi.mock('../services/projectManager.js', () => ({
45
+ projectManager: {
46
+ instance: {
47
+ discoverProjectsEffect: vi.fn(),
48
+ },
49
+ getRecentProjects: vi.fn().mockReturnValue([]),
50
+ },
51
+ }));
52
+ // Mock globalSessionOrchestrator
53
+ vi.mock('../services/globalSessionOrchestrator.js', () => ({
54
+ globalSessionOrchestrator: {
55
+ getProjectPaths: vi.fn().mockReturnValue([]),
56
+ getProjectSessions: vi.fn().mockReturnValue([]),
57
+ getManagerForProject: vi.fn().mockReturnValue({
58
+ on: vi.fn(),
59
+ off: vi.fn(),
60
+ getAllSessions: vi.fn().mockReturnValue([]),
61
+ }),
62
+ },
63
+ }));
64
+ // Mock WorktreeService
65
+ vi.mock('../services/worktreeService.js', () => ({
66
+ WorktreeService: vi.fn().mockImplementation(() => ({
67
+ getWorktreesEffect: vi.fn(),
68
+ getGitRootPath: vi.fn().mockReturnValue('/test'),
69
+ })),
70
+ }));
71
+ // Mock useGitStatus to avoid async polling in tests
72
+ vi.mock('../hooks/useGitStatus.js', () => ({
73
+ useGitStatus: vi.fn((worktrees) => worktrees),
74
+ }));
75
+ // Mock SessionManager static methods
76
+ vi.mock('../services/sessionManager.js', () => ({
77
+ SessionManager: {
78
+ getSessionCounts: vi.fn().mockReturnValue({
79
+ idle: 0,
80
+ busy: 0,
81
+ waiting_input: 0,
82
+ pending_auto_approval: 0,
83
+ total: 0,
84
+ backgroundTasks: 0,
85
+ teamMembers: 0,
86
+ }),
87
+ formatSessionCounts: vi.fn().mockReturnValue(''),
88
+ },
89
+ }));
90
+ // Now import after mocking
91
+ const { default: Dashboard } = await import('./Dashboard.js');
92
+ const { projectManager } = await import('../services/projectManager.js');
93
+ const { globalSessionOrchestrator } = await import('../services/globalSessionOrchestrator.js');
94
+ const { SessionManager } = await import('../services/sessionManager.js');
95
+ const { WorktreeService } = await import('../services/worktreeService.js');
96
+ const { Effect } = await import('effect');
97
+ describe('Dashboard', () => {
98
+ const mockOnSelectSession = vi.fn();
99
+ const mockOnSelectProject = vi.fn();
100
+ const mockOnDismissError = vi.fn();
101
+ const mockProjects = [
102
+ {
103
+ name: 'my-app',
104
+ path: '/projects/my-app',
105
+ relativePath: 'my-app',
106
+ isValid: true,
107
+ },
108
+ {
109
+ name: 'api-server',
110
+ path: '/projects/api-server',
111
+ relativePath: 'api-server',
112
+ isValid: true,
113
+ },
114
+ {
115
+ name: 'shared-lib',
116
+ path: '/projects/shared-lib',
117
+ relativePath: 'shared-lib',
118
+ isValid: true,
119
+ },
120
+ ];
121
+ beforeEach(() => {
122
+ vi.clearAllMocks();
123
+ vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
124
+ vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([]);
125
+ vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue([]);
126
+ vi.mocked(globalSessionOrchestrator.getManagerForProject).mockReturnValue({
127
+ on: vi.fn(),
128
+ off: vi.fn(),
129
+ getAllSessions: vi.fn().mockReturnValue([]),
130
+ });
131
+ });
132
+ it('should render dashboard with correct title and version', () => {
133
+ const { lastFrame } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
134
+ expect(lastFrame()).toContain('CCManager - Dashboard v3.8.1');
135
+ });
136
+ it('should display loading state initially', () => {
137
+ vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.async(() => { }));
138
+ const { lastFrame } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
139
+ expect(lastFrame()).toContain('Discovering projects...');
140
+ });
141
+ it('should display projects after loading', async () => {
142
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
143
+ await new Promise(resolve => setTimeout(resolve, 100));
144
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
145
+ await vi.waitFor(() => {
146
+ const frame = lastFrame();
147
+ return frame && !frame.includes('Discovering projects...');
148
+ }, { timeout: 2000 });
149
+ const frame = lastFrame();
150
+ expect(frame).toContain('my-app');
151
+ expect(frame).toContain('api-server');
152
+ expect(frame).toContain('shared-lib');
153
+ });
154
+ it('should display Projects section separator', async () => {
155
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
156
+ await new Promise(resolve => setTimeout(resolve, 100));
157
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
158
+ await vi.waitFor(() => {
159
+ return lastFrame()?.includes('my-app') ?? false;
160
+ });
161
+ expect(lastFrame()).toContain('Projects');
162
+ });
163
+ it('should display Other section with Refresh and Exit', async () => {
164
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
165
+ await new Promise(resolve => setTimeout(resolve, 100));
166
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
167
+ await vi.waitFor(() => {
168
+ return lastFrame()?.includes('my-app') ?? false;
169
+ });
170
+ const frame = lastFrame();
171
+ expect(frame).toContain('Other');
172
+ expect(frame).toContain('Refresh');
173
+ expect(frame).toContain('Exit');
174
+ });
175
+ it('should display number shortcuts for projects', async () => {
176
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
177
+ await new Promise(resolve => setTimeout(resolve, 100));
178
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
179
+ await vi.waitFor(() => {
180
+ return lastFrame()?.includes('my-app') ?? false;
181
+ });
182
+ const frame = lastFrame();
183
+ expect(frame).toContain('0 ❯ my-app');
184
+ expect(frame).toContain('1 ❯ api-server');
185
+ expect(frame).toContain('2 ❯ shared-lib');
186
+ });
187
+ it('should display status legend', () => {
188
+ const { lastFrame } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
189
+ const frame = lastFrame();
190
+ expect(frame).toContain('Busy');
191
+ expect(frame).toContain('Waiting');
192
+ expect(frame).toContain('Idle');
193
+ });
194
+ it('should display error when provided', () => {
195
+ const { lastFrame } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: "Failed to load", onDismissError: mockOnDismissError, version: "3.8.1" }));
196
+ expect(lastFrame()).toContain('Error: Failed to load');
197
+ expect(lastFrame()).toContain('Press any key to dismiss');
198
+ });
199
+ it('should show empty state when no projects found', async () => {
200
+ vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed([]));
201
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
202
+ await new Promise(resolve => setTimeout(resolve, 100));
203
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
204
+ await vi.waitFor(() => {
205
+ const frame = lastFrame();
206
+ return frame && !frame.includes('Discovering projects...');
207
+ }, { timeout: 2000 });
208
+ expect(lastFrame()).toContain('No git repositories found in /projects');
209
+ });
210
+ it('should not show Active Sessions section when there are no sessions', async () => {
211
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
212
+ await new Promise(resolve => setTimeout(resolve, 100));
213
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
214
+ await vi.waitFor(() => {
215
+ return lastFrame()?.includes('my-app') ?? false;
216
+ });
217
+ expect(lastFrame()).not.toContain('Active Sessions');
218
+ });
219
+ it('should show Active Sessions when sessions exist', async () => {
220
+ const mockSession = {
221
+ id: 'session-1',
222
+ worktreePath: '/projects/my-app/worktrees/feature-auth',
223
+ lastActivity: new Date(),
224
+ isActive: true,
225
+ stateMutex: {
226
+ getSnapshot: () => ({
227
+ state: 'busy',
228
+ backgroundTaskCount: 0,
229
+ teamMemberCount: 0,
230
+ }),
231
+ },
232
+ };
233
+ const mockWorktrees = [
234
+ {
235
+ path: '/projects/my-app/worktrees/feature-auth',
236
+ branch: 'feature/auth',
237
+ isMainWorktree: false,
238
+ hasSession: true,
239
+ },
240
+ ];
241
+ vi.mocked(globalSessionOrchestrator.getProjectPaths).mockReturnValue([
242
+ '/projects/my-app',
243
+ ]);
244
+ vi.mocked(globalSessionOrchestrator.getProjectSessions).mockReturnValue([
245
+ mockSession,
246
+ ]);
247
+ vi.mocked(WorktreeService).mockImplementation(function () {
248
+ return {
249
+ getWorktreesEffect: () => Effect.succeed(mockWorktrees),
250
+ getGitRootPath: () => '/projects/my-app',
251
+ };
252
+ });
253
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
254
+ await new Promise(resolve => setTimeout(resolve, 200));
255
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
256
+ await vi.waitFor(() => {
257
+ const frame = lastFrame();
258
+ return frame?.includes('Active Sessions') ?? false;
259
+ }, { timeout: 3000 });
260
+ const frame = lastFrame();
261
+ expect(frame).toContain('Active Sessions');
262
+ expect(frame).toContain('my-app :: feature/auth');
263
+ expect(frame).toContain('Busy');
264
+ });
265
+ it('should show session counts next to projects', async () => {
266
+ vi.mocked(SessionManager.formatSessionCounts).mockReturnValue(' (1 Busy)');
267
+ vi.mocked(globalSessionOrchestrator.getProjectSessions).mockImplementation((path) => {
268
+ if (path === '/projects/my-app') {
269
+ return [{ id: 'session-1' }];
270
+ }
271
+ return [];
272
+ });
273
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
274
+ await new Promise(resolve => setTimeout(resolve, 100));
275
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
276
+ await vi.waitFor(() => {
277
+ return lastFrame()?.includes('my-app') ?? false;
278
+ });
279
+ expect(lastFrame()).toContain('(1 Busy)');
280
+ });
281
+ it('should use relativePath for duplicate project names', async () => {
282
+ const duplicateProjects = [
283
+ {
284
+ name: 'utils',
285
+ path: '/projects/team-a/utils',
286
+ relativePath: 'team-a/utils',
287
+ isValid: true,
288
+ },
289
+ {
290
+ name: 'utils',
291
+ path: '/projects/team-b/utils',
292
+ relativePath: 'team-b/utils',
293
+ isValid: true,
294
+ },
295
+ ];
296
+ vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(duplicateProjects));
297
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
298
+ await new Promise(resolve => setTimeout(resolve, 100));
299
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
300
+ await vi.waitFor(() => {
301
+ return lastFrame()?.includes('team-a/utils') ?? false;
302
+ });
303
+ const frame = lastFrame();
304
+ expect(frame).toContain('team-a/utils');
305
+ expect(frame).toContain('team-b/utils');
306
+ });
307
+ it('should display controls help text', () => {
308
+ const { lastFrame } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
309
+ expect(lastFrame()).toContain('Controls:');
310
+ expect(lastFrame()).toContain('0-9 Quick Select');
311
+ expect(lastFrame()).toContain('R-Refresh');
312
+ expect(lastFrame()).toContain('Q-Quit');
313
+ });
314
+ it('should handle filesystem error during project discovery', async () => {
315
+ const { FileSystemError } = await import('../types/errors.js');
316
+ vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.fail(new FileSystemError({
317
+ operation: 'read',
318
+ path: '/projects',
319
+ cause: 'Permission denied',
320
+ })));
321
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
322
+ await new Promise(resolve => setTimeout(resolve, 100));
323
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
324
+ await vi.waitFor(() => {
325
+ const frame = lastFrame();
326
+ return frame && !frame.includes('Discovering projects...');
327
+ }, { timeout: 2000 });
328
+ expect(lastFrame()).toContain('Error:');
329
+ });
330
+ it('should show recent projects first in the Projects section', async () => {
331
+ vi.mocked(projectManager.getRecentProjects).mockReturnValue([
332
+ {
333
+ path: '/projects/shared-lib',
334
+ name: 'shared-lib',
335
+ lastAccessed: Date.now(),
336
+ },
337
+ ]);
338
+ const { lastFrame, rerender } = render(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
339
+ await new Promise(resolve => setTimeout(resolve, 100));
340
+ rerender(_jsx(Dashboard, { projectsDir: "/projects", onSelectSession: mockOnSelectSession, onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError, version: "3.8.1" }));
341
+ await vi.waitFor(() => {
342
+ return lastFrame()?.includes('shared-lib') ?? false;
343
+ });
344
+ const frame = lastFrame();
345
+ // shared-lib should appear first (index 0) because it's recent
346
+ expect(frame).toContain('0 ❯ shared-lib');
347
+ });
348
+ });
@@ -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;