@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.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +123 -0
  3. package/app/api/claude/hidden/route.ts +66 -0
  4. package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
  5. package/app/api/claude/projects/route.ts +44 -0
  6. package/app/api/code-search/available/route.ts +12 -0
  7. package/app/api/code-search/route.ts +47 -0
  8. package/app/api/dev-servers/[id]/logs/route.ts +23 -0
  9. package/app/api/dev-servers/[id]/restart/route.ts +20 -0
  10. package/app/api/dev-servers/[id]/route.ts +51 -0
  11. package/app/api/dev-servers/[id]/stop/route.ts +20 -0
  12. package/app/api/dev-servers/detect/route.ts +39 -0
  13. package/app/api/dev-servers/route.ts +48 -0
  14. package/app/api/exec/route.ts +60 -0
  15. package/app/api/files/content/route.ts +76 -0
  16. package/app/api/files/route.ts +37 -0
  17. package/app/api/files/upload-temp/route.ts +41 -0
  18. package/app/api/git/check/route.ts +54 -0
  19. package/app/api/git/clone/route.ts +99 -0
  20. package/app/api/git/commit/route.ts +75 -0
  21. package/app/api/git/discard/route.ts +38 -0
  22. package/app/api/git/file-content/route.ts +64 -0
  23. package/app/api/git/history/[hash]/diff/route.ts +38 -0
  24. package/app/api/git/history/[hash]/route.ts +34 -0
  25. package/app/api/git/history/route.ts +27 -0
  26. package/app/api/git/multi-status/route.ts +46 -0
  27. package/app/api/git/pr/route.ts +164 -0
  28. package/app/api/git/push/route.ts +64 -0
  29. package/app/api/git/stage/route.ts +40 -0
  30. package/app/api/git/status/route.ts +51 -0
  31. package/app/api/git/unstage/route.ts +46 -0
  32. package/app/api/groups/[...path]/route.ts +136 -0
  33. package/app/api/groups/route.ts +93 -0
  34. package/app/api/orchestrate/spawn/route.ts +45 -0
  35. package/app/api/orchestrate/workers/[id]/route.ts +89 -0
  36. package/app/api/orchestrate/workers/route.ts +31 -0
  37. package/app/api/projects/[id]/detect/route.ts +27 -0
  38. package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
  39. package/app/api/projects/[id]/dev-servers/route.ts +51 -0
  40. package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
  41. package/app/api/projects/[id]/repositories/route.ts +74 -0
  42. package/app/api/projects/[id]/route.ts +108 -0
  43. package/app/api/projects/detect/route.ts +33 -0
  44. package/app/api/projects/route.ts +59 -0
  45. package/app/api/sessions/[id]/claude-session/route.ts +42 -0
  46. package/app/api/sessions/[id]/fork/route.ts +74 -0
  47. package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
  48. package/app/api/sessions/[id]/messages/route.ts +60 -0
  49. package/app/api/sessions/[id]/pr/route.ts +188 -0
  50. package/app/api/sessions/[id]/preview/route.ts +42 -0
  51. package/app/api/sessions/[id]/route.ts +229 -0
  52. package/app/api/sessions/[id]/send-keys/route.ts +119 -0
  53. package/app/api/sessions/[id]/summarize/route.ts +331 -0
  54. package/app/api/sessions/init-script/route.ts +84 -0
  55. package/app/api/sessions/route.ts +209 -0
  56. package/app/api/sessions/status/route.ts +237 -0
  57. package/app/api/system/route.ts +9 -0
  58. package/app/api/tmux/kill-all/route.ts +57 -0
  59. package/app/api/tmux/rename/route.ts +30 -0
  60. package/app/globals.css +174 -0
  61. package/app/icon.svg +11 -0
  62. package/app/layout.tsx +122 -0
  63. package/app/page.tsx +629 -0
  64. package/components/ChatMessage.tsx +65 -0
  65. package/components/ChatView.tsx +276 -0
  66. package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
  67. package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
  68. package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
  69. package/components/ClaudeProjects/index.ts +1 -0
  70. package/components/CodeSearch/CodeSearchResults.tsx +177 -0
  71. package/components/ConductorPanel.tsx +256 -0
  72. package/components/DevServers/DevServerCard.tsx +311 -0
  73. package/components/DevServers/DevServersSection.tsx +91 -0
  74. package/components/DevServers/ServerLogsModal.tsx +151 -0
  75. package/components/DevServers/StartServerDialog.tsx +359 -0
  76. package/components/DevServers/index.ts +4 -0
  77. package/components/DiffViewer/DiffModal.tsx +151 -0
  78. package/components/DiffViewer/UnifiedDiff.tsx +185 -0
  79. package/components/DiffViewer/index.tsx +2 -0
  80. package/components/DirectoryPicker.tsx +355 -0
  81. package/components/FileExplorer/FileEditor.tsx +276 -0
  82. package/components/FileExplorer/FileTabs.tsx +118 -0
  83. package/components/FileExplorer/FileTree.tsx +214 -0
  84. package/components/FileExplorer/HtmlRenderer.tsx +16 -0
  85. package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
  86. package/components/FileExplorer/index.tsx +520 -0
  87. package/components/FilePicker.tsx +339 -0
  88. package/components/FolderPicker.tsx +201 -0
  89. package/components/GitDrawer/FileEditDialog.tsx +400 -0
  90. package/components/GitDrawer/index.tsx +464 -0
  91. package/components/GitPanel/CommitForm.tsx +205 -0
  92. package/components/GitPanel/CommitHistory.tsx +174 -0
  93. package/components/GitPanel/CommitItem.tsx +196 -0
  94. package/components/GitPanel/FileChanges.tsx +414 -0
  95. package/components/GitPanel/GitPanelTabs.tsx +39 -0
  96. package/components/GitPanel/index.tsx +817 -0
  97. package/components/MessageInput.tsx +82 -0
  98. package/components/NewClaudeSessionDialog.tsx +166 -0
  99. package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
  100. package/components/NewSessionDialog/AgentSelector.tsx +37 -0
  101. package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
  102. package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
  103. package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
  104. package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
  105. package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
  106. package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
  107. package/components/NewSessionDialog/index.tsx +106 -0
  108. package/components/NotificationSettings.tsx +127 -0
  109. package/components/PRCreationModal.tsx +272 -0
  110. package/components/Pane/DesktopTabBar.tsx +353 -0
  111. package/components/Pane/MobileTabBar.tsx +210 -0
  112. package/components/Pane/OpenInVSCode.tsx +69 -0
  113. package/components/Pane/PaneSkeletons.tsx +57 -0
  114. package/components/Pane/index.tsx +558 -0
  115. package/components/PaneLayout.tsx +60 -0
  116. package/components/Projects/DevServersSection.tsx +140 -0
  117. package/components/Projects/DirectoryField.tsx +92 -0
  118. package/components/Projects/NewProjectDialog.tsx +188 -0
  119. package/components/Projects/NewProjectDialog.types.ts +46 -0
  120. package/components/Projects/ProjectCard.tsx +276 -0
  121. package/components/Projects/ProjectSettingsDialog.tsx +811 -0
  122. package/components/Projects/hooks/useNewProjectForm.ts +249 -0
  123. package/components/Projects/index.ts +3 -0
  124. package/components/Providers.tsx +49 -0
  125. package/components/QuickSwitcher.tsx +306 -0
  126. package/components/SessionList/KillAllConfirm.tsx +46 -0
  127. package/components/SessionList/SelectionToolbar.tsx +164 -0
  128. package/components/SessionList/SessionList.types.ts +37 -0
  129. package/components/SessionList/SessionListHeader.tsx +71 -0
  130. package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
  131. package/components/SessionList/index.tsx +189 -0
  132. package/components/ShellDrawer/index.tsx +106 -0
  133. package/components/SidebarFooter.tsx +55 -0
  134. package/components/Terminal/KeybarToggleButton.tsx +45 -0
  135. package/components/Terminal/ScrollToBottomButton.tsx +32 -0
  136. package/components/Terminal/SearchBar.tsx +71 -0
  137. package/components/Terminal/TerminalToolbar.tsx +551 -0
  138. package/components/Terminal/VirtualKeyboard.tsx +711 -0
  139. package/components/Terminal/constants.ts +20 -0
  140. package/components/Terminal/hooks/index.ts +5 -0
  141. package/components/Terminal/hooks/resize-handlers.ts +140 -0
  142. package/components/Terminal/hooks/terminal-init.ts +151 -0
  143. package/components/Terminal/hooks/touch-scroll.ts +155 -0
  144. package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
  145. package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
  146. package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
  147. package/components/Terminal/hooks/websocket-connection.ts +274 -0
  148. package/components/Terminal/index.tsx +320 -0
  149. package/components/ThemeToggle.tsx +168 -0
  150. package/components/TmuxSessions.tsx +132 -0
  151. package/components/ToolCallDisplay.tsx +71 -0
  152. package/components/WorkerCard.tsx +245 -0
  153. package/components/a/ABadge.tsx +115 -0
  154. package/components/a/AButton.tsx +163 -0
  155. package/components/a/ADialog.tsx +93 -0
  156. package/components/a/ADropdownMenu.tsx +279 -0
  157. package/components/a/AIconButton.tsx +190 -0
  158. package/components/a/ASheet.tsx +150 -0
  159. package/components/a/ATooltip.tsx +77 -0
  160. package/components/a/index.ts +64 -0
  161. package/components/mobile/SwipeSidebar.tsx +122 -0
  162. package/components/ui/badge.tsx +41 -0
  163. package/components/ui/button.tsx +60 -0
  164. package/components/ui/context-menu.tsx +197 -0
  165. package/components/ui/dialog.tsx +143 -0
  166. package/components/ui/dropdown-menu.tsx +257 -0
  167. package/components/ui/input.tsx +21 -0
  168. package/components/ui/scroll-area.tsx +52 -0
  169. package/components/ui/select.tsx +159 -0
  170. package/components/ui/skeleton.tsx +111 -0
  171. package/components/ui/switch.tsx +31 -0
  172. package/components/ui/textarea.tsx +21 -0
  173. package/components/ui/tooltip.tsx +32 -0
  174. package/components/views/DesktopView.tsx +244 -0
  175. package/components/views/MobileView.tsx +110 -0
  176. package/components/views/types.ts +75 -0
  177. package/contexts/PaneContext.tsx +336 -0
  178. package/data/claude/index.ts +9 -0
  179. package/data/claude/keys.ts +6 -0
  180. package/data/claude/queries.ts +120 -0
  181. package/data/claude/useClaudeUpdates.ts +37 -0
  182. package/data/code-search/index.ts +2 -0
  183. package/data/code-search/keys.ts +7 -0
  184. package/data/code-search/queries.ts +61 -0
  185. package/data/dev-servers/index.ts +8 -0
  186. package/data/dev-servers/keys.ts +4 -0
  187. package/data/dev-servers/queries.ts +104 -0
  188. package/data/files/index.ts +3 -0
  189. package/data/files/keys.ts +4 -0
  190. package/data/files/queries.ts +25 -0
  191. package/data/git/keys.ts +15 -0
  192. package/data/git/queries.ts +395 -0
  193. package/data/groups/index.ts +1 -0
  194. package/data/groups/mutations.ts +95 -0
  195. package/data/projects/index.ts +10 -0
  196. package/data/projects/keys.ts +4 -0
  197. package/data/projects/queries.ts +193 -0
  198. package/data/repositories/index.ts +7 -0
  199. package/data/repositories/keys.ts +5 -0
  200. package/data/repositories/queries.ts +122 -0
  201. package/data/sessions/index.ts +12 -0
  202. package/data/sessions/keys.ts +8 -0
  203. package/data/sessions/queries.ts +218 -0
  204. package/data/statuses/index.ts +1 -0
  205. package/data/statuses/queries.ts +69 -0
  206. package/hooks/useCopyToClipboard.ts +48 -0
  207. package/hooks/useDevServersManager.ts +73 -0
  208. package/hooks/useDirectoryBrowser.ts +90 -0
  209. package/hooks/useDrawerAnimation.ts +27 -0
  210. package/hooks/useFileDrop.ts +87 -0
  211. package/hooks/useFileEditor.ts +184 -0
  212. package/hooks/useGroups.ts +37 -0
  213. package/hooks/useHomePath.ts +34 -0
  214. package/hooks/useKeyRepeat.ts +55 -0
  215. package/hooks/useKeybarVisibility.ts +42 -0
  216. package/hooks/useNotifications.ts +257 -0
  217. package/hooks/useProjects.ts +53 -0
  218. package/hooks/useSessionStatuses.ts +30 -0
  219. package/hooks/useSessions.ts +86 -0
  220. package/hooks/useSpeechRecognition.ts +124 -0
  221. package/hooks/useViewport.ts +32 -0
  222. package/hooks/useViewportHeight.ts +50 -0
  223. package/lib/async-operations.ts +35 -0
  224. package/lib/banner.ts +81 -0
  225. package/lib/claude/jsonl-cache.ts +86 -0
  226. package/lib/claude/jsonl-reader.ts +271 -0
  227. package/lib/claude/process-manager.ts +278 -0
  228. package/lib/claude/stream-parser.ts +173 -0
  229. package/lib/claude/types.ts +154 -0
  230. package/lib/claude/watcher.ts +71 -0
  231. package/lib/client/session-registry.ts +111 -0
  232. package/lib/code-search.ts +121 -0
  233. package/lib/db/index.ts +48 -0
  234. package/lib/db/migrations.ts +45 -0
  235. package/lib/db/queries.ts +460 -0
  236. package/lib/db/schema.ts +114 -0
  237. package/lib/db/types.ts +92 -0
  238. package/lib/db.ts +2 -0
  239. package/lib/dev-servers.ts +509 -0
  240. package/lib/diff-parser.ts +221 -0
  241. package/lib/env-setup.ts +285 -0
  242. package/lib/file-upload.ts +34 -0
  243. package/lib/file-utils.ts +50 -0
  244. package/lib/files.ts +207 -0
  245. package/lib/git-history.ts +294 -0
  246. package/lib/git-status.ts +391 -0
  247. package/lib/git.ts +257 -0
  248. package/lib/mcp-config.ts +81 -0
  249. package/lib/multi-repo-git.ts +179 -0
  250. package/lib/notifications.ts +219 -0
  251. package/lib/orchestration.ts +448 -0
  252. package/lib/panes.ts +232 -0
  253. package/lib/ports.ts +97 -0
  254. package/lib/pr-generation.ts +307 -0
  255. package/lib/pr.ts +234 -0
  256. package/lib/projects.ts +578 -0
  257. package/lib/providers/registry.ts +70 -0
  258. package/lib/providers.ts +121 -0
  259. package/lib/query-client.ts +14 -0
  260. package/lib/rangeSelectionUtils.ts +65 -0
  261. package/lib/status-detector.ts +375 -0
  262. package/lib/terminal-themes.ts +265 -0
  263. package/lib/theme-config.ts +327 -0
  264. package/lib/utils.ts +6 -0
  265. package/lib/worktrees.ts +262 -0
  266. package/mcp/orchestration-server.ts +438 -0
  267. package/package.json +139 -0
  268. package/postcss.config.mjs +7 -0
  269. package/public/icon.svg +10 -0
  270. package/public/icons/icon-128x128.png +0 -0
  271. package/public/icons/icon-144x144.png +0 -0
  272. package/public/icons/icon-152x152.png +0 -0
  273. package/public/icons/icon-192x192.png +0 -0
  274. package/public/icons/icon-384x384.png +0 -0
  275. package/public/icons/icon-512x512.png +0 -0
  276. package/public/icons/icon-72x72.png +0 -0
  277. package/public/icons/icon-96x96.png +0 -0
  278. package/public/manifest.json +61 -0
  279. package/public/sw.js +64 -0
  280. package/scripts/agent-os +91 -0
  281. package/scripts/install.sh +48 -0
  282. package/scripts/lib/ai-clis.sh +132 -0
  283. package/scripts/lib/commands.sh +487 -0
  284. package/scripts/lib/common.sh +89 -0
  285. package/scripts/lib/prerequisites.sh +462 -0
  286. package/scripts/setup.sh +134 -0
  287. package/server.ts +155 -0
  288. package/stores/fileOpen.ts +26 -0
  289. package/stores/index.ts +1 -0
  290. package/stores/initialPrompt.ts +24 -0
  291. package/stores/sessionSelection.ts +48 -0
  292. package/styles/themes.css +603 -0
  293. 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
+ }