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
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState, useEffect } from 'react';
|
|
3
|
-
import { Box, Text, useInput } from 'ink';
|
|
4
|
-
import { Effect } from 'effect';
|
|
5
|
-
import SelectInput from 'ink-select-input';
|
|
6
|
-
import { projectManager } from '../services/projectManager.js';
|
|
7
|
-
import { MENU_ICONS } from '../constants/statusIcons.js';
|
|
8
|
-
import TextInputWrapper from './TextInputWrapper.js';
|
|
9
|
-
import { useSearchMode } from '../hooks/useSearchMode.js';
|
|
10
|
-
import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
|
|
11
|
-
import { SessionManager } from '../services/sessionManager.js';
|
|
12
|
-
const ProjectList = ({ projectsDir, onSelectProject, error, onDismissError, }) => {
|
|
13
|
-
const [projects, setProjects] = useState([]);
|
|
14
|
-
const [recentProjects, setRecentProjects] = useState([]);
|
|
15
|
-
const [items, setItems] = useState([]);
|
|
16
|
-
const [loading, setLoading] = useState(true);
|
|
17
|
-
const [loadError, setLoadError] = useState(null);
|
|
18
|
-
const limit = 10;
|
|
19
|
-
// Helper function to format error messages based on error type using _tag discrimination
|
|
20
|
-
const formatErrorMessage = (error) => {
|
|
21
|
-
switch (error._tag) {
|
|
22
|
-
case 'ProcessError':
|
|
23
|
-
return `Process error: ${error.message}`;
|
|
24
|
-
case 'ConfigError':
|
|
25
|
-
return `Configuration error (${error.reason}): ${error.details}`;
|
|
26
|
-
case 'GitError':
|
|
27
|
-
return `Git command failed: ${error.command} (exit ${error.exitCode})\n${error.stderr}`;
|
|
28
|
-
case 'FileSystemError':
|
|
29
|
-
return `File ${error.operation} failed for ${error.path}: ${error.cause}`;
|
|
30
|
-
case 'ValidationError':
|
|
31
|
-
return `Validation failed for ${error.field}: ${error.constraint}`;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
// Use the search mode hook
|
|
35
|
-
const displayError = error || loadError;
|
|
36
|
-
const { isSearchMode, searchQuery, selectedIndex, setSearchQuery } = useSearchMode(items.length, {
|
|
37
|
-
isDisabled: !!displayError,
|
|
38
|
-
skipInTest: false,
|
|
39
|
-
});
|
|
40
|
-
// Helper function to load projects with Effect-based error handling
|
|
41
|
-
const loadProjectsEffect = async (checkCancellation) => {
|
|
42
|
-
setLoading(true);
|
|
43
|
-
setLoadError(null);
|
|
44
|
-
// Use Effect-based project discovery
|
|
45
|
-
const projectsEffect = projectManager.instance.discoverProjectsEffect(projectsDir);
|
|
46
|
-
// Execute the Effect and handle both success and failure cases
|
|
47
|
-
const result = await Effect.runPromise(Effect.either(projectsEffect));
|
|
48
|
-
// Check cancellation flag before updating state (if provided)
|
|
49
|
-
if (checkCancellation && checkCancellation())
|
|
50
|
-
return;
|
|
51
|
-
if (result._tag === 'Left') {
|
|
52
|
-
// Handle error using pattern matching on _tag
|
|
53
|
-
const errorMessage = formatErrorMessage(result.left);
|
|
54
|
-
setLoadError(errorMessage);
|
|
55
|
-
setLoading(false);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
// Success case - extract projects from Right
|
|
59
|
-
const discoveredProjects = result.right;
|
|
60
|
-
setProjects(discoveredProjects);
|
|
61
|
-
// Load recent projects with no limit (pass 0)
|
|
62
|
-
const allRecentProjects = projectManager.getRecentProjects(0);
|
|
63
|
-
setRecentProjects(allRecentProjects);
|
|
64
|
-
setLoading(false);
|
|
65
|
-
};
|
|
66
|
-
const loadProjects = () => loadProjectsEffect();
|
|
67
|
-
useEffect(() => {
|
|
68
|
-
let cancelled = false;
|
|
69
|
-
loadProjectsEffect(() => cancelled);
|
|
70
|
-
// Cleanup function to set cancellation flag
|
|
71
|
-
return () => {
|
|
72
|
-
cancelled = true;
|
|
73
|
-
};
|
|
74
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
-
}, [projectsDir]);
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
const menuItems = [];
|
|
78
|
-
let currentIndex = 0;
|
|
79
|
-
// Filter recent projects based on search query
|
|
80
|
-
const filteredRecentProjects = searchQuery
|
|
81
|
-
? recentProjects.filter(project => project.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
82
|
-
: recentProjects;
|
|
83
|
-
// Add recent projects section if available and not in search mode
|
|
84
|
-
if (filteredRecentProjects.length > 0) {
|
|
85
|
-
// Add "Recent" separator only when not in search mode
|
|
86
|
-
if (!isSearchMode) {
|
|
87
|
-
menuItems.push({
|
|
88
|
-
label: '── Recent ──',
|
|
89
|
-
value: 'separator-recent',
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
// Add recent projects
|
|
93
|
-
filteredRecentProjects.forEach(recentProject => {
|
|
94
|
-
// Find the full project data
|
|
95
|
-
const fullProject = projects.find(p => p.path === recentProject.path);
|
|
96
|
-
if (fullProject) {
|
|
97
|
-
// Get session counts for this project
|
|
98
|
-
const projectSessions = globalSessionOrchestrator.getProjectSessions(recentProject.path);
|
|
99
|
-
const counts = SessionManager.getSessionCounts(projectSessions);
|
|
100
|
-
const countsFormatted = SessionManager.formatSessionCounts(counts);
|
|
101
|
-
const numberPrefix = !isSearchMode && currentIndex < 10 ? `${currentIndex} ❯ ` : '❯ ';
|
|
102
|
-
menuItems.push({
|
|
103
|
-
label: numberPrefix + recentProject.name + countsFormatted,
|
|
104
|
-
value: recentProject.path,
|
|
105
|
-
project: fullProject,
|
|
106
|
-
});
|
|
107
|
-
currentIndex++;
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
// Filter projects based on search query
|
|
112
|
-
const filteredProjects = searchQuery
|
|
113
|
-
? projects.filter(project => project.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
114
|
-
: projects;
|
|
115
|
-
// Filter out recent projects from all projects to avoid duplicates
|
|
116
|
-
const recentPaths = new Set(filteredRecentProjects.map(rp => rp.path));
|
|
117
|
-
const nonRecentProjects = filteredProjects.filter(project => !recentPaths.has(project.path));
|
|
118
|
-
// Add "All Projects" separator if we have both recent and other projects
|
|
119
|
-
if (filteredRecentProjects.length > 0 &&
|
|
120
|
-
nonRecentProjects.length > 0 &&
|
|
121
|
-
!isSearchMode) {
|
|
122
|
-
menuItems.push({
|
|
123
|
-
label: '── All Projects ──',
|
|
124
|
-
value: 'separator-all',
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
// Build menu items from filtered non-recent projects
|
|
128
|
-
nonRecentProjects.forEach(project => {
|
|
129
|
-
// Get session counts for this project
|
|
130
|
-
const projectSessions = globalSessionOrchestrator.getProjectSessions(project.path);
|
|
131
|
-
const counts = SessionManager.getSessionCounts(projectSessions);
|
|
132
|
-
const countsFormatted = SessionManager.formatSessionCounts(counts);
|
|
133
|
-
// Only show numbers for total items (0-9) when not in search mode
|
|
134
|
-
const numberPrefix = !isSearchMode && currentIndex < 10 ? `${currentIndex} ❯ ` : '❯ ';
|
|
135
|
-
menuItems.push({
|
|
136
|
-
label: numberPrefix + project.name + countsFormatted,
|
|
137
|
-
value: project.path,
|
|
138
|
-
project,
|
|
139
|
-
});
|
|
140
|
-
currentIndex++;
|
|
141
|
-
});
|
|
142
|
-
// Add menu options only when not in search mode
|
|
143
|
-
if (!isSearchMode) {
|
|
144
|
-
if (projects.length > 0) {
|
|
145
|
-
menuItems.push({
|
|
146
|
-
label: '─────────────',
|
|
147
|
-
value: 'separator',
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
menuItems.push({
|
|
151
|
-
label: `R 🔄 Refresh`,
|
|
152
|
-
value: 'refresh',
|
|
153
|
-
});
|
|
154
|
-
menuItems.push({
|
|
155
|
-
label: `Q ${MENU_ICONS.EXIT} Exit`,
|
|
156
|
-
value: 'exit',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
setItems(menuItems);
|
|
160
|
-
}, [projects, recentProjects, searchQuery, isSearchMode]);
|
|
161
|
-
// Handle hotkeys
|
|
162
|
-
useInput((input, _key) => {
|
|
163
|
-
// Skip in test environment to avoid stdin.ref error
|
|
164
|
-
if (!process.stdin.setRawMode) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
// Dismiss error on any key press when error is shown
|
|
168
|
-
if (displayError && onDismissError) {
|
|
169
|
-
onDismissError();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
// Don't process other keys if in search mode (handled by useSearchMode)
|
|
173
|
-
if (isSearchMode) {
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const keyPressed = input.toLowerCase();
|
|
177
|
-
// Handle number keys 0-9 for project selection
|
|
178
|
-
if (/^[0-9]$/.test(keyPressed)) {
|
|
179
|
-
const index = parseInt(keyPressed);
|
|
180
|
-
// Get all selectable items (recent + non-recent projects)
|
|
181
|
-
const selectableItems = items.filter(item => item.project);
|
|
182
|
-
if (index < Math.min(10, selectableItems.length) &&
|
|
183
|
-
selectableItems[index]?.project) {
|
|
184
|
-
onSelectProject(selectableItems[index].project);
|
|
185
|
-
}
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
switch (keyPressed) {
|
|
189
|
-
case 'r':
|
|
190
|
-
// Refresh project list
|
|
191
|
-
loadProjects();
|
|
192
|
-
break;
|
|
193
|
-
case 'q':
|
|
194
|
-
case 'x':
|
|
195
|
-
// Trigger exit action
|
|
196
|
-
onSelectProject({
|
|
197
|
-
path: 'EXIT_APPLICATION',
|
|
198
|
-
name: '',
|
|
199
|
-
relativePath: '',
|
|
200
|
-
isValid: false,
|
|
201
|
-
});
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
const handleSelect = (item) => {
|
|
206
|
-
if (item.value.startsWith('separator')) {
|
|
207
|
-
// Do nothing for separators
|
|
208
|
-
}
|
|
209
|
-
else if (item.value === 'refresh') {
|
|
210
|
-
loadProjects();
|
|
211
|
-
}
|
|
212
|
-
else if (item.value === 'exit') {
|
|
213
|
-
// Handle exit
|
|
214
|
-
onSelectProject({
|
|
215
|
-
path: 'EXIT_APPLICATION',
|
|
216
|
-
name: '',
|
|
217
|
-
relativePath: '',
|
|
218
|
-
isValid: false,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
else if (item.project) {
|
|
222
|
-
onSelectProject(item.project);
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "CCManager - Multi-Project Mode" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Select a project:" }) }), isSearchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { children: "Search: " }), _jsx(TextInputWrapper, { value: searchQuery, onChange: setSearchQuery, focus: true, placeholder: "Type to filter projects..." })] })), loading ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "Loading projects..." }) })) : projects.length === 0 && !displayError ? (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: ["No git repositories found in ", projectsDir] }) })) : isSearchMode && items.length === 0 ? (_jsx(Box, { children: _jsx(Text, { color: "yellow", children: "No projects match your search" }) })) : isSearchMode ? (
|
|
226
|
-
// In search mode, show the items as a list without SelectInput
|
|
227
|
-
_jsx(Box, { flexDirection: "column", children: items.slice(0, limit).map((item, index) => (_jsxs(Text, { color: index === selectedIndex ? 'green' : undefined, children: [index === selectedIndex ? '❯ ' : ' ', item.label] }, item.value))) })) : (_jsx(SelectInput, { items: items, onSelect: handleSelect, isFocused: !displayError, limit: limit, initialIndex: selectedIndex })), displayError && (_jsx(Box, { marginTop: 1, paddingX: 1, borderStyle: "round", borderColor: "red", children: _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", bold: true, children: ["Error: ", displayError] }), _jsx(Text, { color: "gray", dimColor: true, children: "Press any key to dismiss" })] }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [(isSearchMode || searchQuery) && (_jsxs(Text, { dimColor: true, children: ["Projects: ", items.filter(item => item.project).length, " of", ' ', projects.length, " shown"] })), _jsx(Text, { dimColor: true, children: isSearchMode
|
|
228
|
-
? 'Search Mode: Type to filter, Enter to exit search, ESC to exit search'
|
|
229
|
-
: searchQuery
|
|
230
|
-
? `Filtered: "${searchQuery}" | ↑↓ Navigate Enter Select | /-Search ESC-Clear 0-9 Quick Select R-Refresh Q-Quit`
|
|
231
|
-
: 'Controls: ↑↓ Navigate Enter Select | Hotkeys: 0-9 Quick Select /-Search R-Refresh Q-Quit' })] })] }));
|
|
232
|
-
};
|
|
233
|
-
export default ProjectList;
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { render } from 'ink-testing-library';
|
|
3
|
-
import { expect, describe, it, vi, beforeEach, afterEach } from 'vitest';
|
|
4
|
-
import ProjectList from './ProjectList.js';
|
|
5
|
-
import { projectManager } from '../services/projectManager.js';
|
|
6
|
-
import { Effect } from 'effect';
|
|
7
|
-
// Mock bunTerminal to avoid native module loading issues
|
|
8
|
-
vi.mock('../services/bunTerminal.js', () => ({
|
|
9
|
-
spawn: vi.fn(function () {
|
|
10
|
-
return null;
|
|
11
|
-
}),
|
|
12
|
-
}));
|
|
13
|
-
// Mock ink to avoid stdin.ref issues
|
|
14
|
-
vi.mock('ink', async () => {
|
|
15
|
-
const actual = await vi.importActual('ink');
|
|
16
|
-
return {
|
|
17
|
-
...actual,
|
|
18
|
-
useInput: vi.fn(),
|
|
19
|
-
};
|
|
20
|
-
});
|
|
21
|
-
// Mock SelectInput to render items as simple text
|
|
22
|
-
vi.mock('ink-select-input', async () => {
|
|
23
|
-
const React = await vi.importActual('react');
|
|
24
|
-
const { Text, Box } = await vi.importActual('ink');
|
|
25
|
-
return {
|
|
26
|
-
default: ({ items }) => {
|
|
27
|
-
return (_jsx(Box, { flexDirection: "column", children: items.map(item => (_jsx(Text, { children: item.label }, item.value))) }));
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
});
|
|
31
|
-
vi.mock('../services/projectManager.js', () => ({
|
|
32
|
-
projectManager: {
|
|
33
|
-
instance: {
|
|
34
|
-
discoverProjectsEffect: vi.fn(),
|
|
35
|
-
},
|
|
36
|
-
getRecentProjects: vi.fn(),
|
|
37
|
-
},
|
|
38
|
-
}));
|
|
39
|
-
describe('ProjectList - Recent Projects', () => {
|
|
40
|
-
const mockOnSelectProject = vi.fn();
|
|
41
|
-
const mockOnDismissError = vi.fn();
|
|
42
|
-
let originalSetRawMode;
|
|
43
|
-
const createProject = (name, path) => ({
|
|
44
|
-
name,
|
|
45
|
-
path,
|
|
46
|
-
relativePath: `./${name}`,
|
|
47
|
-
isValid: true,
|
|
48
|
-
});
|
|
49
|
-
const mockProjects = [
|
|
50
|
-
createProject('project-a', '/home/user/projects/project-a'),
|
|
51
|
-
createProject('project-b', '/home/user/projects/project-b'),
|
|
52
|
-
createProject('project-c', '/home/user/projects/project-c'),
|
|
53
|
-
createProject('project-d', '/home/user/projects/project-d'),
|
|
54
|
-
createProject('project-e', '/home/user/projects/project-e'),
|
|
55
|
-
];
|
|
56
|
-
beforeEach(() => {
|
|
57
|
-
vi.clearAllMocks();
|
|
58
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(mockProjects));
|
|
59
|
-
vi.mocked(projectManager.getRecentProjects).mockReturnValue([]);
|
|
60
|
-
// Mock stdin.setRawMode
|
|
61
|
-
originalSetRawMode = process.stdin.setRawMode;
|
|
62
|
-
process.stdin.setRawMode = vi.fn();
|
|
63
|
-
});
|
|
64
|
-
afterEach(() => {
|
|
65
|
-
// Restore original setRawMode
|
|
66
|
-
process.stdin.setRawMode = originalSetRawMode;
|
|
67
|
-
});
|
|
68
|
-
it('should display recent projects at the top when available', async () => {
|
|
69
|
-
// Mock recent projects
|
|
70
|
-
vi.mocked(projectManager.getRecentProjects).mockReturnValue([
|
|
71
|
-
{
|
|
72
|
-
name: 'project-c',
|
|
73
|
-
path: '/home/user/projects/project-c',
|
|
74
|
-
lastAccessed: Date.now() - 1000,
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: 'project-e',
|
|
78
|
-
path: '/home/user/projects/project-e',
|
|
79
|
-
lastAccessed: Date.now() - 2000,
|
|
80
|
-
},
|
|
81
|
-
]);
|
|
82
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
83
|
-
// Wait for projects to load
|
|
84
|
-
await vi.waitFor(() => {
|
|
85
|
-
expect(lastFrame()).toContain('project-c');
|
|
86
|
-
});
|
|
87
|
-
// Check that recent projects section is shown
|
|
88
|
-
expect(lastFrame()).toContain('Recent');
|
|
89
|
-
// Check that recent projects are at the top
|
|
90
|
-
const output = lastFrame();
|
|
91
|
-
const projectCIndex = output?.indexOf('project-c') ?? -1;
|
|
92
|
-
const projectEIndex = output?.indexOf('project-e') ?? -1;
|
|
93
|
-
const allProjectsIndex = output?.indexOf('All Projects') ?? -1;
|
|
94
|
-
// Recent projects should appear before "All Projects" section
|
|
95
|
-
expect(projectCIndex).toBeLessThan(allProjectsIndex);
|
|
96
|
-
expect(projectEIndex).toBeLessThan(allProjectsIndex);
|
|
97
|
-
});
|
|
98
|
-
it('should not show recent projects section when there are none', async () => {
|
|
99
|
-
vi.mocked(projectManager.getRecentProjects).mockReturnValue([]);
|
|
100
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
101
|
-
// Wait for projects to load
|
|
102
|
-
await vi.waitFor(() => {
|
|
103
|
-
expect(lastFrame()).toContain('project-a');
|
|
104
|
-
});
|
|
105
|
-
// Check that recent projects section is not shown
|
|
106
|
-
expect(lastFrame()).not.toContain('Recent');
|
|
107
|
-
});
|
|
108
|
-
it('should handle selection of recent projects', async () => {
|
|
109
|
-
// Skip this test for now - SelectInput interaction is complex to test
|
|
110
|
-
// The selection functionality is covered by the number key test
|
|
111
|
-
});
|
|
112
|
-
it('should filter recent projects based on search query', async () => {
|
|
113
|
-
// This functionality is tested in the main ProjectList.test.tsx
|
|
114
|
-
// Skip in recent projects specific tests to avoid complexity
|
|
115
|
-
});
|
|
116
|
-
it('should show recent projects with correct number prefixes', async () => {
|
|
117
|
-
// Mock recent projects that match existing projects
|
|
118
|
-
vi.mocked(projectManager.getRecentProjects).mockReturnValue([
|
|
119
|
-
{
|
|
120
|
-
name: 'project-c',
|
|
121
|
-
path: '/home/user/projects/project-c',
|
|
122
|
-
lastAccessed: Date.now() - 1000,
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
name: 'project-e',
|
|
126
|
-
path: '/home/user/projects/project-e',
|
|
127
|
-
lastAccessed: Date.now() - 2000,
|
|
128
|
-
},
|
|
129
|
-
]);
|
|
130
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
131
|
-
// Wait for projects to load
|
|
132
|
-
await vi.waitFor(() => {
|
|
133
|
-
expect(lastFrame()).toContain('project-c');
|
|
134
|
-
});
|
|
135
|
-
// Check that recent projects have correct number prefixes
|
|
136
|
-
const output = lastFrame();
|
|
137
|
-
expect(output).toContain('0 ❯ project-c');
|
|
138
|
-
expect(output).toContain('1 ❯ project-e');
|
|
139
|
-
// Check that regular projects start from the next available number
|
|
140
|
-
expect(output).toContain('2 ❯ project-a');
|
|
141
|
-
});
|
|
142
|
-
it('should show all recent projects without limit', async () => {
|
|
143
|
-
// Create 10 projects
|
|
144
|
-
const manyProjects = Array.from({ length: 10 }, (_, i) => createProject(`project-${i}`, `/home/user/projects/project-${i}`));
|
|
145
|
-
// Mock discovered projects
|
|
146
|
-
vi.mocked(projectManager.instance.discoverProjectsEffect).mockReturnValue(Effect.succeed(manyProjects));
|
|
147
|
-
// Mock more than 5 recent projects
|
|
148
|
-
const manyRecentProjects = Array.from({ length: 10 }, (_, i) => ({
|
|
149
|
-
name: `project-${i}`,
|
|
150
|
-
path: `/home/user/projects/project-${i}`,
|
|
151
|
-
lastAccessed: Date.now() - i * 1000,
|
|
152
|
-
}));
|
|
153
|
-
vi.mocked(projectManager.getRecentProjects).mockReturnValue(manyRecentProjects);
|
|
154
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
155
|
-
// Wait for projects to load
|
|
156
|
-
await vi.waitFor(() => {
|
|
157
|
-
expect(lastFrame()).toContain('project-0');
|
|
158
|
-
});
|
|
159
|
-
// Check that all 10 recent projects are shown (not limited to 5)
|
|
160
|
-
const output = lastFrame();
|
|
161
|
-
for (let i = 0; i < 10; i++) {
|
|
162
|
-
expect(output).toContain(`project-${i}`);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
it('should allow number key selection for recent projects', async () => {
|
|
166
|
-
// Mock recent projects
|
|
167
|
-
vi.mocked(projectManager.getRecentProjects).mockReturnValue([
|
|
168
|
-
{
|
|
169
|
-
name: 'project-c',
|
|
170
|
-
path: '/home/user/projects/project-c',
|
|
171
|
-
lastAccessed: Date.now(),
|
|
172
|
-
},
|
|
173
|
-
]);
|
|
174
|
-
// Mock the useInput hook to capture the handler
|
|
175
|
-
const mockUseInput = vi.mocked(await import('ink')).useInput;
|
|
176
|
-
let inputHandler = () => { };
|
|
177
|
-
mockUseInput.mockImplementation(handler => {
|
|
178
|
-
inputHandler = handler;
|
|
179
|
-
});
|
|
180
|
-
const { lastFrame } = render(_jsx(ProjectList, { projectsDir: "/home/user/projects", onSelectProject: mockOnSelectProject, error: null, onDismissError: mockOnDismissError }));
|
|
181
|
-
// Wait for projects to load
|
|
182
|
-
await vi.waitFor(() => {
|
|
183
|
-
expect(lastFrame()).toContain('project-c');
|
|
184
|
-
});
|
|
185
|
-
// Simulate pressing 0 to select first recent project
|
|
186
|
-
inputHandler('0');
|
|
187
|
-
// Check that the recent project was selected
|
|
188
|
-
expect(mockOnSelectProject).toHaveBeenCalledWith(expect.objectContaining({
|
|
189
|
-
name: 'project-c',
|
|
190
|
-
path: '/home/user/projects/project-c',
|
|
191
|
-
}));
|
|
192
|
-
});
|
|
193
|
-
});
|