@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,817 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef } from "react";
4
+ import { useQueryClient } from "@tanstack/react-query";
5
+ import {
6
+ GitBranch,
7
+ GitPullRequest,
8
+ RefreshCw,
9
+ Loader2,
10
+ AlertCircle,
11
+ ArrowUp,
12
+ ArrowDown,
13
+ Plus,
14
+ Minus,
15
+ ArrowLeft,
16
+ FileCode,
17
+ ExternalLink,
18
+ } from "lucide-react";
19
+ import { Button } from "@/components/ui/button";
20
+ import { FileChanges } from "./FileChanges";
21
+ import { CommitForm } from "./CommitForm";
22
+ import { PRCreationModal } from "@/components/PRCreationModal";
23
+ import { GitPanelTabs, type GitTab } from "./GitPanelTabs";
24
+ import { CommitHistory } from "./CommitHistory";
25
+ import { DiffView } from "@/components/DiffViewer/DiffModal";
26
+ import { useViewport } from "@/hooks/useViewport";
27
+ import {
28
+ useGitStatus,
29
+ usePRStatus,
30
+ useCreatePR,
31
+ useStageFiles,
32
+ useUnstageFiles,
33
+ useMultiRepoGitStatus,
34
+ gitKeys,
35
+ } from "@/data/git/queries";
36
+ import type { GitStatus, GitFile } from "@/lib/git-status";
37
+ import type { MultiRepoGitFile } from "@/lib/multi-repo-git";
38
+ import type { ProjectRepository } from "@/lib/db";
39
+
40
+ interface GitPanelProps {
41
+ workingDirectory: string;
42
+ projectId?: string;
43
+ repositories?: ProjectRepository[];
44
+ onFileSelect?: (file: GitFile, diff: string) => void;
45
+ }
46
+
47
+ interface SelectedFile {
48
+ file: GitFile | MultiRepoGitFile;
49
+ diff: string;
50
+ repoPath?: string;
51
+ }
52
+
53
+ export function GitPanel({
54
+ workingDirectory,
55
+ projectId,
56
+ repositories = [],
57
+ }: GitPanelProps) {
58
+ const { isMobile } = useViewport();
59
+ const queryClient = useQueryClient();
60
+ const [activeTab, setActiveTab] = useState<GitTab>("changes");
61
+ const [showPRModal, setShowPRModal] = useState(false);
62
+
63
+ // Determine if we're in multi-repo mode
64
+ const isMultiRepo = repositories.length > 0;
65
+
66
+ // Single-repo mode hooks
67
+ const singleRepoQuery = useGitStatus(workingDirectory, {
68
+ enabled: !isMultiRepo,
69
+ });
70
+
71
+ // Multi-repo mode hooks
72
+ const multiRepoQuery = useMultiRepoGitStatus(projectId, workingDirectory, {
73
+ enabled: isMultiRepo,
74
+ });
75
+
76
+ // Unified status based on mode
77
+ const loading = isMultiRepo
78
+ ? multiRepoQuery.isPending
79
+ : singleRepoQuery.isPending;
80
+ const isError = isMultiRepo
81
+ ? multiRepoQuery.isError
82
+ : singleRepoQuery.isError;
83
+ const error = isMultiRepo ? multiRepoQuery.error : singleRepoQuery.error;
84
+ const isRefetching = isMultiRepo
85
+ ? multiRepoQuery.isRefetching
86
+ : singleRepoQuery.isRefetching;
87
+
88
+ // Convert multi-repo status to single-repo-like status for unified handling
89
+ const status: GitStatus | null = isMultiRepo
90
+ ? multiRepoQuery.data
91
+ ? {
92
+ // Use first repo's branch or "Multiple"
93
+ branch:
94
+ multiRepoQuery.data.repositories.length === 1
95
+ ? multiRepoQuery.data.repositories[0]?.branch || ""
96
+ : `${multiRepoQuery.data.repositories.length} repos`,
97
+ ahead: multiRepoQuery.data.repositories.reduce(
98
+ (sum, r) => sum + r.ahead,
99
+ 0
100
+ ),
101
+ behind: multiRepoQuery.data.repositories.reduce(
102
+ (sum, r) => sum + r.behind,
103
+ 0
104
+ ),
105
+ staged: multiRepoQuery.data.staged,
106
+ unstaged: multiRepoQuery.data.unstaged,
107
+ untracked: multiRepoQuery.data.untracked,
108
+ }
109
+ : null
110
+ : singleRepoQuery.data || null;
111
+
112
+ const refetchStatus = isMultiRepo
113
+ ? multiRepoQuery.refetch
114
+ : singleRepoQuery.refetch;
115
+
116
+ // For PR status, use the primary repo or first repo in multi-repo mode
117
+ const primaryRepoPath = isMultiRepo
118
+ ? repositories.find((r) => r.is_primary)?.path ||
119
+ repositories[0]?.path ||
120
+ workingDirectory
121
+ : workingDirectory;
122
+
123
+ const { data: prData } = usePRStatus(primaryRepoPath);
124
+ const existingPR = prData?.existingPR ?? null;
125
+
126
+ const createPRMutation = useCreatePR(primaryRepoPath);
127
+ const stageMutation = useStageFiles(primaryRepoPath);
128
+ const unstageMutation = useUnstageFiles(primaryRepoPath);
129
+
130
+ // Selected file for diff view
131
+ const [selectedFile, setSelectedFile] = useState<SelectedFile | null>(null);
132
+ const [loadingDiff, setLoadingDiff] = useState(false);
133
+
134
+ // Resizable panel state (desktop)
135
+ const [listWidth, setListWidth] = useState(300);
136
+ const containerRef = useRef<HTMLDivElement>(null);
137
+ const isDragging = useRef(false);
138
+
139
+ const handleRefresh = async () => {
140
+ await refetchStatus();
141
+ };
142
+
143
+ const handleFileClick = async (file: GitFile | MultiRepoGitFile) => {
144
+ setLoadingDiff(true);
145
+ try {
146
+ const isUntracked = file.status === "untracked";
147
+ // In multi-repo mode, use the file's repoPath
148
+ const repoPath =
149
+ "repoPath" in file && file.repoPath ? file.repoPath : workingDirectory;
150
+ const params = new URLSearchParams({
151
+ path: repoPath,
152
+ file: file.path,
153
+ staged: file.staged.toString(),
154
+ ...(isUntracked && { untracked: "true" }),
155
+ });
156
+
157
+ const res = await fetch(`/api/git/status?${params}`);
158
+ const data = await res.json();
159
+
160
+ if (data.diff !== undefined) {
161
+ setSelectedFile({ file, diff: data.diff, repoPath });
162
+ }
163
+ } catch {
164
+ // Ignore errors
165
+ } finally {
166
+ setLoadingDiff(false);
167
+ }
168
+ };
169
+
170
+ const handleStage = async (file: GitFile | MultiRepoGitFile) => {
171
+ // In multi-repo mode, use the file's repoPath
172
+ const repoPath =
173
+ "repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
174
+ try {
175
+ await fetch("/api/git/stage", {
176
+ method: "POST",
177
+ headers: { "Content-Type": "application/json" },
178
+ body: JSON.stringify({ path: repoPath, files: [file.path] }),
179
+ });
180
+ // Invalidate queries to refresh
181
+ queryClient.invalidateQueries({ queryKey: gitKeys.all });
182
+ // Update selected file's staged status if it's the same file
183
+ if (selectedFile?.file.path === file.path) {
184
+ setSelectedFile({ ...selectedFile, file: { ...file, staged: true } });
185
+ }
186
+ } catch {
187
+ // Ignore errors
188
+ }
189
+ };
190
+
191
+ const handleUnstage = async (file: GitFile | MultiRepoGitFile) => {
192
+ // In multi-repo mode, use the file's repoPath
193
+ const repoPath =
194
+ "repoPath" in file && file.repoPath ? file.repoPath : primaryRepoPath;
195
+ try {
196
+ await fetch("/api/git/unstage", {
197
+ method: "POST",
198
+ headers: { "Content-Type": "application/json" },
199
+ body: JSON.stringify({ path: repoPath, files: [file.path] }),
200
+ });
201
+ // Invalidate queries to refresh
202
+ queryClient.invalidateQueries({ queryKey: gitKeys.all });
203
+ // Update selected file's staged status if it's the same file
204
+ if (selectedFile?.file.path === file.path) {
205
+ setSelectedFile({
206
+ ...selectedFile,
207
+ file: { ...file, staged: false },
208
+ });
209
+ }
210
+ } catch {
211
+ // Ignore errors
212
+ }
213
+ };
214
+
215
+ const handleStageAll = () => {
216
+ stageMutation.mutate(undefined);
217
+ };
218
+
219
+ const handleUnstageAll = () => {
220
+ unstageMutation.mutate(undefined);
221
+ };
222
+
223
+ // Resize handle for desktop
224
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
225
+ e.preventDefault();
226
+ isDragging.current = true;
227
+ document.body.style.cursor = "col-resize";
228
+ document.body.style.userSelect = "none";
229
+
230
+ const handleMouseMove = (e: MouseEvent) => {
231
+ if (!isDragging.current || !containerRef.current) return;
232
+ const containerRect = containerRef.current.getBoundingClientRect();
233
+ const newWidth = e.clientX - containerRect.left;
234
+ setListWidth(Math.max(200, Math.min(500, newWidth)));
235
+ };
236
+
237
+ const handleMouseUp = () => {
238
+ isDragging.current = false;
239
+ document.body.style.cursor = "";
240
+ document.body.style.userSelect = "";
241
+ document.removeEventListener("mousemove", handleMouseMove);
242
+ document.removeEventListener("mouseup", handleMouseUp);
243
+ };
244
+
245
+ document.addEventListener("mousemove", handleMouseMove);
246
+ document.addEventListener("mouseup", handleMouseUp);
247
+ }, []);
248
+
249
+ if (loading) {
250
+ return (
251
+ <div className="bg-background flex h-full w-full flex-col">
252
+ <Header
253
+ branch=""
254
+ ahead={0}
255
+ behind={0}
256
+ onRefresh={handleRefresh}
257
+ refreshing={false}
258
+ />
259
+ <div className="flex flex-1 items-center justify-center">
260
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ if (isError) {
267
+ return (
268
+ <div className="bg-background flex h-full w-full flex-col">
269
+ <Header
270
+ branch=""
271
+ ahead={0}
272
+ behind={0}
273
+ onRefresh={handleRefresh}
274
+ refreshing={isRefetching}
275
+ existingPR={existingPR}
276
+ />
277
+ <div className="flex flex-1 flex-col items-center justify-center p-4">
278
+ <AlertCircle className="text-muted-foreground mb-2 h-8 w-8" />
279
+ <p className="text-muted-foreground text-center text-sm">
280
+ {error?.message ?? "Failed to load git status"}
281
+ </p>
282
+ </div>
283
+ </div>
284
+ );
285
+ }
286
+
287
+ if (!status) {
288
+ return null;
289
+ }
290
+
291
+ const hasChanges =
292
+ status.staged.length > 0 ||
293
+ status.unstaged.length > 0 ||
294
+ status.untracked.length > 0;
295
+
296
+ // Mobile layout: full-screen list OR full-screen diff
297
+ if (isMobile) {
298
+ return (
299
+ <MobileGitPanel
300
+ status={status}
301
+ hasChanges={hasChanges}
302
+ selectedFile={selectedFile}
303
+ loadingDiff={loadingDiff}
304
+ refreshing={isRefetching}
305
+ showPRModal={showPRModal}
306
+ workingDirectory={workingDirectory}
307
+ activeTab={activeTab}
308
+ existingPR={existingPR}
309
+ creatingPR={createPRMutation.isPending}
310
+ onTabChange={setActiveTab}
311
+ onRefresh={handleRefresh}
312
+ onFileClick={handleFileClick}
313
+ onStage={handleStage}
314
+ onUnstage={handleUnstage}
315
+ onStageAll={handleStageAll}
316
+ onUnstageAll={handleUnstageAll}
317
+ onBack={() => setSelectedFile(null)}
318
+ onCommit={() => {
319
+ queryClient.invalidateQueries({
320
+ queryKey: gitKeys.status(workingDirectory),
321
+ });
322
+ queryClient.invalidateQueries({
323
+ queryKey: gitKeys.pr(workingDirectory),
324
+ });
325
+ }}
326
+ onShowPRModal={() => setShowPRModal(true)}
327
+ onClosePRModal={() => setShowPRModal(false)}
328
+ onCreatePR={() => createPRMutation.mutate()}
329
+ />
330
+ );
331
+ }
332
+
333
+ // Desktop layout: side-by-side for Changes, or CommitHistory for History
334
+ if (activeTab === "history") {
335
+ return (
336
+ <div className="bg-background flex h-full w-full flex-col">
337
+ <Header
338
+ branch={status.branch}
339
+ ahead={status.ahead}
340
+ behind={status.behind}
341
+ onRefresh={handleRefresh}
342
+ refreshing={isRefetching}
343
+ existingPR={existingPR}
344
+ />
345
+ <GitPanelTabs activeTab={activeTab} onTabChange={setActiveTab} />
346
+ <CommitHistory workingDirectory={workingDirectory} />
347
+ </div>
348
+ );
349
+ }
350
+
351
+ // Desktop layout: side-by-side (Changes tab)
352
+ return (
353
+ <div
354
+ ref={containerRef}
355
+ className="bg-background flex h-full w-full flex-col"
356
+ >
357
+ <div className="flex min-h-0 flex-1">
358
+ {/* Left panel - file list */}
359
+ <div className="flex h-full flex-col" style={{ width: listWidth }}>
360
+ <Header
361
+ branch={status.branch}
362
+ ahead={status.ahead}
363
+ behind={status.behind}
364
+ onRefresh={handleRefresh}
365
+ refreshing={isRefetching}
366
+ />
367
+ <GitPanelTabs activeTab={activeTab} onTabChange={setActiveTab} />
368
+
369
+ <div className="flex-1 overflow-y-auto">
370
+ {!hasChanges ? (
371
+ <div className="flex h-32 flex-col items-center justify-center gap-3">
372
+ <p className="text-muted-foreground text-sm">No changes</p>
373
+ {status.branch !== "main" &&
374
+ status.branch !== "master" &&
375
+ !existingPR && (
376
+ <Button
377
+ variant="outline"
378
+ size="sm"
379
+ onClick={() => createPRMutation.mutate()}
380
+ disabled={createPRMutation.isPending}
381
+ className="gap-1.5"
382
+ >
383
+ {createPRMutation.isPending ? (
384
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
385
+ ) : (
386
+ <GitPullRequest className="h-3.5 w-3.5" />
387
+ )}
388
+ Create PR
389
+ </Button>
390
+ )}
391
+ </div>
392
+ ) : (
393
+ <div className="py-2">
394
+ {/* Staged section */}
395
+ {status.staged.length > 0 && (
396
+ <FileChanges
397
+ files={status.staged}
398
+ title="Staged Changes"
399
+ emptyMessage="No staged changes"
400
+ selectedPath={selectedFile?.file.path}
401
+ onFileClick={handleFileClick}
402
+ onUnstage={handleUnstage}
403
+ onUnstageAll={handleUnstageAll}
404
+ isStaged={true}
405
+ />
406
+ )}
407
+
408
+ {/* Unstaged section */}
409
+ {status.unstaged.length > 0 && (
410
+ <FileChanges
411
+ files={status.unstaged}
412
+ title="Changes"
413
+ emptyMessage="No changes"
414
+ selectedPath={selectedFile?.file.path}
415
+ onFileClick={handleFileClick}
416
+ onStage={handleStage}
417
+ onStageAll={handleStageAll}
418
+ isStaged={false}
419
+ />
420
+ )}
421
+
422
+ {/* Untracked section */}
423
+ {status.untracked.length > 0 && (
424
+ <FileChanges
425
+ files={status.untracked}
426
+ title="Untracked Files"
427
+ emptyMessage="No untracked files"
428
+ selectedPath={selectedFile?.file.path}
429
+ onFileClick={handleFileClick}
430
+ onStage={handleStage}
431
+ isStaged={false}
432
+ />
433
+ )}
434
+ </div>
435
+ )}
436
+ </div>
437
+
438
+ {/* Commit form */}
439
+ <CommitForm
440
+ workingDirectory={workingDirectory}
441
+ stagedCount={status.staged.length}
442
+ branch={status.branch}
443
+ onCommit={() => {
444
+ queryClient.invalidateQueries({
445
+ queryKey: gitKeys.status(workingDirectory),
446
+ });
447
+ queryClient.invalidateQueries({
448
+ queryKey: gitKeys.pr(workingDirectory),
449
+ });
450
+ }}
451
+ />
452
+ </div>
453
+
454
+ {/* Resize handle */}
455
+ <div
456
+ className="bg-muted/50 hover:bg-primary/50 active:bg-primary w-1 flex-shrink-0 cursor-col-resize transition-colors"
457
+ onMouseDown={handleMouseDown}
458
+ />
459
+
460
+ {/* Right panel - diff viewer */}
461
+ <div className="bg-muted/20 flex h-full min-w-0 flex-1 flex-col">
462
+ {loadingDiff ? (
463
+ <div className="flex flex-1 items-center justify-center">
464
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
465
+ </div>
466
+ ) : selectedFile ? (
467
+ <>
468
+ {/* Diff header with stage/unstage */}
469
+ <div className="bg-background/50 flex items-center gap-2 p-3">
470
+ <FileCode className="text-muted-foreground h-4 w-4" />
471
+ <span className="flex-1 truncate text-sm font-medium">
472
+ {selectedFile.file.path}
473
+ </span>
474
+ <Button
475
+ variant={selectedFile.file.staged ? "outline" : "default"}
476
+ size="sm"
477
+ onClick={() =>
478
+ selectedFile.file.staged
479
+ ? handleUnstage(selectedFile.file)
480
+ : handleStage(selectedFile.file)
481
+ }
482
+ >
483
+ {selectedFile.file.staged ? (
484
+ <>
485
+ <Minus className="mr-1 h-4 w-4" />
486
+ Unstage
487
+ </>
488
+ ) : (
489
+ <>
490
+ <Plus className="mr-1 h-4 w-4" />
491
+ Stage
492
+ </>
493
+ )}
494
+ </Button>
495
+ </div>
496
+ {/* Diff content */}
497
+ <div className="flex-1 overflow-auto p-3">
498
+ <DiffView
499
+ diff={selectedFile.diff}
500
+ fileName={selectedFile.file.path}
501
+ />
502
+ </div>
503
+ </>
504
+ ) : (
505
+ <div className="text-muted-foreground flex flex-1 flex-col items-center justify-center">
506
+ <FileCode className="mb-4 h-12 w-12 opacity-50" />
507
+ <p className="text-sm">Select a file to view diff</p>
508
+ </div>
509
+ )}
510
+ </div>
511
+ </div>
512
+
513
+ {/* PR Creation Modal */}
514
+ {showPRModal && (
515
+ <PRCreationModal
516
+ workingDirectory={workingDirectory}
517
+ onClose={() => setShowPRModal(false)}
518
+ />
519
+ )}
520
+ </div>
521
+ );
522
+ }
523
+
524
+ // Mobile layout component
525
+ interface MobileGitPanelProps {
526
+ status: GitStatus;
527
+ hasChanges: boolean;
528
+ selectedFile: SelectedFile | null;
529
+ loadingDiff: boolean;
530
+ refreshing: boolean;
531
+ showPRModal: boolean;
532
+ workingDirectory: string;
533
+ activeTab: GitTab;
534
+ existingPR: {
535
+ number: number;
536
+ url: string;
537
+ state: string;
538
+ title: string;
539
+ } | null;
540
+ creatingPR: boolean;
541
+ onTabChange: (tab: GitTab) => void;
542
+ onRefresh: () => void;
543
+ onFileClick: (file: GitFile) => void;
544
+ onStage: (file: GitFile) => void;
545
+ onUnstage: (file: GitFile) => void;
546
+ onStageAll: () => void;
547
+ onUnstageAll: () => void;
548
+ onBack: () => void;
549
+ onCommit: () => void;
550
+ onShowPRModal: () => void;
551
+ onClosePRModal: () => void;
552
+ onCreatePR: () => void;
553
+ }
554
+
555
+ function MobileGitPanel({
556
+ status,
557
+ hasChanges,
558
+ selectedFile,
559
+ loadingDiff,
560
+ refreshing,
561
+ showPRModal,
562
+ workingDirectory,
563
+ activeTab,
564
+ existingPR,
565
+ creatingPR,
566
+ onTabChange,
567
+ onRefresh,
568
+ onFileClick,
569
+ onStage,
570
+ onUnstage,
571
+ onStageAll,
572
+ onUnstageAll,
573
+ onBack,
574
+ onCommit,
575
+ onShowPRModal,
576
+ onClosePRModal,
577
+ onCreatePR,
578
+ }: MobileGitPanelProps) {
579
+ // History tab
580
+ if (activeTab === "history") {
581
+ return (
582
+ <div className="bg-background flex h-full w-full flex-col">
583
+ <Header
584
+ branch={status.branch}
585
+ ahead={status.ahead}
586
+ behind={status.behind}
587
+ onRefresh={onRefresh}
588
+ refreshing={refreshing}
589
+ existingPR={existingPR}
590
+ />
591
+ <GitPanelTabs activeTab={activeTab} onTabChange={onTabChange} />
592
+ <CommitHistory workingDirectory={workingDirectory} />
593
+ </div>
594
+ );
595
+ }
596
+
597
+ // Show diff view when file is selected
598
+ if (selectedFile) {
599
+ return (
600
+ <div className="bg-background flex h-full w-full flex-col">
601
+ {/* Header */}
602
+ <div className="bg-muted/30 flex items-center gap-2 p-2">
603
+ <Button variant="ghost" size="icon-sm" onClick={onBack}>
604
+ <ArrowLeft className="h-5 w-5" />
605
+ </Button>
606
+ <div className="min-w-0 flex-1">
607
+ <p className="truncate text-sm font-medium">
608
+ {selectedFile.file.path}
609
+ </p>
610
+ </div>
611
+ <Button
612
+ variant={selectedFile.file.staged ? "outline" : "default"}
613
+ size="sm"
614
+ onClick={() =>
615
+ selectedFile.file.staged
616
+ ? onUnstage(selectedFile.file)
617
+ : onStage(selectedFile.file)
618
+ }
619
+ >
620
+ {selectedFile.file.staged ? "Unstage" : "Stage"}
621
+ </Button>
622
+ </div>
623
+
624
+ {/* Diff content */}
625
+ <div className="flex-1 overflow-auto p-3">
626
+ {loadingDiff ? (
627
+ <div className="flex h-32 items-center justify-center">
628
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
629
+ </div>
630
+ ) : (
631
+ <DiffView
632
+ diff={selectedFile.diff}
633
+ fileName={selectedFile.file.path}
634
+ />
635
+ )}
636
+ </div>
637
+ </div>
638
+ );
639
+ }
640
+
641
+ // Show file list (Changes tab)
642
+ return (
643
+ <div className="bg-background flex h-full w-full flex-col">
644
+ <Header
645
+ branch={status.branch}
646
+ ahead={status.ahead}
647
+ behind={status.behind}
648
+ onRefresh={onRefresh}
649
+ refreshing={refreshing}
650
+ existingPR={existingPR}
651
+ />
652
+ <GitPanelTabs activeTab={activeTab} onTabChange={onTabChange} />
653
+
654
+ <div className="flex-1 overflow-y-auto">
655
+ {!hasChanges ? (
656
+ <div className="flex h-32 flex-col items-center justify-center gap-3">
657
+ <p className="text-muted-foreground text-sm">No changes</p>
658
+ {status.branch !== "main" &&
659
+ status.branch !== "master" &&
660
+ !existingPR && (
661
+ <Button
662
+ variant="outline"
663
+ size="sm"
664
+ onClick={onCreatePR}
665
+ disabled={creatingPR}
666
+ className="gap-1.5"
667
+ >
668
+ {creatingPR ? (
669
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
670
+ ) : (
671
+ <GitPullRequest className="h-3.5 w-3.5" />
672
+ )}
673
+ Create PR
674
+ </Button>
675
+ )}
676
+ </div>
677
+ ) : (
678
+ <div className="py-2">
679
+ {/* Staged section */}
680
+ {status.staged.length > 0 && (
681
+ <FileChanges
682
+ files={status.staged}
683
+ title="Staged Changes"
684
+ emptyMessage="No staged changes"
685
+ onFileClick={onFileClick}
686
+ onUnstage={onUnstage}
687
+ onUnstageAll={onUnstageAll}
688
+ isStaged={true}
689
+ />
690
+ )}
691
+
692
+ {/* Unstaged section */}
693
+ {status.unstaged.length > 0 && (
694
+ <FileChanges
695
+ files={status.unstaged}
696
+ title="Changes"
697
+ emptyMessage="No changes"
698
+ onFileClick={onFileClick}
699
+ onStage={onStage}
700
+ onStageAll={onStageAll}
701
+ isStaged={false}
702
+ />
703
+ )}
704
+
705
+ {/* Untracked section */}
706
+ {status.untracked.length > 0 && (
707
+ <FileChanges
708
+ files={status.untracked}
709
+ title="Untracked Files"
710
+ emptyMessage="No untracked files"
711
+ onFileClick={onFileClick}
712
+ onStage={onStage}
713
+ isStaged={false}
714
+ />
715
+ )}
716
+ </div>
717
+ )}
718
+ </div>
719
+
720
+ {/* Commit form */}
721
+ <CommitForm
722
+ workingDirectory={workingDirectory}
723
+ stagedCount={status.staged.length}
724
+ branch={status.branch}
725
+ onCommit={onCommit}
726
+ />
727
+
728
+ {/* Mobile hint */}
729
+ {hasChanges && status.staged.length === 0 && (
730
+ <div className="px-3 py-2">
731
+ <p className="text-muted-foreground text-center text-xs">
732
+ Swipe right to stage, left to unstage
733
+ </p>
734
+ </div>
735
+ )}
736
+
737
+ {/* PR Creation Modal */}
738
+ {showPRModal && (
739
+ <PRCreationModal
740
+ workingDirectory={workingDirectory}
741
+ onClose={onClosePRModal}
742
+ />
743
+ )}
744
+ </div>
745
+ );
746
+ }
747
+
748
+ interface HeaderProps {
749
+ branch: string;
750
+ ahead: number;
751
+ behind: number;
752
+ onRefresh: () => void;
753
+ refreshing: boolean;
754
+ existingPR?: {
755
+ number: number;
756
+ url: string;
757
+ title: string;
758
+ } | null;
759
+ }
760
+
761
+ function Header({
762
+ branch,
763
+ ahead,
764
+ behind,
765
+ onRefresh,
766
+ refreshing,
767
+ existingPR,
768
+ }: HeaderProps) {
769
+ return (
770
+ <div className="flex items-center gap-2 p-3">
771
+ <GitBranch className="text-muted-foreground h-4 w-4 flex-shrink-0" />
772
+ <div className="min-w-0 flex-1">
773
+ <div className="flex items-center gap-2">
774
+ <p className="truncate text-sm font-medium">
775
+ {branch || "Git Status"}
776
+ </p>
777
+ {existingPR && (
778
+ <button
779
+ onClick={() => window.open(existingPR.url, "_blank")}
780
+ className="bg-muted hover:bg-accent inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs transition-colors"
781
+ title={`${existingPR.title} (#${existingPR.number})`}
782
+ >
783
+ <GitPullRequest className="h-3 w-3" />
784
+ PR
785
+ <ExternalLink className="h-2.5 w-2.5" />
786
+ </button>
787
+ )}
788
+ </div>
789
+ {(ahead > 0 || behind > 0) && (
790
+ <div className="text-muted-foreground flex items-center gap-2 text-xs">
791
+ {ahead > 0 && (
792
+ <span className="flex items-center gap-0.5">
793
+ <ArrowUp className="h-3 w-3" />
794
+ {ahead}
795
+ </span>
796
+ )}
797
+ {behind > 0 && (
798
+ <span className="flex items-center gap-0.5">
799
+ <ArrowDown className="h-3 w-3" />
800
+ {behind}
801
+ </span>
802
+ )}
803
+ </div>
804
+ )}
805
+ </div>
806
+ <Button
807
+ variant="ghost"
808
+ size="icon-sm"
809
+ onClick={onRefresh}
810
+ disabled={refreshing}
811
+ className="h-8 w-8"
812
+ >
813
+ <RefreshCw className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`} />
814
+ </Button>
815
+ </div>
816
+ );
817
+ }