ccmanager 1.4.5 → 2.0.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/README.md +34 -1
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +30 -2
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +67 -0
- package/dist/components/App.d.ts +1 -0
- package/dist/components/App.js +107 -37
- package/dist/components/Menu.d.ts +6 -1
- package/dist/components/Menu.js +227 -50
- package/dist/components/Menu.recent-projects.test.d.ts +1 -0
- package/dist/components/Menu.recent-projects.test.js +159 -0
- package/dist/components/Menu.test.d.ts +1 -0
- package/dist/components/Menu.test.js +196 -0
- package/dist/components/ProjectList.d.ts +10 -0
- package/dist/components/ProjectList.js +231 -0
- package/dist/components/ProjectList.recent-projects.test.d.ts +1 -0
- package/dist/components/ProjectList.recent-projects.test.js +186 -0
- package/dist/components/ProjectList.test.d.ts +1 -0
- package/dist/components/ProjectList.test.js +501 -0
- package/dist/constants/env.d.ts +3 -0
- package/dist/constants/env.js +4 -0
- package/dist/constants/error.d.ts +6 -0
- package/dist/constants/error.js +7 -0
- package/dist/hooks/useSearchMode.d.ts +15 -0
- package/dist/hooks/useSearchMode.js +67 -0
- package/dist/services/configurationManager.d.ts +1 -0
- package/dist/services/configurationManager.js +14 -7
- package/dist/services/globalSessionOrchestrator.d.ts +16 -0
- package/dist/services/globalSessionOrchestrator.js +73 -0
- package/dist/services/globalSessionOrchestrator.test.d.ts +1 -0
- package/dist/services/globalSessionOrchestrator.test.js +180 -0
- package/dist/services/projectManager.d.ts +60 -0
- package/dist/services/projectManager.js +418 -0
- package/dist/services/projectManager.test.d.ts +1 -0
- package/dist/services/projectManager.test.js +342 -0
- package/dist/services/sessionManager.d.ts +8 -0
- package/dist/services/sessionManager.js +38 -0
- package/dist/services/sessionManager.test.js +79 -0
- package/dist/services/worktreeService.d.ts +1 -0
- package/dist/services/worktreeService.js +20 -5
- package/dist/services/worktreeService.test.js +72 -0
- package/dist/types/index.d.ts +55 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CCManager - AI Code Assistant Session Manager
|
|
2
2
|
|
|
3
|
-
CCManager is a TUI application for managing multiple AI coding assistant sessions (Claude Code, Gemini CLI) across Git worktrees.
|
|
3
|
+
CCManager is a TUI application for managing multiple AI coding assistant sessions (Claude Code, Gemini CLI) across Git worktrees and projects.
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ https://github.com/user-attachments/assets/15914a88-e288-4ac9-94d5-8127f2e19dbf
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- Run multiple AI assistant sessions in parallel across different Git worktrees
|
|
14
|
+
- **Multi-project support**: Manage multiple git repositories from a single interface
|
|
14
15
|
- Support for multiple AI coding assistants (Claude Code, Gemini CLI)
|
|
15
16
|
- Switch between sessions seamlessly
|
|
16
17
|
- Visual status indicators for session states (busy, waiting, idle)
|
|
@@ -243,6 +244,38 @@ The devcontainer integration requires both commands:
|
|
|
243
244
|
|
|
244
245
|
For detailed setup and configuration, see [docs/devcontainer.md](docs/devcontainer.md).
|
|
245
246
|
|
|
247
|
+
## Multi-Project Mode
|
|
248
|
+
|
|
249
|
+
CCManager can manage multiple git repositories from a single interface, allowing you to organize and navigate between different projects and their worktrees efficiently.
|
|
250
|
+
|
|
251
|
+
### Quick Start
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Set the root directory containing your git projects
|
|
255
|
+
export CCMANAGER_MULTI_PROJECT_ROOT="/path/to/your/projects"
|
|
256
|
+
|
|
257
|
+
# Run CCManager in multi-project mode
|
|
258
|
+
npx ccmanager --multi-project
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Features
|
|
262
|
+
|
|
263
|
+
- **Automatic project discovery**: Recursively finds all git repositories
|
|
264
|
+
- **Recent projects**: Frequently used projects appear at the top
|
|
265
|
+
- **Vi-like search**: Press `/` to filter projects or worktrees
|
|
266
|
+
- **Session persistence**: Sessions remain active when switching projects
|
|
267
|
+
- **Visual indicators**: See session counts `[active/busy/waiting]` for each project
|
|
268
|
+
|
|
269
|
+
### Navigation
|
|
270
|
+
|
|
271
|
+
1. **Project List**: Select from all discovered git repositories
|
|
272
|
+
2. **Worktree Menu**: Manage worktrees for the selected project
|
|
273
|
+
3. **Session View**: Interact with your AI assistant
|
|
274
|
+
|
|
275
|
+
Use `B` key to navigate back from worktrees to project list.
|
|
276
|
+
|
|
277
|
+
For detailed configuration and usage, see [docs/multi-project.md](docs/multi-project.md).
|
|
278
|
+
|
|
246
279
|
## Git Worktree Configuration
|
|
247
280
|
|
|
248
281
|
CCManager can display enhanced git status information for each worktree when Git's worktree configuration extension is enabled.
|
package/dist/cli.d.ts
CHANGED
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { render } from 'ink';
|
|
|
4
4
|
import meow from 'meow';
|
|
5
5
|
import App from './components/App.js';
|
|
6
6
|
import { worktreeConfigManager } from './services/worktreeConfigManager.js';
|
|
7
|
+
import { globalSessionOrchestrator } from './services/globalSessionOrchestrator.js';
|
|
7
8
|
const cli = meow(`
|
|
8
9
|
Usage
|
|
9
10
|
$ ccmanager
|
|
@@ -11,15 +12,21 @@ const cli = meow(`
|
|
|
11
12
|
Options
|
|
12
13
|
--help Show help
|
|
13
14
|
--version Show version
|
|
15
|
+
--multi-project Enable multi-project mode
|
|
14
16
|
--devc-up-command Command to start devcontainer
|
|
15
17
|
--devc-exec-command Command to execute in devcontainer
|
|
16
18
|
|
|
17
19
|
Examples
|
|
18
20
|
$ ccmanager
|
|
21
|
+
$ ccmanager --multi-project
|
|
19
22
|
$ ccmanager --devc-up-command "devcontainer up --workspace-folder ." --devc-exec-command "devcontainer exec --workspace-folder ."
|
|
20
23
|
`, {
|
|
21
24
|
importMeta: import.meta,
|
|
22
25
|
flags: {
|
|
26
|
+
multiProject: {
|
|
27
|
+
type: 'boolean',
|
|
28
|
+
default: false,
|
|
29
|
+
},
|
|
23
30
|
devcUpCommand: {
|
|
24
31
|
type: 'string',
|
|
25
32
|
},
|
|
@@ -38,6 +45,13 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
|
38
45
|
console.error('Error: ccmanager must be run in an interactive terminal (TTY)');
|
|
39
46
|
process.exit(1);
|
|
40
47
|
}
|
|
48
|
+
// Check for CCMANAGER_MULTI_PROJECT_ROOT when using --multi-project
|
|
49
|
+
if (cli.flags.multiProject && !process.env['CCMANAGER_MULTI_PROJECT_ROOT']) {
|
|
50
|
+
console.error('Error: CCMANAGER_MULTI_PROJECT_ROOT environment variable must be set when using --multi-project');
|
|
51
|
+
console.error('Please set it to the root directory containing your projects, e.g.:');
|
|
52
|
+
console.error(' export CCMANAGER_MULTI_PROJECT_ROOT=/path/to/projects');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
41
55
|
// Initialize worktree config manager
|
|
42
56
|
worktreeConfigManager.initialize();
|
|
43
57
|
// Prepare devcontainer config
|
|
@@ -48,7 +62,21 @@ const devcontainerConfig = cli.flags.devcUpCommand && cli.flags.devcExecCommand
|
|
|
48
62
|
}
|
|
49
63
|
: undefined;
|
|
50
64
|
// Pass config to App
|
|
51
|
-
const appProps =
|
|
52
|
-
|
|
65
|
+
const appProps = {
|
|
66
|
+
...(devcontainerConfig ? { devcontainerConfig } : {}),
|
|
67
|
+
multiProject: cli.flags.multiProject,
|
|
68
|
+
};
|
|
69
|
+
const app = render(React.createElement(App, { ...appProps }));
|
|
70
|
+
// Clean up sessions on exit
|
|
71
|
+
process.on('SIGINT', () => {
|
|
72
|
+
globalSessionOrchestrator.destroyAllSessions();
|
|
73
|
+
app.unmount();
|
|
74
|
+
process.exit(0);
|
|
75
|
+
});
|
|
76
|
+
process.on('SIGTERM', () => {
|
|
77
|
+
globalSessionOrchestrator.destroyAllSessions();
|
|
78
|
+
app.unmount();
|
|
79
|
+
process.exit(0);
|
|
80
|
+
});
|
|
53
81
|
// Export for testing
|
|
54
82
|
export const parsedArgs = cli;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.test.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
describe('CLI', () => {
|
|
9
|
+
let originalEnv;
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
originalEnv = { ...process.env };
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
process.env = originalEnv;
|
|
15
|
+
});
|
|
16
|
+
describe('--multi-project flag', () => {
|
|
17
|
+
it('should exit with error when CCMANAGER_MULTI_PROJECT_ROOT is not set', async () => {
|
|
18
|
+
// Ensure the env var is not set
|
|
19
|
+
delete process.env['CCMANAGER_MULTI_PROJECT_ROOT'];
|
|
20
|
+
// Create a wrapper script that mocks TTY
|
|
21
|
+
const wrapperScript = `
|
|
22
|
+
process.stdin.isTTY = true;
|
|
23
|
+
process.stdout.isTTY = true;
|
|
24
|
+
process.stderr.isTTY = true;
|
|
25
|
+
process.argv = ['node', 'cli.js', '--multi-project'];
|
|
26
|
+
import('./cli.js');
|
|
27
|
+
`;
|
|
28
|
+
const result = await new Promise(resolve => {
|
|
29
|
+
const proc = spawn('node', ['--input-type=module', '-e', wrapperScript], {
|
|
30
|
+
cwd: path.join(__dirname, '../dist'),
|
|
31
|
+
env: { ...process.env },
|
|
32
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
let stderr = '';
|
|
35
|
+
proc.stderr?.on('data', data => {
|
|
36
|
+
stderr += data.toString();
|
|
37
|
+
});
|
|
38
|
+
proc.on('close', code => {
|
|
39
|
+
resolve({ code: code ?? 1, stderr });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
expect(result.code).toBe(1);
|
|
43
|
+
expect(result.stderr).toContain('CCMANAGER_MULTI_PROJECT_ROOT environment variable must be set');
|
|
44
|
+
expect(result.stderr).toContain('export CCMANAGER_MULTI_PROJECT_ROOT=/path/to/projects');
|
|
45
|
+
});
|
|
46
|
+
it('should not check for env var when --multi-project is not used', async () => {
|
|
47
|
+
// Ensure the env var is not set
|
|
48
|
+
delete process.env['CCMANAGER_MULTI_PROJECT_ROOT'];
|
|
49
|
+
const result = await new Promise(resolve => {
|
|
50
|
+
const cliPath = path.join(__dirname, '../dist/cli.js');
|
|
51
|
+
const proc = spawn('node', [cliPath, '--help'], {
|
|
52
|
+
env: { ...process.env },
|
|
53
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
54
|
+
});
|
|
55
|
+
let stderr = '';
|
|
56
|
+
proc.stderr?.on('data', data => {
|
|
57
|
+
stderr += data.toString();
|
|
58
|
+
});
|
|
59
|
+
proc.on('close', code => {
|
|
60
|
+
resolve({ code: code ?? 1, stderr });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
expect(result.code).toBe(0);
|
|
64
|
+
expect(result.stderr).not.toContain('CCMANAGER_MULTI_PROJECT_ROOT');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
package/dist/components/App.d.ts
CHANGED
package/dist/components/App.js
CHANGED
|
@@ -1,25 +1,46 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { useApp, Box, Text } from 'ink';
|
|
3
3
|
import Menu from './Menu.js';
|
|
4
|
+
import ProjectList from './ProjectList.js';
|
|
4
5
|
import Session from './Session.js';
|
|
5
6
|
import NewWorktree from './NewWorktree.js';
|
|
6
7
|
import DeleteWorktree from './DeleteWorktree.js';
|
|
7
8
|
import MergeWorktree from './MergeWorktree.js';
|
|
8
9
|
import Configuration from './Configuration.js';
|
|
9
10
|
import PresetSelector from './PresetSelector.js';
|
|
10
|
-
import {
|
|
11
|
+
import { globalSessionOrchestrator } from '../services/globalSessionOrchestrator.js';
|
|
11
12
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
12
13
|
import { shortcutManager } from '../services/shortcutManager.js';
|
|
13
14
|
import { configurationManager } from '../services/configurationManager.js';
|
|
14
|
-
|
|
15
|
+
import { ENV_VARS } from '../constants/env.js';
|
|
16
|
+
import { MULTI_PROJECT_ERRORS } from '../constants/error.js';
|
|
17
|
+
import { projectManager } from '../services/projectManager.js';
|
|
18
|
+
const App = ({ devcontainerConfig, multiProject }) => {
|
|
15
19
|
const { exit } = useApp();
|
|
16
|
-
const [view, setView] = useState('menu');
|
|
17
|
-
const [sessionManager] = useState(() =>
|
|
18
|
-
const [worktreeService] = useState(() => new WorktreeService());
|
|
20
|
+
const [view, setView] = useState(multiProject ? 'project-list' : 'menu');
|
|
21
|
+
const [sessionManager, setSessionManager] = useState(() => globalSessionOrchestrator.getManagerForProject());
|
|
22
|
+
const [worktreeService, setWorktreeService] = useState(() => new WorktreeService());
|
|
19
23
|
const [activeSession, setActiveSession] = useState(null);
|
|
20
24
|
const [error, setError] = useState(null);
|
|
21
25
|
const [menuKey, setMenuKey] = useState(0); // Force menu refresh
|
|
22
26
|
const [selectedWorktree, setSelectedWorktree] = useState(null); // Store selected worktree for preset selection
|
|
27
|
+
const [selectedProject, setSelectedProject] = useState(null); // Store selected project in multi-project mode
|
|
28
|
+
// Helper function to clear terminal screen
|
|
29
|
+
const clearScreen = () => {
|
|
30
|
+
if (process.stdout.isTTY) {
|
|
31
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// Helper function to navigate with screen clearing
|
|
35
|
+
const navigateWithClear = useCallback((newView, callback) => {
|
|
36
|
+
clearScreen();
|
|
37
|
+
setView('clearing');
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
setView(newView);
|
|
40
|
+
if (callback)
|
|
41
|
+
callback();
|
|
42
|
+
}, 10); // Small delay to ensure screen clear is processed
|
|
43
|
+
}, []);
|
|
23
44
|
useEffect(() => {
|
|
24
45
|
// Listen for session exits to return to menu automatically
|
|
25
46
|
const handleSessionExit = (session) => {
|
|
@@ -27,53 +48,61 @@ const App = ({ devcontainerConfig }) => {
|
|
|
27
48
|
setActiveSession(current => {
|
|
28
49
|
if (current && session.id === current.id) {
|
|
29
50
|
// Session that exited is the active one, trigger return to menu
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
setActiveSession(null);
|
|
52
|
+
setError(null);
|
|
53
|
+
const targetView = multiProject && selectedProject
|
|
54
|
+
? 'menu'
|
|
55
|
+
: multiProject
|
|
56
|
+
? 'project-list'
|
|
57
|
+
: 'menu';
|
|
58
|
+
navigateWithClear(targetView, () => {
|
|
34
59
|
setMenuKey(prev => prev + 1);
|
|
35
|
-
if (process.stdout.isTTY) {
|
|
36
|
-
process.stdout.write('\x1B[2J\x1B[H');
|
|
37
|
-
}
|
|
38
60
|
process.stdin.resume();
|
|
39
61
|
process.stdin.setEncoding('utf8');
|
|
40
|
-
}
|
|
62
|
+
});
|
|
41
63
|
}
|
|
42
64
|
return current;
|
|
43
65
|
});
|
|
44
66
|
};
|
|
45
67
|
sessionManager.on('sessionExit', handleSessionExit);
|
|
46
|
-
//
|
|
68
|
+
// Re-attach listener when session manager changes
|
|
47
69
|
return () => {
|
|
48
70
|
sessionManager.off('sessionExit', handleSessionExit);
|
|
49
|
-
|
|
71
|
+
// Don't destroy sessions on unmount - they persist in memory
|
|
50
72
|
};
|
|
51
|
-
}, [sessionManager]);
|
|
73
|
+
}, [sessionManager, multiProject, selectedProject, navigateWithClear]);
|
|
52
74
|
const handleSelectWorktree = async (worktree) => {
|
|
53
75
|
// Check if this is the new worktree option
|
|
54
76
|
if (worktree.path === '') {
|
|
55
|
-
|
|
77
|
+
navigateWithClear('new-worktree');
|
|
56
78
|
return;
|
|
57
79
|
}
|
|
58
80
|
// Check if this is the delete worktree option
|
|
59
81
|
if (worktree.path === 'DELETE_WORKTREE') {
|
|
60
|
-
|
|
82
|
+
navigateWithClear('delete-worktree');
|
|
61
83
|
return;
|
|
62
84
|
}
|
|
63
85
|
// Check if this is the merge worktree option
|
|
64
86
|
if (worktree.path === 'MERGE_WORKTREE') {
|
|
65
|
-
|
|
87
|
+
navigateWithClear('merge-worktree');
|
|
66
88
|
return;
|
|
67
89
|
}
|
|
68
90
|
// Check if this is the configuration option
|
|
69
91
|
if (worktree.path === 'CONFIGURATION') {
|
|
70
|
-
|
|
92
|
+
navigateWithClear('configuration');
|
|
71
93
|
return;
|
|
72
94
|
}
|
|
73
95
|
// Check if this is the exit application option
|
|
74
96
|
if (worktree.path === 'EXIT_APPLICATION') {
|
|
75
|
-
|
|
76
|
-
|
|
97
|
+
// In multi-project mode with a selected project, go back to project list
|
|
98
|
+
if (multiProject && selectedProject) {
|
|
99
|
+
handleBackToProjectList();
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Only destroy all sessions when actually exiting the app
|
|
103
|
+
globalSessionOrchestrator.destroyAllSessions();
|
|
104
|
+
exit();
|
|
105
|
+
}
|
|
77
106
|
return;
|
|
78
107
|
}
|
|
79
108
|
// Get or create session for this worktree
|
|
@@ -82,7 +111,7 @@ const App = ({ devcontainerConfig }) => {
|
|
|
82
111
|
// Check if we should show preset selector
|
|
83
112
|
if (configurationManager.getSelectPresetOnStart()) {
|
|
84
113
|
setSelectedWorktree(worktree);
|
|
85
|
-
|
|
114
|
+
navigateWithClear('preset-selector');
|
|
86
115
|
return;
|
|
87
116
|
}
|
|
88
117
|
try {
|
|
@@ -100,7 +129,7 @@ const App = ({ devcontainerConfig }) => {
|
|
|
100
129
|
}
|
|
101
130
|
}
|
|
102
131
|
setActiveSession(session);
|
|
103
|
-
|
|
132
|
+
navigateWithClear('session');
|
|
104
133
|
};
|
|
105
134
|
const handlePresetSelected = async (presetId) => {
|
|
106
135
|
if (!selectedWorktree)
|
|
@@ -115,7 +144,7 @@ const App = ({ devcontainerConfig }) => {
|
|
|
115
144
|
session = await sessionManager.createSessionWithPreset(selectedWorktree.path, presetId);
|
|
116
145
|
}
|
|
117
146
|
setActiveSession(session);
|
|
118
|
-
|
|
147
|
+
navigateWithClear('session');
|
|
119
148
|
setSelectedWorktree(null);
|
|
120
149
|
}
|
|
121
150
|
catch (error) {
|
|
@@ -126,20 +155,20 @@ const App = ({ devcontainerConfig }) => {
|
|
|
126
155
|
};
|
|
127
156
|
const handlePresetSelectorCancel = () => {
|
|
128
157
|
setSelectedWorktree(null);
|
|
129
|
-
|
|
130
|
-
|
|
158
|
+
navigateWithClear('menu', () => {
|
|
159
|
+
setMenuKey(prev => prev + 1);
|
|
160
|
+
});
|
|
131
161
|
};
|
|
132
162
|
const handleReturnToMenu = () => {
|
|
133
163
|
setActiveSession(null);
|
|
134
164
|
// Don't clear error here - let user dismiss it manually
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
165
|
+
const targetView = multiProject && selectedProject
|
|
166
|
+
? 'menu'
|
|
167
|
+
: multiProject
|
|
168
|
+
? 'project-list'
|
|
169
|
+
: 'menu';
|
|
170
|
+
navigateWithClear(targetView, () => {
|
|
138
171
|
setMenuKey(prev => prev + 1); // Force menu refresh
|
|
139
|
-
// Clear the screen when returning to menu
|
|
140
|
-
if (process.stdout.isTTY) {
|
|
141
|
-
process.stdout.write('\x1B[2J\x1B[H');
|
|
142
|
-
}
|
|
143
172
|
// Ensure stdin is in a clean state for Ink components
|
|
144
173
|
if (process.stdin.isTTY) {
|
|
145
174
|
// Flush any pending input to prevent escape sequences from leaking
|
|
@@ -148,7 +177,7 @@ const App = ({ devcontainerConfig }) => {
|
|
|
148
177
|
process.stdin.resume();
|
|
149
178
|
process.stdin.setEncoding('utf8');
|
|
150
179
|
}
|
|
151
|
-
}
|
|
180
|
+
});
|
|
152
181
|
};
|
|
153
182
|
const handleCreateWorktree = async (path, branch, baseBranch, copySessionData, copyClaudeDirectory) => {
|
|
154
183
|
setView('creating-worktree');
|
|
@@ -193,8 +222,45 @@ const App = ({ devcontainerConfig }) => {
|
|
|
193
222
|
const handleCancelDeleteWorktree = () => {
|
|
194
223
|
handleReturnToMenu();
|
|
195
224
|
};
|
|
225
|
+
const handleSelectProject = (project) => {
|
|
226
|
+
// Handle special exit case
|
|
227
|
+
if (project.path === 'EXIT_APPLICATION') {
|
|
228
|
+
globalSessionOrchestrator.destroyAllSessions();
|
|
229
|
+
exit();
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Set the selected project and update services
|
|
233
|
+
setSelectedProject(project);
|
|
234
|
+
setWorktreeService(new WorktreeService(project.path));
|
|
235
|
+
// Get or create session manager for this project
|
|
236
|
+
const projectSessionManager = globalSessionOrchestrator.getManagerForProject(project.path);
|
|
237
|
+
setSessionManager(projectSessionManager);
|
|
238
|
+
// Add to recent projects
|
|
239
|
+
projectManager.addRecentProject(project);
|
|
240
|
+
navigateWithClear('menu');
|
|
241
|
+
};
|
|
242
|
+
const handleBackToProjectList = () => {
|
|
243
|
+
// Sessions persist in their project-specific managers
|
|
244
|
+
setSelectedProject(null);
|
|
245
|
+
setWorktreeService(new WorktreeService()); // Reset to default
|
|
246
|
+
// Reset to global session manager for project list view
|
|
247
|
+
setSessionManager(globalSessionOrchestrator.getManagerForProject());
|
|
248
|
+
navigateWithClear('project-list', () => {
|
|
249
|
+
setMenuKey(prev => prev + 1);
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
if (view === 'project-list' && multiProject) {
|
|
253
|
+
const projectsDir = process.env[ENV_VARS.MULTI_PROJECT_ROOT];
|
|
254
|
+
if (!projectsDir) {
|
|
255
|
+
return (React.createElement(Box, null,
|
|
256
|
+
React.createElement(Text, { color: "red" },
|
|
257
|
+
"Error: ",
|
|
258
|
+
MULTI_PROJECT_ERRORS.NO_PROJECTS_DIR)));
|
|
259
|
+
}
|
|
260
|
+
return (React.createElement(ProjectList, { projectsDir: projectsDir, onSelectProject: handleSelectProject, error: error, onDismissError: () => setError(null) }));
|
|
261
|
+
}
|
|
196
262
|
if (view === 'menu') {
|
|
197
|
-
return (React.createElement(Menu, { key: menuKey, sessionManager: sessionManager, onSelectWorktree: handleSelectWorktree, error: error, onDismissError: () => setError(null) }));
|
|
263
|
+
return (React.createElement(Menu, { key: menuKey, sessionManager: sessionManager, worktreeService: worktreeService, onSelectWorktree: handleSelectWorktree, onSelectRecentProject: handleSelectProject, error: error, onDismissError: () => setError(null), projectName: selectedProject?.name, multiProject: multiProject }));
|
|
198
264
|
}
|
|
199
265
|
if (view === 'session' && activeSession) {
|
|
200
266
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
@@ -243,6 +309,10 @@ const App = ({ devcontainerConfig }) => {
|
|
|
243
309
|
if (view === 'preset-selector') {
|
|
244
310
|
return (React.createElement(PresetSelector, { onSelect: handlePresetSelected, onCancel: handlePresetSelectorCancel }));
|
|
245
311
|
}
|
|
312
|
+
if (view === 'clearing') {
|
|
313
|
+
// Render nothing during the clearing phase to ensure clean transition
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
246
316
|
return null;
|
|
247
317
|
};
|
|
248
318
|
export default App;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Worktree } from '../types/index.js';
|
|
2
|
+
import { Worktree, GitProject } from '../types/index.js';
|
|
3
|
+
import { WorktreeService } from '../services/worktreeService.js';
|
|
3
4
|
import { SessionManager } from '../services/sessionManager.js';
|
|
4
5
|
interface MenuProps {
|
|
5
6
|
sessionManager: SessionManager;
|
|
7
|
+
worktreeService: WorktreeService;
|
|
6
8
|
onSelectWorktree: (worktree: Worktree) => void;
|
|
9
|
+
onSelectRecentProject?: (project: GitProject) => void;
|
|
7
10
|
error?: string | null;
|
|
8
11
|
onDismissError?: () => void;
|
|
12
|
+
projectName?: string;
|
|
13
|
+
multiProject?: boolean;
|
|
9
14
|
}
|
|
10
15
|
declare const Menu: React.FC<MenuProps>;
|
|
11
16
|
export default Menu;
|