hjworktree-cli 2.0.0 → 2.1.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/.claude/settings.local.json +14 -0
- package/README.md +121 -0
- package/dist/server/routes/api.d.ts.map +1 -1
- package/dist/server/routes/api.js +39 -1
- package/dist/server/routes/api.js.map +1 -1
- package/dist/server/services/worktreeService.d.ts +8 -0
- package/dist/server/services/worktreeService.d.ts.map +1 -1
- package/dist/server/services/worktreeService.js +42 -0
- package/dist/server/services/worktreeService.js.map +1 -1
- package/dist/shared/types/index.d.ts +35 -0
- package/dist/shared/types/index.d.ts.map +1 -1
- package/dist/web/assets/{index-C61yAbey.css → index-CsixHL-D.css} +1 -1
- package/dist/web/assets/index-D8dr9mJa.js +53 -0
- package/dist/web/assets/index-D8dr9mJa.js.map +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/server/routes/api.ts +44 -2
- package/server/services/worktreeService.ts +52 -0
- package/shared/types/index.ts +44 -0
- package/web/src/App.tsx +3 -0
- package/web/src/components/Layout/LeftNavBar.tsx +290 -26
- package/web/src/components/Layout/MainLayout.tsx +2 -6
- package/web/src/components/Modals/AddWorktreeModal.tsx +87 -0
- package/web/src/components/Modals/ConfirmDeleteModal.tsx +114 -0
- package/web/src/components/Modals/ModalContainer.tsx +21 -0
- package/web/src/components/Terminal/TerminalPanel.tsx +32 -37
- package/web/src/stores/useAppStore.ts +200 -3
- package/web/src/styles/global.css +522 -0
- package/dist/web/assets/index-WEdVUKxb.js +0 -53
- package/dist/web/assets/index-WEdVUKxb.js.map +0 -1
package/dist/web/index.html
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-D8dr9mJa.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CsixHL-D.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/routes/api.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Router, Request, Response } from 'express';
|
|
2
2
|
import { GitService } from '../services/gitService.js';
|
|
3
3
|
import { WorktreeService } from '../services/worktreeService.js';
|
|
4
|
-
import type { CreateWorktreesRequest } from '../../shared/types/index.js';
|
|
4
|
+
import type { CreateWorktreesRequest, CreateSingleWorktreeRequest, BatchDeleteRequest } from '../../shared/types/index.js';
|
|
5
5
|
|
|
6
6
|
export function apiRouter(cwd: string): Router {
|
|
7
7
|
const router = Router();
|
|
@@ -52,7 +52,7 @@ export function apiRouter(cwd: string): Router {
|
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
// Create worktrees
|
|
55
|
+
// Create worktrees (multiple)
|
|
56
56
|
router.post('/worktrees', async (req: Request, res: Response) => {
|
|
57
57
|
try {
|
|
58
58
|
const { branch, count } = req.body as CreateWorktreesRequest;
|
|
@@ -71,6 +71,48 @@ export function apiRouter(cwd: string): Router {
|
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
// Create single worktree
|
|
75
|
+
router.post('/worktrees/single', async (req: Request, res: Response) => {
|
|
76
|
+
try {
|
|
77
|
+
const { branch } = req.body as CreateSingleWorktreeRequest;
|
|
78
|
+
|
|
79
|
+
if (!branch) {
|
|
80
|
+
res.status(400).json({ error: 'Invalid request: branch is required' });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const worktree = await worktreeService.createSingleWorktree(branch);
|
|
85
|
+
res.json({ worktree });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
res.status(500).json({
|
|
88
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Batch delete worktrees
|
|
94
|
+
router.delete('/worktrees/batch', async (req: Request, res: Response) => {
|
|
95
|
+
try {
|
|
96
|
+
const { names } = req.body as BatchDeleteRequest;
|
|
97
|
+
|
|
98
|
+
if (!names || !Array.isArray(names) || names.length === 0) {
|
|
99
|
+
res.status(400).json({ error: 'Invalid request: names array is required' });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const result = await worktreeService.removeWorktreesByNames(names);
|
|
104
|
+
res.json({
|
|
105
|
+
success: result.failed.length === 0,
|
|
106
|
+
deleted: result.deleted,
|
|
107
|
+
failed: result.failed,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
res.status(500).json({
|
|
111
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
74
116
|
// Delete a worktree
|
|
75
117
|
router.delete('/worktrees/:name', async (req: Request, res: Response) => {
|
|
76
118
|
try {
|
|
@@ -149,6 +149,58 @@ export class WorktreeService {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
async createSingleWorktree(baseBranch: string): Promise<Worktree> {
|
|
153
|
+
// Find next available index for this branch
|
|
154
|
+
const existingWorktrees = await this.listWorktrees();
|
|
155
|
+
const branchWorktrees = existingWorktrees.filter(
|
|
156
|
+
wt => wt.name.startsWith(`${baseBranch}-project-`)
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Find the highest index used
|
|
160
|
+
let maxIndex = 0;
|
|
161
|
+
for (const wt of branchWorktrees) {
|
|
162
|
+
const match = wt.name.match(/-project-(\d+)$/);
|
|
163
|
+
if (match) {
|
|
164
|
+
const index = parseInt(match[1], 10);
|
|
165
|
+
if (index > maxIndex) maxIndex = index;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const nextIndex = maxIndex + 1;
|
|
170
|
+
return this.createWorktree(baseBranch, nextIndex);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async removeWorktreesByNames(names: string[]): Promise<{
|
|
174
|
+
deleted: string[];
|
|
175
|
+
failed: { name: string; error: string }[];
|
|
176
|
+
}> {
|
|
177
|
+
const worktrees = await this.listWorktrees();
|
|
178
|
+
const results: { deleted: string[]; failed: { name: string; error: string }[] } = {
|
|
179
|
+
deleted: [],
|
|
180
|
+
failed: [],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
for (const name of names) {
|
|
184
|
+
const worktree = worktrees.find(wt => wt.name === name);
|
|
185
|
+
if (!worktree) {
|
|
186
|
+
results.failed.push({ name, error: 'Worktree not found' });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await this.removeWorktree(worktree.path);
|
|
192
|
+
results.deleted.push(name);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
results.failed.push({
|
|
195
|
+
name,
|
|
196
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
|
|
152
204
|
async cleanup(): Promise<void> {
|
|
153
205
|
// Remove all worktrees created in this session
|
|
154
206
|
for (const worktreePath of this.createdWorktrees) {
|
package/shared/types/index.ts
CHANGED
|
@@ -90,3 +90,47 @@ export interface CreateWorktreesRequest {
|
|
|
90
90
|
export interface CreateWorktreesResponse {
|
|
91
91
|
worktrees: Worktree[];
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
// Single worktree creation
|
|
95
|
+
export interface CreateSingleWorktreeRequest {
|
|
96
|
+
branch: string;
|
|
97
|
+
agentType: AgentId;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface CreateSingleWorktreeResponse {
|
|
101
|
+
worktree: Worktree;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Batch delete
|
|
105
|
+
export interface BatchDeleteRequest {
|
|
106
|
+
names: string[];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface BatchDeleteResponse {
|
|
110
|
+
success: boolean;
|
|
111
|
+
deleted: string[];
|
|
112
|
+
failed: { name: string; error: string }[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Session management types
|
|
116
|
+
export interface WorktreeSession extends TerminalInfo {
|
|
117
|
+
id: string;
|
|
118
|
+
createdAt: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface SessionGroup {
|
|
122
|
+
branchName: string;
|
|
123
|
+
sessions: WorktreeSession[];
|
|
124
|
+
isCollapsed: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Modal types
|
|
128
|
+
export type ModalType = 'addWorktree' | 'confirmDelete' | null;
|
|
129
|
+
|
|
130
|
+
export interface ModalState {
|
|
131
|
+
type: ModalType;
|
|
132
|
+
data?: {
|
|
133
|
+
sessionIds?: string[];
|
|
134
|
+
branchName?: string;
|
|
135
|
+
};
|
|
136
|
+
}
|
package/web/src/App.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import BranchStep from './components/Steps/BranchStep.js';
|
|
|
7
7
|
import AgentStep from './components/Steps/AgentStep.js';
|
|
8
8
|
import WorktreeStep from './components/Steps/WorktreeStep.js';
|
|
9
9
|
import TerminalPanel from './components/Terminal/TerminalPanel.js';
|
|
10
|
+
import ModalContainer from './components/Modals/ModalContainer.js';
|
|
10
11
|
|
|
11
12
|
function App() {
|
|
12
13
|
const { step, isLoading, loadingMessage, error } = useAppStore();
|
|
@@ -58,6 +59,8 @@ function App() {
|
|
|
58
59
|
<span className={`dot ${connected ? 'connected' : 'disconnected'}`} />
|
|
59
60
|
{connected ? 'Connected' : 'Disconnected'}
|
|
60
61
|
</div>
|
|
62
|
+
|
|
63
|
+
<ModalContainer />
|
|
61
64
|
</div>
|
|
62
65
|
);
|
|
63
66
|
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useAppStore } from '../../stores/useAppStore.js';
|
|
3
|
-
import type { NavigationStep, StepStatus } from '../../../../shared/types/index.js';
|
|
3
|
+
import type { NavigationStep, StepStatus, AgentStatus } from '../../../../shared/types/index.js';
|
|
4
|
+
import { AI_AGENTS } from '../../../../shared/constants.js';
|
|
5
|
+
|
|
6
|
+
// Step configuration
|
|
7
|
+
const STEP_CONFIG = [
|
|
8
|
+
{ id: 'branch' as const, number: 1, label: 'Branch' },
|
|
9
|
+
{ id: 'agent' as const, number: 2, label: 'Agent' },
|
|
10
|
+
{ id: 'worktree' as const, number: 3, label: 'Count' },
|
|
11
|
+
];
|
|
4
12
|
|
|
5
13
|
interface StepItemProps {
|
|
6
14
|
step: NavigationStep;
|
|
@@ -11,13 +19,6 @@ interface StepItemProps {
|
|
|
11
19
|
disabled: boolean;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
const STEP_CONFIG = [
|
|
15
|
-
{ id: 'branch' as const, number: 1, label: 'Branch Selection' },
|
|
16
|
-
{ id: 'agent' as const, number: 2, label: 'Agent Selection' },
|
|
17
|
-
{ id: 'worktree' as const, number: 3, label: 'Worktree Count' },
|
|
18
|
-
{ id: 'running' as const, number: 4, label: 'Running' },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
22
|
function StepItem({ number, label, status, onClick, disabled }: StepItemProps) {
|
|
22
23
|
return (
|
|
23
24
|
<button
|
|
@@ -34,32 +35,295 @@ function StepItem({ number, label, status, onClick, disabled }: StepItemProps) {
|
|
|
34
35
|
);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
// Setup Section Component
|
|
39
|
+
function SetupSection() {
|
|
40
|
+
const {
|
|
41
|
+
step,
|
|
42
|
+
goToStep,
|
|
43
|
+
canNavigateTo,
|
|
44
|
+
getStepStatus,
|
|
45
|
+
isSetupCollapsed,
|
|
46
|
+
toggleSetupCollapse,
|
|
47
|
+
terminals,
|
|
48
|
+
} = useAppStore();
|
|
49
|
+
|
|
50
|
+
const hasRunningTerminals = terminals.length > 0;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="lnb-section setup-section">
|
|
54
|
+
<div
|
|
55
|
+
className="lnb-section-header"
|
|
56
|
+
onClick={toggleSetupCollapse}
|
|
57
|
+
role="button"
|
|
58
|
+
tabIndex={0}
|
|
59
|
+
>
|
|
60
|
+
<span className="section-title">SETUP</span>
|
|
61
|
+
<span className="collapse-icon">{isSetupCollapsed ? '+' : '−'}</span>
|
|
62
|
+
</div>
|
|
63
|
+
{!isSetupCollapsed && (
|
|
64
|
+
<div className="lnb-steps">
|
|
65
|
+
{STEP_CONFIG.map((config) => (
|
|
66
|
+
<StepItem
|
|
67
|
+
key={config.id}
|
|
68
|
+
step={config.id}
|
|
69
|
+
number={config.number}
|
|
70
|
+
label={config.label}
|
|
71
|
+
status={getStepStatus(config.id)}
|
|
72
|
+
onClick={() => goToStep(config.id)}
|
|
73
|
+
disabled={!canNavigateTo(config.id)}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
{hasRunningTerminals && (
|
|
77
|
+
<button
|
|
78
|
+
className={`lnb-step ${step === 'running' ? 'current' : 'completed'}`}
|
|
79
|
+
onClick={() => goToStep('running')}
|
|
80
|
+
type="button"
|
|
81
|
+
>
|
|
82
|
+
<span className="step-number">4</span>
|
|
83
|
+
<span className="step-label">Running</span>
|
|
84
|
+
</button>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Status indicator component
|
|
93
|
+
function StatusIndicator({ status }: { status: AgentStatus }) {
|
|
94
|
+
const statusColors: Record<AgentStatus, string> = {
|
|
95
|
+
initializing: '#f59e0b',
|
|
96
|
+
installing: '#f59e0b',
|
|
97
|
+
running: '#22c55e',
|
|
98
|
+
stopped: '#6b7280',
|
|
99
|
+
error: '#ef4444',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<span
|
|
104
|
+
className="status-indicator"
|
|
105
|
+
style={{ backgroundColor: statusColors[status] }}
|
|
106
|
+
title={status}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Session Item Component
|
|
112
|
+
interface SessionItemProps {
|
|
113
|
+
sessionId: string;
|
|
114
|
+
worktreeName: string;
|
|
115
|
+
agentType: string;
|
|
116
|
+
status: AgentStatus;
|
|
117
|
+
isActive: boolean;
|
|
118
|
+
isSelected: boolean;
|
|
119
|
+
onSelect: () => void;
|
|
120
|
+
onToggleSelect: (e: React.MouseEvent) => void;
|
|
121
|
+
onClose: (e: React.MouseEvent) => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function SessionItem({
|
|
125
|
+
sessionId,
|
|
126
|
+
worktreeName,
|
|
127
|
+
agentType,
|
|
128
|
+
status,
|
|
129
|
+
isActive,
|
|
130
|
+
isSelected,
|
|
131
|
+
onSelect,
|
|
132
|
+
onToggleSelect,
|
|
133
|
+
onClose,
|
|
134
|
+
}: SessionItemProps) {
|
|
135
|
+
const agent = AI_AGENTS.find(a => a.id === agentType);
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div
|
|
139
|
+
className={`session-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''}`}
|
|
140
|
+
onClick={onSelect}
|
|
141
|
+
role="button"
|
|
142
|
+
tabIndex={0}
|
|
143
|
+
>
|
|
144
|
+
<input
|
|
145
|
+
type="checkbox"
|
|
146
|
+
className="session-checkbox"
|
|
147
|
+
checked={isSelected}
|
|
148
|
+
onClick={onToggleSelect}
|
|
149
|
+
onChange={() => {}}
|
|
150
|
+
/>
|
|
151
|
+
<StatusIndicator status={status} />
|
|
152
|
+
<span className="session-name" title={worktreeName}>
|
|
153
|
+
{worktreeName}
|
|
154
|
+
</span>
|
|
155
|
+
<span className="session-agent" title={agent?.name}>
|
|
156
|
+
{agentType.charAt(0).toUpperCase()}
|
|
157
|
+
</span>
|
|
158
|
+
<button
|
|
159
|
+
className="session-close"
|
|
160
|
+
onClick={onClose}
|
|
161
|
+
title="Close session"
|
|
162
|
+
type="button"
|
|
163
|
+
>
|
|
164
|
+
×
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
39
169
|
|
|
40
|
-
|
|
41
|
-
|
|
170
|
+
// Session Group Component
|
|
171
|
+
interface SessionGroupProps {
|
|
172
|
+
branchName: string;
|
|
173
|
+
sessionCount: number;
|
|
174
|
+
isCollapsed: boolean;
|
|
175
|
+
onToggleCollapse: () => void;
|
|
176
|
+
children: React.ReactNode;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function SessionGroup({
|
|
180
|
+
branchName,
|
|
181
|
+
sessionCount,
|
|
182
|
+
isCollapsed,
|
|
183
|
+
onToggleCollapse,
|
|
184
|
+
children,
|
|
185
|
+
}: SessionGroupProps) {
|
|
186
|
+
return (
|
|
187
|
+
<div className="session-group">
|
|
188
|
+
<div
|
|
189
|
+
className="session-group-header"
|
|
190
|
+
onClick={onToggleCollapse}
|
|
191
|
+
role="button"
|
|
192
|
+
tabIndex={0}
|
|
193
|
+
>
|
|
194
|
+
<span className="group-collapse-icon">{isCollapsed ? '▶' : '▼'}</span>
|
|
195
|
+
<span className="group-name">{branchName}</span>
|
|
196
|
+
<span className="group-count">({sessionCount})</span>
|
|
197
|
+
</div>
|
|
198
|
+
{!isCollapsed && <div className="session-group-items">{children}</div>}
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Sessions Section Component
|
|
204
|
+
function SessionsSection() {
|
|
205
|
+
const {
|
|
206
|
+
terminals,
|
|
207
|
+
activeSessionId,
|
|
208
|
+
selectedSessionIds,
|
|
209
|
+
getSessionGroups,
|
|
210
|
+
setActiveSession,
|
|
211
|
+
toggleSessionSelection,
|
|
212
|
+
toggleBranchCollapse,
|
|
213
|
+
removeTerminal,
|
|
214
|
+
openModal,
|
|
215
|
+
} = useAppStore();
|
|
216
|
+
|
|
217
|
+
const sessionGroups = getSessionGroups();
|
|
218
|
+
|
|
219
|
+
const handleCloseSession = async (e: React.MouseEvent, sessionId: string, worktreeName: string) => {
|
|
220
|
+
e.stopPropagation();
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const response = await fetch(`/api/worktrees/${encodeURIComponent(worktreeName)}`, {
|
|
224
|
+
method: 'DELETE',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (response.ok) {
|
|
228
|
+
removeTerminal(sessionId);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Failed to close session:', error);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
if (terminals.length === 0) {
|
|
42
236
|
return null;
|
|
43
237
|
}
|
|
44
238
|
|
|
45
239
|
return (
|
|
46
|
-
<
|
|
47
|
-
<div className="lnb-header">
|
|
48
|
-
<span>
|
|
240
|
+
<div className="lnb-section sessions-section">
|
|
241
|
+
<div className="lnb-section-header">
|
|
242
|
+
<span className="section-title">SESSIONS</span>
|
|
243
|
+
<span className="session-count">{terminals.length}</span>
|
|
49
244
|
</div>
|
|
50
|
-
<div className="
|
|
51
|
-
{
|
|
52
|
-
<
|
|
53
|
-
key={
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
245
|
+
<div className="session-list">
|
|
246
|
+
{sessionGroups.map((group) => (
|
|
247
|
+
<SessionGroup
|
|
248
|
+
key={group.branchName}
|
|
249
|
+
branchName={group.branchName}
|
|
250
|
+
sessionCount={group.sessions.length}
|
|
251
|
+
isCollapsed={group.isCollapsed}
|
|
252
|
+
onToggleCollapse={() => toggleBranchCollapse(group.branchName)}
|
|
253
|
+
>
|
|
254
|
+
{group.sessions.map((session) => (
|
|
255
|
+
<SessionItem
|
|
256
|
+
key={session.sessionId}
|
|
257
|
+
sessionId={session.sessionId}
|
|
258
|
+
worktreeName={session.worktreeName}
|
|
259
|
+
agentType={session.agentType}
|
|
260
|
+
status={session.status}
|
|
261
|
+
isActive={session.sessionId === activeSessionId}
|
|
262
|
+
isSelected={selectedSessionIds.includes(session.sessionId)}
|
|
263
|
+
onSelect={() => setActiveSession(session.sessionId)}
|
|
264
|
+
onToggleSelect={(e) => {
|
|
265
|
+
e.stopPropagation();
|
|
266
|
+
toggleSessionSelection(session.sessionId);
|
|
267
|
+
}}
|
|
268
|
+
onClose={(e) => handleCloseSession(e, session.sessionId, session.worktreeName)}
|
|
269
|
+
/>
|
|
270
|
+
))}
|
|
271
|
+
</SessionGroup>
|
|
61
272
|
))}
|
|
62
273
|
</div>
|
|
274
|
+
<button
|
|
275
|
+
className="add-session-btn"
|
|
276
|
+
onClick={() => openModal('addWorktree')}
|
|
277
|
+
type="button"
|
|
278
|
+
>
|
|
279
|
+
+ Add Session
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Bulk Actions Bar Component
|
|
286
|
+
function BulkActionsBar() {
|
|
287
|
+
const {
|
|
288
|
+
selectedSessionIds,
|
|
289
|
+
clearSessionSelection,
|
|
290
|
+
openModal,
|
|
291
|
+
} = useAppStore();
|
|
292
|
+
|
|
293
|
+
if (selectedSessionIds.length === 0) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div className="bulk-actions-bar">
|
|
299
|
+
<span className="selected-count">
|
|
300
|
+
{selectedSessionIds.length} selected
|
|
301
|
+
</span>
|
|
302
|
+
<button
|
|
303
|
+
className="bulk-delete-btn"
|
|
304
|
+
onClick={() => openModal('confirmDelete', { sessionIds: selectedSessionIds })}
|
|
305
|
+
type="button"
|
|
306
|
+
>
|
|
307
|
+
Delete
|
|
308
|
+
</button>
|
|
309
|
+
<button
|
|
310
|
+
className="bulk-cancel-btn"
|
|
311
|
+
onClick={clearSessionSelection}
|
|
312
|
+
type="button"
|
|
313
|
+
>
|
|
314
|
+
Cancel
|
|
315
|
+
</button>
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Main LeftNavBar Component
|
|
321
|
+
function LeftNavBar() {
|
|
322
|
+
return (
|
|
323
|
+
<nav className="left-nav-bar">
|
|
324
|
+
<SetupSection />
|
|
325
|
+
<SessionsSection />
|
|
326
|
+
<BulkActionsBar />
|
|
63
327
|
</nav>
|
|
64
328
|
);
|
|
65
329
|
}
|
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import LeftNavBar from './LeftNavBar.js';
|
|
3
|
-
import { useAppStore } from '../../stores/useAppStore.js';
|
|
4
3
|
|
|
5
4
|
interface MainLayoutProps {
|
|
6
5
|
children: React.ReactNode;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
8
|
function MainLayout({ children }: MainLayoutProps) {
|
|
10
|
-
const step = useAppStore((state) => state.step);
|
|
11
|
-
const showLNB = step !== 'running';
|
|
12
|
-
|
|
13
9
|
return (
|
|
14
10
|
<div className="main-layout">
|
|
15
|
-
|
|
16
|
-
<div className=
|
|
11
|
+
<LeftNavBar />
|
|
12
|
+
<div className="main-content-area with-lnb">
|
|
17
13
|
{children}
|
|
18
14
|
</div>
|
|
19
15
|
</div>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useAppStore } from '../../stores/useAppStore.js';
|
|
3
|
+
import { AI_AGENTS } from '../../../../shared/constants.js';
|
|
4
|
+
import type { AgentId } from '../../../../shared/types/index.js';
|
|
5
|
+
|
|
6
|
+
function AddWorktreeModal() {
|
|
7
|
+
const { branches, closeModal, createSingleWorktree, isLoading } = useAppStore();
|
|
8
|
+
const [selectedBranch, setSelectedBranch] = useState<string>('');
|
|
9
|
+
const [selectedAgent, setSelectedAgent] = useState<AgentId>('claude');
|
|
10
|
+
|
|
11
|
+
const handleCreate = async () => {
|
|
12
|
+
if (!selectedBranch) return;
|
|
13
|
+
await createSingleWorktree(selectedBranch, selectedAgent);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="modal-overlay" onClick={closeModal}>
|
|
18
|
+
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
|
19
|
+
<div className="modal-header">
|
|
20
|
+
<h2>Add New Session</h2>
|
|
21
|
+
<button className="modal-close" onClick={closeModal} type="button">
|
|
22
|
+
×
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div className="modal-body">
|
|
27
|
+
<div className="form-group">
|
|
28
|
+
<label htmlFor="branch-select">Branch</label>
|
|
29
|
+
<select
|
|
30
|
+
id="branch-select"
|
|
31
|
+
value={selectedBranch}
|
|
32
|
+
onChange={(e) => setSelectedBranch(e.target.value)}
|
|
33
|
+
disabled={isLoading}
|
|
34
|
+
>
|
|
35
|
+
<option value="">Select a branch...</option>
|
|
36
|
+
{branches.map((branch) => (
|
|
37
|
+
<option key={branch.name} value={branch.name}>
|
|
38
|
+
{branch.name}
|
|
39
|
+
{branch.isCurrent ? ' (current)' : ''}
|
|
40
|
+
{branch.isRemote ? ' (remote)' : ''}
|
|
41
|
+
</option>
|
|
42
|
+
))}
|
|
43
|
+
</select>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div className="form-group">
|
|
47
|
+
<label>Agent</label>
|
|
48
|
+
<div className="agent-options">
|
|
49
|
+
{AI_AGENTS.map((agent) => (
|
|
50
|
+
<button
|
|
51
|
+
key={agent.id}
|
|
52
|
+
type="button"
|
|
53
|
+
className={`agent-option ${selectedAgent === agent.id ? 'selected' : ''}`}
|
|
54
|
+
onClick={() => setSelectedAgent(agent.id)}
|
|
55
|
+
disabled={isLoading}
|
|
56
|
+
>
|
|
57
|
+
<span className="agent-name">{agent.name}</span>
|
|
58
|
+
</button>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="modal-footer">
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
className="btn-secondary"
|
|
68
|
+
onClick={closeModal}
|
|
69
|
+
disabled={isLoading}
|
|
70
|
+
>
|
|
71
|
+
Cancel
|
|
72
|
+
</button>
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className="btn-primary"
|
|
76
|
+
onClick={handleCreate}
|
|
77
|
+
disabled={!selectedBranch || isLoading}
|
|
78
|
+
>
|
|
79
|
+
{isLoading ? 'Creating...' : 'Create Session'}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export default AddWorktreeModal;
|