@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,414 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useCallback } from "react";
4
+ import {
5
+ File,
6
+ Plus,
7
+ Minus,
8
+ Edit3,
9
+ ArrowRight,
10
+ ChevronRight,
11
+ Check,
12
+ MoreVertical,
13
+ Undo2,
14
+ } from "lucide-react";
15
+ import {
16
+ DropdownMenu,
17
+ DropdownMenuContent,
18
+ DropdownMenuItem,
19
+ DropdownMenuTrigger,
20
+ } from "@/components/ui/dropdown-menu";
21
+ import { cn } from "@/lib/utils";
22
+ import type { GitFile } from "@/lib/git-status";
23
+ import type { MultiRepoGitFile } from "@/lib/multi-repo-git";
24
+
25
+ type AnyGitFile = GitFile | MultiRepoGitFile;
26
+
27
+ interface FileChangesProps {
28
+ files: AnyGitFile[];
29
+ title: string;
30
+ emptyMessage: string;
31
+ selectedPath?: string;
32
+ onFileClick: (file: AnyGitFile) => void;
33
+ onStage?: (file: AnyGitFile) => void;
34
+ onUnstage?: (file: AnyGitFile) => void;
35
+ onStageAll?: () => void;
36
+ onUnstageAll?: () => void;
37
+ onDiscard?: (file: AnyGitFile) => void;
38
+ isStaged?: boolean;
39
+ groupByRepo?: boolean;
40
+ }
41
+
42
+ const SWIPE_THRESHOLD = 80;
43
+
44
+ export function FileChanges({
45
+ files,
46
+ title,
47
+ emptyMessage,
48
+ selectedPath,
49
+ onFileClick,
50
+ onStage,
51
+ onUnstage,
52
+ onStageAll,
53
+ onUnstageAll,
54
+ onDiscard,
55
+ isStaged = false,
56
+ groupByRepo = false,
57
+ }: FileChangesProps) {
58
+ const [expanded, setExpanded] = useState(true);
59
+
60
+ if (files.length === 0) {
61
+ return null;
62
+ }
63
+
64
+ const showAllButton = files.length > 1 && (onStageAll || onUnstageAll);
65
+
66
+ // Group files by repo if enabled
67
+ const groupedFiles = groupByRepo
68
+ ? (() => {
69
+ const grouped = new Map<string, AnyGitFile[]>();
70
+ for (const f of files) {
71
+ const repoKey = "repoName" in f && f.repoName ? f.repoName : "";
72
+ const existing = grouped.get(repoKey) || [];
73
+ existing.push(f);
74
+ grouped.set(repoKey, existing);
75
+ }
76
+ return Array.from(grouped.entries());
77
+ })()
78
+ : [["", files] as [string, AnyGitFile[]]];
79
+
80
+ return (
81
+ <div className="mb-4">
82
+ <div className="flex items-center gap-2 px-3 py-2">
83
+ <button
84
+ onClick={() => setExpanded(!expanded)}
85
+ className="text-muted-foreground hover:text-foreground flex items-center gap-2 text-sm font-medium transition-colors"
86
+ >
87
+ <ChevronRight
88
+ className={cn(
89
+ "h-4 w-4 transition-transform",
90
+ expanded && "rotate-90"
91
+ )}
92
+ />
93
+ <span>{title}</span>
94
+ </button>
95
+ <span className="bg-muted ml-auto rounded-full px-2 py-0.5 text-xs">
96
+ {files.length}
97
+ </span>
98
+ {showAllButton && (
99
+ <button
100
+ onClick={(e) => {
101
+ e.stopPropagation();
102
+ isStaged ? onUnstageAll?.() : onStageAll?.();
103
+ }}
104
+ className="text-muted-foreground hover:text-foreground flex items-center gap-1 text-xs transition-colors"
105
+ >
106
+ {isStaged ? (
107
+ <Minus className="h-3 w-3" />
108
+ ) : (
109
+ <Plus className="h-3 w-3" />
110
+ )}
111
+ All
112
+ </button>
113
+ )}
114
+ </div>
115
+
116
+ {expanded && (
117
+ <div className="space-y-0.5">
118
+ {groupedFiles.map(([repoName, repoFiles]) => (
119
+ <div key={repoName || "default"}>
120
+ {repoName && (
121
+ <div className="bg-muted/30 text-muted-foreground mx-2 mt-2 mb-1 rounded px-2 py-1 text-xs font-medium">
122
+ {repoName}
123
+ </div>
124
+ )}
125
+ {repoFiles.map((file) => {
126
+ const fileKey =
127
+ "repoPath" in file
128
+ ? `${file.repoPath}-${file.path}`
129
+ : file.path;
130
+ return (
131
+ <FileItem
132
+ key={fileKey}
133
+ file={file}
134
+ isSelected={file.path === selectedPath}
135
+ onClick={() => onFileClick(file)}
136
+ onStage={onStage ? () => onStage(file) : undefined}
137
+ onUnstage={onUnstage ? () => onUnstage(file) : undefined}
138
+ onDiscard={onDiscard ? () => onDiscard(file) : undefined}
139
+ onSwipeLeft={isStaged ? () => onUnstage?.(file) : undefined}
140
+ onSwipeRight={!isStaged ? () => onStage?.(file) : undefined}
141
+ isStaged={isStaged}
142
+ />
143
+ );
144
+ })}
145
+ </div>
146
+ ))}
147
+ </div>
148
+ )}
149
+ </div>
150
+ );
151
+ }
152
+
153
+ interface FileItemProps {
154
+ file: AnyGitFile;
155
+ isSelected?: boolean;
156
+ onClick: () => void;
157
+ onStage?: () => void;
158
+ onUnstage?: () => void;
159
+ onDiscard?: () => void;
160
+ onSwipeLeft?: () => void;
161
+ onSwipeRight?: () => void;
162
+ isStaged: boolean;
163
+ }
164
+
165
+ function FileItem({
166
+ file,
167
+ isSelected = false,
168
+ onClick,
169
+ onStage,
170
+ onUnstage,
171
+ onDiscard,
172
+ onSwipeLeft,
173
+ onSwipeRight,
174
+ isStaged,
175
+ }: FileItemProps) {
176
+ const [swipeOffset, setSwipeOffset] = useState(0);
177
+ const [isSwiping, setIsSwiping] = useState(false);
178
+ const startXRef = useRef(0);
179
+ const containerRef = useRef<HTMLDivElement>(null);
180
+
181
+ const handleTouchStart = useCallback((e: React.TouchEvent) => {
182
+ startXRef.current = e.touches[0].clientX;
183
+ setIsSwiping(true);
184
+ }, []);
185
+
186
+ const handleTouchMove = useCallback(
187
+ (e: React.TouchEvent) => {
188
+ if (!isSwiping) return;
189
+
190
+ const currentX = e.touches[0].clientX;
191
+ const diff = currentX - startXRef.current;
192
+
193
+ // Limit swipe direction based on whether we can stage/unstage
194
+ if (diff > 0 && !onSwipeRight) return;
195
+ if (diff < 0 && !onSwipeLeft) return;
196
+
197
+ // Add resistance at the edges
198
+ const maxSwipe = 100;
199
+ const resistedDiff =
200
+ diff > 0 ? Math.min(diff, maxSwipe) : Math.max(diff, -maxSwipe);
201
+
202
+ setSwipeOffset(resistedDiff);
203
+ },
204
+ [isSwiping, onSwipeLeft, onSwipeRight]
205
+ );
206
+
207
+ const handleTouchEnd = useCallback(() => {
208
+ setIsSwiping(false);
209
+
210
+ // Trigger action if swipe threshold reached
211
+ if (swipeOffset > SWIPE_THRESHOLD && onSwipeRight) {
212
+ onSwipeRight();
213
+ } else if (swipeOffset < -SWIPE_THRESHOLD && onSwipeLeft) {
214
+ onSwipeLeft();
215
+ }
216
+
217
+ // Reset position
218
+ setSwipeOffset(0);
219
+ }, [swipeOffset, onSwipeLeft, onSwipeRight]);
220
+
221
+ const statusIcon = getStatusIcon(file.status);
222
+ const statusColor = getStatusColor(file.status);
223
+ const fileName = file.path.split("/").pop() || file.path;
224
+ const filePath = file.path.includes("/")
225
+ ? file.path.slice(0, file.path.lastIndexOf("/"))
226
+ : "";
227
+
228
+ return (
229
+ <div
230
+ ref={containerRef}
231
+ className="group relative overflow-hidden"
232
+ onTouchStart={handleTouchStart}
233
+ onTouchMove={handleTouchMove}
234
+ onTouchEnd={handleTouchEnd}
235
+ >
236
+ {/* Background action indicators */}
237
+ <div className="absolute inset-0 flex">
238
+ {/* Stage indicator (swipe right) */}
239
+ {onSwipeRight && (
240
+ <div
241
+ className={cn(
242
+ "flex items-center justify-start bg-green-500/20 pl-4",
243
+ swipeOffset > 0 ? "flex-1" : "w-0"
244
+ )}
245
+ style={{ width: swipeOffset > 0 ? `${swipeOffset}px` : 0 }}
246
+ >
247
+ {swipeOffset > SWIPE_THRESHOLD / 2 && (
248
+ <Plus className="h-5 w-5 text-green-500" />
249
+ )}
250
+ </div>
251
+ )}
252
+
253
+ {/* Spacer */}
254
+ <div className="flex-1" />
255
+
256
+ {/* Unstage indicator (swipe left) */}
257
+ {onSwipeLeft && (
258
+ <div
259
+ className={cn(
260
+ "flex items-center justify-end bg-yellow-500/20 pr-4",
261
+ swipeOffset < 0 ? "flex-1" : "w-0"
262
+ )}
263
+ style={{
264
+ width: swipeOffset < 0 ? `${Math.abs(swipeOffset)}px` : 0,
265
+ }}
266
+ >
267
+ {swipeOffset < -SWIPE_THRESHOLD / 2 && (
268
+ <Minus className="h-5 w-5 text-yellow-500" />
269
+ )}
270
+ </div>
271
+ )}
272
+ </div>
273
+
274
+ {/* File item */}
275
+ <div
276
+ className={cn(
277
+ "relative flex w-full items-center gap-2 px-3 py-2 text-sm",
278
+ "transition-colors",
279
+ "min-h-[44px]", // Mobile touch target
280
+ isSelected ? "bg-accent" : "bg-background hover:bg-accent/50"
281
+ )}
282
+ style={{
283
+ transform: `translateX(${swipeOffset}px)`,
284
+ transition: isSwiping ? "none" : "transform 0.2s ease-out",
285
+ }}
286
+ >
287
+ {/* Clickable area for file */}
288
+ <button
289
+ onClick={onClick}
290
+ className="flex min-w-0 flex-1 items-center gap-2 text-left"
291
+ >
292
+ {/* Status icon */}
293
+ <span className={cn("flex-shrink-0", statusColor)}>{statusIcon}</span>
294
+
295
+ {/* File info */}
296
+ <div className="min-w-0 flex-1">
297
+ <span className="block truncate">{fileName}</span>
298
+ {filePath && (
299
+ <span className="text-muted-foreground block truncate text-xs">
300
+ {filePath}
301
+ </span>
302
+ )}
303
+ </div>
304
+ </button>
305
+
306
+ {/* Action buttons - visible on hover (desktop) */}
307
+ <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
308
+ {/* Stage/Unstage button */}
309
+ {isStaged
310
+ ? onUnstage && (
311
+ <button
312
+ onClick={(e) => {
313
+ e.stopPropagation();
314
+ onUnstage();
315
+ }}
316
+ className="hover:bg-accent flex h-7 w-7 items-center justify-center rounded text-yellow-500 transition-colors"
317
+ title="Unstage"
318
+ >
319
+ <Minus className="h-4 w-4" />
320
+ </button>
321
+ )
322
+ : onStage && (
323
+ <button
324
+ onClick={(e) => {
325
+ e.stopPropagation();
326
+ onStage();
327
+ }}
328
+ className="hover:bg-accent flex h-7 w-7 items-center justify-center rounded text-green-500 transition-colors"
329
+ title="Stage"
330
+ >
331
+ <Plus className="h-4 w-4" />
332
+ </button>
333
+ )}
334
+
335
+ {/* Context menu */}
336
+ <DropdownMenu>
337
+ <DropdownMenuTrigger asChild>
338
+ <button
339
+ onClick={(e) => e.stopPropagation()}
340
+ className="text-muted-foreground hover:text-foreground hover:bg-accent flex h-7 w-7 items-center justify-center rounded transition-colors"
341
+ >
342
+ <MoreVertical className="h-4 w-4" />
343
+ </button>
344
+ </DropdownMenuTrigger>
345
+ <DropdownMenuContent align="end">
346
+ {isStaged
347
+ ? onUnstage && (
348
+ <DropdownMenuItem onClick={onUnstage}>
349
+ <Minus className="mr-2 h-4 w-4" />
350
+ Unstage
351
+ </DropdownMenuItem>
352
+ )
353
+ : onStage && (
354
+ <DropdownMenuItem onClick={onStage}>
355
+ <Plus className="mr-2 h-4 w-4" />
356
+ Stage
357
+ </DropdownMenuItem>
358
+ )}
359
+ {onDiscard && !isStaged && (
360
+ <DropdownMenuItem
361
+ onClick={onDiscard}
362
+ className="text-red-500 focus:text-red-500"
363
+ >
364
+ <Undo2 className="mr-2 h-4 w-4" />
365
+ Discard Changes
366
+ </DropdownMenuItem>
367
+ )}
368
+ </DropdownMenuContent>
369
+ </DropdownMenu>
370
+ </div>
371
+
372
+ {/* Staged indicator - always visible */}
373
+ {isStaged && (
374
+ <Check className="h-4 w-4 flex-shrink-0 text-green-500 group-hover:hidden" />
375
+ )}
376
+
377
+ {/* Arrow - visible when not hovering */}
378
+ <ArrowRight className="text-muted-foreground h-4 w-4 flex-shrink-0 group-hover:hidden" />
379
+ </div>
380
+ </div>
381
+ );
382
+ }
383
+
384
+ function getStatusIcon(status: GitFile["status"]) {
385
+ switch (status) {
386
+ case "modified":
387
+ return <Edit3 className="h-4 w-4" />;
388
+ case "added":
389
+ case "untracked":
390
+ return <Plus className="h-4 w-4" />;
391
+ case "deleted":
392
+ return <Minus className="h-4 w-4" />;
393
+ case "renamed":
394
+ return <ArrowRight className="h-4 w-4" />;
395
+ default:
396
+ return <File className="h-4 w-4" />;
397
+ }
398
+ }
399
+
400
+ function getStatusColor(status: GitFile["status"]): string {
401
+ switch (status) {
402
+ case "modified":
403
+ return "text-yellow-500";
404
+ case "added":
405
+ case "untracked":
406
+ return "text-green-500";
407
+ case "deleted":
408
+ return "text-red-500";
409
+ case "renamed":
410
+ return "text-blue-500";
411
+ default:
412
+ return "text-muted-foreground";
413
+ }
414
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export type GitTab = "changes" | "history";
6
+
7
+ interface GitPanelTabsProps {
8
+ activeTab: GitTab;
9
+ onTabChange: (tab: GitTab) => void;
10
+ }
11
+
12
+ export function GitPanelTabs({ activeTab, onTabChange }: GitPanelTabsProps) {
13
+ return (
14
+ <div className="border-border/50 flex border-b">
15
+ <button
16
+ onClick={() => onTabChange("changes")}
17
+ className={cn(
18
+ "flex-1 px-4 py-2 text-sm font-medium transition-colors",
19
+ activeTab === "changes"
20
+ ? "text-foreground border-primary border-b-2"
21
+ : "text-muted-foreground hover:text-foreground"
22
+ )}
23
+ >
24
+ Changes
25
+ </button>
26
+ <button
27
+ onClick={() => onTabChange("history")}
28
+ className={cn(
29
+ "flex-1 px-4 py-2 text-sm font-medium transition-colors",
30
+ activeTab === "history"
31
+ ? "text-foreground border-primary border-b-2"
32
+ : "text-muted-foreground hover:text-foreground"
33
+ )}
34
+ >
35
+ History
36
+ </button>
37
+ </div>
38
+ );
39
+ }