@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,339 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+ import {
5
+ X,
6
+ Folder,
7
+ FileIcon,
8
+ FileImage,
9
+ ChevronLeft,
10
+ Loader2,
11
+ Home,
12
+ ChevronRight,
13
+ Upload,
14
+ Clipboard,
15
+ Search,
16
+ } from "lucide-react";
17
+ import { Input } from "@/components/ui/input";
18
+ import { Button } from "@/components/ui/button";
19
+ import { cn } from "@/lib/utils";
20
+ import { uploadFileToTemp } from "@/lib/file-upload";
21
+ import { useFileDrop } from "@/hooks/useFileDrop";
22
+ import { useViewport } from "@/hooks/useViewport";
23
+ import { useDirectoryBrowser } from "@/hooks/useDirectoryBrowser";
24
+ import type { FileNode } from "@/lib/file-utils";
25
+
26
+ const IMAGE_EXTENSIONS = [
27
+ "png",
28
+ "jpg",
29
+ "jpeg",
30
+ "gif",
31
+ "webp",
32
+ "svg",
33
+ "bmp",
34
+ "ico",
35
+ ];
36
+
37
+ interface FilePickerProps {
38
+ initialPath?: string;
39
+ onSelect: (path: string) => void;
40
+ onClose: () => void;
41
+ }
42
+
43
+ function isImageFile(node: FileNode) {
44
+ if (node.type !== "file") return false;
45
+ const ext = node.extension?.toLowerCase() || "";
46
+ return IMAGE_EXTENSIONS.includes(ext);
47
+ }
48
+
49
+ export function FilePicker({
50
+ initialPath,
51
+ onSelect,
52
+ onClose,
53
+ }: FilePickerProps) {
54
+ const {
55
+ currentPath,
56
+ filteredFiles,
57
+ loading,
58
+ error,
59
+ search,
60
+ setSearch,
61
+ pathSegments,
62
+ navigateTo,
63
+ navigateUp,
64
+ navigateHome,
65
+ } = useDirectoryBrowser({ initialPath });
66
+
67
+ const [uploading, setUploading] = useState(false);
68
+ const dropZoneRef = useRef<HTMLDivElement>(null);
69
+ const fileInputRef = useRef<HTMLInputElement>(null);
70
+ const { isMobile } = useViewport();
71
+
72
+ // Handle dropped/pasted/selected file
73
+ const handleFile = useCallback(
74
+ async (file: File) => {
75
+ setUploading(true);
76
+ try {
77
+ const path = await uploadFileToTemp(file);
78
+ if (path) {
79
+ onSelect(path);
80
+ }
81
+ } catch (err) {
82
+ console.error("Failed to upload file:", err);
83
+ } finally {
84
+ setUploading(false);
85
+ }
86
+ },
87
+ [onSelect]
88
+ );
89
+
90
+ // Drag and drop (desktop only)
91
+ const { isDragging, dragHandlers } = useFileDrop(dropZoneRef, handleFile, {
92
+ disabled: uploading || isMobile,
93
+ });
94
+
95
+ // Clipboard paste handler
96
+ useEffect(() => {
97
+ const handlePaste = (e: ClipboardEvent) => {
98
+ const items = e.clipboardData?.items;
99
+ if (!items) return;
100
+
101
+ for (const item of items) {
102
+ if (item.kind === "file") {
103
+ const file = item.getAsFile();
104
+ if (file) {
105
+ e.preventDefault();
106
+ handleFile(file);
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ };
112
+
113
+ document.addEventListener("paste", handlePaste);
114
+ return () => document.removeEventListener("paste", handlePaste);
115
+ }, [handleFile]);
116
+
117
+ const handleItemClick = (node: FileNode) => {
118
+ if (node.type === "directory") {
119
+ navigateTo(node.path);
120
+ } else if (node.type === "file") {
121
+ onSelect(node.path);
122
+ }
123
+ };
124
+
125
+ return (
126
+ <div className="bg-background fixed inset-0 z-50 flex flex-col">
127
+ {/* Header */}
128
+ <div className="border-border bg-background/95 flex items-center gap-2 border-b p-3 backdrop-blur-sm">
129
+ <Button
130
+ variant="ghost"
131
+ size="icon-sm"
132
+ onClick={onClose}
133
+ className="h-9 w-9"
134
+ >
135
+ <X className="h-5 w-5" />
136
+ </Button>
137
+ <div className="min-w-0 flex-1">
138
+ <h3 className="text-sm font-medium">Select File</h3>
139
+ <p className="text-muted-foreground truncate text-xs">
140
+ {currentPath}
141
+ </p>
142
+ </div>
143
+ </div>
144
+
145
+ {/* Navigation bar */}
146
+ <div className="border-border flex items-center gap-1 overflow-x-auto border-b px-3 py-2">
147
+ <Button
148
+ variant="ghost"
149
+ size="icon-sm"
150
+ onClick={navigateHome}
151
+ className="h-8 w-8 shrink-0"
152
+ title="Home"
153
+ >
154
+ <Home className="h-4 w-4" />
155
+ </Button>
156
+ <Button
157
+ variant="ghost"
158
+ size="icon-sm"
159
+ onClick={navigateUp}
160
+ className="h-8 w-8 shrink-0"
161
+ title="Go up"
162
+ >
163
+ <ChevronLeft className="h-4 w-4" />
164
+ </Button>
165
+ <div className="text-muted-foreground flex items-center gap-0.5 overflow-x-auto text-xs">
166
+ <span>/</span>
167
+ {pathSegments.map((segment, i) => (
168
+ <button
169
+ key={i}
170
+ onClick={() =>
171
+ navigateTo("/" + pathSegments.slice(0, i + 1).join("/"))
172
+ }
173
+ className="hover:text-foreground flex shrink-0 items-center transition-colors"
174
+ >
175
+ <span className="max-w-[100px] truncate">{segment}</span>
176
+ {i < pathSegments.length - 1 && (
177
+ <ChevronRight className="mx-0.5 h-3 w-3" />
178
+ )}
179
+ </button>
180
+ ))}
181
+ </div>
182
+ </div>
183
+
184
+ {/* Upload zone */}
185
+ {isMobile ? (
186
+ <div className="mx-3 mt-3 flex items-center justify-center gap-2">
187
+ <input
188
+ ref={fileInputRef}
189
+ type="file"
190
+ className="hidden"
191
+ onChange={(e) => {
192
+ const file = e.target.files?.[0];
193
+ if (file) handleFile(file);
194
+ }}
195
+ />
196
+ <Button
197
+ variant="outline"
198
+ size="sm"
199
+ onClick={() => fileInputRef.current?.click()}
200
+ disabled={uploading}
201
+ className="gap-2"
202
+ >
203
+ {uploading ? (
204
+ <Loader2 className="h-4 w-4 animate-spin" />
205
+ ) : (
206
+ <Upload className="h-4 w-4" />
207
+ )}
208
+ {uploading ? "Uploading..." : "Upload file"}
209
+ </Button>
210
+ <span className="text-muted-foreground text-xs">
211
+ or select a file below
212
+ </span>
213
+ </div>
214
+ ) : (
215
+ <div
216
+ ref={dropZoneRef}
217
+ {...dragHandlers}
218
+ className={cn(
219
+ "border-border mx-3 mt-3 flex flex-col items-center justify-center rounded-lg border-2 border-dashed p-4 transition-colors",
220
+ isDragging && "border-primary bg-primary/10",
221
+ uploading && "opacity-50"
222
+ )}
223
+ >
224
+ {uploading ? (
225
+ <div className="flex items-center gap-2">
226
+ <Loader2 className="h-5 w-5 animate-spin" />
227
+ <span className="text-sm">Uploading...</span>
228
+ </div>
229
+ ) : isDragging ? (
230
+ <div className="flex items-center gap-2">
231
+ <Upload className="text-primary h-5 w-5" />
232
+ <span className="text-primary text-sm font-medium">
233
+ Drop file here
234
+ </span>
235
+ </div>
236
+ ) : (
237
+ <div className="flex flex-col items-center gap-1 text-center">
238
+ <div className="text-muted-foreground flex items-center gap-2">
239
+ <Upload className="h-4 w-4" />
240
+ <span className="text-sm">Drop file here</span>
241
+ </div>
242
+ <div className="text-muted-foreground flex items-center gap-1 text-xs">
243
+ <Clipboard className="h-3 w-3" />
244
+ <span>or paste from clipboard</span>
245
+ </div>
246
+ </div>
247
+ )}
248
+ </div>
249
+ )}
250
+
251
+ {/* Search */}
252
+ <div className="px-3 py-2">
253
+ <div className="relative">
254
+ <Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
255
+ <Input
256
+ type="text"
257
+ placeholder="Search files..."
258
+ value={search}
259
+ onChange={(e) => setSearch(e.target.value)}
260
+ className="h-9 pl-9"
261
+ />
262
+ </div>
263
+ </div>
264
+
265
+ {/* Content */}
266
+ <div className="flex-1 overflow-y-auto">
267
+ {loading ? (
268
+ <div className="flex h-32 items-center justify-center">
269
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
270
+ </div>
271
+ ) : error ? (
272
+ <div className="text-muted-foreground flex h-32 flex-col items-center justify-center p-4">
273
+ <p className="text-center text-sm">{error}</p>
274
+ <Button
275
+ variant="outline"
276
+ size="sm"
277
+ onClick={navigateUp}
278
+ className="mt-2"
279
+ >
280
+ Go back
281
+ </Button>
282
+ </div>
283
+ ) : filteredFiles.length === 0 ? (
284
+ <div className="text-muted-foreground flex h-32 items-center justify-center">
285
+ <p className="text-sm">
286
+ {search ? "No matching files" : "Empty directory"}
287
+ </p>
288
+ </div>
289
+ ) : (
290
+ <div className="grid grid-cols-2 gap-2 p-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
291
+ {filteredFiles.map((node) => {
292
+ const isImg = isImageFile(node);
293
+ const isDir = node.type === "directory";
294
+ const isFile = node.type === "file";
295
+
296
+ return (
297
+ <button
298
+ key={node.path}
299
+ onClick={() => handleItemClick(node)}
300
+ className={cn(
301
+ "flex flex-col items-center gap-2 rounded-lg border p-3 text-center transition-colors",
302
+ "hover:bg-muted/50 hover:border-primary/50 cursor-pointer",
303
+ isImg && "border-primary/30 bg-primary/5"
304
+ )}
305
+ >
306
+ {isDir ? (
307
+ <Folder className="text-primary/70 h-10 w-10" />
308
+ ) : isImg ? (
309
+ <div className="bg-muted flex h-10 w-10 items-center justify-center overflow-hidden rounded">
310
+ <FileImage className="text-primary h-6 w-6" />
311
+ </div>
312
+ ) : isFile ? (
313
+ <div className="bg-muted/50 flex h-10 w-10 items-center justify-center rounded">
314
+ <FileIcon className="text-muted-foreground h-6 w-6" />
315
+ </div>
316
+ ) : (
317
+ <div className="bg-muted/50 flex h-10 w-10 items-center justify-center rounded">
318
+ <span className="text-muted-foreground text-xs">
319
+ {node.extension?.toUpperCase() || "?"}
320
+ </span>
321
+ </div>
322
+ )}
323
+ <span className="w-full truncate text-xs">{node.name}</span>
324
+ </button>
325
+ );
326
+ })}
327
+ </div>
328
+ )}
329
+ </div>
330
+
331
+ {/* Footer hint */}
332
+ <div className="border-border border-t p-3 text-center">
333
+ <p className="text-muted-foreground text-xs">
334
+ Select any file or navigate into folders
335
+ </p>
336
+ </div>
337
+ </div>
338
+ );
339
+ }
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import {
5
+ X,
6
+ Folder,
7
+ ChevronLeft,
8
+ Loader2,
9
+ Home,
10
+ ChevronRight,
11
+ Check,
12
+ GitBranch,
13
+ Search,
14
+ } from "lucide-react";
15
+ import { Input } from "@/components/ui/input";
16
+ import { Button } from "@/components/ui/button";
17
+ import { useDirectoryBrowser } from "@/hooks/useDirectoryBrowser";
18
+
19
+ const DIRS_ONLY = (f: { type: string }) => f.type === "directory";
20
+
21
+ interface FolderPickerProps {
22
+ initialPath?: string;
23
+ onSelect: (path: string) => void;
24
+ onClose: () => void;
25
+ }
26
+
27
+ export function FolderPicker({
28
+ initialPath,
29
+ onSelect,
30
+ onClose,
31
+ }: FolderPickerProps) {
32
+ const {
33
+ currentPath,
34
+ filteredFiles,
35
+ loading,
36
+ error,
37
+ search,
38
+ setSearch,
39
+ pathSegments,
40
+ navigateTo,
41
+ navigateUp,
42
+ navigateHome,
43
+ } = useDirectoryBrowser({ initialPath, filter: DIRS_ONLY });
44
+
45
+ // Git repo check for current directory
46
+ const [isGitRepo, setIsGitRepo] = useState(false);
47
+
48
+ useEffect(() => {
49
+ fetch("/api/git/check", {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ path: currentPath }),
53
+ })
54
+ .then((res) => res.json())
55
+ .then((data) => setIsGitRepo(data.isGitRepo || false))
56
+ .catch(() => setIsGitRepo(false));
57
+ }, [currentPath]);
58
+
59
+ const folderName = pathSegments[pathSegments.length - 1] || "root";
60
+
61
+ return (
62
+ <div className="bg-background fixed inset-0 z-[100] flex flex-col">
63
+ {/* Header */}
64
+ <div className="bg-background/95 flex items-center gap-2 p-3 shadow-sm backdrop-blur-sm">
65
+ <Button
66
+ variant="ghost"
67
+ size="icon-sm"
68
+ onClick={onClose}
69
+ className="h-9 w-9"
70
+ >
71
+ <X className="h-5 w-5" />
72
+ </Button>
73
+ <div className="min-w-0 flex-1">
74
+ <h3 className="text-sm font-medium">Select Folder</h3>
75
+ <p className="text-muted-foreground truncate text-xs">
76
+ {currentPath}
77
+ </p>
78
+ </div>
79
+ </div>
80
+
81
+ {/* Search */}
82
+ <div className="px-3 py-2">
83
+ <div className="relative">
84
+ <Search className="text-muted-foreground absolute top-1/2 left-2.5 h-4 w-4 -translate-y-1/2" />
85
+ <Input
86
+ type="text"
87
+ placeholder="Search folders..."
88
+ value={search}
89
+ onChange={(e) => setSearch(e.target.value)}
90
+ className="h-9 pl-9"
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ {/* Navigation bar */}
96
+ <div className="flex items-center gap-1 overflow-x-auto px-3 pb-2">
97
+ <Button
98
+ variant="ghost"
99
+ size="icon-sm"
100
+ onClick={navigateHome}
101
+ className="h-8 w-8 shrink-0"
102
+ title="Home"
103
+ >
104
+ <Home className="h-4 w-4" />
105
+ </Button>
106
+ <Button
107
+ variant="ghost"
108
+ size="icon-sm"
109
+ onClick={navigateUp}
110
+ className="h-8 w-8 shrink-0"
111
+ title="Go up"
112
+ >
113
+ <ChevronLeft className="h-4 w-4" />
114
+ </Button>
115
+ <div className="text-muted-foreground flex items-center gap-0.5 overflow-x-auto text-xs">
116
+ <span>/</span>
117
+ {pathSegments.map((segment, i) => (
118
+ <button
119
+ key={i}
120
+ onClick={() =>
121
+ navigateTo("/" + pathSegments.slice(0, i + 1).join("/"))
122
+ }
123
+ className="hover:text-foreground flex shrink-0 items-center transition-colors"
124
+ >
125
+ <span className="max-w-[100px] truncate">{segment}</span>
126
+ {i < pathSegments.length - 1 && (
127
+ <ChevronRight className="mx-0.5 h-3 w-3" />
128
+ )}
129
+ </button>
130
+ ))}
131
+ </div>
132
+ </div>
133
+
134
+ {/* Content */}
135
+ <div className="flex-1 overflow-y-auto">
136
+ {loading ? (
137
+ <div className="flex h-32 items-center justify-center">
138
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
139
+ </div>
140
+ ) : error ? (
141
+ <div className="text-muted-foreground flex h-32 flex-col items-center justify-center p-4">
142
+ <p className="text-center text-sm">{error}</p>
143
+ <Button
144
+ variant="outline"
145
+ size="sm"
146
+ onClick={navigateUp}
147
+ className="mt-2"
148
+ >
149
+ Go back
150
+ </Button>
151
+ </div>
152
+ ) : filteredFiles.length === 0 ? (
153
+ <div className="text-muted-foreground flex h-32 items-center justify-center">
154
+ <p className="text-sm">
155
+ {search ? "No matching folders" : "No subfolders"}
156
+ </p>
157
+ </div>
158
+ ) : (
159
+ <div className="space-y-0.5 px-2 pt-1">
160
+ {filteredFiles.map((node) => (
161
+ <button
162
+ key={node.path}
163
+ onClick={() => navigateTo(node.path)}
164
+ className="hover:bg-muted/50 flex w-full items-center gap-3 rounded-md px-3 py-3 text-left transition-colors"
165
+ >
166
+ <Folder className="text-muted-foreground h-5 w-5 shrink-0" />
167
+ <span className="min-w-0 flex-1 truncate text-sm">
168
+ {node.name}
169
+ </span>
170
+ <ChevronRight className="text-muted-foreground h-4 w-4 shrink-0" />
171
+ </button>
172
+ ))}
173
+ </div>
174
+ )}
175
+ </div>
176
+
177
+ {/* Footer with select button */}
178
+ <div className="flex items-center justify-between gap-3 p-3 shadow-[0_-2px_8px_rgba(0,0,0,0.08)]">
179
+ <div className="min-w-0 flex-1">
180
+ <div className="flex items-center gap-2">
181
+ <Folder className="text-primary h-5 w-5 shrink-0" />
182
+ <span className="truncate font-medium">{folderName}</span>
183
+ {isGitRepo && (
184
+ <span className="bg-muted text-muted-foreground flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-xs">
185
+ <GitBranch className="h-3 w-3" />
186
+ Git
187
+ </span>
188
+ )}
189
+ </div>
190
+ </div>
191
+ <Button
192
+ onClick={() => onSelect(currentPath)}
193
+ className="shrink-0 gap-2"
194
+ >
195
+ <Check className="h-4 w-4" />
196
+ Select
197
+ </Button>
198
+ </div>
199
+ </div>
200
+ );
201
+ }