@atercates/claude-deck 0.2.14 → 0.2.15
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/app/api/worktrees/route.ts +29 -0
- package/components/NewClaudeSessionDialog.tsx +197 -15
- package/components/Pane/DesktopTabBar.tsx +11 -0
- package/components/Pane/MobileTabBar.tsx +6 -0
- package/lib/worktrees.ts +9 -0
- package/package.json +1 -1
- package/scripts/nginx-http.conf +19 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +0 -69
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { createWorktree } from "@/lib/worktrees";
|
|
3
|
+
|
|
4
|
+
export async function POST(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const { projectPath, featureName, baseBranch } = await request.json();
|
|
7
|
+
|
|
8
|
+
if (!projectPath || !featureName) {
|
|
9
|
+
return NextResponse.json(
|
|
10
|
+
{ error: "projectPath and featureName are required" },
|
|
11
|
+
{ status: 400 }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const worktree = await createWorktree({
|
|
16
|
+
projectPath,
|
|
17
|
+
featureName,
|
|
18
|
+
baseBranch,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return NextResponse.json(worktree, { status: 201 });
|
|
22
|
+
} catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
24
|
+
return NextResponse.json(
|
|
25
|
+
{ error: `Failed to create worktree: ${message}` },
|
|
26
|
+
{ status: 400 }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useRef } from "react";
|
|
3
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
4
4
|
import {
|
|
5
5
|
Dialog,
|
|
6
6
|
DialogContent,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
SelectTrigger,
|
|
17
17
|
SelectValue,
|
|
18
18
|
} from "@/components/ui/select";
|
|
19
|
-
import { Sparkles } from "lucide-react";
|
|
19
|
+
import { Sparkles, GitBranch, Loader2 } from "lucide-react";
|
|
20
20
|
import type { ClaudeProject } from "@/data/claude";
|
|
21
21
|
|
|
22
22
|
const ADJECTIVES = [
|
|
@@ -50,6 +50,12 @@ function generateName() {
|
|
|
50
50
|
return `${adj}-${noun}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
interface GitInfo {
|
|
54
|
+
isGitRepo: boolean;
|
|
55
|
+
branches: string[];
|
|
56
|
+
defaultBranch: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
interface NewClaudeSessionDialogProps {
|
|
54
60
|
open: boolean;
|
|
55
61
|
projectName: string;
|
|
@@ -69,10 +75,57 @@ export function NewClaudeSessionDialog({
|
|
|
69
75
|
const [selectedProject, setSelectedProject] = useState(projectName);
|
|
70
76
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
71
77
|
|
|
78
|
+
// Worktree state
|
|
79
|
+
const [gitInfo, setGitInfo] = useState<GitInfo | null>(null);
|
|
80
|
+
const [useWorktree, setUseWorktree] = useState(false);
|
|
81
|
+
const [featureName, setFeatureName] = useState("");
|
|
82
|
+
const [baseBranch, setBaseBranch] = useState("main");
|
|
83
|
+
const [creating, setCreating] = useState(false);
|
|
84
|
+
|
|
85
|
+
const cwd = (() => {
|
|
86
|
+
if (selectedProject) {
|
|
87
|
+
const project = projects?.find((p) => p.name === selectedProject);
|
|
88
|
+
return project?.directory || undefined;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
})();
|
|
92
|
+
|
|
93
|
+
// Check git repo when cwd changes
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!cwd) {
|
|
96
|
+
setGitInfo(null);
|
|
97
|
+
setUseWorktree(false);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let cancelled = false;
|
|
102
|
+
fetch("/api/git/check", {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ path: cwd }),
|
|
106
|
+
})
|
|
107
|
+
.then((r) => r.json())
|
|
108
|
+
.then((data) => {
|
|
109
|
+
if (cancelled) return;
|
|
110
|
+
setGitInfo(data);
|
|
111
|
+
if (data.defaultBranch) setBaseBranch(data.defaultBranch);
|
|
112
|
+
})
|
|
113
|
+
.catch(() => {
|
|
114
|
+
if (!cancelled) setGitInfo(null);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return () => {
|
|
118
|
+
cancelled = true;
|
|
119
|
+
};
|
|
120
|
+
}, [cwd]);
|
|
121
|
+
|
|
72
122
|
useEffect(() => {
|
|
73
123
|
if (open) {
|
|
74
124
|
setName(generateName());
|
|
75
125
|
setSelectedProject(projectName);
|
|
126
|
+
setUseWorktree(false);
|
|
127
|
+
setFeatureName("");
|
|
128
|
+
setCreating(false);
|
|
76
129
|
setTimeout(() => {
|
|
77
130
|
inputRef.current?.focus();
|
|
78
131
|
inputRef.current?.select();
|
|
@@ -80,20 +133,63 @@ export function NewClaudeSessionDialog({
|
|
|
80
133
|
}
|
|
81
134
|
}, [open, projectName]);
|
|
82
135
|
|
|
83
|
-
const handleSubmit = (
|
|
84
|
-
e.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
136
|
+
const handleSubmit = useCallback(
|
|
137
|
+
async (e: React.FormEvent) => {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
const sessionName = name.trim() || generateName();
|
|
140
|
+
let targetCwd = cwd;
|
|
141
|
+
|
|
142
|
+
if (useWorktree && featureName.trim() && cwd) {
|
|
143
|
+
setCreating(true);
|
|
144
|
+
try {
|
|
145
|
+
const res = await fetch("/api/worktrees", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({
|
|
149
|
+
projectPath: cwd,
|
|
150
|
+
featureName: featureName.trim(),
|
|
151
|
+
baseBranch,
|
|
152
|
+
}),
|
|
153
|
+
});
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
const data = await res.json();
|
|
156
|
+
alert(data.error || "Failed to create worktree");
|
|
157
|
+
setCreating(false);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const worktree = await res.json();
|
|
161
|
+
targetCwd = worktree.worktreePath;
|
|
162
|
+
} catch {
|
|
163
|
+
alert("Failed to create worktree");
|
|
164
|
+
setCreating(false);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
setCreating(false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
onConfirm(sessionName, targetCwd, selectedProject || undefined);
|
|
171
|
+
},
|
|
172
|
+
[
|
|
173
|
+
name,
|
|
174
|
+
cwd,
|
|
175
|
+
useWorktree,
|
|
176
|
+
featureName,
|
|
177
|
+
baseBranch,
|
|
178
|
+
selectedProject,
|
|
179
|
+
onConfirm,
|
|
180
|
+
]
|
|
181
|
+
);
|
|
92
182
|
|
|
93
183
|
const showProjectSelector = projects && projects.length > 0 && !projectName;
|
|
184
|
+
const branchPreview = featureName
|
|
185
|
+
.trim()
|
|
186
|
+
.toLowerCase()
|
|
187
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
188
|
+
.replace(/^-+|-+$/g, "")
|
|
189
|
+
.slice(0, 50);
|
|
94
190
|
|
|
95
191
|
return (
|
|
96
|
-
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
|
192
|
+
<Dialog open={open} onOpenChange={(o) => !o && !creating && onClose()}>
|
|
97
193
|
<DialogContent className="sm:max-w-sm">
|
|
98
194
|
<DialogHeader>
|
|
99
195
|
<DialogTitle className="text-base">New session</DialogTitle>
|
|
@@ -127,6 +223,7 @@ export function NewClaudeSessionDialog({
|
|
|
127
223
|
<p className="text-muted-foreground text-xs">{projectName}</p>
|
|
128
224
|
)
|
|
129
225
|
)}
|
|
226
|
+
|
|
130
227
|
<div>
|
|
131
228
|
<div className="flex gap-2">
|
|
132
229
|
<Input
|
|
@@ -135,6 +232,7 @@ export function NewClaudeSessionDialog({
|
|
|
135
232
|
onChange={(e) => setName(e.target.value)}
|
|
136
233
|
placeholder="Session name"
|
|
137
234
|
className="h-9"
|
|
235
|
+
disabled={creating}
|
|
138
236
|
/>
|
|
139
237
|
<Button
|
|
140
238
|
type="button"
|
|
@@ -142,21 +240,105 @@ export function NewClaudeSessionDialog({
|
|
|
142
240
|
size="icon-sm"
|
|
143
241
|
className="h-9 w-9 shrink-0"
|
|
144
242
|
onClick={() => setName(generateName())}
|
|
243
|
+
disabled={creating}
|
|
145
244
|
>
|
|
146
245
|
<Sparkles className="h-3.5 w-3.5" />
|
|
147
246
|
</Button>
|
|
148
247
|
</div>
|
|
149
248
|
</div>
|
|
249
|
+
|
|
250
|
+
{gitInfo?.isGitRepo && (
|
|
251
|
+
<div className="bg-accent/40 space-y-3 rounded-lg p-3">
|
|
252
|
+
<div className="flex items-center gap-2">
|
|
253
|
+
<input
|
|
254
|
+
type="checkbox"
|
|
255
|
+
id="useWorktree"
|
|
256
|
+
checked={useWorktree}
|
|
257
|
+
onChange={(e) => setUseWorktree(e.target.checked)}
|
|
258
|
+
className="border-border bg-background accent-primary h-4 w-4 rounded"
|
|
259
|
+
disabled={creating}
|
|
260
|
+
/>
|
|
261
|
+
<label
|
|
262
|
+
htmlFor="useWorktree"
|
|
263
|
+
className="flex cursor-pointer items-center gap-1.5 text-sm font-medium"
|
|
264
|
+
>
|
|
265
|
+
<GitBranch className="h-3.5 w-3.5" />
|
|
266
|
+
Create isolated worktree
|
|
267
|
+
</label>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
{useWorktree && (
|
|
271
|
+
<div className="space-y-3 pl-6">
|
|
272
|
+
<div className="space-y-1">
|
|
273
|
+
<label className="text-muted-foreground text-xs">
|
|
274
|
+
Feature name
|
|
275
|
+
</label>
|
|
276
|
+
<Input
|
|
277
|
+
value={featureName}
|
|
278
|
+
onChange={(e) => setFeatureName(e.target.value)}
|
|
279
|
+
placeholder="add-dark-mode"
|
|
280
|
+
className="h-8 text-sm"
|
|
281
|
+
disabled={creating}
|
|
282
|
+
/>
|
|
283
|
+
{branchPreview && (
|
|
284
|
+
<p className="text-muted-foreground text-xs">
|
|
285
|
+
Branch: feature/{branchPreview}
|
|
286
|
+
</p>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
<div className="space-y-1">
|
|
290
|
+
<label className="text-muted-foreground text-xs">
|
|
291
|
+
Base branch
|
|
292
|
+
</label>
|
|
293
|
+
<Select
|
|
294
|
+
value={baseBranch}
|
|
295
|
+
onValueChange={setBaseBranch}
|
|
296
|
+
disabled={creating}
|
|
297
|
+
>
|
|
298
|
+
<SelectTrigger className="h-8 text-sm">
|
|
299
|
+
<SelectValue />
|
|
300
|
+
</SelectTrigger>
|
|
301
|
+
<SelectContent>
|
|
302
|
+
{gitInfo.branches.map((branch) => (
|
|
303
|
+
<SelectItem key={branch} value={branch}>
|
|
304
|
+
{branch}
|
|
305
|
+
</SelectItem>
|
|
306
|
+
))}
|
|
307
|
+
</SelectContent>
|
|
308
|
+
</Select>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
|
|
150
315
|
<div className="flex justify-end gap-2">
|
|
151
|
-
<Button
|
|
316
|
+
<Button
|
|
317
|
+
type="button"
|
|
318
|
+
variant="ghost"
|
|
319
|
+
size="sm"
|
|
320
|
+
onClick={onClose}
|
|
321
|
+
disabled={creating}
|
|
322
|
+
>
|
|
152
323
|
Cancel
|
|
153
324
|
</Button>
|
|
154
325
|
<Button
|
|
155
326
|
type="submit"
|
|
156
327
|
size="sm"
|
|
157
|
-
disabled={
|
|
328
|
+
disabled={
|
|
329
|
+
creating ||
|
|
330
|
+
(showProjectSelector && !selectedProject) ||
|
|
331
|
+
(useWorktree && !featureName.trim())
|
|
332
|
+
}
|
|
158
333
|
>
|
|
159
|
-
|
|
334
|
+
{creating ? (
|
|
335
|
+
<>
|
|
336
|
+
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
|
|
337
|
+
Creating worktree...
|
|
338
|
+
</>
|
|
339
|
+
) : (
|
|
340
|
+
"Create"
|
|
341
|
+
)}
|
|
160
342
|
</Button>
|
|
161
343
|
</div>
|
|
162
344
|
</form>
|
|
@@ -291,6 +291,17 @@ export function DesktopTabBar({
|
|
|
291
291
|
|
|
292
292
|
{/* Pane Controls */}
|
|
293
293
|
<div className="ml-auto flex items-center gap-0.5 px-2">
|
|
294
|
+
{session?.branch_name && (
|
|
295
|
+
<Tooltip>
|
|
296
|
+
<TooltipTrigger asChild>
|
|
297
|
+
<span className="text-muted-foreground flex items-center gap-1 text-[10px]">
|
|
298
|
+
<GitBranch className="h-3 w-3" />
|
|
299
|
+
{session.branch_name}
|
|
300
|
+
</span>
|
|
301
|
+
</TooltipTrigger>
|
|
302
|
+
<TooltipContent>Worktree branch</TooltipContent>
|
|
303
|
+
</Tooltip>
|
|
304
|
+
)}
|
|
294
305
|
{session?.working_directory && session.working_directory !== "~" && (
|
|
295
306
|
<OpenInVSCode workingDirectory={session.working_directory} />
|
|
296
307
|
)}
|
|
@@ -122,6 +122,12 @@ export function MobileTabBar({
|
|
|
122
122
|
<span className="truncate text-sm font-medium">
|
|
123
123
|
{session?.name || "No session"}
|
|
124
124
|
</span>
|
|
125
|
+
{session?.branch_name && (
|
|
126
|
+
<span className="text-muted-foreground flex items-center gap-0.5 text-[10px]">
|
|
127
|
+
<GitBranch className="h-2.5 w-2.5" />
|
|
128
|
+
{session.branch_name}
|
|
129
|
+
</span>
|
|
130
|
+
)}
|
|
125
131
|
{sessionList.length > 0 && (
|
|
126
132
|
<ChevronDown className="text-muted-foreground h-3 w-3 shrink-0" />
|
|
127
133
|
)}
|
package/lib/worktrees.ts
CHANGED
|
@@ -111,6 +111,15 @@ export async function createWorktree(
|
|
|
111
111
|
{ timeout: 30000 }
|
|
112
112
|
);
|
|
113
113
|
lastError = null;
|
|
114
|
+
|
|
115
|
+
// Init submodules if present
|
|
116
|
+
if (fs.existsSync(path.join(worktreePath, ".gitmodules"))) {
|
|
117
|
+
await execAsync(
|
|
118
|
+
`git -C "${worktreePath}" submodule update --init --recursive`,
|
|
119
|
+
{ timeout: 120000 }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
break; // Success!
|
|
115
124
|
} catch (error: unknown) {
|
|
116
125
|
lastError = error instanceof Error ? error : new Error(String(error));
|
package/package.json
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
server {
|
|
2
|
+
listen 80;
|
|
3
|
+
server_name claudedeck.example.com;
|
|
4
|
+
|
|
5
|
+
location / {
|
|
6
|
+
proxy_pass http://127.0.0.1:3011;
|
|
7
|
+
proxy_set_header Host $host;
|
|
8
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
9
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
10
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
11
|
+
|
|
12
|
+
proxy_http_version 1.1;
|
|
13
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
14
|
+
proxy_set_header Connection "upgrade";
|
|
15
|
+
|
|
16
|
+
proxy_read_timeout 86400s;
|
|
17
|
+
proxy_send_timeout 86400s;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { ChevronRight } from "lucide-react";
|
|
2
|
-
import { CLAUDE_AUTO_APPROVE_FLAG } from "@/lib/providers";
|
|
3
|
-
|
|
4
|
-
interface AdvancedSettingsProps {
|
|
5
|
-
open: boolean;
|
|
6
|
-
onOpenChange: (open: boolean) => void;
|
|
7
|
-
useTmux: boolean;
|
|
8
|
-
onUseTmuxChange: (checked: boolean) => void;
|
|
9
|
-
skipPermissions: boolean;
|
|
10
|
-
onSkipPermissionsChange: (checked: boolean) => void;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function AdvancedSettings({
|
|
14
|
-
open,
|
|
15
|
-
onOpenChange,
|
|
16
|
-
useTmux,
|
|
17
|
-
onUseTmuxChange,
|
|
18
|
-
skipPermissions,
|
|
19
|
-
onSkipPermissionsChange,
|
|
20
|
-
}: AdvancedSettingsProps) {
|
|
21
|
-
return (
|
|
22
|
-
<div className="border-border rounded-lg border">
|
|
23
|
-
<button
|
|
24
|
-
type="button"
|
|
25
|
-
onClick={() => onOpenChange(!open)}
|
|
26
|
-
className="text-muted-foreground hover:text-foreground flex w-full items-center gap-2 px-3 py-2 text-sm transition-colors"
|
|
27
|
-
>
|
|
28
|
-
<ChevronRight
|
|
29
|
-
className={`h-4 w-4 transition-transform ${open ? "rotate-90" : ""}`}
|
|
30
|
-
/>
|
|
31
|
-
Advanced Settings
|
|
32
|
-
</button>
|
|
33
|
-
{open && (
|
|
34
|
-
<div className="space-y-3 border-t px-3 py-3">
|
|
35
|
-
<div className="flex items-center gap-2">
|
|
36
|
-
<input
|
|
37
|
-
type="checkbox"
|
|
38
|
-
id="useTmux"
|
|
39
|
-
checked={useTmux}
|
|
40
|
-
onChange={(e) => onUseTmuxChange(e.target.checked)}
|
|
41
|
-
className="border-border bg-background accent-primary h-4 w-4 rounded"
|
|
42
|
-
/>
|
|
43
|
-
<label htmlFor="useTmux" className="cursor-pointer text-sm">
|
|
44
|
-
Use tmux session
|
|
45
|
-
<span className="text-muted-foreground ml-1">
|
|
46
|
-
(enables detach/attach)
|
|
47
|
-
</span>
|
|
48
|
-
</label>
|
|
49
|
-
</div>
|
|
50
|
-
<div className="flex items-center gap-2">
|
|
51
|
-
<input
|
|
52
|
-
type="checkbox"
|
|
53
|
-
id="skipPermissions"
|
|
54
|
-
checked={skipPermissions}
|
|
55
|
-
onChange={(e) => onSkipPermissionsChange(e.target.checked)}
|
|
56
|
-
className="border-border bg-background accent-primary h-4 w-4 rounded"
|
|
57
|
-
/>
|
|
58
|
-
<label htmlFor="skipPermissions" className="cursor-pointer text-sm">
|
|
59
|
-
Auto-approve tool calls
|
|
60
|
-
<span className="text-muted-foreground ml-1">
|
|
61
|
-
({CLAUDE_AUTO_APPROVE_FLAG})
|
|
62
|
-
</span>
|
|
63
|
-
</label>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
)}
|
|
67
|
-
</div>
|
|
68
|
-
);
|
|
69
|
-
}
|