@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,464 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import {
6
+ GitBranch,
7
+ RefreshCw,
8
+ Loader2,
9
+ AlertCircle,
10
+ ArrowUp,
11
+ ArrowDown,
12
+ X,
13
+ AlertTriangle,
14
+ ExternalLink,
15
+ GitPullRequest,
16
+ } from "lucide-react";
17
+ import { Button } from "@/components/ui/button";
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogFooter,
23
+ DialogHeader,
24
+ DialogTitle,
25
+ } from "@/components/ui/dialog";
26
+ import { FileChanges } from "@/components/GitPanel/FileChanges";
27
+ import { CommitForm } from "@/components/GitPanel/CommitForm";
28
+ import { FileEditDialog } from "./FileEditDialog";
29
+ import { cn } from "@/lib/utils";
30
+ import { useDrawerAnimation } from "@/hooks/useDrawerAnimation";
31
+ import {
32
+ useGitStatus,
33
+ usePRStatus,
34
+ useCreatePR,
35
+ useStageFiles,
36
+ useUnstageFiles,
37
+ useMultiRepoGitStatus,
38
+ gitKeys,
39
+ } from "@/data/git/queries";
40
+ import type { GitFile } from "@/lib/git-status";
41
+ import type { MultiRepoGitFile } from "@/lib/multi-repo-git";
42
+ import type { ProjectRepository } from "@/lib/db";
43
+
44
+ interface GitDrawerProps {
45
+ open: boolean;
46
+ onOpenChange: (open: boolean) => void;
47
+ workingDirectory: string;
48
+ projectId?: string;
49
+ repositories?: ProjectRepository[];
50
+ }
51
+
52
+ export function GitDrawer({
53
+ open,
54
+ onOpenChange,
55
+ workingDirectory,
56
+ projectId,
57
+ repositories = [],
58
+ }: GitDrawerProps) {
59
+ const queryClient = useQueryClient();
60
+
61
+ // Determine if we're in multi-repo mode
62
+ const isMultiRepo = repositories.length > 0;
63
+
64
+ // Single-repo mode hooks - only poll when drawer is open
65
+ const singleRepoQuery = useGitStatus(workingDirectory, {
66
+ enabled: open && !isMultiRepo,
67
+ });
68
+
69
+ // Multi-repo mode hooks
70
+ const multiRepoQuery = useMultiRepoGitStatus(projectId, workingDirectory, {
71
+ enabled: open && isMultiRepo,
72
+ });
73
+
74
+ // Unified status based on mode
75
+ const loading = isMultiRepo
76
+ ? multiRepoQuery.isPending
77
+ : singleRepoQuery.isPending;
78
+ const isError = isMultiRepo
79
+ ? multiRepoQuery.isError
80
+ : singleRepoQuery.isError;
81
+ const error = isMultiRepo ? multiRepoQuery.error : singleRepoQuery.error;
82
+ const isRefetching = isMultiRepo
83
+ ? multiRepoQuery.isRefetching
84
+ : singleRepoQuery.isRefetching;
85
+
86
+ // Convert to unified status
87
+ const status = isMultiRepo
88
+ ? multiRepoQuery.data
89
+ ? {
90
+ branch:
91
+ multiRepoQuery.data.repositories.length === 1
92
+ ? multiRepoQuery.data.repositories[0]?.branch || ""
93
+ : `${multiRepoQuery.data.repositories.length} repos`,
94
+ ahead: multiRepoQuery.data.repositories.reduce(
95
+ (sum, r) => sum + r.ahead,
96
+ 0
97
+ ),
98
+ behind: multiRepoQuery.data.repositories.reduce(
99
+ (sum, r) => sum + r.behind,
100
+ 0
101
+ ),
102
+ staged: multiRepoQuery.data.staged,
103
+ unstaged: multiRepoQuery.data.unstaged,
104
+ untracked: multiRepoQuery.data.untracked,
105
+ }
106
+ : null
107
+ : singleRepoQuery.data || null;
108
+
109
+ const refetchStatus = isMultiRepo
110
+ ? multiRepoQuery.refetch
111
+ : singleRepoQuery.refetch;
112
+
113
+ // For PR status, use the primary repo or first repo in multi-repo mode
114
+ const primaryRepoPath = isMultiRepo
115
+ ? repositories.find((r) => r.is_primary)?.path ||
116
+ repositories[0]?.path ||
117
+ workingDirectory
118
+ : workingDirectory;
119
+
120
+ const { data: prData } = usePRStatus(primaryRepoPath);
121
+ const existingPR = prData?.existingPR ?? null;
122
+
123
+ const createPRMutation = useCreatePR(primaryRepoPath);
124
+ const stageMutation = useStageFiles(primaryRepoPath);
125
+ const unstageMutation = useUnstageFiles(primaryRepoPath);
126
+
127
+ // Local UI state
128
+ const [selectedFile, setSelectedFile] = useState<
129
+ GitFile | MultiRepoGitFile | null
130
+ >(null);
131
+ const [discardFile, setDiscardFile] = useState<
132
+ GitFile | MultiRepoGitFile | null
133
+ >(null);
134
+ const [discarding, setDiscarding] = useState(false);
135
+
136
+ // Animation
137
+ const isAnimatingIn = useDrawerAnimation(open);
138
+
139
+ // Clear selected file when drawer opens
140
+ const handleFileClick = (file: GitFile | MultiRepoGitFile) => {
141
+ setSelectedFile(file);
142
+ };
143
+
144
+ const handleStage = async (file: GitFile | MultiRepoGitFile) => {
145
+ // In multi-repo mode, use the file's repoPath
146
+ const repoPath =
147
+ "repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
148
+ try {
149
+ await fetch("/api/git/stage", {
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ path: repoPath, files: [file.path] }),
153
+ });
154
+ queryClient.invalidateQueries({ queryKey: gitKeys.all });
155
+ } catch {
156
+ // Ignore errors
157
+ }
158
+ };
159
+
160
+ const handleUnstage = async (file: GitFile | MultiRepoGitFile) => {
161
+ // In multi-repo mode, use the file's repoPath
162
+ const repoPath =
163
+ "repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
164
+ try {
165
+ await fetch("/api/git/unstage", {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({ path: repoPath, files: [file.path] }),
169
+ });
170
+ queryClient.invalidateQueries({ queryKey: gitKeys.all });
171
+ } catch {
172
+ // Ignore errors
173
+ }
174
+ };
175
+
176
+ const handleStageAll = () => {
177
+ stageMutation.mutate(undefined);
178
+ };
179
+
180
+ const handleUnstageAll = () => {
181
+ unstageMutation.mutate(undefined);
182
+ };
183
+
184
+ const handleDiscardConfirm = async () => {
185
+ if (!discardFile) return;
186
+
187
+ setDiscarding(true);
188
+ try {
189
+ // In multi-repo mode, use the file's repoPath
190
+ const repoPath =
191
+ "repoPath" in discardFile && discardFile.repoPath
192
+ ? discardFile.repoPath
193
+ : primaryRepoPath;
194
+ await fetch("/api/git/discard", {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json" },
197
+ body: JSON.stringify({
198
+ path: repoPath,
199
+ file: discardFile.path,
200
+ }),
201
+ });
202
+ queryClient.invalidateQueries({ queryKey: gitKeys.all });
203
+ setDiscardFile(null);
204
+ } catch {
205
+ // Ignore errors
206
+ } finally {
207
+ setDiscarding(false);
208
+ }
209
+ };
210
+
211
+ const stagedFiles = status?.staged || [];
212
+ const unstagedFiles = [
213
+ ...(status?.unstaged || []),
214
+ ...(status?.untracked || []),
215
+ ];
216
+ const isOnMainBranch = ["main", "master"].includes(status?.branch || "");
217
+
218
+ // In multi-repo mode, determine which repo has staged changes for commit
219
+ const reposWithStagedChanges =
220
+ isMultiRepo && multiRepoQuery.data
221
+ ? multiRepoQuery.data.repositories.filter((repo) =>
222
+ multiRepoQuery.data!.staged.some((f) => f.repoId === repo.id)
223
+ )
224
+ : [];
225
+
226
+ // Use the first repo with staged changes, or fall back to primary repo
227
+ const commitRepoPath =
228
+ reposWithStagedChanges.length > 0
229
+ ? reposWithStagedChanges[0].path
230
+ : primaryRepoPath;
231
+
232
+ const commitRepoName =
233
+ reposWithStagedChanges.length > 0
234
+ ? reposWithStagedChanges[0].name
235
+ : undefined;
236
+
237
+ const commitRepoBranch =
238
+ reposWithStagedChanges.length > 0
239
+ ? reposWithStagedChanges[0].branch
240
+ : status?.branch || "";
241
+
242
+ const multipleReposHaveStagedChanges = reposWithStagedChanges.length > 1;
243
+
244
+ if (!open) return null;
245
+
246
+ return (
247
+ <>
248
+ <div
249
+ className={cn(
250
+ "bg-muted/30 flex h-full flex-col transition-all duration-200 ease-out",
251
+ isAnimatingIn
252
+ ? "translate-x-0 opacity-100"
253
+ : "translate-x-4 opacity-0"
254
+ )}
255
+ >
256
+ {/* Header */}
257
+ <div className="px-3 py-2">
258
+ <div className="flex items-center justify-between">
259
+ <div className="flex items-center gap-2">
260
+ <span className="text-sm font-medium">Git Changes</span>
261
+ {status && (
262
+ <span className="bg-muted rounded-full px-2 py-0.5 text-xs">
263
+ <GitBranch className="mr-1 inline h-3 w-3" />
264
+ {status.branch}
265
+ </span>
266
+ )}
267
+ {existingPR && (
268
+ <button
269
+ onClick={() => window.open(existingPR.url, "_blank")}
270
+ className="bg-muted hover:bg-accent inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs transition-colors"
271
+ title={`${existingPR.title} (#${existingPR.number})`}
272
+ >
273
+ <GitPullRequest className="h-3 w-3" />
274
+ View PR
275
+ <ExternalLink className="h-2.5 w-2.5" />
276
+ </button>
277
+ )}
278
+ </div>
279
+ <div className="flex items-center gap-1">
280
+ <Button
281
+ variant="ghost"
282
+ size="icon"
283
+ onClick={() => refetchStatus()}
284
+ disabled={isRefetching || loading}
285
+ className="h-7 w-7"
286
+ >
287
+ <RefreshCw
288
+ className={cn("h-3.5 w-3.5", isRefetching && "animate-spin")}
289
+ />
290
+ </Button>
291
+ <Button
292
+ variant="ghost"
293
+ size="icon"
294
+ onClick={() => onOpenChange(false)}
295
+ className="h-7 w-7"
296
+ >
297
+ <X className="h-3.5 w-3.5" />
298
+ </Button>
299
+ </div>
300
+ </div>
301
+
302
+ {/* Ahead/behind indicator */}
303
+ {status && (status.ahead > 0 || status.behind > 0) && (
304
+ <div className="text-muted-foreground mt-1 flex items-center gap-3 text-xs">
305
+ {status.ahead > 0 && (
306
+ <span className="flex items-center gap-1">
307
+ <ArrowUp className="h-3 w-3" />
308
+ {status.ahead} ahead
309
+ </span>
310
+ )}
311
+ {status.behind > 0 && (
312
+ <span className="flex items-center gap-1">
313
+ <ArrowDown className="h-3 w-3" />
314
+ {status.behind} behind
315
+ </span>
316
+ )}
317
+ </div>
318
+ )}
319
+ </div>
320
+
321
+ {/* Content */}
322
+ <div className="flex-1 overflow-y-auto py-2">
323
+ {loading ? (
324
+ <div className="flex items-center justify-center py-8">
325
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
326
+ </div>
327
+ ) : isError ? (
328
+ <div className="flex flex-col items-center gap-2 py-8 text-center">
329
+ <AlertCircle className="h-8 w-8 text-red-500" />
330
+ <p className="text-muted-foreground text-sm">
331
+ {error?.message ?? "Failed to load git status"}
332
+ </p>
333
+ <Button
334
+ variant="outline"
335
+ size="sm"
336
+ onClick={() => refetchStatus()}
337
+ >
338
+ Retry
339
+ </Button>
340
+ </div>
341
+ ) : stagedFiles.length === 0 && unstagedFiles.length === 0 ? (
342
+ <div className="flex flex-col items-center gap-3 py-8 text-center">
343
+ <span className="text-muted-foreground text-sm">No changes</span>
344
+ {!isOnMainBranch && !existingPR && (
345
+ <Button
346
+ variant="outline"
347
+ size="sm"
348
+ onClick={() => createPRMutation.mutate()}
349
+ disabled={createPRMutation.isPending}
350
+ className="gap-1.5"
351
+ >
352
+ {createPRMutation.isPending ? (
353
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
354
+ ) : (
355
+ <GitPullRequest className="h-3.5 w-3.5" />
356
+ )}
357
+ Create PR
358
+ </Button>
359
+ )}
360
+ </div>
361
+ ) : (
362
+ <>
363
+ {/* Staged files */}
364
+ <FileChanges
365
+ files={stagedFiles}
366
+ title="Staged Changes"
367
+ emptyMessage="No staged changes"
368
+ onFileClick={handleFileClick}
369
+ onUnstage={handleUnstage}
370
+ onUnstageAll={handleUnstageAll}
371
+ isStaged={true}
372
+ groupByRepo={isMultiRepo}
373
+ />
374
+
375
+ {/* Unstaged files */}
376
+ <FileChanges
377
+ files={unstagedFiles}
378
+ title="Unstaged Changes"
379
+ emptyMessage="No unstaged changes"
380
+ onFileClick={handleFileClick}
381
+ onStage={handleStage}
382
+ onStageAll={handleStageAll}
383
+ onDiscard={setDiscardFile}
384
+ isStaged={false}
385
+ groupByRepo={isMultiRepo}
386
+ />
387
+ </>
388
+ )}
389
+ </div>
390
+
391
+ {/* Commit form at bottom */}
392
+ {status && (
393
+ <CommitForm
394
+ workingDirectory={commitRepoPath}
395
+ stagedCount={stagedFiles.length}
396
+ branch={commitRepoBranch}
397
+ repoName={isMultiRepo ? commitRepoName : undefined}
398
+ multipleReposWarning={multipleReposHaveStagedChanges}
399
+ onCommit={() => {
400
+ queryClient.invalidateQueries({ queryKey: gitKeys.all });
401
+ }}
402
+ />
403
+ )}
404
+ </div>
405
+
406
+ {/* File Edit Dialog */}
407
+ {selectedFile && (
408
+ <FileEditDialog
409
+ open={!!selectedFile}
410
+ onOpenChange={(open) => !open && setSelectedFile(null)}
411
+ workingDirectory={workingDirectory}
412
+ file={selectedFile}
413
+ allFiles={[...stagedFiles, ...unstagedFiles]}
414
+ onFileSelect={setSelectedFile}
415
+ onStage={handleStage}
416
+ onUnstage={handleUnstage}
417
+ onSave={() =>
418
+ queryClient.invalidateQueries({
419
+ queryKey: gitKeys.status(workingDirectory),
420
+ })
421
+ }
422
+ />
423
+ )}
424
+
425
+ {/* Discard Confirmation Modal */}
426
+ <Dialog
427
+ open={!!discardFile}
428
+ onOpenChange={(o) => !o && setDiscardFile(null)}
429
+ >
430
+ <DialogContent>
431
+ <DialogHeader>
432
+ <DialogTitle className="flex items-center gap-2">
433
+ <AlertTriangle className="h-5 w-5 text-red-500" />
434
+ Discard Changes
435
+ </DialogTitle>
436
+ <DialogDescription>
437
+ Are you sure you want to discard changes to{" "}
438
+ <span className="font-mono font-medium">
439
+ {discardFile?.path.split("/").pop()}
440
+ </span>
441
+ ? This action cannot be undone.
442
+ </DialogDescription>
443
+ </DialogHeader>
444
+ <DialogFooter>
445
+ <Button
446
+ variant="outline"
447
+ onClick={() => setDiscardFile(null)}
448
+ disabled={discarding}
449
+ >
450
+ Cancel
451
+ </Button>
452
+ <Button
453
+ variant="destructive"
454
+ onClick={handleDiscardConfirm}
455
+ disabled={discarding}
456
+ >
457
+ {discarding ? "Discarding..." : "Discard"}
458
+ </Button>
459
+ </DialogFooter>
460
+ </DialogContent>
461
+ </Dialog>
462
+ </>
463
+ );
464
+ }
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ GitCommit,
6
+ GitBranch,
7
+ Send,
8
+ Loader2,
9
+ AlertTriangle,
10
+ } from "lucide-react";
11
+ import { Button } from "@/components/ui/button";
12
+ import { cn } from "@/lib/utils";
13
+
14
+ interface CommitFormProps {
15
+ workingDirectory: string;
16
+ stagedCount: number;
17
+ branch: string;
18
+ repoName?: string;
19
+ multipleReposWarning?: boolean;
20
+ onCommit: () => void;
21
+ }
22
+
23
+ export function CommitForm({
24
+ workingDirectory,
25
+ stagedCount,
26
+ branch,
27
+ repoName,
28
+ multipleReposWarning,
29
+ onCommit,
30
+ }: CommitFormProps) {
31
+ const [message, setMessage] = useState("");
32
+ const [committing, setCommitting] = useState(false);
33
+ const [pushing, setPushing] = useState(false);
34
+ const [error, setError] = useState<string | null>(null);
35
+ const [success, setSuccess] = useState<string | null>(null);
36
+
37
+ const canCommit = stagedCount > 0 && message.trim().length > 0;
38
+
39
+ const handleCommit = async (): Promise<boolean> => {
40
+ if (!canCommit) return false;
41
+
42
+ setError(null);
43
+ setSuccess(null);
44
+ setCommitting(true);
45
+
46
+ try {
47
+ const res = await fetch("/api/git/commit", {
48
+ method: "POST",
49
+ headers: { "Content-Type": "application/json" },
50
+ body: JSON.stringify({
51
+ path: workingDirectory,
52
+ message: message.trim(),
53
+ }),
54
+ });
55
+
56
+ const data = await res.json();
57
+
58
+ if (!res.ok || data.error) {
59
+ setError(data.error || "Commit failed");
60
+ return false;
61
+ }
62
+
63
+ // Clear form
64
+ setMessage("");
65
+ setSuccess("Committed successfully!");
66
+ onCommit();
67
+ return true;
68
+ } catch {
69
+ setError("Failed to commit");
70
+ return false;
71
+ } finally {
72
+ setCommitting(false);
73
+ }
74
+ };
75
+
76
+ const handlePush = async () => {
77
+ setError(null);
78
+ setSuccess(null);
79
+ setPushing(true);
80
+
81
+ try {
82
+ const res = await fetch("/api/git/push", {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json" },
85
+ body: JSON.stringify({ path: workingDirectory }),
86
+ });
87
+
88
+ const data = await res.json();
89
+
90
+ if (data.error) {
91
+ setError(data.error);
92
+ return;
93
+ }
94
+
95
+ if (data.pushed) {
96
+ setSuccess("Pushed successfully!");
97
+ } else {
98
+ setSuccess(data.message || "Already up to date");
99
+ }
100
+
101
+ onCommit();
102
+ } catch {
103
+ setError("Failed to push");
104
+ } finally {
105
+ setPushing(false);
106
+ }
107
+ };
108
+
109
+ const handleCommitAndPush = async () => {
110
+ const commitSucceeded = await handleCommit();
111
+ // Only push if commit was successful
112
+ if (commitSucceeded) {
113
+ await handlePush();
114
+ }
115
+ };
116
+
117
+ // Only show commit form when there are staged files
118
+ if (stagedCount === 0) {
119
+ return null;
120
+ }
121
+
122
+ return (
123
+ <div className="bg-muted/20 space-y-3 p-3">
124
+ {/* Repo indicator (multi-repo mode) */}
125
+ {repoName && (
126
+ <div className="text-muted-foreground flex items-center gap-1 text-xs">
127
+ <GitBranch className="h-3 w-3" />
128
+ Committing to:{" "}
129
+ <span className="text-foreground font-medium">{repoName}</span>
130
+ <span className="text-muted-foreground/70">({branch})</span>
131
+ </div>
132
+ )}
133
+
134
+ {/* Warning for multiple repos with staged changes */}
135
+ {multipleReposWarning && (
136
+ <div className="flex items-start gap-2 rounded-md bg-yellow-500/10 px-2 py-1.5 text-xs text-yellow-600 dark:text-yellow-500">
137
+ <AlertTriangle className="mt-0.5 h-3 w-3 flex-shrink-0" />
138
+ <span>
139
+ Multiple repos have staged changes. Only the first will be
140
+ committed.
141
+ </span>
142
+ </div>
143
+ )}
144
+
145
+ {/* Commit message input */}
146
+ <div className="space-y-1.5">
147
+ <label className="text-muted-foreground flex items-center gap-1 text-xs">
148
+ <GitCommit className="h-3 w-3" />
149
+ Commit message
150
+ </label>
151
+ <textarea
152
+ value={message}
153
+ onChange={(e) => setMessage(e.target.value)}
154
+ placeholder="Describe your changes..."
155
+ rows={3}
156
+ className={cn(
157
+ "w-full resize-none rounded-md px-3 py-2 text-sm",
158
+ "bg-muted/50",
159
+ "focus:ring-primary/50 focus:ring-2 focus:outline-none",
160
+ "placeholder:text-muted-foreground/50"
161
+ )}
162
+ />
163
+ </div>
164
+
165
+ {/* Error message */}
166
+ {error && <p className="px-1 text-xs text-red-500">{error}</p>}
167
+
168
+ {/* Success message */}
169
+ {success && <p className="px-1 text-xs text-green-500">{success}</p>}
170
+
171
+ {/* Buttons */}
172
+ <div className="flex gap-2">
173
+ <Button
174
+ variant="outline"
175
+ size="default"
176
+ onClick={handleCommit}
177
+ disabled={!canCommit || committing || pushing}
178
+ className="min-h-[44px] flex-1"
179
+ >
180
+ {committing ? (
181
+ <Loader2 className="mr-1 h-4 w-4 animate-spin" />
182
+ ) : (
183
+ <GitCommit className="mr-1 h-4 w-4" />
184
+ )}
185
+ Commit
186
+ </Button>
187
+
188
+ <Button
189
+ variant="default"
190
+ size="default"
191
+ onClick={handleCommitAndPush}
192
+ disabled={!canCommit || committing || pushing}
193
+ className="min-h-[44px] flex-1"
194
+ >
195
+ {pushing ? (
196
+ <Loader2 className="mr-1 h-4 w-4 animate-spin" />
197
+ ) : (
198
+ <Send className="mr-1 h-4 w-4" />
199
+ )}
200
+ Commit & Push
201
+ </Button>
202
+ </div>
203
+ </div>
204
+ );
205
+ }