ccmanager 3.9.0 → 3.10.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.
@@ -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
+ });
@@ -10,6 +10,7 @@ declare class GlobalSessionOrchestrator {
10
10
  getAllActiveSessions(): Session[];
11
11
  destroyAllSessions(): void;
12
12
  destroyProjectSessions(projectPath: string): void;
13
+ getProjectPaths(): string[];
13
14
  getProjectSessions(projectPath: string): Session[];
14
15
  }
15
16
  export declare const globalSessionOrchestrator: GlobalSessionOrchestrator;
@@ -53,6 +53,9 @@ class GlobalSessionOrchestrator {
53
53
  this.projectManagers.delete(projectPath);
54
54
  }
55
55
  }
56
+ getProjectPaths() {
57
+ return Array.from(this.projectManagers.keys());
58
+ }
56
59
  getProjectSessions(projectPath) {
57
60
  const manager = this.projectManagers.get(projectPath);
58
61
  if (manager) {
@@ -32,9 +32,15 @@ export declare class ProjectManager implements IProjectManager {
32
32
  */
33
33
  private discoverDirectories;
34
34
  /**
35
- * Quick check for .git directory without running git commands
35
+ * Quick check for .git presence (directory or file) without running git commands.
36
+ * Returns true for both main repositories and worktrees.
36
37
  */
37
38
  private hasGitDirectory;
39
+ /**
40
+ * Check if a directory is a main git repository (not a worktree).
41
+ * Main repositories have .git as a directory; worktrees have .git as a file.
42
+ */
43
+ private isMainGitRepository;
38
44
  /**
39
45
  * Process directories in parallel using worker pool pattern
40
46
  */
@@ -167,12 +167,15 @@ export class ProjectManager {
167
167
  // Quick check if this is a git repository
168
168
  const hasGitDir = await this.hasGitDirectory(fullPath);
169
169
  if (hasGitDir) {
170
- // Found a git repository - add to tasks and skip subdirectories
171
- if (!seen.has(fullPath)) {
170
+ // Only add main repositories (.git is a directory),
171
+ // not worktrees (.git is a file pointing to the main repo)
172
+ const isMain = await this.isMainGitRepository(fullPath);
173
+ if (isMain && !seen.has(fullPath)) {
172
174
  seen.add(fullPath);
173
175
  tasks.push({ path: fullPath, relativePath });
174
176
  }
175
- return; // Early termination - don't walk subdirectories
177
+ // Early termination for any git-related dir
178
+ return;
176
179
  }
177
180
  // Not a git repo, continue walking subdirectories
178
181
  await walk(fullPath, depth + 1);
@@ -189,13 +192,28 @@ export class ProjectManager {
189
192
  return tasks;
190
193
  }
191
194
  /**
192
- * Quick check for .git directory without running git commands
195
+ * Quick check for .git presence (directory or file) without running git commands.
196
+ * Returns true for both main repositories and worktrees.
193
197
  */
194
198
  async hasGitDirectory(dirPath) {
195
199
  try {
196
200
  const gitPath = path.join(dirPath, '.git');
197
201
  const stats = await fs.stat(gitPath);
198
- return stats.isDirectory() || stats.isFile(); // File for worktrees
202
+ return stats.isDirectory() || stats.isFile();
203
+ }
204
+ catch {
205
+ return false;
206
+ }
207
+ }
208
+ /**
209
+ * Check if a directory is a main git repository (not a worktree).
210
+ * Main repositories have .git as a directory; worktrees have .git as a file.
211
+ */
212
+ async isMainGitRepository(dirPath) {
213
+ try {
214
+ const gitPath = path.join(dirPath, '.git');
215
+ const stats = await fs.stat(gitPath);
216
+ return stats.isDirectory();
199
217
  }
200
218
  catch {
201
219
  return false;
@@ -242,11 +260,9 @@ export class ProjectManager {
242
260
  name: path.basename(task.path),
243
261
  };
244
262
  try {
245
- // Check if directory has .git (already validated in discoverDirectories)
246
- // Double-check here to ensure it's still valid
247
- const hasGit = await this.hasGitDirectory(task.path);
248
- if (!hasGit) {
249
- // Not a git repo, return null to filter it out
263
+ // Double-check here to ensure it's still a valid main repository
264
+ const isMain = await this.isMainGitRepository(task.path);
265
+ if (!isMain) {
250
266
  return null;
251
267
  }
252
268
  }
@@ -2,7 +2,7 @@ import { Worktree, Session } from '../types/index.js';
2
2
  /**
3
3
  * Worktree item with formatted content for display.
4
4
  */
5
- interface WorktreeItem {
5
+ export interface WorktreeItem {
6
6
  worktree: Worktree;
7
7
  session?: Session;
8
8
  baseLabel: string;
@@ -39,4 +39,3 @@ export declare function calculateColumnPositions(items: WorktreeItem[]): {
39
39
  * Assembles the final worktree label with proper column alignment
40
40
  */
41
41
  export declare function assembleWorktreeLabel(item: WorktreeItem, columns: ReturnType<typeof calculateColumnPositions>): string;
42
- export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccmanager",
3
- "version": "3.9.0",
3
+ "version": "3.10.0",
4
4
  "description": "TUI application for managing multiple Claude Code sessions across Git worktrees",
5
5
  "license": "MIT",
6
6
  "author": "Kodai Kabasawa",
@@ -41,11 +41,11 @@
41
41
  "bin"
42
42
  ],
43
43
  "optionalDependencies": {
44
- "@kodaikabasawa/ccmanager-darwin-arm64": "3.9.0",
45
- "@kodaikabasawa/ccmanager-darwin-x64": "3.9.0",
46
- "@kodaikabasawa/ccmanager-linux-arm64": "3.9.0",
47
- "@kodaikabasawa/ccmanager-linux-x64": "3.9.0",
48
- "@kodaikabasawa/ccmanager-win32-x64": "3.9.0"
44
+ "@kodaikabasawa/ccmanager-darwin-arm64": "3.10.0",
45
+ "@kodaikabasawa/ccmanager-darwin-x64": "3.10.0",
46
+ "@kodaikabasawa/ccmanager-linux-arm64": "3.10.0",
47
+ "@kodaikabasawa/ccmanager-linux-x64": "3.10.0",
48
+ "@kodaikabasawa/ccmanager-win32-x64": "3.10.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@eslint/js": "^9.28.0",
@@ -1,10 +0,0 @@
1
- import React from 'react';
2
- import { GitProject } from '../types/index.js';
3
- interface ProjectListProps {
4
- projectsDir: string;
5
- onSelectProject: (project: GitProject) => void;
6
- error: string | null;
7
- onDismissError: () => void;
8
- }
9
- declare const ProjectList: React.FC<ProjectListProps>;
10
- export default ProjectList;