@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,336 @@
1
+ "use client";
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useState,
7
+ useCallback,
8
+ useEffect,
9
+ type ReactNode,
10
+ } from "react";
11
+ import {
12
+ type PaneState,
13
+ type PaneData,
14
+ type TabData,
15
+ createInitialPaneState,
16
+ createPaneData,
17
+ createTab,
18
+ splitPane,
19
+ closePane,
20
+ countPanes,
21
+ savePaneState,
22
+ loadPaneState,
23
+ MAX_PANES,
24
+ } from "@/lib/panes";
25
+ import { useViewport } from "@/hooks/useViewport";
26
+
27
+ interface PaneContextValue {
28
+ state: PaneState;
29
+ focusedPaneId: string;
30
+ canSplit: boolean;
31
+ canClose: boolean;
32
+ isMobile: boolean;
33
+ focusPane: (paneId: string) => void;
34
+ splitHorizontal: (paneId: string) => void;
35
+ splitVertical: (paneId: string) => void;
36
+ close: (paneId: string) => void;
37
+ // Tab management
38
+ addTab: (paneId: string) => void;
39
+ closeTab: (paneId: string, tabId: string) => void;
40
+ switchTab: (paneId: string, tabId: string) => void;
41
+ // Session management (operates on active tab)
42
+ attachSession: (
43
+ paneId: string,
44
+ sessionId: string,
45
+ tmuxName: string,
46
+ sessionName?: string,
47
+ claudeProjectName?: string,
48
+ workingDirectory?: string
49
+ ) => void;
50
+ detachSession: (paneId: string) => void;
51
+ reattachSession: (paneId: string) => void;
52
+ getPaneData: (paneId: string) => PaneData;
53
+ getActiveTab: (paneId: string) => TabData | null;
54
+ }
55
+
56
+ const PaneContext = createContext<PaneContextValue | null>(null);
57
+
58
+ // Default pane data for migration from old format
59
+ const defaultPaneData: PaneData = createPaneData();
60
+
61
+ export function PaneProvider({ children }: { children: ReactNode }) {
62
+ const [state, setState] = useState<PaneState>(createInitialPaneState);
63
+ const [hydrated, setHydrated] = useState(false);
64
+ const { isMobile } = useViewport();
65
+
66
+ // Load from localStorage after hydration
67
+ useEffect(() => {
68
+ const saved = loadPaneState();
69
+ if (saved) {
70
+ // Migrate old pane data format if needed
71
+ const migratedPanes: Record<string, PaneData> = {};
72
+ for (const [paneId, paneData] of Object.entries(saved.panes)) {
73
+ if ("tabs" in paneData && Array.isArray(paneData.tabs)) {
74
+ // New format
75
+ migratedPanes[paneId] = paneData as PaneData;
76
+ } else {
77
+ // Old format - migrate to new
78
+ const oldData = paneData as {
79
+ sessionId?: string | null;
80
+ attachedTmux?: string | null;
81
+ };
82
+ const tab = createTab();
83
+ tab.sessionId = oldData.sessionId || null;
84
+ tab.attachedTmux = oldData.attachedTmux || null;
85
+ migratedPanes[paneId] = {
86
+ tabs: [tab],
87
+ activeTabId: tab.id,
88
+ };
89
+ }
90
+ }
91
+ setState({ ...saved, panes: migratedPanes });
92
+ }
93
+ setHydrated(true);
94
+ }, []);
95
+
96
+ // Persist state changes to localStorage (only after hydration)
97
+ useEffect(() => {
98
+ if (hydrated) {
99
+ savePaneState(state);
100
+ }
101
+ }, [state, hydrated]);
102
+
103
+ const focusPane = useCallback((paneId: string) => {
104
+ setState((prev) => ({ ...prev, focusedPaneId: paneId }));
105
+ }, []);
106
+
107
+ const splitHorizontal = useCallback((paneId: string) => {
108
+ setState((prev) => {
109
+ const newState = splitPane(prev, paneId, "horizontal");
110
+ return newState || prev;
111
+ });
112
+ }, []);
113
+
114
+ const splitVertical = useCallback((paneId: string) => {
115
+ setState((prev) => {
116
+ const newState = splitPane(prev, paneId, "vertical");
117
+ return newState || prev;
118
+ });
119
+ }, []);
120
+
121
+ const close = useCallback((paneId: string) => {
122
+ setState((prev) => {
123
+ const newState = closePane(prev, paneId);
124
+ return newState || prev;
125
+ });
126
+ }, []);
127
+
128
+ // Tab management
129
+ const addTab = useCallback((paneId: string) => {
130
+ setState((prev) => {
131
+ const pane = prev.panes[paneId];
132
+ if (!pane) return prev;
133
+ const newTab = createTab();
134
+ return {
135
+ ...prev,
136
+ panes: {
137
+ ...prev.panes,
138
+ [paneId]: {
139
+ ...pane,
140
+ tabs: [...pane.tabs, newTab],
141
+ activeTabId: newTab.id,
142
+ },
143
+ },
144
+ };
145
+ });
146
+ }, []);
147
+
148
+ const closeTab = useCallback((paneId: string, tabId: string) => {
149
+ setState((prev) => {
150
+ const pane = prev.panes[paneId];
151
+ if (!pane || pane.tabs.length <= 1) return prev; // Keep at least one tab
152
+
153
+ const newTabs = pane.tabs.filter((t) => t.id !== tabId);
154
+ const newActiveTabId =
155
+ pane.activeTabId === tabId ? newTabs[0].id : pane.activeTabId;
156
+
157
+ return {
158
+ ...prev,
159
+ panes: {
160
+ ...prev.panes,
161
+ [paneId]: {
162
+ ...pane,
163
+ tabs: newTabs,
164
+ activeTabId: newActiveTabId,
165
+ },
166
+ },
167
+ };
168
+ });
169
+ }, []);
170
+
171
+ const switchTab = useCallback((paneId: string, tabId: string) => {
172
+ setState((prev) => {
173
+ const pane = prev.panes[paneId];
174
+ if (!pane) return prev;
175
+ return {
176
+ ...prev,
177
+ panes: {
178
+ ...prev.panes,
179
+ [paneId]: {
180
+ ...pane,
181
+ activeTabId: tabId,
182
+ },
183
+ },
184
+ };
185
+ });
186
+ }, []);
187
+
188
+ // Attach session to active tab
189
+ const attachSession = useCallback(
190
+ (
191
+ paneId: string,
192
+ sessionId: string,
193
+ tmuxName: string,
194
+ sessionName?: string,
195
+ claudeProjectName?: string,
196
+ workingDirectory?: string
197
+ ) => {
198
+ setState((prev) => {
199
+ const pane = prev.panes[paneId];
200
+ if (!pane) return prev;
201
+
202
+ const newTabs = pane.tabs.map((tab) =>
203
+ tab.id === pane.activeTabId
204
+ ? {
205
+ ...tab,
206
+ sessionId,
207
+ sessionName: sessionName ?? null,
208
+ claudeProjectName: claudeProjectName ?? null,
209
+ workingDirectory: workingDirectory ?? null,
210
+ attachedTmux: tmuxName,
211
+ }
212
+ : tab
213
+ );
214
+
215
+ return {
216
+ ...prev,
217
+ panes: {
218
+ ...prev.panes,
219
+ [paneId]: { ...pane, tabs: newTabs },
220
+ },
221
+ };
222
+ });
223
+ },
224
+ []
225
+ );
226
+
227
+ // Detach session from active tab (preserves reference for re-attach)
228
+ const detachSession = useCallback((paneId: string) => {
229
+ setState((prev) => {
230
+ const pane = prev.panes[paneId];
231
+ if (!pane) return prev;
232
+
233
+ const newTabs = pane.tabs.map((tab) =>
234
+ tab.id === pane.activeTabId
235
+ ? {
236
+ ...tab,
237
+ detachedTmux: tab.attachedTmux,
238
+ detachedSessionId: tab.sessionId,
239
+ sessionId: null,
240
+ attachedTmux: null,
241
+ }
242
+ : tab
243
+ );
244
+
245
+ return {
246
+ ...prev,
247
+ panes: {
248
+ ...prev.panes,
249
+ [paneId]: { ...pane, tabs: newTabs },
250
+ },
251
+ };
252
+ });
253
+ }, []);
254
+
255
+ // Re-attach to the last detached session
256
+ const reattachSession = useCallback((paneId: string) => {
257
+ setState((prev) => {
258
+ const pane = prev.panes[paneId];
259
+ if (!pane) return prev;
260
+
261
+ const newTabs = pane.tabs.map((tab) =>
262
+ tab.id === pane.activeTabId && tab.detachedTmux
263
+ ? {
264
+ ...tab,
265
+ sessionId: tab.detachedSessionId,
266
+ attachedTmux: tab.detachedTmux,
267
+ detachedTmux: null,
268
+ detachedSessionId: null,
269
+ }
270
+ : tab
271
+ );
272
+
273
+ return {
274
+ ...prev,
275
+ panes: {
276
+ ...prev.panes,
277
+ [paneId]: { ...pane, tabs: newTabs },
278
+ },
279
+ };
280
+ });
281
+ }, []);
282
+
283
+ const getPaneData = useCallback(
284
+ (paneId: string): PaneData => {
285
+ return state.panes[paneId] || defaultPaneData;
286
+ },
287
+ [state.panes]
288
+ );
289
+
290
+ const getActiveTab = useCallback(
291
+ (paneId: string): TabData | null => {
292
+ const pane = state.panes[paneId];
293
+ if (!pane) return null;
294
+ return pane.tabs.find((t) => t.id === pane.activeTabId) || null;
295
+ },
296
+ [state.panes]
297
+ );
298
+
299
+ // On mobile: disable splits (single pane only)
300
+ const canSplit = !isMobile && countPanes(state.layout) < MAX_PANES;
301
+ const canClose = !isMobile && countPanes(state.layout) > 1;
302
+
303
+ return (
304
+ <PaneContext.Provider
305
+ value={{
306
+ state,
307
+ focusedPaneId: state.focusedPaneId,
308
+ canSplit,
309
+ canClose,
310
+ isMobile,
311
+ focusPane,
312
+ splitHorizontal,
313
+ splitVertical,
314
+ close,
315
+ addTab,
316
+ closeTab,
317
+ switchTab,
318
+ attachSession,
319
+ detachSession,
320
+ reattachSession,
321
+ getPaneData,
322
+ getActiveTab,
323
+ }}
324
+ >
325
+ {children}
326
+ </PaneContext.Provider>
327
+ );
328
+ }
329
+
330
+ export function usePanes() {
331
+ const context = useContext(PaneContext);
332
+ if (!context) {
333
+ throw new Error("usePanes must be used within a PaneProvider");
334
+ }
335
+ return context;
336
+ }
@@ -0,0 +1,9 @@
1
+ export { claudeKeys } from "./keys";
2
+ export {
3
+ useClaudeProjectsQuery,
4
+ useClaudeSessionsQuery,
5
+ useHideItem,
6
+ useUnhideItem,
7
+ } from "./queries";
8
+ export type { ClaudeProject, ClaudeSession } from "./queries";
9
+ export { useClaudeUpdates } from "./useClaudeUpdates";
@@ -0,0 +1,6 @@
1
+ export const claudeKeys = {
2
+ all: ["claude"] as const,
3
+ projects: () => [...claudeKeys.all, "projects"] as const,
4
+ sessions: (projectName: string) =>
5
+ [...claudeKeys.all, "sessions", projectName] as const,
6
+ };
@@ -0,0 +1,120 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { claudeKeys } from "./keys";
3
+
4
+ export interface ClaudeProject {
5
+ name: string;
6
+ directory: string;
7
+ displayName: string;
8
+ sessionCount: number;
9
+ lastActivity: string;
10
+ hidden: boolean;
11
+ }
12
+
13
+ export interface ClaudeSession {
14
+ sessionId: string;
15
+ summary: string;
16
+ lastActivity: string;
17
+ messageCount: number;
18
+ cwd: string | null;
19
+ hidden: boolean;
20
+ }
21
+
22
+ interface ClaudeSessionsResponse {
23
+ sessions: ClaudeSession[];
24
+ total: number;
25
+ hasMore: boolean;
26
+ }
27
+
28
+ async function fetchClaudeProjects(): Promise<ClaudeProject[]> {
29
+ const res = await fetch("/api/claude/projects");
30
+ if (!res.ok) throw new Error("Failed to fetch Claude projects");
31
+ const data = await res.json();
32
+ return data.projects || [];
33
+ }
34
+
35
+ async function fetchClaudeSessions(
36
+ projectName: string,
37
+ limit = 50,
38
+ offset = 0,
39
+ includeHidden = true
40
+ ): Promise<ClaudeSessionsResponse> {
41
+ const params = new URLSearchParams({
42
+ limit: String(limit),
43
+ offset: String(offset),
44
+ includeHidden: String(includeHidden),
45
+ });
46
+ const res = await fetch(
47
+ `/api/claude/projects/${encodeURIComponent(projectName)}/sessions?${params}`
48
+ );
49
+ if (!res.ok) throw new Error("Failed to fetch Claude sessions");
50
+ return res.json();
51
+ }
52
+
53
+ export function useClaudeProjectsQuery() {
54
+ return useQuery({
55
+ queryKey: claudeKeys.projects(),
56
+ queryFn: fetchClaudeProjects,
57
+ staleTime: 30000,
58
+ });
59
+ }
60
+
61
+ export function useClaudeSessionsQuery(projectName: string | null) {
62
+ return useQuery({
63
+ queryKey: claudeKeys.sessions(projectName || ""),
64
+ queryFn: () => fetchClaudeSessions(projectName!),
65
+ enabled: !!projectName,
66
+ staleTime: 30000,
67
+ });
68
+ }
69
+
70
+ export function useHideItem() {
71
+ const queryClient = useQueryClient();
72
+
73
+ return useMutation({
74
+ mutationFn: async ({
75
+ itemType,
76
+ itemId,
77
+ }: {
78
+ itemType: "project" | "session";
79
+ itemId: string;
80
+ }) => {
81
+ const res = await fetch("/api/claude/hidden", {
82
+ method: "POST",
83
+ headers: { "Content-Type": "application/json" },
84
+ body: JSON.stringify({ itemType, itemId }),
85
+ });
86
+ if (!res.ok) throw new Error("Failed to hide item");
87
+ return res.json();
88
+ },
89
+ onSuccess: () => {
90
+ queryClient.invalidateQueries({ queryKey: claudeKeys.projects() });
91
+ queryClient.invalidateQueries({ queryKey: claudeKeys.all });
92
+ },
93
+ });
94
+ }
95
+
96
+ export function useUnhideItem() {
97
+ const queryClient = useQueryClient();
98
+
99
+ return useMutation({
100
+ mutationFn: async ({
101
+ itemType,
102
+ itemId,
103
+ }: {
104
+ itemType: "project" | "session";
105
+ itemId: string;
106
+ }) => {
107
+ const res = await fetch("/api/claude/hidden", {
108
+ method: "DELETE",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({ itemType, itemId }),
111
+ });
112
+ if (!res.ok) throw new Error("Failed to unhide item");
113
+ return res.json();
114
+ },
115
+ onSuccess: () => {
116
+ queryClient.invalidateQueries({ queryKey: claudeKeys.projects() });
117
+ queryClient.invalidateQueries({ queryKey: claudeKeys.all });
118
+ },
119
+ });
120
+ }
@@ -0,0 +1,37 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import { claudeKeys } from "./keys";
6
+
7
+ export function useClaudeUpdates() {
8
+ const queryClient = useQueryClient();
9
+
10
+ useEffect(() => {
11
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
12
+ const ws = new WebSocket(
13
+ `${protocol}//${window.location.host}/ws/updates`
14
+ );
15
+
16
+ ws.onmessage = (event) => {
17
+ try {
18
+ const msg = JSON.parse(event.data);
19
+ if (msg.type === "project-updated") {
20
+ queryClient.refetchQueries({
21
+ queryKey: claudeKeys.sessions(msg.projectName),
22
+ });
23
+ queryClient.refetchQueries({
24
+ queryKey: claudeKeys.projects(),
25
+ });
26
+ }
27
+ if (msg.type === "projects-changed") {
28
+ queryClient.refetchQueries({
29
+ queryKey: claudeKeys.projects(),
30
+ });
31
+ }
32
+ } catch {}
33
+ };
34
+
35
+ return () => ws.close();
36
+ }, [queryClient]);
37
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./keys";
2
+ export * from "./queries";
@@ -0,0 +1,7 @@
1
+ export const codeSearchKeys = {
2
+ all: ["code-search"] as const,
3
+ available: () => [...codeSearchKeys.all, "available"] as const,
4
+ searches: () => [...codeSearchKeys.all, "searches"] as const,
5
+ search: (path: string, query: string) =>
6
+ [...codeSearchKeys.searches(), path, query] as const,
7
+ };
@@ -0,0 +1,61 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { codeSearchKeys } from "./keys";
3
+ import type { FormattedMatch } from "@/lib/code-search";
4
+
5
+ interface AvailabilityResponse {
6
+ available: boolean;
7
+ }
8
+
9
+ interface SearchResponse {
10
+ results: FormattedMatch[];
11
+ query: string;
12
+ path: string;
13
+ count: number;
14
+ error?: string;
15
+ }
16
+
17
+ async function fetchRipgrepAvailability(): Promise<boolean> {
18
+ const res = await fetch("/api/code-search/available");
19
+ const data: AvailabilityResponse = await res.json();
20
+ return data.available;
21
+ }
22
+
23
+ async function fetchCodeSearch(
24
+ path: string,
25
+ query: string
26
+ ): Promise<SearchResponse> {
27
+ const params = new URLSearchParams({
28
+ path,
29
+ query,
30
+ maxResults: "100",
31
+ contextLines: "2",
32
+ });
33
+
34
+ const res = await fetch(`/api/code-search?${params}`);
35
+ const data = await res.json();
36
+
37
+ if (!res.ok) {
38
+ throw new Error(data.error || "Failed to search code");
39
+ }
40
+
41
+ return data;
42
+ }
43
+
44
+ export function useRipgrepAvailable() {
45
+ return useQuery({
46
+ queryKey: codeSearchKeys.available(),
47
+ queryFn: fetchRipgrepAvailability,
48
+ staleTime: Infinity, // Never refetch - ripgrep installation doesn't change during runtime
49
+ gcTime: Infinity,
50
+ });
51
+ }
52
+
53
+ export function useCodeSearch(path: string, query: string, enabled = false) {
54
+ return useQuery({
55
+ queryKey: codeSearchKeys.search(path, query),
56
+ queryFn: () => fetchCodeSearch(path, query),
57
+ enabled: enabled && query.length > 2,
58
+ staleTime: 30000,
59
+ gcTime: 60000,
60
+ });
61
+ }
@@ -0,0 +1,8 @@
1
+ export { devServerKeys } from "./keys";
2
+ export {
3
+ useDevServersQuery,
4
+ useStopDevServer,
5
+ useRestartDevServer,
6
+ useRemoveDevServer,
7
+ useCreateDevServer,
8
+ } from "./queries";
@@ -0,0 +1,4 @@
1
+ export const devServerKeys = {
2
+ all: ["dev-servers"] as const,
3
+ list: () => [...devServerKeys.all, "list"] as const,
4
+ };
@@ -0,0 +1,104 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import type { DevServer } from "@/lib/db";
3
+ import { devServerKeys } from "./keys";
4
+
5
+ async function fetchDevServers(): Promise<DevServer[]> {
6
+ const res = await fetch("/api/dev-servers");
7
+ if (!res.ok) throw new Error("Failed to fetch dev servers");
8
+ const data = await res.json();
9
+ return data.servers || [];
10
+ }
11
+
12
+ export function useDevServersQuery() {
13
+ return useQuery({
14
+ queryKey: devServerKeys.list(),
15
+ queryFn: fetchDevServers,
16
+ staleTime: 3000,
17
+ refetchInterval: (query) => {
18
+ const servers = query.state.data;
19
+ if (!servers?.length) return false;
20
+
21
+ const hasRunning = servers.some((s) => s.status === "running");
22
+ return hasRunning ? 5000 : 30000;
23
+ },
24
+ });
25
+ }
26
+
27
+ export function useStopDevServer() {
28
+ const queryClient = useQueryClient();
29
+
30
+ return useMutation({
31
+ mutationFn: async (serverId: string) => {
32
+ const res = await fetch(`/api/dev-servers/${serverId}/stop`, {
33
+ method: "POST",
34
+ });
35
+ if (!res.ok) throw new Error("Failed to stop dev server");
36
+ return res.json();
37
+ },
38
+ onSuccess: () => {
39
+ queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
40
+ },
41
+ });
42
+ }
43
+
44
+ export function useRestartDevServer() {
45
+ const queryClient = useQueryClient();
46
+
47
+ return useMutation({
48
+ mutationFn: async (serverId: string) => {
49
+ const res = await fetch(`/api/dev-servers/${serverId}/restart`, {
50
+ method: "POST",
51
+ });
52
+ if (!res.ok) throw new Error("Failed to restart dev server");
53
+ return res.json();
54
+ },
55
+ onSuccess: () => {
56
+ queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
57
+ },
58
+ });
59
+ }
60
+
61
+ export function useRemoveDevServer() {
62
+ const queryClient = useQueryClient();
63
+
64
+ return useMutation({
65
+ mutationFn: async (serverId: string) => {
66
+ const res = await fetch(`/api/dev-servers/${serverId}`, {
67
+ method: "DELETE",
68
+ });
69
+ if (!res.ok) throw new Error("Failed to remove dev server");
70
+ return res.json();
71
+ },
72
+ onSuccess: () => {
73
+ queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
74
+ },
75
+ });
76
+ }
77
+
78
+ interface CreateDevServerOptions {
79
+ projectId: string;
80
+ type: "node" | "docker";
81
+ name: string;
82
+ command: string;
83
+ workingDirectory: string;
84
+ ports?: number[];
85
+ }
86
+
87
+ export function useCreateDevServer() {
88
+ const queryClient = useQueryClient();
89
+
90
+ return useMutation({
91
+ mutationFn: async (opts: CreateDevServerOptions) => {
92
+ const res = await fetch("/api/dev-servers", {
93
+ method: "POST",
94
+ headers: { "Content-Type": "application/json" },
95
+ body: JSON.stringify(opts),
96
+ });
97
+ if (!res.ok) throw new Error("Failed to create dev server");
98
+ return res.json();
99
+ },
100
+ onSuccess: () => {
101
+ queryClient.invalidateQueries({ queryKey: devServerKeys.list() });
102
+ },
103
+ });
104
+ }
@@ -0,0 +1,3 @@
1
+ export { fileKeys } from "./keys";
2
+ export { useDirectoryFilesQuery } from "./queries";
3
+ export type { DirectoryData } from "./queries";