hjworktree-cli 2.0.0 → 2.2.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 +10 -0
- package/.context-snapshots/context-snapshot-20260106-180000.md +108 -0
- package/.context-snapshots/context-snapshot-20260106-235500.md +108 -0
- package/README.md +134 -0
- package/dist/server/routes/api.d.ts.map +1 -1
- package/dist/server/routes/api.js +45 -1
- package/dist/server/routes/api.js.map +1 -1
- package/dist/server/services/worktreeService.d.ts +23 -0
- package/dist/server/services/worktreeService.d.ts.map +1 -1
- package/dist/server/services/worktreeService.js +156 -14
- package/dist/server/services/worktreeService.js.map +1 -1
- package/dist/server/socketHandlers.d.ts.map +1 -1
- package/dist/server/socketHandlers.js +74 -1
- package/dist/server/socketHandlers.js.map +1 -1
- package/dist/shared/types/index.d.ts +36 -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 +51 -2
- package/server/services/worktreeService.ts +181 -13
- package/server/socketHandlers.ts +91 -2
- package/shared/types/index.ts +45 -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
|
@@ -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;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useAppStore } from '../../stores/useAppStore.js';
|
|
3
|
+
import { useSocket } from '../../hooks/useSocket.js';
|
|
4
|
+
|
|
5
|
+
function ConfirmDeleteModal() {
|
|
6
|
+
const {
|
|
7
|
+
modalData,
|
|
8
|
+
terminals,
|
|
9
|
+
closeModal,
|
|
10
|
+
removeTerminal,
|
|
11
|
+
clearAllTerminals,
|
|
12
|
+
clearSessionSelection,
|
|
13
|
+
} = useAppStore();
|
|
14
|
+
const { emit } = useSocket();
|
|
15
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
16
|
+
|
|
17
|
+
const sessionIds = modalData?.sessionIds || [];
|
|
18
|
+
const sessionsToDelete = terminals.filter(t => sessionIds.includes(t.sessionId));
|
|
19
|
+
const isDeleteAll = sessionIds.length === terminals.length;
|
|
20
|
+
|
|
21
|
+
const handleDelete = async () => {
|
|
22
|
+
setIsDeleting(true);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// Kill all terminals via socket
|
|
26
|
+
for (const sessionId of sessionIds) {
|
|
27
|
+
emit('terminal:kill', { sessionId });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (isDeleteAll) {
|
|
31
|
+
// Delete all worktrees
|
|
32
|
+
await fetch('/api/worktrees', { method: 'DELETE' });
|
|
33
|
+
clearAllTerminals();
|
|
34
|
+
} else {
|
|
35
|
+
// Batch delete specific worktrees
|
|
36
|
+
const names = sessionsToDelete.map(t => t.worktreeName);
|
|
37
|
+
await fetch('/api/worktrees/batch', {
|
|
38
|
+
method: 'DELETE',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({ names }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Remove terminals from state
|
|
44
|
+
for (const sessionId of sessionIds) {
|
|
45
|
+
removeTerminal(sessionId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clearSessionSelection();
|
|
50
|
+
closeModal();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Failed to delete sessions:', error);
|
|
53
|
+
} finally {
|
|
54
|
+
setIsDeleting(false);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="modal-overlay" onClick={closeModal}>
|
|
60
|
+
<div className="modal-content modal-confirm" onClick={(e) => e.stopPropagation()}>
|
|
61
|
+
<div className="modal-header">
|
|
62
|
+
<h2>Confirm Delete</h2>
|
|
63
|
+
<button className="modal-close" onClick={closeModal} type="button">
|
|
64
|
+
×
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="modal-body">
|
|
69
|
+
<p className="confirm-message">
|
|
70
|
+
{isDeleteAll
|
|
71
|
+
? 'Are you sure you want to delete ALL sessions?'
|
|
72
|
+
: `Are you sure you want to delete ${sessionIds.length} session${sessionIds.length > 1 ? 's' : ''}?`}
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
{sessionsToDelete.length > 0 && (
|
|
76
|
+
<ul className="delete-list">
|
|
77
|
+
{sessionsToDelete.map((session) => (
|
|
78
|
+
<li key={session.sessionId}>
|
|
79
|
+
<span className={`status ${session.status}`} />
|
|
80
|
+
{session.worktreeName}
|
|
81
|
+
</li>
|
|
82
|
+
))}
|
|
83
|
+
</ul>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
<p className="warning-text">
|
|
87
|
+
This action will terminate running agents and delete the worktrees. This cannot be undone.
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="modal-footer">
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
className="btn-secondary"
|
|
95
|
+
onClick={closeModal}
|
|
96
|
+
disabled={isDeleting}
|
|
97
|
+
>
|
|
98
|
+
Cancel
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
className="btn-danger"
|
|
103
|
+
onClick={handleDelete}
|
|
104
|
+
disabled={isDeleting}
|
|
105
|
+
>
|
|
106
|
+
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default ConfirmDeleteModal;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useAppStore } from '../../stores/useAppStore.js';
|
|
3
|
+
import AddWorktreeModal from './AddWorktreeModal.js';
|
|
4
|
+
import ConfirmDeleteModal from './ConfirmDeleteModal.js';
|
|
5
|
+
|
|
6
|
+
function ModalContainer() {
|
|
7
|
+
const modalType = useAppStore((state) => state.modalType);
|
|
8
|
+
|
|
9
|
+
if (!modalType) return null;
|
|
10
|
+
|
|
11
|
+
switch (modalType) {
|
|
12
|
+
case 'addWorktree':
|
|
13
|
+
return <AddWorktreeModal />;
|
|
14
|
+
case 'confirmDelete':
|
|
15
|
+
return <ConfirmDeleteModal />;
|
|
16
|
+
default:
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default ModalContainer;
|
|
@@ -4,7 +4,15 @@ import { useSocket } from '../../hooks/useSocket.js';
|
|
|
4
4
|
import XTerminal from './XTerminal.js';
|
|
5
5
|
|
|
6
6
|
function TerminalPanel() {
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
terminals,
|
|
9
|
+
activeTerminalIndex,
|
|
10
|
+
activeSessionId,
|
|
11
|
+
setActiveSession,
|
|
12
|
+
removeTerminal,
|
|
13
|
+
clearAllTerminals,
|
|
14
|
+
openModal,
|
|
15
|
+
} = useAppStore();
|
|
8
16
|
const { emit } = useSocket();
|
|
9
17
|
|
|
10
18
|
const handleCloseTerminal = async (sessionId: string, e: React.MouseEvent) => {
|
|
@@ -28,37 +36,27 @@ function TerminalPanel() {
|
|
|
28
36
|
removeTerminal(sessionId);
|
|
29
37
|
};
|
|
30
38
|
|
|
31
|
-
const handleCloseAll =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
emit('terminal:kill', { sessionId: terminal.sessionId });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Delete all worktrees
|
|
38
|
-
try {
|
|
39
|
-
await fetch('/api/worktrees', {
|
|
40
|
-
method: 'DELETE',
|
|
41
|
-
});
|
|
42
|
-
} catch (error) {
|
|
43
|
-
console.error('Failed to delete worktrees:', error);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
clearAllTerminals();
|
|
47
|
-
reset();
|
|
39
|
+
const handleCloseAll = () => {
|
|
40
|
+
const sessionIds = terminals.map(t => t.sessionId);
|
|
41
|
+
openModal('confirmDelete', { sessionIds });
|
|
48
42
|
};
|
|
49
43
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
// Find active index based on sessionId
|
|
45
|
+
const currentActiveIndex = activeSessionId
|
|
46
|
+
? terminals.findIndex(t => t.sessionId === activeSessionId)
|
|
47
|
+
: activeTerminalIndex;
|
|
48
|
+
const effectiveActiveIndex = currentActiveIndex >= 0 ? currentActiveIndex : 0;
|
|
49
|
+
|
|
50
|
+
if (terminals.length === 0) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="terminal-panel empty">
|
|
53
|
+
<div className="empty-message">
|
|
54
|
+
<p>No active sessions</p>
|
|
55
|
+
<p>Use the Setup wizard or click "+ Add Session" in the sidebar</p>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
62
60
|
|
|
63
61
|
return (
|
|
64
62
|
<div className="terminal-panel">
|
|
@@ -66,8 +64,8 @@ function TerminalPanel() {
|
|
|
66
64
|
{terminals.map((terminal, index) => (
|
|
67
65
|
<button
|
|
68
66
|
key={terminal.sessionId}
|
|
69
|
-
className={`terminal-tab ${index ===
|
|
70
|
-
onClick={() =>
|
|
67
|
+
className={`terminal-tab ${terminal.sessionId === activeSessionId || index === effectiveActiveIndex ? 'active' : ''}`}
|
|
68
|
+
onClick={() => setActiveSession(terminal.sessionId)}
|
|
71
69
|
>
|
|
72
70
|
<span className={`status ${terminal.status}`} />
|
|
73
71
|
<span>{terminal.worktreeName}</span>
|
|
@@ -82,9 +80,6 @@ function TerminalPanel() {
|
|
|
82
80
|
))}
|
|
83
81
|
|
|
84
82
|
<div className="terminal-actions">
|
|
85
|
-
<button onClick={handleBackToSetup}>
|
|
86
|
-
Back to Setup
|
|
87
|
-
</button>
|
|
88
83
|
<button className="danger" onClick={handleCloseAll}>
|
|
89
84
|
Close All
|
|
90
85
|
</button>
|
|
@@ -95,13 +90,13 @@ function TerminalPanel() {
|
|
|
95
90
|
{terminals.map((terminal, index) => (
|
|
96
91
|
<div
|
|
97
92
|
key={terminal.sessionId}
|
|
98
|
-
className={`terminal-wrapper ${index !==
|
|
93
|
+
className={`terminal-wrapper ${index !== effectiveActiveIndex ? 'hidden' : ''}`}
|
|
99
94
|
>
|
|
100
95
|
<XTerminal
|
|
101
96
|
sessionId={terminal.sessionId}
|
|
102
97
|
worktreePath={terminal.worktreePath}
|
|
103
98
|
agentType={terminal.agentType}
|
|
104
|
-
isActive={index ===
|
|
99
|
+
isActive={index === effectiveActiveIndex}
|
|
105
100
|
/>
|
|
106
101
|
</div>
|
|
107
102
|
))}
|