@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,4 @@
1
+ export const fileKeys = {
2
+ all: ["files"] as const,
3
+ list: (path: string) => [...fileKeys.all, "list", path] as const,
4
+ };
@@ -0,0 +1,25 @@
1
+ import { useQuery } from "@tanstack/react-query";
2
+ import { fileKeys } from "./keys";
3
+ import type { FileNode } from "@/lib/file-utils";
4
+
5
+ export { fileKeys };
6
+
7
+ export interface DirectoryData {
8
+ files: FileNode[];
9
+ resolvedPath: string;
10
+ }
11
+
12
+ async function fetchDirectory(path: string): Promise<DirectoryData> {
13
+ const res = await fetch(`/api/files?path=${encodeURIComponent(path)}`);
14
+ const data = await res.json();
15
+ if (data.error) throw new Error(data.error);
16
+ return { files: data.files || [], resolvedPath: data.path || path };
17
+ }
18
+
19
+ export function useDirectoryFilesQuery(path: string) {
20
+ return useQuery({
21
+ queryKey: fileKeys.list(path),
22
+ queryFn: () => fetchDirectory(path),
23
+ staleTime: 10000,
24
+ });
25
+ }
@@ -0,0 +1,15 @@
1
+ export const gitKeys = {
2
+ all: ["git"] as const,
3
+ check: (path: string) => [...gitKeys.all, "check", path] as const,
4
+ status: (workingDir: string) =>
5
+ [...gitKeys.all, "status", workingDir] as const,
6
+ multiStatus: (projectId: string, fallbackPath?: string) =>
7
+ [...gitKeys.all, "multi-status", projectId, fallbackPath || ""] as const,
8
+ pr: (workingDir: string) => [...gitKeys.all, "pr", workingDir] as const,
9
+ history: (workingDir: string) =>
10
+ [...gitKeys.all, "history", workingDir] as const,
11
+ commitDetail: (workingDir: string, hash: string) =>
12
+ [...gitKeys.all, "commit", workingDir, hash] as const,
13
+ commitFileDiff: (workingDir: string, hash: string, file: string) =>
14
+ [...gitKeys.all, "diff", workingDir, hash, file] as const,
15
+ };
@@ -0,0 +1,395 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { gitKeys } from "./keys";
3
+
4
+ // Re-export for convenience
5
+ export { gitKeys };
6
+ import type { CommitSummary, CommitDetail } from "@/lib/git-history";
7
+ import type { GitStatus } from "@/lib/git-status";
8
+ import type { MultiRepoGitStatus } from "@/lib/multi-repo-git";
9
+ import type { ProjectRepository } from "@/lib/db";
10
+
11
+ export interface PRInfo {
12
+ number: number;
13
+ url: string;
14
+ state: string;
15
+ title: string;
16
+ }
17
+
18
+ export interface PRData {
19
+ branch: string;
20
+ baseBranch: string;
21
+ existingPR: PRInfo | null;
22
+ commits: { hash: string; subject: string }[];
23
+ suggestedTitle: string;
24
+ suggestedBody: string;
25
+ }
26
+
27
+ // --- Git Check ---
28
+
29
+ async function fetchGitCheck(path: string): Promise<{ isGitRepo: boolean }> {
30
+ const res = await fetch("/api/git/check", {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({ path }),
34
+ });
35
+ return res.json();
36
+ }
37
+
38
+ export function useGitCheck(path: string) {
39
+ return useQuery({
40
+ queryKey: gitKeys.check(path),
41
+ queryFn: () => fetchGitCheck(path),
42
+ staleTime: 10000,
43
+ enabled: !!path && path !== "~",
44
+ });
45
+ }
46
+
47
+ // --- Git Clone ---
48
+
49
+ async function cloneRepo(data: {
50
+ url: string;
51
+ directory: string;
52
+ }): Promise<{ path: string; name: string }> {
53
+ const res = await fetch("/api/git/clone", {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify(data),
57
+ });
58
+ if (!res.ok) {
59
+ const err = await res.json();
60
+ throw new Error(err.error || "Failed to clone repository");
61
+ }
62
+ return res.json();
63
+ }
64
+
65
+ export function useCloneRepo() {
66
+ return useMutation({ mutationFn: cloneRepo });
67
+ }
68
+
69
+ // --- Git Status ---
70
+
71
+ async function fetchGitStatus(workingDir: string): Promise<GitStatus> {
72
+ const res = await fetch(
73
+ `/api/git/status?path=${encodeURIComponent(workingDir)}`
74
+ );
75
+ const data = await res.json();
76
+ if (data.error) throw new Error(data.error);
77
+ return data;
78
+ }
79
+
80
+ export function useGitStatus(
81
+ workingDir: string,
82
+ options?: { enabled?: boolean }
83
+ ) {
84
+ return useQuery({
85
+ queryKey: gitKeys.status(workingDir),
86
+ queryFn: () => fetchGitStatus(workingDir),
87
+ staleTime: 10000, // Consider fresh for 10s
88
+ refetchInterval: 15000, // Poll every 15s (was 3s)
89
+ enabled: !!workingDir && (options?.enabled ?? true),
90
+ });
91
+ }
92
+
93
+ // --- PR Status ---
94
+
95
+ async function fetchPRData(workingDir: string): Promise<PRData | null> {
96
+ const res = await fetch(`/api/git/pr?path=${encodeURIComponent(workingDir)}`);
97
+ const data = await res.json();
98
+ if (data.error) return null;
99
+ return data;
100
+ }
101
+
102
+ export function usePRStatus(workingDir: string) {
103
+ return useQuery({
104
+ queryKey: gitKeys.pr(workingDir),
105
+ queryFn: () => fetchPRData(workingDir),
106
+ staleTime: 60000, // 1 minute - PR status doesn't change often
107
+ gcTime: 5 * 60 * 1000, // Keep in cache for 5 minutes
108
+ enabled: !!workingDir,
109
+ });
110
+ }
111
+
112
+ // --- Mutations ---
113
+
114
+ export function useCreatePR(workingDir: string) {
115
+ const queryClient = useQueryClient();
116
+
117
+ return useMutation({
118
+ mutationFn: async () => {
119
+ // First get suggested content (with generate=true for AI generation)
120
+ const infoRes = await fetch(
121
+ `/api/git/pr?path=${encodeURIComponent(workingDir)}&generate=true`
122
+ );
123
+ const info = await infoRes.json();
124
+
125
+ if (info.error) throw new Error(info.error);
126
+
127
+ if (info.existingPR) {
128
+ // PR already exists, just return it
129
+ return { pr: info.existingPR, created: false };
130
+ }
131
+
132
+ // Create the PR with auto-generated content
133
+ const createRes = await fetch("/api/git/pr", {
134
+ method: "POST",
135
+ headers: { "Content-Type": "application/json" },
136
+ body: JSON.stringify({
137
+ path: workingDir,
138
+ title: info.suggestedTitle,
139
+ description: info.suggestedBody,
140
+ baseBranch: info.baseBranch,
141
+ }),
142
+ });
143
+
144
+ const result = await createRes.json();
145
+ if (result.error) throw new Error(result.error);
146
+
147
+ return { pr: result.pr, created: true };
148
+ },
149
+ onSuccess: (data) => {
150
+ // Open PR in browser
151
+ if (data.pr?.url) {
152
+ window.open(data.pr.url, "_blank");
153
+ }
154
+ // Invalidate PR status
155
+ queryClient.invalidateQueries({ queryKey: gitKeys.pr(workingDir) });
156
+ },
157
+ });
158
+ }
159
+
160
+ export function useStageFiles(workingDir: string) {
161
+ const queryClient = useQueryClient();
162
+
163
+ return useMutation({
164
+ mutationFn: async (files?: string[]) => {
165
+ const res = await fetch("/api/git/stage", {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({ path: workingDir, files }),
169
+ });
170
+ const data = await res.json();
171
+ if (data.error) throw new Error(data.error);
172
+ return data;
173
+ },
174
+ onSuccess: () => {
175
+ queryClient.invalidateQueries({ queryKey: gitKeys.status(workingDir) });
176
+ },
177
+ });
178
+ }
179
+
180
+ export function useUnstageFiles(workingDir: string) {
181
+ const queryClient = useQueryClient();
182
+
183
+ return useMutation({
184
+ mutationFn: async (files?: string[]) => {
185
+ const res = await fetch("/api/git/unstage", {
186
+ method: "POST",
187
+ headers: { "Content-Type": "application/json" },
188
+ body: JSON.stringify({ path: workingDir, files }),
189
+ });
190
+ const data = await res.json();
191
+ if (data.error) throw new Error(data.error);
192
+ return data;
193
+ },
194
+ onSuccess: () => {
195
+ queryClient.invalidateQueries({ queryKey: gitKeys.status(workingDir) });
196
+ },
197
+ });
198
+ }
199
+
200
+ export function useCommitAndPush(workingDir: string) {
201
+ const queryClient = useQueryClient();
202
+
203
+ return useMutation({
204
+ mutationFn: async ({
205
+ message,
206
+ branchName,
207
+ push = true,
208
+ }: {
209
+ message: string;
210
+ branchName?: string;
211
+ push?: boolean;
212
+ }) => {
213
+ // Commit
214
+ const commitRes = await fetch("/api/git/commit", {
215
+ method: "POST",
216
+ headers: { "Content-Type": "application/json" },
217
+ body: JSON.stringify({
218
+ path: workingDir,
219
+ message,
220
+ branchName,
221
+ }),
222
+ });
223
+ const commitData = await commitRes.json();
224
+ if (!commitRes.ok || commitData.error) {
225
+ throw new Error(commitData.error || "Commit failed");
226
+ }
227
+
228
+ // Push if requested
229
+ if (push) {
230
+ const pushRes = await fetch("/api/git/push", {
231
+ method: "POST",
232
+ headers: { "Content-Type": "application/json" },
233
+ body: JSON.stringify({ path: workingDir }),
234
+ });
235
+ const pushData = await pushRes.json();
236
+ if (!pushRes.ok || pushData.error) {
237
+ throw new Error(pushData.error || "Push failed");
238
+ }
239
+ return { commit: commitData, push: pushData };
240
+ }
241
+
242
+ return { commit: commitData };
243
+ },
244
+ onSuccess: () => {
245
+ queryClient.invalidateQueries({ queryKey: gitKeys.status(workingDir) });
246
+ queryClient.invalidateQueries({ queryKey: gitKeys.pr(workingDir) });
247
+ queryClient.invalidateQueries({ queryKey: gitKeys.history(workingDir) });
248
+ },
249
+ });
250
+ }
251
+
252
+ async function fetchCommitHistory(
253
+ workingDir: string,
254
+ limit: number = 30
255
+ ): Promise<CommitSummary[]> {
256
+ const res = await fetch(
257
+ `/api/git/history?path=${encodeURIComponent(workingDir)}&limit=${limit}`
258
+ );
259
+ if (!res.ok) throw new Error("Failed to fetch commit history");
260
+ const data = await res.json();
261
+ return data.commits || [];
262
+ }
263
+
264
+ async function fetchCommitDetail(
265
+ workingDir: string,
266
+ hash: string
267
+ ): Promise<CommitDetail> {
268
+ const res = await fetch(
269
+ `/api/git/history/${hash}?path=${encodeURIComponent(workingDir)}`
270
+ );
271
+ if (!res.ok) throw new Error("Failed to fetch commit detail");
272
+ const data = await res.json();
273
+ return data.commit;
274
+ }
275
+
276
+ async function fetchCommitFileDiff(
277
+ workingDir: string,
278
+ hash: string,
279
+ file: string
280
+ ): Promise<string> {
281
+ const res = await fetch(
282
+ `/api/git/history/${hash}/diff?path=${encodeURIComponent(workingDir)}&file=${encodeURIComponent(file)}`
283
+ );
284
+ if (!res.ok) throw new Error("Failed to fetch commit file diff");
285
+ const data = await res.json();
286
+ return data.diff || "";
287
+ }
288
+
289
+ export function useCommitHistory(workingDir: string, limit: number = 30) {
290
+ return useQuery({
291
+ queryKey: gitKeys.history(workingDir),
292
+ queryFn: () => fetchCommitHistory(workingDir, limit),
293
+ staleTime: 30000,
294
+ enabled: !!workingDir,
295
+ });
296
+ }
297
+
298
+ export function useCommitDetail(workingDir: string, hash: string | null) {
299
+ return useQuery({
300
+ queryKey: gitKeys.commitDetail(workingDir, hash || ""),
301
+ queryFn: () => fetchCommitDetail(workingDir, hash!),
302
+ staleTime: 60000, // Commit details don't change
303
+ enabled: !!workingDir && !!hash,
304
+ });
305
+ }
306
+
307
+ export function useCommitFileDiff(
308
+ workingDir: string,
309
+ hash: string | null,
310
+ file: string | null
311
+ ) {
312
+ return useQuery({
313
+ queryKey: gitKeys.commitFileDiff(workingDir, hash || "", file || ""),
314
+ queryFn: () => fetchCommitFileDiff(workingDir, hash!, file!),
315
+ staleTime: 60000, // Diffs don't change
316
+ enabled: !!workingDir && !!hash && !!file,
317
+ });
318
+ }
319
+
320
+ // --- Multi-repo Git Status ---
321
+
322
+ async function fetchMultiRepoGitStatus(
323
+ projectId?: string,
324
+ fallbackPath?: string
325
+ ): Promise<MultiRepoGitStatus> {
326
+ const params = new URLSearchParams();
327
+ if (projectId) params.set("projectId", projectId);
328
+ if (fallbackPath) params.set("fallbackPath", fallbackPath);
329
+
330
+ const res = await fetch(`/api/git/multi-status?${params}`);
331
+ const data = await res.json();
332
+ if (data.error) throw new Error(data.error);
333
+ return data;
334
+ }
335
+
336
+ export function useMultiRepoGitStatus(
337
+ projectId?: string,
338
+ fallbackPath?: string,
339
+ options?: { enabled?: boolean }
340
+ ) {
341
+ return useQuery({
342
+ queryKey: gitKeys.multiStatus(projectId || "", fallbackPath),
343
+ queryFn: () => fetchMultiRepoGitStatus(projectId, fallbackPath),
344
+ staleTime: 10000, // Consider fresh for 10s
345
+ refetchInterval: 15000, // Poll every 15s
346
+ enabled: (!!projectId || !!fallbackPath) && (options?.enabled ?? true),
347
+ });
348
+ }
349
+
350
+ // Multi-repo stage/unstage mutations
351
+ export function useMultiRepoStageFiles(repoPath: string) {
352
+ const queryClient = useQueryClient();
353
+
354
+ return useMutation({
355
+ mutationFn: async (files?: string[]) => {
356
+ const res = await fetch("/api/git/stage", {
357
+ method: "POST",
358
+ headers: { "Content-Type": "application/json" },
359
+ body: JSON.stringify({ path: repoPath, files }),
360
+ });
361
+ const data = await res.json();
362
+ if (data.error) throw new Error(data.error);
363
+ return data;
364
+ },
365
+ onSuccess: () => {
366
+ // Invalidate all multi-status queries since we don't know which project
367
+ queryClient.invalidateQueries({
368
+ queryKey: gitKeys.all,
369
+ });
370
+ },
371
+ });
372
+ }
373
+
374
+ export function useMultiRepoUnstageFiles(repoPath: string) {
375
+ const queryClient = useQueryClient();
376
+
377
+ return useMutation({
378
+ mutationFn: async (files?: string[]) => {
379
+ const res = await fetch("/api/git/unstage", {
380
+ method: "POST",
381
+ headers: { "Content-Type": "application/json" },
382
+ body: JSON.stringify({ path: repoPath, files }),
383
+ });
384
+ const data = await res.json();
385
+ if (data.error) throw new Error(data.error);
386
+ return data;
387
+ },
388
+ onSuccess: () => {
389
+ // Invalidate all multi-status queries since we don't know which project
390
+ queryClient.invalidateQueries({
391
+ queryKey: gitKeys.all,
392
+ });
393
+ },
394
+ });
395
+ }
@@ -0,0 +1 @@
1
+ export { useToggleGroup, useCreateGroup, useDeleteGroup } from "./mutations";
@@ -0,0 +1,95 @@
1
+ import { useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { sessionKeys } from "../sessions/keys";
3
+
4
+ export function useToggleGroup() {
5
+ const queryClient = useQueryClient();
6
+
7
+ return useMutation({
8
+ mutationFn: async ({
9
+ path,
10
+ expanded,
11
+ }: {
12
+ path: string;
13
+ expanded: boolean;
14
+ }) => {
15
+ const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
16
+ method: "PATCH",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify({ expanded }),
19
+ });
20
+ if (!res.ok) throw new Error("Failed to toggle group");
21
+ return res.json();
22
+ },
23
+ onMutate: async ({ path, expanded }) => {
24
+ await queryClient.cancelQueries({ queryKey: sessionKeys.list() });
25
+ const previous = queryClient.getQueryData(sessionKeys.list());
26
+ queryClient.setQueryData(
27
+ sessionKeys.list(),
28
+ (
29
+ old:
30
+ | {
31
+ sessions: unknown[];
32
+ groups: Array<{ path: string; expanded: boolean }>;
33
+ }
34
+ | undefined
35
+ ) =>
36
+ old
37
+ ? {
38
+ ...old,
39
+ groups: old.groups.map((g) =>
40
+ g.path === path ? { ...g, expanded } : g
41
+ ),
42
+ }
43
+ : old
44
+ );
45
+ return { previous };
46
+ },
47
+ onError: (_, __, context) => {
48
+ if (context?.previous) {
49
+ queryClient.setQueryData(sessionKeys.list(), context.previous);
50
+ }
51
+ },
52
+ });
53
+ }
54
+
55
+ export function useCreateGroup() {
56
+ const queryClient = useQueryClient();
57
+
58
+ return useMutation({
59
+ mutationFn: async ({
60
+ name,
61
+ parentPath,
62
+ }: {
63
+ name: string;
64
+ parentPath?: string;
65
+ }) => {
66
+ const res = await fetch("/api/groups", {
67
+ method: "POST",
68
+ headers: { "Content-Type": "application/json" },
69
+ body: JSON.stringify({ name, parentPath }),
70
+ });
71
+ if (!res.ok) throw new Error("Failed to create group");
72
+ return res.json();
73
+ },
74
+ onSuccess: () => {
75
+ queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
76
+ },
77
+ });
78
+ }
79
+
80
+ export function useDeleteGroup() {
81
+ const queryClient = useQueryClient();
82
+
83
+ return useMutation({
84
+ mutationFn: async (path: string) => {
85
+ const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
86
+ method: "DELETE",
87
+ });
88
+ if (!res.ok) throw new Error("Failed to delete group");
89
+ return res.json();
90
+ },
91
+ onSuccess: () => {
92
+ queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
93
+ },
94
+ });
95
+ }
@@ -0,0 +1,10 @@
1
+ export { projectKeys } from "./keys";
2
+ export {
3
+ useProjectsQuery,
4
+ useCreateProject,
5
+ useToggleProject,
6
+ useDeleteProject,
7
+ useRenameProject,
8
+ useUpdateProject,
9
+ useDetectDevServers,
10
+ } from "./queries";
@@ -0,0 +1,4 @@
1
+ export const projectKeys = {
2
+ all: ["projects"] as const,
3
+ list: () => [...projectKeys.all, "list"] as const,
4
+ };