@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,119 @@
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 { appendFileSync } from "fs";
6
+
7
+ const execAsync = promisify(exec);
8
+
9
+ // Log to file for debugging
10
+ const LOG_FILE = "/tmp/claude-deck-send-keys.log";
11
+ function log(msg: string) {
12
+ const timestamp = new Date().toISOString();
13
+ const line = `[${timestamp}] ${msg}\n`;
14
+ console.log(`[send-keys] ${msg}`);
15
+ try {
16
+ appendFileSync(LOG_FILE, line);
17
+ } catch {}
18
+ }
19
+
20
+ // POST /api/sessions/[id]/send-keys - Send text to a tmux session
21
+ export async function POST(
22
+ request: NextRequest,
23
+ { params }: { params: Promise<{ id: string }> }
24
+ ) {
25
+ try {
26
+ const { id } = await params;
27
+ const body = await request.json();
28
+ const { text, pressEnter = true } = body;
29
+
30
+ log(`=== START send-keys for session ${id} ===`);
31
+ log(`Text length: ${text?.length || 0}, pressEnter: ${pressEnter}`);
32
+
33
+ if (!text) {
34
+ log("ERROR: No text provided");
35
+ return NextResponse.json({ error: "No text provided" }, { status: 400 });
36
+ }
37
+
38
+ const session = await queries.getSession(id);
39
+
40
+ if (!session) {
41
+ log(`ERROR: Session ${id} not found in DB`);
42
+ return NextResponse.json({ error: "Session not found" }, { status: 404 });
43
+ }
44
+
45
+ const tmuxSessionName = `${session.agent_type}-${id}`;
46
+ log(`Tmux session name: ${tmuxSessionName}`);
47
+
48
+ // Check if tmux session exists
49
+ try {
50
+ await execAsync(`tmux has-session -t "${tmuxSessionName}" 2>/dev/null`);
51
+ log(`Tmux session exists`);
52
+ } catch {
53
+ log(`ERROR: Tmux session ${tmuxSessionName} not running`);
54
+ return NextResponse.json(
55
+ { error: "Tmux session not running" },
56
+ { status: 400 }
57
+ );
58
+ }
59
+
60
+ // Write text to a temp file
61
+ const tempFile = `/tmp/claude-deck-send-${id}.txt`;
62
+ const fs = await import("fs/promises");
63
+ await fs.writeFile(tempFile, text);
64
+ log(`Wrote ${text.length} bytes to ${tempFile}`);
65
+
66
+ // Use a named buffer to avoid race conditions
67
+ const bufferName = `send-${id}`;
68
+
69
+ try {
70
+ // Load file into named tmux buffer
71
+ log(`Loading buffer "${bufferName}" from ${tempFile}`);
72
+ const loadCmd = `tmux load-buffer -b "${bufferName}" "${tempFile}"`;
73
+ log(`Running: ${loadCmd}`);
74
+ const loadResult = await execAsync(loadCmd);
75
+ log(
76
+ `Load stdout: "${loadResult.stdout}", stderr: "${loadResult.stderr}"`
77
+ );
78
+
79
+ // Paste the named buffer to the session
80
+ log(`Pasting buffer "${bufferName}" to ${tmuxSessionName}`);
81
+ const pasteCmd = `tmux paste-buffer -b "${bufferName}" -t "${tmuxSessionName}"`;
82
+ log(`Running: ${pasteCmd}`);
83
+ const pasteResult = await execAsync(pasteCmd);
84
+ log(
85
+ `Paste stdout: "${pasteResult.stdout}", stderr: "${pasteResult.stderr}"`
86
+ );
87
+
88
+ // Delete the buffer after use
89
+ await execAsync(`tmux delete-buffer -b "${bufferName}"`).catch(() => {});
90
+
91
+ // Send Enter if requested
92
+ if (pressEnter) {
93
+ log(`Sending Enter to ${tmuxSessionName}`);
94
+ const enterCmd = `tmux send-keys -t "${tmuxSessionName}" Enter`;
95
+ log(`Running: ${enterCmd}`);
96
+ const enterResult = await execAsync(enterCmd);
97
+ log(
98
+ `Enter stdout: "${enterResult.stdout}", stderr: "${enterResult.stderr}"`
99
+ );
100
+ }
101
+
102
+ log(`=== SUCCESS ===`);
103
+ return NextResponse.json({ success: true });
104
+ } catch (cmdError) {
105
+ const msg =
106
+ cmdError instanceof Error ? cmdError.message : String(cmdError);
107
+ log(`ERROR in commands: ${msg}`);
108
+ throw cmdError;
109
+ } finally {
110
+ // Clean up temp file
111
+ await fs.unlink(tempFile).catch(() => {});
112
+ }
113
+ } catch (error) {
114
+ const msg = error instanceof Error ? error.message : String(error);
115
+ log(`ERROR: ${msg}`);
116
+ console.error("Error sending keys:", error);
117
+ return NextResponse.json({ error: msg }, { status: 500 });
118
+ }
119
+ }
@@ -0,0 +1,331 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { exec, spawn } from "child_process";
3
+ import { promisify } from "util";
4
+ import { queries, getDb, type Session } from "@/lib/db";
5
+ import { randomUUID } from "crypto";
6
+ import { writeFileSync, unlinkSync, readFileSync, existsSync } from "fs";
7
+ import { homedir } from "os";
8
+
9
+ const execAsync = promisify(exec);
10
+
11
+ // Get Claude session ID from tmux environment
12
+ async function getClaudeSessionId(tmuxSession: string): Promise<string | null> {
13
+ try {
14
+ const { stdout } = await execAsync(
15
+ `tmux show-environment -t "${tmuxSession}" CLAUDE_SESSION_ID 2>/dev/null || echo ""`
16
+ );
17
+ const line = stdout.trim();
18
+ if (line.startsWith("CLAUDE_SESSION_ID=")) {
19
+ const sessionId = line.replace("CLAUDE_SESSION_ID=", "");
20
+ return sessionId && sessionId !== "null" ? sessionId : null;
21
+ }
22
+ return null;
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ // Encode path for Claude's project directory format (/ becomes -)
29
+ function encodeProjectPath(cwd: string): string {
30
+ return cwd.replace(/\//g, "-");
31
+ }
32
+
33
+ // Read and parse Claude session JSONL file
34
+ function readClaudeSessionHistory(
35
+ cwd: string,
36
+ claudeSessionId: string
37
+ ): string | null {
38
+ const projectPath = encodeProjectPath(cwd);
39
+ const jsonlPath = `${homedir()}/.claude/projects/${projectPath}/${claudeSessionId}.jsonl`;
40
+
41
+ if (!existsSync(jsonlPath)) {
42
+ console.log(`[summarize] JSONL not found: ${jsonlPath}`);
43
+ return null;
44
+ }
45
+
46
+ try {
47
+ const content = readFileSync(jsonlPath, "utf-8");
48
+ const lines = content.trim().split("\n");
49
+ const messages: string[] = [];
50
+
51
+ for (const line of lines) {
52
+ try {
53
+ const entry = JSON.parse(line);
54
+
55
+ // Extract user messages
56
+ if (entry.type === "user" && entry.message?.content) {
57
+ const content =
58
+ typeof entry.message.content === "string"
59
+ ? entry.message.content
60
+ : JSON.stringify(entry.message.content);
61
+ messages.push(`User: ${content}`);
62
+ }
63
+
64
+ // Extract assistant text responses (skip tool calls and thinking)
65
+ if (entry.type === "assistant" && entry.message?.content) {
66
+ const textBlocks = entry.message.content
67
+ .filter((block: { type: string }) => block.type === "text")
68
+ .map((block: { text: string }) => block.text)
69
+ .join("\n");
70
+ if (textBlocks) {
71
+ messages.push(`Assistant: ${textBlocks}`);
72
+ }
73
+ }
74
+ } catch {
75
+ // Skip malformed lines
76
+ }
77
+ }
78
+
79
+ return messages.join("\n\n");
80
+ } catch (error) {
81
+ console.error(`[summarize] Error reading JSONL:`, error);
82
+ return null;
83
+ }
84
+ }
85
+
86
+ // Fallback: Capture recent tmux scrollback (last 500 lines)
87
+ async function captureScrollback(sessionName: string): Promise<string> {
88
+ try {
89
+ const { stdout } = await execAsync(
90
+ `tmux capture-pane -t "${sessionName}" -p -S -500 2>/dev/null`
91
+ );
92
+ return stdout;
93
+ } catch {
94
+ return "";
95
+ }
96
+ }
97
+
98
+ // Get the actual working directory from tmux pane
99
+ async function getTmuxCwd(sessionName: string): Promise<string | null> {
100
+ try {
101
+ const { stdout } = await execAsync(
102
+ `tmux display-message -t "${sessionName}" -p "#{pane_current_path}" 2>/dev/null`
103
+ );
104
+ return stdout.trim() || null;
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ // Generate summary using Claude CLI with stdin
111
+ async function generateSummary(conversation: string): Promise<string> {
112
+ const prompt = `Summarize this Claude Code conversation in under 300 words. Focus on: what was built, key files changed, current state, and any pending work. Be specific.`;
113
+
114
+ return new Promise((resolve, reject) => {
115
+ const claude = spawn("claude", ["-p", prompt], {
116
+ stdio: ["pipe", "pipe", "pipe"],
117
+ });
118
+
119
+ let stdout = "";
120
+ let stderr = "";
121
+
122
+ claude.stdout.on("data", (data) => {
123
+ stdout += data.toString();
124
+ });
125
+
126
+ claude.stderr.on("data", (data) => {
127
+ stderr += data.toString();
128
+ });
129
+
130
+ claude.on("close", (code) => {
131
+ if (code === 0) {
132
+ resolve(stdout.trim());
133
+ } else {
134
+ console.error("Claude CLI failed:", stderr);
135
+ reject(new Error(`Claude CLI exited with code ${code}`));
136
+ }
137
+ });
138
+
139
+ claude.on("error", (err) => {
140
+ reject(err);
141
+ });
142
+
143
+ // Write conversation to stdin
144
+ claude.stdin.write(conversation);
145
+ claude.stdin.end();
146
+ });
147
+ }
148
+
149
+ // Wait for Claude prompt to appear in tmux session
150
+ async function waitForClaudeReady(
151
+ sessionName: string,
152
+ maxAttempts = 30
153
+ ): Promise<boolean> {
154
+ for (let i = 0; i < maxAttempts; i++) {
155
+ try {
156
+ const { stdout } = await execAsync(
157
+ `tmux capture-pane -t "${sessionName}" -p 2>/dev/null`
158
+ );
159
+ // Look for Claude's status line which appears when UI is ready
160
+ if (stdout.includes("⏵⏵") || stdout.includes("accept edits")) {
161
+ return true;
162
+ }
163
+ } catch {
164
+ // Ignore errors, keep polling
165
+ }
166
+ await new Promise((r) => setTimeout(r, 300));
167
+ }
168
+ return false;
169
+ }
170
+
171
+ // Send text to tmux session using load-buffer + paste-buffer
172
+ async function sendToTmux(
173
+ sessionName: string,
174
+ text: string,
175
+ pressEnter = true
176
+ ): Promise<void> {
177
+ const tempFile = `/tmp/claude-deck-send-${Date.now()}.txt`;
178
+ const bufferName = `send-${Date.now()}`;
179
+
180
+ try {
181
+ writeFileSync(tempFile, text);
182
+ await execAsync(`tmux load-buffer -b "${bufferName}" "${tempFile}"`);
183
+ await execAsync(`tmux paste-buffer -b "${bufferName}" -t "${sessionName}"`);
184
+ await execAsync(`tmux delete-buffer -b "${bufferName}"`).catch(() => {});
185
+
186
+ if (pressEnter) {
187
+ // Wait for Claude to process pasted text before sending Enter
188
+ await new Promise((r) => setTimeout(r, 500));
189
+ await execAsync(`tmux send-keys -t "${sessionName}" Enter`);
190
+ }
191
+ } finally {
192
+ try {
193
+ unlinkSync(tempFile);
194
+ } catch {}
195
+ }
196
+ }
197
+
198
+ // POST /api/sessions/[id]/summarize - Summarize and create fresh session
199
+ export async function POST(
200
+ request: NextRequest,
201
+ { params }: { params: Promise<{ id: string }> }
202
+ ) {
203
+ try {
204
+ const { id } = await params;
205
+ const body = await request.json().catch(() => ({}));
206
+ const { createFork = true, sendContext = true } = body;
207
+
208
+ const session = (await queries.getSession(id)) as Session | undefined;
209
+
210
+ if (!session) {
211
+ return NextResponse.json({ error: "Session not found" }, { status: 404 });
212
+ }
213
+
214
+ // Get tmux session name (pattern: {agent_type}-{id})
215
+ const tmuxSessionName = `${session.agent_type}-${id}`;
216
+
217
+ // Get actual working directory from tmux
218
+ const cwd =
219
+ (await getTmuxCwd(tmuxSessionName)) || session.working_directory;
220
+ const cwdExpanded =
221
+ cwd?.replace(/^~/, process.env.HOME || "~") || process.env.HOME || "~";
222
+
223
+ // Try to get full conversation from Claude's JSONL (only for Claude sessions)
224
+ let conversation: string | null = null;
225
+ if (session.agent_type === "claude") {
226
+ const claudeSessionId = await getClaudeSessionId(tmuxSessionName);
227
+ if (claudeSessionId && cwdExpanded) {
228
+ console.log(`[summarize] Found Claude session ID: ${claudeSessionId}`);
229
+ conversation = readClaudeSessionHistory(cwdExpanded, claudeSessionId);
230
+ if (conversation) {
231
+ console.log(
232
+ `[summarize] Read ${conversation.length} chars from JSONL`
233
+ );
234
+ }
235
+ }
236
+ }
237
+
238
+ // Fallback to terminal scrollback for non-Claude or if JSONL not available
239
+ if (!conversation) {
240
+ console.log(
241
+ `[summarize] Using terminal scrollback for ${session.agent_type}`
242
+ );
243
+ conversation = await captureScrollback(tmuxSessionName);
244
+ }
245
+
246
+ if (!conversation || conversation.trim().length < 100) {
247
+ return NextResponse.json(
248
+ { error: "No conversation found to summarize" },
249
+ { status: 400 }
250
+ );
251
+ }
252
+
253
+ // Generate summary
254
+ const summary = await generateSummary(conversation);
255
+
256
+ // Create a new session with the summary as context
257
+ let newSession: Session | null = null;
258
+ if (createFork) {
259
+ const newId = randomUUID();
260
+ const newName = `${session.name} (fresh)`;
261
+ const agentType = session.agent_type || "claude";
262
+ const tmuxName = `${agentType}-${newId}`;
263
+
264
+ // Create new session in DB (using cwd already fetched above)
265
+ getDb()
266
+ .prepare(
267
+ `INSERT INTO sessions (id, name, tmux_name, working_directory, parent_session_id, model, initial_prompt, group_path, agent_type, auto_approve, project_id)
268
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
269
+ )
270
+ .run(
271
+ newId,
272
+ newName,
273
+ tmuxName,
274
+ cwd,
275
+ null,
276
+ session.model,
277
+ `Continue from previous session. Here's a summary of the work so far:\n\n${summary}`,
278
+ session.group_path,
279
+ agentType,
280
+ session.auto_approve ? 1 : 0,
281
+ session.project_id || "uncategorized"
282
+ );
283
+
284
+ newSession = (await queries.getSession(newId)) as Session;
285
+ const newTmuxSession = tmuxName;
286
+
287
+ // Start new tmux session with Claude directly
288
+ const isRoot = process.getuid?.() === 0;
289
+ const envPrefix = isRoot ? "IS_SANDBOX=1 " : "";
290
+ const claudeCmd = session.auto_approve
291
+ ? `${envPrefix}claude --dangerously-skip-permissions`
292
+ : "claude";
293
+
294
+ const tmuxCmd = `tmux set -g mouse on 2>/dev/null; tmux new-session -d -s "${newTmuxSession}" -c "${cwdExpanded}" "${claudeCmd}"`;
295
+ console.log(`[summarize] Creating tmux session: ${tmuxCmd}`);
296
+ await execAsync(tmuxCmd);
297
+ console.log(`[summarize] Tmux session created: ${newTmuxSession}`);
298
+
299
+ // Give Claude a moment to start up before polling
300
+ await new Promise((r) => setTimeout(r, 2000));
301
+
302
+ // Wait for Claude to be ready and send context
303
+ if (sendContext) {
304
+ console.log(`[summarize] Waiting for Claude to be ready...`);
305
+ const ready = await waitForClaudeReady(newTmuxSession);
306
+ console.log(`[summarize] Claude ready: ${ready}`);
307
+ if (ready) {
308
+ const contextMessage = `Here's a summary of the previous session to continue from:\n\n${summary}\n\nPlease acknowledge you've received this context and are ready to continue.`;
309
+ console.log(
310
+ `[summarize] Sending context message (${contextMessage.length} chars)`
311
+ );
312
+ await sendToTmux(newTmuxSession, contextMessage, true);
313
+ console.log(`[summarize] Context sent!`);
314
+ } else {
315
+ console.log(
316
+ `[summarize] WARNING: Claude not ready, skipping context send`
317
+ );
318
+ }
319
+ }
320
+ }
321
+
322
+ return NextResponse.json({
323
+ summary,
324
+ newSession,
325
+ });
326
+ } catch (error) {
327
+ console.error("Error summarizing session:", error);
328
+ const message = error instanceof Error ? error.message : "Unknown error";
329
+ return NextResponse.json({ error: message }, { status: 500 });
330
+ }
331
+ }
@@ -0,0 +1,84 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+
6
+ /**
7
+ * Generate an init script that shows the ClaudeDeck banner and configures tmux
8
+ */
9
+ function generateInitScript(agentCommand: string): string {
10
+ return `#!/bin/bash
11
+ # ClaudeDeck Session Init Script
12
+ # Auto-generated - do not edit manually
13
+
14
+ # ANSI Colors (purple theme)
15
+ C_RESET=$'\\033[0m'
16
+ C_PURPLE=$'\\033[38;5;141m'
17
+ C_PURPLE2=$'\\033[38;5;177m'
18
+ C_PINK=$'\\033[38;5;213m'
19
+ C_MUTED=$'\\033[38;5;245m'
20
+
21
+ # Configure tmux status bar
22
+ tmux set-option status-style 'bg=#1e1e2e,fg=#cdd6f4' 2>/dev/null
23
+ tmux set-option status-left '#[fg=#cba6f7,bold] ClaudeDeck #[fg=#6c7086]| ' 2>/dev/null
24
+ tmux set-option status-left-length 20 2>/dev/null
25
+ tmux set-option status-right '#[fg=#6c7086]| #[fg=#89b4fa]#S #[fg=#6c7086]| #[fg=#a6adc8]%H:%M ' 2>/dev/null
26
+ tmux set-option status-right-length 40 2>/dev/null
27
+ tmux set-option status-position bottom 2>/dev/null
28
+
29
+ # Clear and show banner
30
+ clear
31
+
32
+ printf "\\n"
33
+ printf "\${C_PURPLE} _ _ ___ ____ \${C_RESET}\\n"
34
+ printf "\${C_PURPLE} / \\\\ __ _ ___ _ __ | |_ / _ \\\\/ ___| \${C_RESET}\\n"
35
+ printf "\${C_PURPLE2} / _ \\\\ / _\\\` |/ _ \\\\ '_ \\\\| __| | | \\\\___ \\\\ \${C_RESET}\\n"
36
+ printf "\${C_PURPLE2} / ___ \\\\ (_| | __/ | | | |_| |_| |___) |\${C_RESET}\\n"
37
+ printf "\${C_PINK} /_/ \\\\_\\\\__, |\\\\___|_| |_|\\\\__|\\\\___/|____/ \${C_RESET}\\n"
38
+ printf "\${C_PINK} |___/ \${C_RESET}\\n"
39
+ printf "\\n"
40
+ printf "\${C_MUTED} AI Coding Session Manager\${C_RESET}\\n"
41
+ printf "\\n"
42
+
43
+ # Brief pause to show banner
44
+ sleep 0.8
45
+
46
+ # Ensure ~/.local/bin is in PATH (where claude is installed)
47
+ export PATH="$HOME/.local/bin:$PATH"
48
+
49
+ # If running as root, set IS_SANDBOX=1 so Claude Code allows --dangerously-skip-permissions
50
+ if [ "$(id -u)" = "0" ]; then
51
+ export IS_SANDBOX=1
52
+ fi
53
+
54
+ # Start the agent
55
+ exec ${agentCommand}
56
+ `;
57
+ }
58
+
59
+ // POST /api/sessions/init-script - Create init script and return path
60
+ export async function POST(request: NextRequest) {
61
+ try {
62
+ const { agentCommand } = await request.json();
63
+
64
+ if (!agentCommand) {
65
+ return NextResponse.json(
66
+ { error: "agentCommand is required" },
67
+ { status: 400 }
68
+ );
69
+ }
70
+
71
+ const scriptContent = generateInitScript(agentCommand);
72
+ const scriptPath = path.join(os.tmpdir(), `claude-deck-init-${Date.now()}.sh`);
73
+
74
+ fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
75
+
76
+ return NextResponse.json({ scriptPath, command: `bash ${scriptPath}` });
77
+ } catch (error) {
78
+ console.error("Error creating init script:", error);
79
+ return NextResponse.json(
80
+ { error: "Failed to create init script" },
81
+ { status: 500 }
82
+ );
83
+ }
84
+ }