@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,73 @@
1
+ import { useState, useCallback } from "react";
2
+ import {
3
+ useDevServersQuery,
4
+ useStopDevServer,
5
+ useRestartDevServer,
6
+ useRemoveDevServer,
7
+ useCreateDevServer,
8
+ } from "@/data/dev-servers";
9
+
10
+ interface CreateDevServerOptions {
11
+ projectId: string;
12
+ type: "node" | "docker";
13
+ name: string;
14
+ command: string;
15
+ workingDirectory: string;
16
+ ports?: number[];
17
+ }
18
+
19
+ export function useDevServersManager() {
20
+ const { data: devServers = [] } = useDevServersQuery();
21
+ const [startDevServerProjectId, setStartDevServerProjectId] = useState<
22
+ string | null
23
+ >(null);
24
+
25
+ const stopMutation = useStopDevServer();
26
+ const restartMutation = useRestartDevServer();
27
+ const removeMutation = useRemoveDevServer();
28
+ const createMutation = useCreateDevServer();
29
+
30
+ const startDevServer = useCallback((projectId: string) => {
31
+ setStartDevServerProjectId(projectId);
32
+ }, []);
33
+
34
+ const stopDevServer = useCallback(
35
+ async (serverId: string) => {
36
+ await stopMutation.mutateAsync(serverId);
37
+ },
38
+ [stopMutation]
39
+ );
40
+
41
+ const restartDevServer = useCallback(
42
+ async (serverId: string) => {
43
+ await restartMutation.mutateAsync(serverId);
44
+ },
45
+ [restartMutation]
46
+ );
47
+
48
+ const removeDevServer = useCallback(
49
+ async (serverId: string) => {
50
+ await removeMutation.mutateAsync(serverId);
51
+ },
52
+ [removeMutation]
53
+ );
54
+
55
+ const createDevServer = useCallback(
56
+ async (opts: CreateDevServerOptions) => {
57
+ await createMutation.mutateAsync(opts);
58
+ setStartDevServerProjectId(null);
59
+ },
60
+ [createMutation]
61
+ );
62
+
63
+ return {
64
+ devServers,
65
+ startDevServerProjectId,
66
+ setStartDevServerProjectId,
67
+ startDevServer,
68
+ stopDevServer,
69
+ restartDevServer,
70
+ removeDevServer,
71
+ createDevServer,
72
+ };
73
+ }
@@ -0,0 +1,90 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useMemo, useRef } from "react";
4
+ import { useDirectoryFilesQuery } from "@/data/files";
5
+ import type { FileNode } from "@/lib/file-utils";
6
+
7
+ interface UseDirectoryBrowserOptions {
8
+ initialPath?: string;
9
+ /** Filter which files to show (e.g., directories only) */
10
+ filter?: (node: FileNode) => boolean;
11
+ }
12
+
13
+ function sortFiles(files: FileNode[]): FileNode[] {
14
+ return [...files].sort((a, b) => {
15
+ if (a.type === "directory" && b.type !== "directory") return -1;
16
+ if (a.type !== "directory" && b.type === "directory") return 1;
17
+ return a.name.localeCompare(b.name);
18
+ });
19
+ }
20
+
21
+ export function useDirectoryBrowser(options: UseDirectoryBrowserOptions = {}) {
22
+ const { initialPath = "~", filter } = options;
23
+ const filterRef = useRef(filter);
24
+ filterRef.current = filter;
25
+
26
+ const [requestedPath, setRequestedPath] = useState(initialPath);
27
+ const [search, setSearch] = useState("");
28
+
29
+ const { data, isPending, error } = useDirectoryFilesQuery(requestedPath);
30
+
31
+ // Resolved path for display/navigation (e.g., "~" → "/Users/saad")
32
+ const currentPath = data?.resolvedPath || requestedPath;
33
+
34
+ // Filter and sort files from query data
35
+ const files = useMemo(() => {
36
+ if (!data?.files) return [];
37
+ const items = filterRef.current
38
+ ? data.files.filter(filterRef.current)
39
+ : data.files;
40
+ return sortFiles(items);
41
+ }, [data?.files]);
42
+
43
+ const filteredFiles = useMemo(
44
+ () =>
45
+ search
46
+ ? files.filter((f) =>
47
+ f.name.toLowerCase().includes(search.toLowerCase())
48
+ )
49
+ : files,
50
+ [files, search]
51
+ );
52
+
53
+ const navigateTo = useCallback((path: string) => {
54
+ setSearch("");
55
+ setRequestedPath(path);
56
+ }, []);
57
+
58
+ const navigateUp = useCallback(() => {
59
+ const parts = currentPath.split("/").filter(Boolean);
60
+ if (parts.length > 1) {
61
+ parts.pop();
62
+ navigateTo("/" + parts.join("/"));
63
+ } else {
64
+ navigateTo("/");
65
+ }
66
+ }, [currentPath, navigateTo]);
67
+
68
+ const navigateHome = useCallback(() => {
69
+ navigateTo("~");
70
+ }, [navigateTo]);
71
+
72
+ const pathSegments = useMemo(
73
+ () => currentPath.split("/").filter(Boolean),
74
+ [currentPath]
75
+ );
76
+
77
+ return {
78
+ currentPath,
79
+ files,
80
+ filteredFiles,
81
+ loading: isPending,
82
+ error: error?.message || null,
83
+ search,
84
+ setSearch,
85
+ pathSegments,
86
+ navigateTo,
87
+ navigateUp,
88
+ navigateHome,
89
+ };
90
+ }
@@ -0,0 +1,27 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Hook for smooth drawer enter animations.
5
+ * Uses double requestAnimationFrame to trigger CSS transition after mount.
6
+ */
7
+ export function useDrawerAnimation(open: boolean) {
8
+ const [isAnimatingIn, setIsAnimatingIn] = useState(false);
9
+ const hasAnimated = useRef(false);
10
+
11
+ useEffect(() => {
12
+ if (open && !hasAnimated.current) {
13
+ hasAnimated.current = true;
14
+ requestAnimationFrame(() => {
15
+ requestAnimationFrame(() => {
16
+ setIsAnimatingIn(true);
17
+ });
18
+ });
19
+ }
20
+ if (!open) {
21
+ hasAnimated.current = false;
22
+ setIsAnimatingIn(false);
23
+ }
24
+ }, [open]);
25
+
26
+ return isAnimatingIn;
27
+ }
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useEffect, type RefObject } from "react";
4
+
5
+ interface UseFileDropOptions {
6
+ /** Disable drop handling (e.g., while uploading) */
7
+ disabled?: boolean;
8
+ }
9
+
10
+ interface DragHandlers {
11
+ onDragOver: (e: React.DragEvent) => void;
12
+ onDragLeave: (e: React.DragEvent) => void;
13
+ onDrop: (e: React.DragEvent) => void;
14
+ }
15
+
16
+ /**
17
+ * Hook for handling file drag and drop on a container element.
18
+ *
19
+ * @param containerRef - Ref to the container element for relatedTarget checking
20
+ * @param onFileDrop - Callback when a file is dropped
21
+ * @param options - Optional configuration
22
+ * @returns isDragging state and drag event handlers to spread onto the container
23
+ */
24
+ export function useFileDrop(
25
+ containerRef: RefObject<HTMLElement | null>,
26
+ onFileDrop: (file: File) => void,
27
+ options?: UseFileDropOptions
28
+ ): { isDragging: boolean; dragHandlers: DragHandlers } {
29
+ const [isDragging, setIsDragging] = useState(false);
30
+
31
+ // Reset drag state when disabled
32
+ useEffect(() => {
33
+ if (options?.disabled) {
34
+ setIsDragging(false);
35
+ }
36
+ }, [options?.disabled]);
37
+
38
+ const handleDragOver = useCallback(
39
+ (e: React.DragEvent) => {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ if (!options?.disabled) {
43
+ setIsDragging(true);
44
+ }
45
+ },
46
+ [options?.disabled]
47
+ );
48
+
49
+ const handleDragLeave = useCallback(
50
+ (e: React.DragEvent) => {
51
+ e.preventDefault();
52
+ e.stopPropagation();
53
+ // Only set to false if leaving the container entirely
54
+ // This prevents flickering when moving over nested elements
55
+ if (!containerRef.current?.contains(e.relatedTarget as Node)) {
56
+ setIsDragging(false);
57
+ }
58
+ },
59
+ [containerRef]
60
+ );
61
+
62
+ const handleDrop = useCallback(
63
+ (e: React.DragEvent) => {
64
+ e.preventDefault();
65
+ e.stopPropagation();
66
+ setIsDragging(false);
67
+
68
+ // Don't process drops if disabled
69
+ if (options?.disabled) return;
70
+
71
+ const files = Array.from(e.dataTransfer.files);
72
+ if (files.length > 0) {
73
+ onFileDrop(files[0]);
74
+ }
75
+ },
76
+ [onFileDrop, options?.disabled]
77
+ );
78
+
79
+ return {
80
+ isDragging,
81
+ dragHandlers: {
82
+ onDragOver: handleDragOver,
83
+ onDragLeave: handleDragLeave,
84
+ onDrop: handleDrop,
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,184 @@
1
+ import { useState, useCallback } from "react";
2
+ import { getLanguageFromExtension } from "@/lib/file-utils";
3
+
4
+ export interface OpenFile {
5
+ path: string;
6
+ content: string;
7
+ currentContent: string;
8
+ isBinary: boolean;
9
+ language: string;
10
+ }
11
+
12
+ export interface UseFileEditorReturn {
13
+ openFiles: OpenFile[];
14
+ activeFilePath: string | null;
15
+ loading: boolean;
16
+ saving: boolean;
17
+ openFile: (path: string) => Promise<void>;
18
+ closeFile: (path: string) => void;
19
+ setActiveFile: (path: string) => void;
20
+ updateContent: (path: string, content: string) => void;
21
+ saveFile: (path: string) => Promise<{ success: boolean; error?: string }>;
22
+ saveAllFiles: () => Promise<void>;
23
+ isDirty: (path: string) => boolean;
24
+ hasUnsavedChanges: boolean;
25
+ getFile: (path: string) => OpenFile | undefined;
26
+ reset: () => void;
27
+ }
28
+
29
+ export function useFileEditor(): UseFileEditorReturn {
30
+ const [openFiles, setOpenFiles] = useState<OpenFile[]>([]);
31
+ const [activeFilePath, setActiveFilePath] = useState<string | null>(null);
32
+ const [loading, setLoading] = useState(false);
33
+ const [saving, setSaving] = useState(false);
34
+
35
+ const getFile = useCallback(
36
+ (path: string) => openFiles.find((f) => f.path === path),
37
+ [openFiles]
38
+ );
39
+
40
+ const isDirty = useCallback(
41
+ (path: string) => {
42
+ const file = openFiles.find((f) => f.path === path);
43
+ return file ? file.content !== file.currentContent : false;
44
+ },
45
+ [openFiles]
46
+ );
47
+
48
+ const hasUnsavedChanges = openFiles.some(
49
+ (f) => f.content !== f.currentContent
50
+ );
51
+
52
+ const openFile = useCallback(
53
+ async (path: string) => {
54
+ // Check if file is already open
55
+ const existing = openFiles.find((f) => f.path === path);
56
+ if (existing) {
57
+ setActiveFilePath(path);
58
+ return;
59
+ }
60
+
61
+ setLoading(true);
62
+ try {
63
+ const res = await fetch(
64
+ `/api/files/content?path=${encodeURIComponent(path)}`
65
+ );
66
+ const data = await res.json();
67
+
68
+ if (data.error) {
69
+ console.error("Failed to open file:", data.error);
70
+ return;
71
+ }
72
+
73
+ const ext = path.split(".").pop() || "";
74
+ const newFile: OpenFile = {
75
+ path: data.path,
76
+ content: data.content,
77
+ currentContent: data.content,
78
+ isBinary: data.isBinary,
79
+ language: getLanguageFromExtension(ext),
80
+ };
81
+
82
+ setOpenFiles((prev) => [...prev, newFile]);
83
+ setActiveFilePath(data.path);
84
+ } catch (error) {
85
+ console.error("Failed to open file:", error);
86
+ } finally {
87
+ setLoading(false);
88
+ }
89
+ },
90
+ [openFiles]
91
+ );
92
+
93
+ const closeFile = useCallback((path: string) => {
94
+ setOpenFiles((prev) => {
95
+ const newFiles = prev.filter((f) => f.path !== path);
96
+ // Update active file if we closed the active one
97
+ setActiveFilePath((currentActive) => {
98
+ if (currentActive !== path) return currentActive;
99
+ // Select the next file, or previous, or null
100
+ const closedIndex = prev.findIndex((f) => f.path === path);
101
+ if (newFiles.length === 0) return null;
102
+ if (closedIndex >= newFiles.length)
103
+ return newFiles[newFiles.length - 1].path;
104
+ return newFiles[closedIndex].path;
105
+ });
106
+ return newFiles;
107
+ });
108
+ }, []);
109
+
110
+ const updateContent = useCallback((path: string, content: string) => {
111
+ setOpenFiles((prev) =>
112
+ prev.map((f) => (f.path === path ? { ...f, currentContent: content } : f))
113
+ );
114
+ }, []);
115
+
116
+ const saveFile = useCallback(
117
+ async (path: string): Promise<{ success: boolean; error?: string }> => {
118
+ const file = openFiles.find((f) => f.path === path);
119
+ if (!file) return { success: false, error: "File not found" };
120
+ if (file.isBinary)
121
+ return { success: false, error: "Cannot save binary files" };
122
+
123
+ setSaving(true);
124
+ try {
125
+ const res = await fetch("/api/files/content", {
126
+ method: "POST",
127
+ headers: { "Content-Type": "application/json" },
128
+ body: JSON.stringify({ path, content: file.currentContent }),
129
+ });
130
+ const data = await res.json();
131
+
132
+ if (data.error) {
133
+ return { success: false, error: data.error };
134
+ }
135
+
136
+ // Update the saved content to match current
137
+ setOpenFiles((prev) =>
138
+ prev.map((f) =>
139
+ f.path === path ? { ...f, content: f.currentContent } : f
140
+ )
141
+ );
142
+
143
+ return { success: true };
144
+ } catch (error) {
145
+ return {
146
+ success: false,
147
+ error: error instanceof Error ? error.message : "Failed to save",
148
+ };
149
+ } finally {
150
+ setSaving(false);
151
+ }
152
+ },
153
+ [openFiles]
154
+ );
155
+
156
+ const saveAllFiles = useCallback(async () => {
157
+ const dirtyFiles = openFiles.filter((f) => f.content !== f.currentContent);
158
+ for (const file of dirtyFiles) {
159
+ await saveFile(file.path);
160
+ }
161
+ }, [openFiles, saveFile]);
162
+
163
+ const reset = useCallback(() => {
164
+ setOpenFiles([]);
165
+ setActiveFilePath(null);
166
+ }, []);
167
+
168
+ return {
169
+ openFiles,
170
+ activeFilePath,
171
+ loading,
172
+ saving,
173
+ openFile,
174
+ closeFile,
175
+ setActiveFile: setActiveFilePath,
176
+ updateContent,
177
+ saveFile,
178
+ saveAllFiles,
179
+ isDirty,
180
+ hasUnsavedChanges,
181
+ getFile,
182
+ reset,
183
+ };
184
+ }
@@ -0,0 +1,37 @@
1
+ import { useCallback } from "react";
2
+ import { useToggleGroup, useCreateGroup, useDeleteGroup } from "@/data/groups";
3
+
4
+ export function useGroups() {
5
+ const toggleMutation = useToggleGroup();
6
+ const createMutation = useCreateGroup();
7
+ const deleteMutation = useDeleteGroup();
8
+
9
+ const toggleGroup = useCallback(
10
+ async (path: string, expanded: boolean) => {
11
+ await toggleMutation.mutateAsync({ path, expanded });
12
+ },
13
+ [toggleMutation]
14
+ );
15
+
16
+ const createGroup = useCallback(
17
+ async (name: string, parentPath?: string) => {
18
+ await createMutation.mutateAsync({ name, parentPath });
19
+ },
20
+ [createMutation]
21
+ );
22
+
23
+ const deleteGroup = useCallback(
24
+ async (path: string) => {
25
+ if (!confirm("Delete this group? Sessions will be moved to parent."))
26
+ return;
27
+ await deleteMutation.mutateAsync(path);
28
+ },
29
+ [deleteMutation]
30
+ );
31
+
32
+ return {
33
+ toggleGroup,
34
+ createGroup,
35
+ deleteGroup,
36
+ };
37
+ }
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+
5
+ let cachedHomePath: string | null = null;
6
+
7
+ export function useHomePath() {
8
+ const [homePath, setHomePath] = useState<string | null>(cachedHomePath);
9
+
10
+ useEffect(() => {
11
+ if (cachedHomePath) return;
12
+ fetch("/api/files?path=~")
13
+ .then((res) => res.json())
14
+ .then((data) => {
15
+ if (data.path) {
16
+ cachedHomePath = data.path;
17
+ setHomePath(data.path);
18
+ }
19
+ })
20
+ .catch(() => {});
21
+ }, []);
22
+
23
+ const toTildePath = useCallback(
24
+ (absolutePath: string) => {
25
+ if (homePath && absolutePath.startsWith(homePath)) {
26
+ return "~" + absolutePath.slice(homePath.length);
27
+ }
28
+ return absolutePath;
29
+ },
30
+ [homePath]
31
+ );
32
+
33
+ return { homePath, toTildePath };
34
+ }
@@ -0,0 +1,55 @@
1
+ "use client";
2
+
3
+ import { useRef, useCallback } from "react";
4
+
5
+ // Trigger haptic feedback if available
6
+ function haptic() {
7
+ if (typeof navigator !== "undefined" && "vibrate" in navigator) {
8
+ navigator.vibrate(5);
9
+ }
10
+ }
11
+
12
+ export function useKeyRepeat(onKeyPress: () => void) {
13
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
14
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
15
+ const repeatCountRef = useRef(0);
16
+
17
+ const startRepeat = useCallback(() => {
18
+ // Immediate first press
19
+ haptic();
20
+ onKeyPress();
21
+ repeatCountRef.current = 0;
22
+
23
+ // Start repeating after initial delay (like native keyboard)
24
+ timeoutRef.current = setTimeout(() => {
25
+ intervalRef.current = setInterval(() => {
26
+ haptic();
27
+ onKeyPress();
28
+ repeatCountRef.current++;
29
+
30
+ // Accelerate after many repeats (more gradual than before)
31
+ if (repeatCountRef.current === 15 && intervalRef.current) {
32
+ clearInterval(intervalRef.current);
33
+ intervalRef.current = setInterval(() => {
34
+ haptic();
35
+ onKeyPress();
36
+ }, 80); // Fast repeat (was 50, now slower)
37
+ }
38
+ }, 150); // Initial repeat speed (was 120, now slower)
39
+ }, 500); // Initial delay before repeat starts (was 400, now longer)
40
+ }, [onKeyPress]);
41
+
42
+ const stopRepeat = useCallback(() => {
43
+ if (timeoutRef.current) {
44
+ clearTimeout(timeoutRef.current);
45
+ timeoutRef.current = null;
46
+ }
47
+ if (intervalRef.current) {
48
+ clearInterval(intervalRef.current);
49
+ intervalRef.current = null;
50
+ }
51
+ repeatCountRef.current = 0;
52
+ }, []);
53
+
54
+ return { startRepeat, stopRepeat };
55
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useEffect } from "react";
4
+
5
+ const STORAGE_KEY = "agentOS-keybar-visible";
6
+
7
+ /**
8
+ * Hook to manage mobile keybar visibility with localStorage persistence.
9
+ * Default: hidden on mobile to maximize terminal space.
10
+ */
11
+ export function useKeybarVisibility() {
12
+ const [isVisible, setIsVisible] = useState(false);
13
+
14
+ // Load persisted state on mount
15
+ useEffect(() => {
16
+ if (typeof window === "undefined") return;
17
+ const stored = localStorage.getItem(STORAGE_KEY);
18
+ if (stored === "true") {
19
+ setIsVisible(true);
20
+ }
21
+ }, []);
22
+
23
+ const toggle = useCallback(() => {
24
+ setIsVisible((prev) => {
25
+ const next = !prev;
26
+ localStorage.setItem(STORAGE_KEY, String(next));
27
+ return next;
28
+ });
29
+ }, []);
30
+
31
+ const show = useCallback(() => {
32
+ setIsVisible(true);
33
+ localStorage.setItem(STORAGE_KEY, "true");
34
+ }, []);
35
+
36
+ const hide = useCallback(() => {
37
+ setIsVisible(false);
38
+ localStorage.setItem(STORAGE_KEY, "false");
39
+ }, []);
40
+
41
+ return { isVisible, toggle, show, hide };
42
+ }