@atercates/claude-deck 0.2.1
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/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { queries, type Session } from "@/lib/db";
|
|
4
|
+
|
|
5
|
+
interface RouteParams {
|
|
6
|
+
params: Promise<{ id: string }>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// POST /api/sessions/[id]/fork - Fork a session
|
|
10
|
+
export async function POST(request: NextRequest, { params }: RouteParams) {
|
|
11
|
+
try {
|
|
12
|
+
const { id: parentId } = await params;
|
|
13
|
+
|
|
14
|
+
// Parse body if present, otherwise use empty object
|
|
15
|
+
let body: { name?: string } = {};
|
|
16
|
+
try {
|
|
17
|
+
body = await request.json();
|
|
18
|
+
} catch {
|
|
19
|
+
// No body provided, use defaults
|
|
20
|
+
}
|
|
21
|
+
const { name } = body;
|
|
22
|
+
|
|
23
|
+
// Get parent session
|
|
24
|
+
const parent = await queries.getSession(parentId);
|
|
25
|
+
if (!parent) {
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: "Parent session not found" },
|
|
28
|
+
{ status: 404 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create new session
|
|
33
|
+
const newId = randomUUID();
|
|
34
|
+
const newName = name || `${parent.name} (fork)`;
|
|
35
|
+
const agentType = parent.agent_type || "claude";
|
|
36
|
+
const tmuxName = `${agentType}-${newId}`;
|
|
37
|
+
|
|
38
|
+
await queries.createSession(
|
|
39
|
+
newId,
|
|
40
|
+
newName,
|
|
41
|
+
tmuxName,
|
|
42
|
+
parent.working_directory,
|
|
43
|
+
parentId,
|
|
44
|
+
parent.model,
|
|
45
|
+
parent.system_prompt,
|
|
46
|
+
parent.group_path || "sessions",
|
|
47
|
+
agentType,
|
|
48
|
+
parent.auto_approve,
|
|
49
|
+
parent.project_id || "uncategorized"
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// NOTE: We do NOT copy claude_session_id here.
|
|
53
|
+
// When the forked session is first attached, it will use --fork-session flag
|
|
54
|
+
// with the parent's claude_session_id to create a new branched conversation.
|
|
55
|
+
// The new session ID will be captured automatically.
|
|
56
|
+
|
|
57
|
+
// Messages are no longer stored in our DB - skipping message copy
|
|
58
|
+
|
|
59
|
+
const session = await queries.getSession(newId);
|
|
60
|
+
|
|
61
|
+
return NextResponse.json(
|
|
62
|
+
{
|
|
63
|
+
session,
|
|
64
|
+
},
|
|
65
|
+
{ status: 201 }
|
|
66
|
+
);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error("Error forking session:", error);
|
|
69
|
+
return NextResponse.json(
|
|
70
|
+
{ error: "Failed to fork session" },
|
|
71
|
+
{ status: 500 }
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { queries, type Session } from "@/lib/db";
|
|
3
|
+
import { ensureMcpConfig } from "@/lib/mcp-config";
|
|
4
|
+
|
|
5
|
+
// POST /api/sessions/[id]/mcp-config - Ensure MCP config exists for this session
|
|
6
|
+
export async function POST(
|
|
7
|
+
request: NextRequest,
|
|
8
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
+
) {
|
|
10
|
+
try {
|
|
11
|
+
const { id } = await params;
|
|
12
|
+
const session = await queries.getSession(id) as Session | undefined;
|
|
13
|
+
|
|
14
|
+
if (!session) {
|
|
15
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Expand ~ to home directory
|
|
19
|
+
const workingDirectory = session.working_directory.replace(
|
|
20
|
+
/^~/,
|
|
21
|
+
process.env.HOME || ""
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
ensureMcpConfig(workingDirectory, id);
|
|
25
|
+
|
|
26
|
+
return NextResponse.json({ success: true });
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("Failed to write MCP config:", error);
|
|
29
|
+
return NextResponse.json(
|
|
30
|
+
{ error: "Failed to write MCP config" },
|
|
31
|
+
{ status: 500 }
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { queries } from "@/lib/db";
|
|
3
|
+
import {
|
|
4
|
+
getSessionMessages,
|
|
5
|
+
getClaudeProjectNames,
|
|
6
|
+
getSessions,
|
|
7
|
+
} from "@/lib/claude/jsonl-reader";
|
|
8
|
+
|
|
9
|
+
interface RouteParams {
|
|
10
|
+
params: Promise<{ id: string }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function findProjectForSession(
|
|
14
|
+
sessionId: string
|
|
15
|
+
): Promise<string | null> {
|
|
16
|
+
const projectNames = getClaudeProjectNames();
|
|
17
|
+
for (const projectName of projectNames) {
|
|
18
|
+
const { sessions } = await getSessions(projectName, 100, 0);
|
|
19
|
+
if (sessions.some((s) => s.sessionId === sessionId)) {
|
|
20
|
+
return projectName;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
27
|
+
try {
|
|
28
|
+
const { id } = await params;
|
|
29
|
+
const { searchParams } = new URL(request.url);
|
|
30
|
+
const limit = parseInt(searchParams.get("limit") || "100", 10);
|
|
31
|
+
const offset = parseInt(searchParams.get("offset") || "0", 10);
|
|
32
|
+
|
|
33
|
+
const session = await queries.getSession(id);
|
|
34
|
+
if (!session) {
|
|
35
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const claudeSessionId = session.claude_session_id || id;
|
|
39
|
+
const projectName = await findProjectForSession(claudeSessionId);
|
|
40
|
+
|
|
41
|
+
if (!projectName) {
|
|
42
|
+
return NextResponse.json({ messages: [], total: 0, hasMore: false });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = await getSessionMessages(
|
|
46
|
+
projectName,
|
|
47
|
+
claudeSessionId,
|
|
48
|
+
limit,
|
|
49
|
+
offset
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return NextResponse.json(result);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Error fetching messages:", error);
|
|
55
|
+
return NextResponse.json(
|
|
56
|
+
{ error: "Failed to fetch messages" },
|
|
57
|
+
{ status: 500 }
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { queries, type Session } from "@/lib/db";
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
interface RouteParams {
|
|
9
|
+
params: Promise<{ id: string }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PRInfo {
|
|
13
|
+
number: number;
|
|
14
|
+
url: string;
|
|
15
|
+
state: string;
|
|
16
|
+
title: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if gh CLI is installed and authenticated
|
|
21
|
+
*/
|
|
22
|
+
async function checkGhCli(): Promise<boolean> {
|
|
23
|
+
try {
|
|
24
|
+
await execAsync("gh auth status", { timeout: 5000 });
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get PR info for a branch
|
|
33
|
+
*/
|
|
34
|
+
async function getPRForBranch(
|
|
35
|
+
projectPath: string,
|
|
36
|
+
branchName: string
|
|
37
|
+
): Promise<PRInfo | null> {
|
|
38
|
+
try {
|
|
39
|
+
const { stdout } = await execAsync(
|
|
40
|
+
`gh pr list --head "${branchName}" --json number,url,state,title --limit 1`,
|
|
41
|
+
{ cwd: projectPath, timeout: 10000 }
|
|
42
|
+
);
|
|
43
|
+
const prs = JSON.parse(stdout);
|
|
44
|
+
return prs.length > 0 ? prs[0] : null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a new PR for a branch
|
|
52
|
+
*/
|
|
53
|
+
async function createPR(
|
|
54
|
+
projectPath: string,
|
|
55
|
+
branchName: string,
|
|
56
|
+
baseBranch: string,
|
|
57
|
+
title: string,
|
|
58
|
+
body?: string
|
|
59
|
+
): Promise<PRInfo> {
|
|
60
|
+
// First push the branch if not already pushed
|
|
61
|
+
try {
|
|
62
|
+
await execAsync(`git push -u origin "${branchName}"`, {
|
|
63
|
+
cwd: projectPath,
|
|
64
|
+
timeout: 30000,
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
// Branch might already be pushed, continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const bodyArg = body ? `--body "${body.replace(/"/g, '\\"')}"` : '--body ""';
|
|
71
|
+
const { stdout } = await execAsync(
|
|
72
|
+
`gh pr create --title "${title.replace(/"/g, '\\"')}" --base "${baseBranch}" ${bodyArg} --json number,url,state,title`,
|
|
73
|
+
{ cwd: projectPath, timeout: 30000 }
|
|
74
|
+
);
|
|
75
|
+
return JSON.parse(stdout);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// GET /api/sessions/[id]/pr - Get PR info for session
|
|
79
|
+
export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
80
|
+
try {
|
|
81
|
+
const { id } = await params;
|
|
82
|
+
const session = await queries.getSession(id);
|
|
83
|
+
|
|
84
|
+
if (!session) {
|
|
85
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!session.worktree_path || !session.branch_name) {
|
|
89
|
+
return NextResponse.json(
|
|
90
|
+
{ error: "Session is not a worktree session" },
|
|
91
|
+
{ status: 400 }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check gh CLI
|
|
96
|
+
if (!(await checkGhCli())) {
|
|
97
|
+
return NextResponse.json(
|
|
98
|
+
{
|
|
99
|
+
error:
|
|
100
|
+
"GitHub CLI not installed or not authenticated. Run 'gh auth login' first.",
|
|
101
|
+
},
|
|
102
|
+
{ status: 400 }
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const pr = await getPRForBranch(session.worktree_path, session.branch_name);
|
|
107
|
+
|
|
108
|
+
// Update session with PR info if found
|
|
109
|
+
if (pr) {
|
|
110
|
+
await queries.updateSessionPR(pr.url, pr.number, pr.state, id);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return NextResponse.json({ pr });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("Error fetching PR:", error);
|
|
116
|
+
return NextResponse.json(
|
|
117
|
+
{ error: "Failed to fetch PR info" },
|
|
118
|
+
{ status: 500 }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// POST /api/sessions/[id]/pr - Create PR for session
|
|
124
|
+
export async function POST(request: NextRequest, { params }: RouteParams) {
|
|
125
|
+
try {
|
|
126
|
+
const { id } = await params;
|
|
127
|
+
const body = await request.json();
|
|
128
|
+
const { title, description } = body;
|
|
129
|
+
|
|
130
|
+
const session = await queries.getSession(id);
|
|
131
|
+
|
|
132
|
+
if (!session) {
|
|
133
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!session.worktree_path || !session.branch_name) {
|
|
137
|
+
return NextResponse.json(
|
|
138
|
+
{ error: "Session is not a worktree session" },
|
|
139
|
+
{ status: 400 }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check gh CLI
|
|
144
|
+
if (!(await checkGhCli())) {
|
|
145
|
+
return NextResponse.json(
|
|
146
|
+
{
|
|
147
|
+
error:
|
|
148
|
+
"GitHub CLI not installed or not authenticated. Run 'gh auth login' first.",
|
|
149
|
+
},
|
|
150
|
+
{ status: 400 }
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Check if PR already exists
|
|
155
|
+
const existingPR = await getPRForBranch(
|
|
156
|
+
session.worktree_path,
|
|
157
|
+
session.branch_name
|
|
158
|
+
);
|
|
159
|
+
if (existingPR) {
|
|
160
|
+
return NextResponse.json(
|
|
161
|
+
{ error: "PR already exists for this branch", pr: existingPR },
|
|
162
|
+
{ status: 409 }
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Create PR
|
|
167
|
+
const prTitle = title || session.name;
|
|
168
|
+
const pr = await createPR(
|
|
169
|
+
session.worktree_path,
|
|
170
|
+
session.branch_name,
|
|
171
|
+
session.base_branch || "main",
|
|
172
|
+
prTitle,
|
|
173
|
+
description
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Save PR info to session
|
|
177
|
+
await queries.updateSessionPR(pr.url, pr.number, pr.state, id);
|
|
178
|
+
|
|
179
|
+
return NextResponse.json({ pr }, { status: 201 });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error("Error creating PR:", error);
|
|
182
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
183
|
+
return NextResponse.json(
|
|
184
|
+
{ error: `Failed to create PR: ${message}` },
|
|
185
|
+
{ status: 500 }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { queries, type Session } from "@/lib/db";
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
// Get terminal preview (last N lines) from tmux session
|
|
9
|
+
export async function GET(
|
|
10
|
+
request: NextRequest,
|
|
11
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
12
|
+
) {
|
|
13
|
+
try {
|
|
14
|
+
const { id } = await params;
|
|
15
|
+
|
|
16
|
+
// Look up session to get the tmux name
|
|
17
|
+
const session = await queries.getSession(id);
|
|
18
|
+
const agentType = session?.agent_type || "claude";
|
|
19
|
+
const sessionName = session?.tmux_name || `${agentType}-${id}`;
|
|
20
|
+
|
|
21
|
+
// Capture visible pane content plus scrollback, take last 50 lines
|
|
22
|
+
const { stdout } = await execAsync(
|
|
23
|
+
`tmux capture-pane -t "${sessionName}" -p -S -100 2>/dev/null || echo ""`
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Take the last 50 non-empty lines (trim trailing empty lines)
|
|
27
|
+
const allLines = stdout.split("\n");
|
|
28
|
+
let lastNonEmpty = allLines.length - 1;
|
|
29
|
+
while (lastNonEmpty > 0 && allLines[lastNonEmpty].trim() === "") {
|
|
30
|
+
lastNonEmpty--;
|
|
31
|
+
}
|
|
32
|
+
const lines = allLines.slice(
|
|
33
|
+
Math.max(0, lastNonEmpty - 49),
|
|
34
|
+
lastNonEmpty + 1
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({ lines });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Error getting session preview:", error);
|
|
40
|
+
return NextResponse.json({ lines: [] });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
import { queries, type Session } from "@/lib/db";
|
|
5
|
+
import { deleteWorktree, isClaudeDeckWorktree } from "@/lib/worktrees";
|
|
6
|
+
import { releasePort } from "@/lib/ports";
|
|
7
|
+
import { killWorker } from "@/lib/orchestration";
|
|
8
|
+
import { generateBranchName, getCurrentBranch, renameBranch } from "@/lib/git";
|
|
9
|
+
import { runInBackground } from "@/lib/async-operations";
|
|
10
|
+
|
|
11
|
+
const execAsync = promisify(exec);
|
|
12
|
+
|
|
13
|
+
// Sanitize a name for use as tmux session name
|
|
14
|
+
function sanitizeTmuxName(name: string): string {
|
|
15
|
+
return name
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9-]/g, "-") // Replace non-alphanumeric with dashes
|
|
18
|
+
.replace(/-+/g, "-") // Collapse multiple dashes
|
|
19
|
+
.replace(/^-|-$/g, "") // Remove leading/trailing dashes
|
|
20
|
+
.slice(0, 50); // Limit length
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RouteParams {
|
|
24
|
+
params: Promise<{ id: string }>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// GET /api/sessions/[id] - Get single session
|
|
28
|
+
export async function GET(request: NextRequest, { params }: RouteParams) {
|
|
29
|
+
try {
|
|
30
|
+
const { id } = await params;
|
|
31
|
+
const session = await queries.getSession(id);
|
|
32
|
+
|
|
33
|
+
if (!session) {
|
|
34
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return NextResponse.json({ session });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error("Error fetching session:", error);
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: "Failed to fetch session" },
|
|
42
|
+
{ status: 500 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// PATCH /api/sessions/[id] - Update session
|
|
48
|
+
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|
49
|
+
try {
|
|
50
|
+
const { id } = await params;
|
|
51
|
+
const body = await request.json();
|
|
52
|
+
|
|
53
|
+
const existing = await queries.getSession(id);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build update query dynamically based on provided fields
|
|
59
|
+
const updates: string[] = [];
|
|
60
|
+
const values: unknown[] = [];
|
|
61
|
+
|
|
62
|
+
// Handle name change - also rename tmux session and git branch (for worktrees)
|
|
63
|
+
if (body.name !== undefined && body.name !== existing.name) {
|
|
64
|
+
const newTmuxName = sanitizeTmuxName(body.name);
|
|
65
|
+
const oldTmuxName = existing.tmux_name;
|
|
66
|
+
|
|
67
|
+
// Try to rename the tmux session
|
|
68
|
+
if (oldTmuxName && newTmuxName) {
|
|
69
|
+
try {
|
|
70
|
+
await execAsync(
|
|
71
|
+
`tmux rename-session -t "${oldTmuxName}" "${newTmuxName}"`
|
|
72
|
+
);
|
|
73
|
+
updates.push(`tmux_name = ?`);
|
|
74
|
+
values.push(newTmuxName);
|
|
75
|
+
} catch {
|
|
76
|
+
// tmux session might not exist or rename failed - that's ok, just update the name
|
|
77
|
+
// Still update tmux_name in DB so future attachments use the new name
|
|
78
|
+
updates.push(`tmux_name = ?`);
|
|
79
|
+
values.push(newTmuxName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// If this is a worktree session, also rename the git branch
|
|
84
|
+
if (existing.worktree_path && isClaudeDeckWorktree(existing.worktree_path)) {
|
|
85
|
+
try {
|
|
86
|
+
const currentBranch = await getCurrentBranch(existing.worktree_path);
|
|
87
|
+
const newBranchName = generateBranchName(body.name);
|
|
88
|
+
|
|
89
|
+
if (currentBranch !== newBranchName) {
|
|
90
|
+
const result = await renameBranch(
|
|
91
|
+
existing.worktree_path,
|
|
92
|
+
currentBranch,
|
|
93
|
+
newBranchName
|
|
94
|
+
);
|
|
95
|
+
console.log(
|
|
96
|
+
`Renamed branch ${currentBranch} → ${newBranchName}`,
|
|
97
|
+
result.remoteRenamed ? "(also on remote)" : "(local only)"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error("Failed to rename git branch:", error);
|
|
102
|
+
// Continue with session rename even if branch rename fails
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
updates.push(`name = ?`);
|
|
107
|
+
values.push(body.name);
|
|
108
|
+
}
|
|
109
|
+
if (body.status !== undefined) {
|
|
110
|
+
updates.push(`status = ?`);
|
|
111
|
+
values.push(body.status);
|
|
112
|
+
}
|
|
113
|
+
if (body.workingDirectory !== undefined) {
|
|
114
|
+
updates.push(`working_directory = ?`);
|
|
115
|
+
values.push(body.workingDirectory);
|
|
116
|
+
}
|
|
117
|
+
if (body.systemPrompt !== undefined) {
|
|
118
|
+
updates.push(`system_prompt = ?`);
|
|
119
|
+
values.push(body.systemPrompt);
|
|
120
|
+
}
|
|
121
|
+
if (body.groupPath !== undefined) {
|
|
122
|
+
updates.push(`group_path = ?`);
|
|
123
|
+
values.push(body.groupPath);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (updates.length > 0) {
|
|
127
|
+
updates.push("updated_at = datetime('now')");
|
|
128
|
+
values.push(id);
|
|
129
|
+
|
|
130
|
+
const { getDb } = await import("@/lib/db");
|
|
131
|
+
getDb()
|
|
132
|
+
.prepare(`UPDATE sessions SET ${updates.join(", ")} WHERE id = ?`)
|
|
133
|
+
.run(...values);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const session = await queries.getSession(id);
|
|
137
|
+
return NextResponse.json({ session });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error("Error updating session:", error);
|
|
140
|
+
return NextResponse.json(
|
|
141
|
+
{ error: "Failed to update session" },
|
|
142
|
+
{ status: 500 }
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// DELETE /api/sessions/[id] - Delete session
|
|
148
|
+
export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
149
|
+
try {
|
|
150
|
+
const { id } = await params;
|
|
151
|
+
|
|
152
|
+
const existing = await queries.getSession(id);
|
|
153
|
+
if (!existing) {
|
|
154
|
+
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If this is a conductor, delete all its workers first
|
|
158
|
+
const workers = await queries.getWorkersByConductor(id);
|
|
159
|
+
for (const worker of workers) {
|
|
160
|
+
try {
|
|
161
|
+
await killWorker(worker.id, false); // false = don't cleanup worktree yet
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error(`Failed to kill worker ${worker.id}:`, error);
|
|
164
|
+
}
|
|
165
|
+
await queries.deleteSession(worker.id);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Release port if this session had one assigned
|
|
169
|
+
if (existing.dev_server_port) {
|
|
170
|
+
releasePort(id);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Delete from database immediately for instant UI feedback
|
|
174
|
+
await queries.deleteSession(id);
|
|
175
|
+
|
|
176
|
+
// Clean up worktree in background (non-blocking)
|
|
177
|
+
if (existing.worktree_path && isClaudeDeckWorktree(existing.worktree_path)) {
|
|
178
|
+
const worktreePath = existing.worktree_path; // Capture for closure
|
|
179
|
+
runInBackground(async () => {
|
|
180
|
+
const { exec } = await import("child_process");
|
|
181
|
+
const { promisify } = await import("util");
|
|
182
|
+
const execAsync = promisify(exec);
|
|
183
|
+
|
|
184
|
+
const { stdout } = await execAsync(
|
|
185
|
+
`git -C "${worktreePath}" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || echo ""`,
|
|
186
|
+
{ timeout: 5000 }
|
|
187
|
+
);
|
|
188
|
+
const gitCommonDir = stdout.trim().replace(/\/.git$/, "");
|
|
189
|
+
|
|
190
|
+
if (gitCommonDir) {
|
|
191
|
+
await deleteWorktree(worktreePath, gitCommonDir, false);
|
|
192
|
+
}
|
|
193
|
+
}, `cleanup-worktree-${id}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Also cleanup worker worktrees in background
|
|
197
|
+
if (workers.length > 0) {
|
|
198
|
+
for (const worker of workers) {
|
|
199
|
+
if (worker.worktree_path && isClaudeDeckWorktree(worker.worktree_path)) {
|
|
200
|
+
const worktreePath = worker.worktree_path; // Capture for closure
|
|
201
|
+
const workerId = worker.id; // Capture ID for task name
|
|
202
|
+
runInBackground(async () => {
|
|
203
|
+
const { exec } = await import("child_process");
|
|
204
|
+
const { promisify } = await import("util");
|
|
205
|
+
const execAsync = promisify(exec);
|
|
206
|
+
|
|
207
|
+
const { stdout } = await execAsync(
|
|
208
|
+
`git -C "${worktreePath}" rev-parse --path-format=absolute --git-common-dir 2>/dev/null || echo ""`,
|
|
209
|
+
{ timeout: 5000 }
|
|
210
|
+
);
|
|
211
|
+
const gitCommonDir = stdout.trim().replace(/\/.git$/, "");
|
|
212
|
+
|
|
213
|
+
if (gitCommonDir) {
|
|
214
|
+
await deleteWorktree(worktreePath, gitCommonDir, false);
|
|
215
|
+
}
|
|
216
|
+
}, `cleanup-worker-worktree-${workerId}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return NextResponse.json({ success: true });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error("Error deleting session:", error);
|
|
224
|
+
return NextResponse.json(
|
|
225
|
+
{ error: "Failed to delete session" },
|
|
226
|
+
{ status: 500 }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|