@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
package/lib/panes.ts ADDED
@@ -0,0 +1,232 @@
1
+ // Multi-pane layout types and helpers
2
+
3
+ export type PaneLayout = PaneLayoutLeaf | PaneLayoutSplit;
4
+
5
+ export interface PaneLayoutLeaf {
6
+ type: "leaf";
7
+ paneId: string;
8
+ }
9
+
10
+ export interface PaneLayoutSplit {
11
+ type: "split";
12
+ direction: "horizontal" | "vertical";
13
+ children: PaneLayout[];
14
+ sizes: number[];
15
+ }
16
+
17
+ export interface TabData {
18
+ id: string;
19
+ sessionId: string | null;
20
+ sessionName: string | null;
21
+ claudeProjectName: string | null;
22
+ workingDirectory: string | null;
23
+ attachedTmux: string | null;
24
+ detachedTmux: string | null;
25
+ detachedSessionId: string | null;
26
+ }
27
+
28
+ export interface PaneData {
29
+ tabs: TabData[];
30
+ activeTabId: string;
31
+ }
32
+
33
+ export interface PaneState {
34
+ layout: PaneLayout;
35
+ focusedPaneId: string;
36
+ panes: Record<string, PaneData>;
37
+ }
38
+
39
+ // Generate unique tab ID
40
+ export function generateTabId(): string {
41
+ return `tab-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
42
+ }
43
+
44
+ // Create a new tab
45
+ export function createTab(): TabData {
46
+ return {
47
+ id: generateTabId(),
48
+ sessionId: null,
49
+ sessionName: null,
50
+ claudeProjectName: null,
51
+ workingDirectory: null,
52
+ attachedTmux: null,
53
+ detachedTmux: null,
54
+ detachedSessionId: null,
55
+ };
56
+ }
57
+
58
+ // Create initial pane data with one tab
59
+ export function createPaneData(): PaneData {
60
+ const tab = createTab();
61
+ return {
62
+ tabs: [tab],
63
+ activeTabId: tab.id,
64
+ };
65
+ }
66
+
67
+ export const MAX_PANES = 4;
68
+
69
+ // Generate unique pane ID
70
+ export function generatePaneId(): string {
71
+ return `pane-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
72
+ }
73
+
74
+ // Create initial state with single pane
75
+ export function createInitialPaneState(): PaneState {
76
+ const paneId = generatePaneId();
77
+ return {
78
+ layout: { type: "leaf", paneId },
79
+ focusedPaneId: paneId,
80
+ panes: {
81
+ [paneId]: createPaneData(),
82
+ },
83
+ };
84
+ }
85
+
86
+ // Count total panes in layout
87
+ export function countPanes(layout: PaneLayout): number {
88
+ if (layout.type === "leaf") {
89
+ return 1;
90
+ }
91
+ return layout.children.reduce((sum, child) => sum + countPanes(child), 0);
92
+ }
93
+
94
+ // Find and replace a pane in the layout
95
+ export function replacePane(
96
+ layout: PaneLayout,
97
+ targetPaneId: string,
98
+ newLayout: PaneLayout
99
+ ): PaneLayout {
100
+ if (layout.type === "leaf") {
101
+ return layout.paneId === targetPaneId ? newLayout : layout;
102
+ }
103
+ return {
104
+ ...layout,
105
+ children: layout.children.map((child) =>
106
+ replacePane(child, targetPaneId, newLayout)
107
+ ),
108
+ };
109
+ }
110
+
111
+ // Split a pane
112
+ export function splitPane(
113
+ state: PaneState,
114
+ paneId: string,
115
+ direction: "horizontal" | "vertical"
116
+ ): PaneState | null {
117
+ if (countPanes(state.layout) >= MAX_PANES) {
118
+ return null; // Max panes reached
119
+ }
120
+
121
+ const newPaneId = generatePaneId();
122
+ const newSplit: PaneLayoutSplit = {
123
+ type: "split",
124
+ direction,
125
+ children: [
126
+ { type: "leaf", paneId },
127
+ { type: "leaf", paneId: newPaneId },
128
+ ],
129
+ sizes: [50, 50],
130
+ };
131
+
132
+ return {
133
+ layout: replacePane(state.layout, paneId, newSplit),
134
+ focusedPaneId: newPaneId,
135
+ panes: {
136
+ ...state.panes,
137
+ [newPaneId]: createPaneData(),
138
+ },
139
+ };
140
+ }
141
+
142
+ // Remove a pane from layout and return the remaining layout
143
+ function removePaneFromLayout(
144
+ layout: PaneLayout,
145
+ paneId: string
146
+ ): PaneLayout | null {
147
+ if (layout.type === "leaf") {
148
+ return layout.paneId === paneId ? null : layout;
149
+ }
150
+
151
+ const newChildren: PaneLayout[] = [];
152
+ for (const child of layout.children) {
153
+ const result = removePaneFromLayout(child, paneId);
154
+ if (result !== null) {
155
+ newChildren.push(result);
156
+ }
157
+ }
158
+
159
+ if (newChildren.length === 0) {
160
+ return null;
161
+ }
162
+ if (newChildren.length === 1) {
163
+ return newChildren[0]; // Collapse single-child splits
164
+ }
165
+
166
+ // Redistribute sizes
167
+ const totalSize = layout.sizes.reduce((a, b) => a + b, 0);
168
+ const newSizes = newChildren.map(() => totalSize / newChildren.length);
169
+
170
+ return {
171
+ ...layout,
172
+ children: newChildren,
173
+ sizes: newSizes,
174
+ };
175
+ }
176
+
177
+ // Close a pane
178
+ export function closePane(state: PaneState, paneId: string): PaneState | null {
179
+ if (countPanes(state.layout) <= 1) {
180
+ return null; // Can't close last pane
181
+ }
182
+
183
+ const newLayout = removePaneFromLayout(state.layout, paneId);
184
+ if (!newLayout) {
185
+ return null;
186
+ }
187
+
188
+ const { [paneId]: _, ...remainingPanes } = state.panes;
189
+
190
+ // If focused pane was closed, focus first remaining pane
191
+ let newFocusedId = state.focusedPaneId;
192
+ if (paneId === state.focusedPaneId) {
193
+ newFocusedId = Object.keys(remainingPanes)[0];
194
+ }
195
+
196
+ return {
197
+ layout: newLayout,
198
+ focusedPaneId: newFocusedId,
199
+ panes: remainingPanes,
200
+ };
201
+ }
202
+
203
+ // Get all pane IDs from layout
204
+ export function getAllPaneIds(layout: PaneLayout): string[] {
205
+ if (layout.type === "leaf") {
206
+ return [layout.paneId];
207
+ }
208
+ return layout.children.flatMap(getAllPaneIds);
209
+ }
210
+
211
+ // localStorage key for persisting pane state
212
+ const PANE_STATE_KEY = "claude-deck-pane-state";
213
+
214
+ export function savePaneState(state: PaneState): void {
215
+ try {
216
+ localStorage.setItem(PANE_STATE_KEY, JSON.stringify(state));
217
+ } catch {
218
+ // localStorage might be unavailable
219
+ }
220
+ }
221
+
222
+ export function loadPaneState(): PaneState | null {
223
+ try {
224
+ const saved = localStorage.getItem(PANE_STATE_KEY);
225
+ if (saved) {
226
+ return JSON.parse(saved);
227
+ }
228
+ } catch {
229
+ // localStorage might be unavailable or data corrupt
230
+ }
231
+ return null;
232
+ }
package/lib/ports.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Port Management for Dev Servers
3
+ *
4
+ * Assigns unique ports to worktree sessions to avoid conflicts.
5
+ */
6
+
7
+ import { exec } from "child_process";
8
+ import { promisify } from "util";
9
+ import { getDb } from "./db";
10
+
11
+ const execAsync = promisify(exec);
12
+
13
+ // Port range for dev servers
14
+ const BASE_PORT = 3100;
15
+ const PORT_INCREMENT = 10;
16
+ const MAX_PORT = 3900;
17
+
18
+ /**
19
+ * Check if a port is in use
20
+ */
21
+ export async function isPortInUse(port: number): Promise<boolean> {
22
+ try {
23
+ const { stdout } = await execAsync(
24
+ `lsof -i :${port} -sTCP:LISTEN 2>/dev/null | head -1`,
25
+ { timeout: 5000 }
26
+ );
27
+ return stdout.trim().length > 0;
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Get all ports currently assigned to sessions
35
+ */
36
+ export async function getAssignedPorts(): Promise<number[]> {
37
+ const rows = getDb()
38
+ .prepare(
39
+ "SELECT dev_server_port FROM sessions WHERE dev_server_port IS NOT NULL"
40
+ )
41
+ .all() as Array<{ dev_server_port: number }>;
42
+ return rows.map((s) => s.dev_server_port);
43
+ }
44
+
45
+ /**
46
+ * Find the next available port
47
+ */
48
+ export async function findAvailablePort(): Promise<number> {
49
+ const assignedPorts = new Set(await getAssignedPorts());
50
+
51
+ for (let port = BASE_PORT; port <= MAX_PORT; port += PORT_INCREMENT) {
52
+ // Skip if already assigned to a session
53
+ if (assignedPorts.has(port)) {
54
+ continue;
55
+ }
56
+
57
+ // Check if port is actually in use (by something outside ClaudeDeck)
58
+ if (!(await isPortInUse(port))) {
59
+ return port;
60
+ }
61
+ }
62
+
63
+ // Fallback: return a random port in range
64
+ return BASE_PORT + Math.floor(Math.random() * 80) * PORT_INCREMENT;
65
+ }
66
+
67
+ /**
68
+ * Assign a port to a session
69
+ */
70
+ export async function assignPort(sessionId: string): Promise<number> {
71
+ const port = await findAvailablePort();
72
+ getDb()
73
+ .prepare("UPDATE sessions SET dev_server_port = ? WHERE id = ?")
74
+ .run(port, sessionId);
75
+ return port;
76
+ }
77
+
78
+ /**
79
+ * Release a port from a session
80
+ */
81
+ export async function releasePort(sessionId: string): Promise<void> {
82
+ getDb()
83
+ .prepare("UPDATE sessions SET dev_server_port = NULL WHERE id = ?")
84
+ .run(sessionId);
85
+ }
86
+
87
+ /**
88
+ * Get the port assigned to a session
89
+ */
90
+ export async function getSessionPort(
91
+ sessionId: string
92
+ ): Promise<number | null> {
93
+ const result = getDb()
94
+ .prepare("SELECT dev_server_port FROM sessions WHERE id = ?")
95
+ .get(sessionId) as { dev_server_port: number | null } | undefined;
96
+ return result?.dev_server_port || null;
97
+ }
@@ -0,0 +1,307 @@
1
+ import { execSync } from "child_process";
2
+
3
+ export interface GeneratedPRContent {
4
+ title: string;
5
+ description: string;
6
+ }
7
+
8
+ /**
9
+ * Generate PR title and description using Claude CLI or fallback heuristics
10
+ */
11
+ export async function generatePRContent(
12
+ workingDir: string,
13
+ baseBranch: string = "main"
14
+ ): Promise<GeneratedPRContent> {
15
+ try {
16
+ // Get git context
17
+ const { diff, commits, changedFiles } = getGitContext(
18
+ workingDir,
19
+ baseBranch
20
+ );
21
+
22
+ if (!diff && commits.length === 0) {
23
+ return generateFallbackContent(changedFiles);
24
+ }
25
+
26
+ // Try Claude CLI first
27
+ try {
28
+ const result = await generateWithClaude(workingDir, diff, commits);
29
+ if (result) {
30
+ return result;
31
+ }
32
+ } catch (error) {
33
+ console.debug("Claude CLI generation failed, using fallback", error);
34
+ }
35
+
36
+ // Fallback to heuristic generation
37
+ return generateHeuristicContent(diff, commits, changedFiles);
38
+ } catch (error) {
39
+ console.error("Failed to generate PR content", error);
40
+ return generateFallbackContent([]);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get git context for PR generation
46
+ */
47
+ function getGitContext(
48
+ workingDir: string,
49
+ baseBranch: string
50
+ ): { diff: string; commits: string[]; changedFiles: string[] } {
51
+ let diff = "";
52
+ let commits: string[] = [];
53
+ let changedFiles: string[] = [];
54
+
55
+ try {
56
+ // Try to get the remote base branch reference
57
+ let baseBranchRef = baseBranch;
58
+ try {
59
+ execSync(`git rev-parse --verify origin/${baseBranch}`, {
60
+ cwd: workingDir,
61
+ stdio: "pipe",
62
+ });
63
+ baseBranchRef = `origin/${baseBranch}`;
64
+ } catch {
65
+ // Fall back to local branch
66
+ try {
67
+ execSync(`git rev-parse --verify ${baseBranch}`, {
68
+ cwd: workingDir,
69
+ stdio: "pipe",
70
+ });
71
+ } catch {
72
+ // Base branch doesn't exist
73
+ return { diff, commits, changedFiles };
74
+ }
75
+ }
76
+
77
+ // Get diff stats
78
+ try {
79
+ diff = execSync(`git diff ${baseBranchRef}...HEAD --stat`, {
80
+ cwd: workingDir,
81
+ encoding: "utf-8",
82
+ maxBuffer: 10 * 1024 * 1024,
83
+ });
84
+ } catch {}
85
+
86
+ // Get changed files
87
+ try {
88
+ const filesOut = execSync(
89
+ `git diff --name-only ${baseBranchRef}...HEAD`,
90
+ {
91
+ cwd: workingDir,
92
+ encoding: "utf-8",
93
+ }
94
+ );
95
+ changedFiles = filesOut
96
+ .split("\n")
97
+ .map((f) => f.trim())
98
+ .filter(Boolean);
99
+ } catch {}
100
+
101
+ // Get commit messages
102
+ try {
103
+ const commitsOut = execSync(
104
+ `git log ${baseBranchRef}..HEAD --pretty=format:"%s"`,
105
+ {
106
+ cwd: workingDir,
107
+ encoding: "utf-8",
108
+ }
109
+ );
110
+ commits = commitsOut
111
+ .split("\n")
112
+ .map((c) => c.trim())
113
+ .filter(Boolean);
114
+ } catch {}
115
+
116
+ // Also include uncommitted changes
117
+ try {
118
+ const workingDiff = execSync("git diff --stat", {
119
+ cwd: workingDir,
120
+ encoding: "utf-8",
121
+ maxBuffer: 10 * 1024 * 1024,
122
+ });
123
+ if (workingDiff) {
124
+ diff = diff ? `${diff}\n${workingDiff}` : workingDiff;
125
+ }
126
+
127
+ const uncommittedFiles = execSync("git diff --name-only", {
128
+ cwd: workingDir,
129
+ encoding: "utf-8",
130
+ })
131
+ .split("\n")
132
+ .map((f) => f.trim())
133
+ .filter(Boolean);
134
+
135
+ changedFiles = [...new Set([...changedFiles, ...uncommittedFiles])];
136
+ } catch {}
137
+ } catch (error) {
138
+ console.warn("Failed to get git context", error);
139
+ }
140
+
141
+ return { diff, commits, changedFiles };
142
+ }
143
+
144
+ /**
145
+ * Generate PR content using Claude CLI
146
+ */
147
+ async function generateWithClaude(
148
+ workingDir: string,
149
+ diff: string,
150
+ commits: string[]
151
+ ): Promise<GeneratedPRContent | null> {
152
+ // Check if Claude CLI is available
153
+ try {
154
+ execSync("claude --version", { stdio: "pipe", timeout: 5000 });
155
+ } catch {
156
+ return null;
157
+ }
158
+
159
+ const prompt = buildPRPrompt(diff, commits);
160
+
161
+ try {
162
+ // Use claude CLI with --print flag for non-interactive output
163
+ const output = execSync(`claude --print "${prompt.replace(/"/g, '\\"')}"`, {
164
+ cwd: workingDir,
165
+ encoding: "utf-8",
166
+ timeout: 30000,
167
+ maxBuffer: 1024 * 1024,
168
+ });
169
+
170
+ return parseClaudeResponse(output);
171
+ } catch (error) {
172
+ console.debug("Claude CLI invocation failed", error);
173
+ return null;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Build prompt for PR generation
179
+ */
180
+ function buildPRPrompt(diff: string, commits: string[]): string {
181
+ const commitContext =
182
+ commits.length > 0
183
+ ? `Commits:\n${commits.map((c) => `- ${c}`).join("\n")}`
184
+ : "";
185
+ const diffContext = diff
186
+ ? `Diff summary:\n${diff.substring(0, 2000)}${diff.length > 2000 ? "..." : ""}`
187
+ : "";
188
+
189
+ return `Generate a concise PR title and description based on these changes:
190
+
191
+ ${commitContext}
192
+
193
+ ${diffContext}
194
+
195
+ Respond ONLY with valid JSON in this exact format:
196
+ {"title": "A concise PR title (max 72 chars)", "description": "A markdown description with ## headers and - bullet points"}`;
197
+ }
198
+
199
+ /**
200
+ * Parse Claude response into PR content
201
+ */
202
+ function parseClaudeResponse(response: string): GeneratedPRContent | null {
203
+ try {
204
+ // Extract JSON from response
205
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
206
+ if (jsonMatch) {
207
+ const parsed = JSON.parse(jsonMatch[0]);
208
+ if (parsed.title && parsed.description) {
209
+ let description = String(parsed.description);
210
+ // Handle escaped newlines
211
+ description = description.replace(/\\n/g, "\n");
212
+ description = description.replace(/\\\\n/g, "\n");
213
+ return {
214
+ title: parsed.title.trim(),
215
+ description: description.trim(),
216
+ };
217
+ }
218
+ }
219
+ } catch (error) {
220
+ console.debug("Failed to parse Claude response", error);
221
+ }
222
+ return null;
223
+ }
224
+
225
+ /**
226
+ * Generate PR content using heuristics
227
+ */
228
+ function generateHeuristicContent(
229
+ diff: string,
230
+ commits: string[],
231
+ changedFiles: string[]
232
+ ): GeneratedPRContent {
233
+ // Use first commit as title
234
+ let title = "chore: update code";
235
+ if (commits.length > 0) {
236
+ title = commits[0];
237
+ if (title.length > 72) {
238
+ title = title.substring(0, 69) + "...";
239
+ }
240
+ } else if (changedFiles.length > 0) {
241
+ const fileName = changedFiles[0].split("/").pop() || "files";
242
+ title = `chore: update ${fileName}`;
243
+ }
244
+
245
+ // Build description
246
+ const parts: string[] = [];
247
+
248
+ if (commits.length > 0) {
249
+ parts.push("## Changes\n");
250
+ commits.forEach((commit) => parts.push(`- ${commit}`));
251
+ }
252
+
253
+ if (changedFiles.length > 0) {
254
+ parts.push("\n## Files Changed\n");
255
+ changedFiles.slice(0, 15).forEach((file) => parts.push(`- \`${file}\``));
256
+ if (changedFiles.length > 15) {
257
+ parts.push(`\n... and ${changedFiles.length - 15} more files`);
258
+ }
259
+ }
260
+
261
+ // Parse diff stats
262
+ if (diff) {
263
+ const statsMatch = diff.match(
264
+ /(\d+)\s+files? changed(?:,\s+(\d+)\s+insertions?\(\+\))?(?:,\s+(\d+)\s+deletions?\(-\))?/
265
+ );
266
+ if (statsMatch) {
267
+ const fileCount = parseInt(statsMatch[1] || "0", 10);
268
+ const insertions = parseInt(statsMatch[2] || "0", 10);
269
+ const deletions = parseInt(statsMatch[3] || "0", 10);
270
+
271
+ if (fileCount > 0 || insertions > 0 || deletions > 0) {
272
+ parts.push("\n## Summary\n");
273
+ if (fileCount > 0) {
274
+ parts.push(
275
+ `- ${fileCount} file${fileCount !== 1 ? "s" : ""} changed`
276
+ );
277
+ }
278
+ const changes: string[] = [];
279
+ if (insertions > 0) changes.push(`+${insertions}`);
280
+ if (deletions > 0) changes.push(`-${deletions}`);
281
+ if (changes.length > 0) {
282
+ parts.push(`- ${changes.join(", ")} lines`);
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ const description = parts.join("\n") || "No description available.";
289
+ return { title, description };
290
+ }
291
+
292
+ /**
293
+ * Fallback content when no context available
294
+ */
295
+ function generateFallbackContent(changedFiles: string[]): GeneratedPRContent {
296
+ const title =
297
+ changedFiles.length > 0
298
+ ? `chore: update ${changedFiles[0].split("/").pop() || "files"}`
299
+ : "chore: update code";
300
+
301
+ const description =
302
+ changedFiles.length > 0
303
+ ? `Updated ${changedFiles.length} file${changedFiles.length !== 1 ? "s" : ""}.`
304
+ : "No changes detected.";
305
+
306
+ return { title, description };
307
+ }