@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,520 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+ import { FileTree } from "./FileTree";
5
+ import { FileEditor } from "./FileEditor";
6
+ import { FileTabs } from "./FileTabs";
7
+ import type { UseFileEditorReturn } from "@/hooks/useFileEditor";
8
+ import { useViewport } from "@/hooks/useViewport";
9
+ import {
10
+ Loader2,
11
+ AlertCircle,
12
+ ArrowLeft,
13
+ Folder,
14
+ Save,
15
+ FolderOpen,
16
+ } from "lucide-react";
17
+ import { Button } from "@/components/ui/button";
18
+ import {
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogFooter,
23
+ DialogHeader,
24
+ DialogTitle,
25
+ } from "@/components/ui/dialog";
26
+ import type { FileNode } from "@/lib/file-utils";
27
+ import type { OpenFile } from "@/hooks/useFileEditor";
28
+
29
+ interface FileExplorerProps {
30
+ workingDirectory: string;
31
+ fileEditor: UseFileEditorReturn;
32
+ }
33
+
34
+ export function FileExplorer({
35
+ workingDirectory,
36
+ fileEditor,
37
+ }: FileExplorerProps) {
38
+ const { isMobile, isHydrated } = useViewport();
39
+ const [files, setFiles] = useState<FileNode[]>([]);
40
+ const [loading, setLoading] = useState(true);
41
+ const [error, setError] = useState<string | null>(null);
42
+ const [pendingClose, setPendingClose] = useState<string | null>(null);
43
+
44
+ const {
45
+ openFiles,
46
+ activeFilePath,
47
+ loading: fileLoading,
48
+ saving,
49
+ openFile,
50
+ closeFile,
51
+ setActiveFile,
52
+ updateContent,
53
+ saveFile,
54
+ isDirty,
55
+ getFile,
56
+ } = fileEditor;
57
+
58
+ // Load directory contents
59
+ useEffect(() => {
60
+ const loadFiles = async () => {
61
+ setLoading(true);
62
+ setError(null);
63
+ try {
64
+ const res = await fetch(
65
+ `/api/files?path=${encodeURIComponent(workingDirectory)}`
66
+ );
67
+ const data = await res.json();
68
+ if (data.error) {
69
+ setError(data.error);
70
+ } else {
71
+ setFiles(data.files || []);
72
+ }
73
+ } catch {
74
+ setError("Failed to load directory");
75
+ } finally {
76
+ setLoading(false);
77
+ }
78
+ };
79
+ loadFiles();
80
+ }, [workingDirectory]);
81
+
82
+ const handleFileClick = useCallback(
83
+ (path: string) => {
84
+ openFile(path);
85
+ },
86
+ [openFile]
87
+ );
88
+
89
+ const handleCloseFile = useCallback(
90
+ (path: string) => {
91
+ if (isDirty(path)) {
92
+ setPendingClose(path);
93
+ } else {
94
+ closeFile(path);
95
+ }
96
+ },
97
+ [isDirty, closeFile]
98
+ );
99
+
100
+ const handleConfirmClose = useCallback(async () => {
101
+ if (!pendingClose) return;
102
+ closeFile(pendingClose);
103
+ setPendingClose(null);
104
+ }, [pendingClose, closeFile]);
105
+
106
+ const handleSaveAndClose = useCallback(async () => {
107
+ if (!pendingClose) return;
108
+ await saveFile(pendingClose);
109
+ closeFile(pendingClose);
110
+ setPendingClose(null);
111
+ }, [pendingClose, saveFile, closeFile]);
112
+
113
+ const handleSave = useCallback(async () => {
114
+ if (activeFilePath) {
115
+ await saveFile(activeFilePath);
116
+ }
117
+ }, [activeFilePath, saveFile]);
118
+
119
+ const activeFile = activeFilePath ? getFile(activeFilePath) : undefined;
120
+
121
+ // Loading state before hydration
122
+ if (!isHydrated) {
123
+ return (
124
+ <div className="bg-background flex h-full w-full items-center justify-center">
125
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
126
+ </div>
127
+ );
128
+ }
129
+
130
+ // Mobile layout: full-screen tree OR full-screen editor
131
+ if (isMobile) {
132
+ return (
133
+ <MobileFileExplorer
134
+ files={files}
135
+ loading={loading}
136
+ error={error}
137
+ fileLoading={fileLoading}
138
+ workingDirectory={workingDirectory}
139
+ openFiles={openFiles}
140
+ activeFilePath={activeFilePath}
141
+ activeFile={activeFile}
142
+ saving={saving}
143
+ onFileClick={handleFileClick}
144
+ onSelectTab={setActiveFile}
145
+ onCloseTab={handleCloseFile}
146
+ onSave={handleSave}
147
+ onBack={() => setActiveFile(null as unknown as string)}
148
+ isDirty={isDirty}
149
+ updateContent={updateContent}
150
+ pendingClose={pendingClose}
151
+ onCancelClose={() => setPendingClose(null)}
152
+ onConfirmClose={handleConfirmClose}
153
+ onSaveAndClose={handleSaveAndClose}
154
+ />
155
+ );
156
+ }
157
+
158
+ // Desktop layout: side-by-side tree + editor
159
+ return (
160
+ <DesktopFileExplorer
161
+ files={files}
162
+ loading={loading}
163
+ error={error}
164
+ fileLoading={fileLoading}
165
+ workingDirectory={workingDirectory}
166
+ openFiles={openFiles}
167
+ activeFilePath={activeFilePath}
168
+ activeFile={activeFile}
169
+ saving={saving}
170
+ onFileClick={handleFileClick}
171
+ onSelectTab={setActiveFile}
172
+ onCloseTab={handleCloseFile}
173
+ onSave={handleSave}
174
+ isDirty={isDirty}
175
+ updateContent={updateContent}
176
+ pendingClose={pendingClose}
177
+ onCancelClose={() => setPendingClose(null)}
178
+ onConfirmClose={handleConfirmClose}
179
+ onSaveAndClose={handleSaveAndClose}
180
+ />
181
+ );
182
+ }
183
+
184
+ // Desktop: Side-by-side tree + editor
185
+ interface DesktopFileExplorerProps {
186
+ files: FileNode[];
187
+ loading: boolean;
188
+ error: string | null;
189
+ fileLoading: boolean;
190
+ workingDirectory: string;
191
+ openFiles: OpenFile[];
192
+ activeFilePath: string | null;
193
+ activeFile: OpenFile | undefined;
194
+ saving: boolean;
195
+ onFileClick: (path: string) => void;
196
+ onSelectTab: (path: string) => void;
197
+ onCloseTab: (path: string) => void;
198
+ onSave: () => void;
199
+ isDirty: (path: string) => boolean;
200
+ updateContent: (path: string, content: string) => void;
201
+ pendingClose: string | null;
202
+ onCancelClose: () => void;
203
+ onConfirmClose: () => void;
204
+ onSaveAndClose: () => void;
205
+ }
206
+
207
+ function DesktopFileExplorer({
208
+ files,
209
+ loading,
210
+ error,
211
+ fileLoading,
212
+ workingDirectory,
213
+ openFiles,
214
+ activeFilePath,
215
+ activeFile,
216
+ saving,
217
+ onFileClick,
218
+ onSelectTab,
219
+ onCloseTab,
220
+ onSave,
221
+ isDirty,
222
+ updateContent,
223
+ pendingClose,
224
+ onCancelClose,
225
+ onConfirmClose,
226
+ onSaveAndClose,
227
+ }: DesktopFileExplorerProps) {
228
+ const [treeWidth, setTreeWidth] = useState(280);
229
+ const containerRef = useRef<HTMLDivElement>(null);
230
+ const isDragging = useRef(false);
231
+
232
+ const handleMouseDown = useCallback((e: React.MouseEvent) => {
233
+ e.preventDefault();
234
+ isDragging.current = true;
235
+ document.body.style.cursor = "col-resize";
236
+ document.body.style.userSelect = "none";
237
+
238
+ const handleMouseMove = (e: MouseEvent) => {
239
+ if (!isDragging.current || !containerRef.current) return;
240
+ const containerRect = containerRef.current.getBoundingClientRect();
241
+ const newWidth = e.clientX - containerRect.left;
242
+ setTreeWidth(Math.max(200, Math.min(500, newWidth)));
243
+ };
244
+
245
+ const handleMouseUp = () => {
246
+ isDragging.current = false;
247
+ document.body.style.cursor = "";
248
+ document.body.style.userSelect = "";
249
+ document.removeEventListener("mousemove", handleMouseMove);
250
+ document.removeEventListener("mouseup", handleMouseUp);
251
+ };
252
+
253
+ document.addEventListener("mousemove", handleMouseMove);
254
+ document.addEventListener("mouseup", handleMouseUp);
255
+ }, []);
256
+
257
+ return (
258
+ <div ref={containerRef} className="bg-background flex h-full w-full">
259
+ {/* File tree panel */}
260
+ <div className="flex h-full flex-col" style={{ width: treeWidth }}>
261
+ <div className="flex items-center gap-2 p-3">
262
+ <FolderOpen className="text-muted-foreground h-4 w-4 flex-shrink-0" />
263
+ <p className="flex-1 truncate text-sm font-medium">Files</p>
264
+ </div>
265
+ <div className="flex-1 overflow-y-auto">
266
+ {loading ? (
267
+ <div className="flex h-32 items-center justify-center">
268
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
269
+ </div>
270
+ ) : error ? (
271
+ <div className="text-muted-foreground flex h-32 flex-col items-center justify-center p-4">
272
+ <AlertCircle className="mb-2 h-8 w-8" />
273
+ <p className="text-center text-sm">{error}</p>
274
+ </div>
275
+ ) : files.length === 0 ? (
276
+ <div className="text-muted-foreground flex h-32 items-center justify-center">
277
+ <p className="text-sm">Empty directory</p>
278
+ </div>
279
+ ) : (
280
+ <FileTree
281
+ nodes={files}
282
+ basePath={workingDirectory}
283
+ onFileClick={onFileClick}
284
+ />
285
+ )}
286
+ </div>
287
+ </div>
288
+
289
+ {/* Resize handle */}
290
+ <div
291
+ className="bg-muted/50 hover:bg-primary/50 active:bg-primary w-1 flex-shrink-0 cursor-col-resize transition-colors"
292
+ onMouseDown={handleMouseDown}
293
+ />
294
+
295
+ {/* Editor panel */}
296
+ <div className="bg-muted/20 flex h-full min-w-0 flex-1 flex-col">
297
+ {/* Tabs */}
298
+ {openFiles.length > 0 && (
299
+ <div className="bg-background/50">
300
+ <FileTabs
301
+ files={openFiles}
302
+ activeFilePath={activeFilePath}
303
+ onSelect={onSelectTab}
304
+ onClose={onCloseTab}
305
+ isDirty={isDirty}
306
+ />
307
+ </div>
308
+ )}
309
+
310
+ {/* Editor or empty state */}
311
+ <div className="flex-1 overflow-hidden">
312
+ {fileLoading ? (
313
+ <div className="flex h-full items-center justify-center">
314
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
315
+ </div>
316
+ ) : activeFile ? (
317
+ <FileEditor
318
+ content={activeFile.currentContent}
319
+ language={activeFile.language}
320
+ isBinary={activeFile.isBinary}
321
+ onChange={(content) => updateContent(activeFile.path, content)}
322
+ onSave={onSave}
323
+ />
324
+ ) : (
325
+ <div className="text-muted-foreground flex h-full flex-col items-center justify-center">
326
+ <Folder className="mb-4 h-12 w-12 opacity-50" />
327
+ <p className="text-sm">Select a file to edit</p>
328
+ </div>
329
+ )}
330
+ </div>
331
+ </div>
332
+
333
+ {/* Unsaved changes dialog */}
334
+ <UnsavedChangesDialog
335
+ open={!!pendingClose}
336
+ fileName={pendingClose?.split("/").pop() || ""}
337
+ onCancel={onCancelClose}
338
+ onDiscard={onConfirmClose}
339
+ onSave={onSaveAndClose}
340
+ />
341
+ </div>
342
+ );
343
+ }
344
+
345
+ // Mobile: Full-screen tree OR full-screen editor
346
+ interface MobileFileExplorerProps extends DesktopFileExplorerProps {
347
+ onBack: () => void;
348
+ }
349
+
350
+ function MobileFileExplorer({
351
+ files,
352
+ loading,
353
+ error,
354
+ fileLoading,
355
+ workingDirectory,
356
+ openFiles,
357
+ activeFilePath,
358
+ activeFile,
359
+ saving,
360
+ onFileClick,
361
+ onSelectTab,
362
+ onCloseTab,
363
+ onSave,
364
+ onBack,
365
+ isDirty,
366
+ updateContent,
367
+ pendingClose,
368
+ onCancelClose,
369
+ onConfirmClose,
370
+ onSaveAndClose,
371
+ }: MobileFileExplorerProps) {
372
+ // Show editor when a file is active
373
+ if (activeFile) {
374
+ const isCurrentDirty = activeFilePath ? isDirty(activeFilePath) : false;
375
+
376
+ return (
377
+ <div className="bg-background flex h-full w-full flex-col">
378
+ {/* Header */}
379
+ <div className="bg-muted/30 flex items-center gap-2 p-2">
380
+ <Button variant="ghost" size="icon-sm" onClick={onBack}>
381
+ <ArrowLeft className="h-5 w-5" />
382
+ </Button>
383
+ <div className="min-w-0 flex-1">
384
+ <FileTabs
385
+ files={openFiles}
386
+ activeFilePath={activeFilePath}
387
+ onSelect={onSelectTab}
388
+ onClose={onCloseTab}
389
+ isDirty={isDirty}
390
+ />
391
+ </div>
392
+ {isCurrentDirty && (
393
+ <Button
394
+ variant="default"
395
+ size="sm"
396
+ onClick={onSave}
397
+ disabled={saving}
398
+ className="flex-shrink-0"
399
+ >
400
+ <Save className="mr-1 h-4 w-4" />
401
+ Save
402
+ </Button>
403
+ )}
404
+ </div>
405
+
406
+ {/* Editor */}
407
+ <div className="flex-1 overflow-hidden">
408
+ {fileLoading ? (
409
+ <div className="flex h-full items-center justify-center">
410
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
411
+ </div>
412
+ ) : (
413
+ <FileEditor
414
+ content={activeFile.currentContent}
415
+ language={activeFile.language}
416
+ isBinary={activeFile.isBinary}
417
+ onChange={(content) => updateContent(activeFile.path, content)}
418
+ onSave={onSave}
419
+ />
420
+ )}
421
+ </div>
422
+
423
+ {/* Unsaved changes dialog */}
424
+ <UnsavedChangesDialog
425
+ open={!!pendingClose}
426
+ fileName={pendingClose?.split("/").pop() || ""}
427
+ onCancel={onCancelClose}
428
+ onDiscard={onConfirmClose}
429
+ onSave={onSaveAndClose}
430
+ />
431
+ </div>
432
+ );
433
+ }
434
+
435
+ // Show file tree
436
+ return (
437
+ <div className="bg-background flex h-full w-full flex-col">
438
+ <div className="flex items-center gap-2 p-3">
439
+ <Folder className="text-muted-foreground h-4 w-4 flex-shrink-0" />
440
+ <div className="min-w-0 flex-1">
441
+ <p className="truncate text-sm font-medium">Files</p>
442
+ <p className="text-muted-foreground truncate text-xs">
443
+ {workingDirectory}
444
+ </p>
445
+ </div>
446
+ </div>
447
+
448
+ <div className="flex-1 overflow-y-auto">
449
+ {loading ? (
450
+ <div className="flex h-32 items-center justify-center">
451
+ <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
452
+ </div>
453
+ ) : error ? (
454
+ <div className="text-muted-foreground flex h-32 flex-col items-center justify-center p-4">
455
+ <AlertCircle className="mb-2 h-8 w-8" />
456
+ <p className="text-center text-sm">{error}</p>
457
+ </div>
458
+ ) : files.length === 0 ? (
459
+ <div className="text-muted-foreground flex h-32 items-center justify-center">
460
+ <p className="text-sm">Empty directory</p>
461
+ </div>
462
+ ) : (
463
+ <FileTree
464
+ nodes={files}
465
+ basePath={workingDirectory}
466
+ onFileClick={onFileClick}
467
+ />
468
+ )}
469
+ </div>
470
+
471
+ {fileLoading && (
472
+ <div className="bg-background/80 fixed inset-0 z-50 flex items-center justify-center backdrop-blur-sm">
473
+ <Loader2 className="text-primary h-8 w-8 animate-spin" />
474
+ </div>
475
+ )}
476
+ </div>
477
+ );
478
+ }
479
+
480
+ // Unsaved changes confirmation dialog
481
+ interface UnsavedChangesDialogProps {
482
+ open: boolean;
483
+ fileName: string;
484
+ onCancel: () => void;
485
+ onDiscard: () => void;
486
+ onSave: () => void;
487
+ }
488
+
489
+ function UnsavedChangesDialog({
490
+ open,
491
+ fileName,
492
+ onCancel,
493
+ onDiscard,
494
+ onSave,
495
+ }: UnsavedChangesDialogProps) {
496
+ return (
497
+ <Dialog
498
+ open={open}
499
+ onOpenChange={(isOpen: boolean) => !isOpen && onCancel()}
500
+ >
501
+ <DialogContent showCloseButton={false}>
502
+ <DialogHeader>
503
+ <DialogTitle>Unsaved changes</DialogTitle>
504
+ <DialogDescription>
505
+ {fileName} has unsaved changes. What would you like to do?
506
+ </DialogDescription>
507
+ </DialogHeader>
508
+ <DialogFooter>
509
+ <Button variant="outline" onClick={onCancel}>
510
+ Cancel
511
+ </Button>
512
+ <Button variant="destructive" onClick={onDiscard}>
513
+ Discard
514
+ </Button>
515
+ <Button onClick={onSave}>Save</Button>
516
+ </DialogFooter>
517
+ </DialogContent>
518
+ </Dialog>
519
+ );
520
+ }