@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,558 @@
1
+ "use client";
2
+
3
+ import { useRef, useCallback, useEffect, memo, useState, useMemo } from "react";
4
+ import dynamic from "next/dynamic";
5
+ import { usePanes } from "@/contexts/PaneContext";
6
+ import { useViewport } from "@/hooks/useViewport";
7
+ import type {
8
+ TerminalHandle,
9
+ TerminalScrollState,
10
+ } from "@/components/Terminal";
11
+ import type { Session, Project } from "@/lib/db";
12
+ import { sessionRegistry } from "@/lib/client/session-registry";
13
+ import { cn } from "@/lib/utils";
14
+ import { ConductorPanel } from "@/components/ConductorPanel";
15
+ import { useFileEditor } from "@/hooks/useFileEditor";
16
+ import { MobileTabBar } from "./MobileTabBar";
17
+ import { DesktopTabBar } from "./DesktopTabBar";
18
+ import {
19
+ TerminalSkeleton,
20
+ FileExplorerSkeleton,
21
+ GitPanelSkeleton,
22
+ } from "./PaneSkeletons";
23
+ import {
24
+ Panel as ResizablePanel,
25
+ Group as ResizablePanelGroup,
26
+ Separator as ResizablePanelHandle,
27
+ } from "react-resizable-panels";
28
+ import { GitDrawer } from "@/components/GitDrawer";
29
+ import { ShellDrawer } from "@/components/ShellDrawer";
30
+ import { useSnapshot } from "valtio";
31
+ import { fileOpenStore, fileOpenActions } from "@/stores/fileOpen";
32
+
33
+ // Dynamic imports for client-only components with loading states
34
+ const Terminal = dynamic(
35
+ () => import("@/components/Terminal").then((mod) => mod.Terminal),
36
+ { ssr: false, loading: () => <TerminalSkeleton /> }
37
+ );
38
+
39
+ const FileExplorer = dynamic(
40
+ () => import("@/components/FileExplorer").then((mod) => mod.FileExplorer),
41
+ { ssr: false, loading: () => <FileExplorerSkeleton /> }
42
+ );
43
+
44
+ const GitPanel = dynamic(
45
+ () => import("@/components/GitPanel").then((mod) => mod.GitPanel),
46
+ { ssr: false, loading: () => <GitPanelSkeleton /> }
47
+ );
48
+
49
+ interface PaneProps {
50
+ paneId: string;
51
+ sessions: Session[];
52
+ projects: Project[];
53
+ onRegisterTerminal: (
54
+ paneId: string,
55
+ tabId: string,
56
+ ref: TerminalHandle | null
57
+ ) => void;
58
+ onMenuClick?: () => void;
59
+ onSelectSession?: (sessionId: string) => void;
60
+ onResumeClaudeSession?: (
61
+ sessionId: string,
62
+ cwd: string,
63
+ summary: string,
64
+ projectName: string
65
+ ) => void;
66
+ }
67
+
68
+ type ViewMode = "terminal" | "files" | "git" | "workers";
69
+
70
+ export const Pane = memo(function Pane({
71
+ paneId,
72
+ sessions,
73
+ projects,
74
+ onRegisterTerminal,
75
+ onMenuClick,
76
+ onSelectSession,
77
+ onResumeClaudeSession,
78
+ }: PaneProps) {
79
+ const { isMobile } = useViewport();
80
+ const {
81
+ focusedPaneId,
82
+ canSplit,
83
+ canClose,
84
+ focusPane,
85
+ splitHorizontal,
86
+ splitVertical,
87
+ close,
88
+ getPaneData,
89
+ getActiveTab,
90
+ addTab,
91
+ closeTab,
92
+ switchTab,
93
+ detachSession,
94
+ reattachSession,
95
+ } = usePanes();
96
+
97
+ const [viewMode, setViewMode] = useState<ViewMode>("terminal");
98
+ const [gitDrawerOpen, setGitDrawerOpen] = useState(() => {
99
+ if (typeof window === "undefined") return true;
100
+ const stored = localStorage.getItem("gitDrawerOpen");
101
+ return stored === null ? true : stored === "true";
102
+ });
103
+ const [shellDrawerOpen, setShellDrawerOpen] = useState(() => {
104
+ if (typeof window === "undefined") return false;
105
+ const stored = localStorage.getItem("shellDrawerOpen");
106
+ return stored === "true";
107
+ });
108
+ const terminalRefs = useRef<Map<string, TerminalHandle | null>>(new Map());
109
+ const paneData = getPaneData(paneId);
110
+ const activeTab = getActiveTab(paneId);
111
+
112
+ // Get ref for active terminal
113
+ const terminalRef = activeTab
114
+ ? (terminalRefs.current.get(activeTab.id) ?? null)
115
+ : null;
116
+ const isFocused = focusedPaneId === paneId;
117
+ const session = activeTab
118
+ ? (sessions.find((s) => s.id === activeTab.sessionId) ??
119
+ (activeTab.sessionId
120
+ ? ({
121
+ id: activeTab.sessionId,
122
+ name: activeTab.sessionName || activeTab.sessionId.slice(0, 8),
123
+ working_directory: activeTab.workingDirectory || "~",
124
+ } as Session)
125
+ : null))
126
+ : null;
127
+
128
+ // File editor state - lifted here so it persists across view switches
129
+ const fileEditor = useFileEditor();
130
+
131
+ // Check if this session is a conductor (has workers)
132
+ const workerCount = useMemo(() => {
133
+ if (!session) return 0;
134
+ return sessions.filter((s) => s.conductor_session_id === session.id).length;
135
+ }, [session, sessions]);
136
+
137
+ const isConductor = workerCount > 0;
138
+
139
+ // Get current project and its repositories
140
+ const currentProject = useMemo(() => {
141
+ if (!session?.project_id) return null;
142
+ return projects.find((p) => p.id === session.project_id) || null;
143
+ }, [session?.project_id, projects]);
144
+
145
+ // Type assertion for repositories (projects passed here should have repositories)
146
+ const projectRepositories = useMemo(() => {
147
+ if (!currentProject) return [];
148
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
149
+ return (currentProject as any).repositories || [];
150
+ }, [currentProject]);
151
+
152
+ // Watch for file open requests
153
+ const { request: fileOpenRequest } = useSnapshot(fileOpenStore);
154
+
155
+ // Reset view mode and file editor when session changes
156
+ useEffect(() => {
157
+ setViewMode("terminal");
158
+ fileEditor.reset();
159
+ }, [session?.id]); // eslint-disable-line react-hooks/exhaustive-deps
160
+
161
+ // Persist drawer states
162
+ useEffect(() => {
163
+ localStorage.setItem("gitDrawerOpen", String(gitDrawerOpen));
164
+ }, [gitDrawerOpen]);
165
+
166
+ useEffect(() => {
167
+ localStorage.setItem("shellDrawerOpen", String(shellDrawerOpen));
168
+ }, [shellDrawerOpen]);
169
+
170
+ // Handle file open requests (only if this pane is focused)
171
+ useEffect(() => {
172
+ if (fileOpenRequest && isFocused && session) {
173
+ // Switch to files view
174
+ setViewMode("files");
175
+ // Open the file
176
+ fileEditor.openFile(fileOpenRequest.path);
177
+ // Clear the request
178
+ fileOpenActions.clearRequest();
179
+ // TODO: Scroll to line (requires FileEditor enhancement)
180
+ }
181
+ }, [fileOpenRequest, isFocused, session, fileEditor]);
182
+
183
+ const handleFocus = useCallback(() => {
184
+ focusPane(paneId);
185
+ }, [focusPane, paneId]);
186
+
187
+ const handleDetach = useCallback(() => {
188
+ const cwd = session?.working_directory;
189
+ if (terminalRef) {
190
+ terminalRef.sendInput("\x02d"); // Ctrl+B d to detach
191
+ if (cwd && cwd !== "~") {
192
+ setTimeout(() => terminalRef.sendCommand(`cd ${cwd}`), 200);
193
+ }
194
+ }
195
+ detachSession(paneId);
196
+ }, [detachSession, paneId, terminalRef, session]);
197
+
198
+ const handleReattach = useCallback(() => {
199
+ const tab = activeTab;
200
+ if (tab?.detachedTmux && terminalRef) {
201
+ terminalRef.sendCommand(`tmux attach -t ${tab.detachedTmux}`);
202
+ reattachSession(paneId);
203
+ }
204
+ }, [activeTab, terminalRef, reattachSession, paneId]);
205
+
206
+ // Create ref callback for a specific tab
207
+ const getTerminalRef = useCallback(
208
+ (tabId: string) => (handle: TerminalHandle | null) => {
209
+ if (handle) {
210
+ terminalRefs.current.set(tabId, handle);
211
+ } else {
212
+ terminalRefs.current.delete(tabId);
213
+ }
214
+ },
215
+ []
216
+ );
217
+
218
+ // Create onConnected callback for a specific tab
219
+ const getTerminalConnectedHandler = useCallback(
220
+ (tab: (typeof paneData.tabs)[0]) => () => {
221
+ console.log(
222
+ `[ClaudeDeck] Terminal connected for pane: ${paneId}, tab: ${tab.id}`
223
+ );
224
+ const handle = terminalRefs.current.get(tab.id);
225
+ if (!handle) return;
226
+
227
+ onRegisterTerminal(paneId, tab.id, handle);
228
+
229
+ // Determine tmux session name to attach
230
+ const tmuxName = tab.sessionId
231
+ ? sessions.find((s) => s.id === tab.sessionId)?.tmux_name ||
232
+ tab.attachedTmux
233
+ : tab.attachedTmux;
234
+
235
+ if (tmuxName) {
236
+ setTimeout(() => handle.sendCommand(`tmux attach -t ${tmuxName}`), 100);
237
+ }
238
+ },
239
+ [paneId, sessions, onRegisterTerminal]
240
+ );
241
+
242
+ // Track current tab ID for cleanup
243
+ const activeTabIdRef = useRef<string | null>(null);
244
+ activeTabIdRef.current = activeTab?.id || null;
245
+
246
+ // Cleanup on unmount only
247
+ useEffect(() => {
248
+ console.log(
249
+ `[ClaudeDeck] Pane ${paneId} mounted, activeTab: ${activeTab?.id || "null"}`
250
+ );
251
+ return () => {
252
+ console.log(
253
+ `[ClaudeDeck] Pane ${paneId} unmounting, clearing terminal ref for tab: ${activeTabIdRef.current}`
254
+ );
255
+ if (activeTabIdRef.current) {
256
+ onRegisterTerminal(paneId, activeTabIdRef.current, null);
257
+ }
258
+ };
259
+ // eslint-disable-next-line react-hooks/exhaustive-deps
260
+ }, [paneId, onRegisterTerminal]);
261
+
262
+ // Swipe gesture handling for mobile session switching (terminal view only)
263
+ const touchStartX = useRef<number | null>(null);
264
+ const currentIndex = session
265
+ ? sessions.findIndex((s) => s.id === session.id)
266
+ : -1;
267
+ const SWIPE_THRESHOLD = 120;
268
+
269
+ const handleTouchStart = useCallback(
270
+ (e: React.TouchEvent) => {
271
+ if (viewMode !== "terminal") return;
272
+ touchStartX.current = e.touches[0].clientX;
273
+ },
274
+ [viewMode]
275
+ );
276
+
277
+ const handleTouchEnd = useCallback(
278
+ (e: React.TouchEvent) => {
279
+ if (viewMode !== "terminal" || touchStartX.current === null) return;
280
+
281
+ const diff = e.changedTouches[0].clientX - touchStartX.current;
282
+ touchStartX.current = null;
283
+
284
+ if (Math.abs(diff) <= SWIPE_THRESHOLD) return;
285
+
286
+ const nextIndex = diff > 0 ? currentIndex - 1 : currentIndex + 1;
287
+ if (nextIndex >= 0 && nextIndex < sessions.length) {
288
+ onSelectSession?.(sessions[nextIndex].id);
289
+ }
290
+ },
291
+ [viewMode, currentIndex, sessions, onSelectSession]
292
+ );
293
+
294
+ return (
295
+ <div
296
+ className={cn(
297
+ "flex h-full w-full flex-col overflow-hidden",
298
+ !isMobile && "rounded-lg shadow-lg shadow-black/10 dark:shadow-black/30"
299
+ )}
300
+ onClick={handleFocus}
301
+ >
302
+ {/* Tab Bar - Mobile vs Desktop */}
303
+ {isMobile ? (
304
+ <MobileTabBar
305
+ session={session}
306
+ claudeProjectName={activeTab?.claudeProjectName ?? null}
307
+ viewMode={viewMode}
308
+ isConductor={isConductor}
309
+ workerCount={workerCount}
310
+ onMenuClick={onMenuClick}
311
+ onViewModeChange={setViewMode}
312
+ onResumeClaudeSession={onResumeClaudeSession}
313
+ />
314
+ ) : (
315
+ <DesktopTabBar
316
+ tabs={paneData.tabs}
317
+ activeTabId={paneData.activeTabId}
318
+ session={session}
319
+ sessions={sessions}
320
+ viewMode={viewMode}
321
+ isFocused={isFocused}
322
+ isConductor={isConductor}
323
+ workerCount={workerCount}
324
+ canSplit={canSplit}
325
+ canClose={canClose}
326
+ hasAttachedTmux={!!activeTab?.attachedTmux}
327
+ gitDrawerOpen={gitDrawerOpen}
328
+ shellDrawerOpen={shellDrawerOpen}
329
+ onTabSwitch={(tabId) => switchTab(paneId, tabId)}
330
+ onTabClose={(tabId) => closeTab(paneId, tabId)}
331
+ onTabAdd={() => addTab(paneId)}
332
+ onViewModeChange={setViewMode}
333
+ onGitDrawerToggle={() => setGitDrawerOpen((prev) => !prev)}
334
+ onShellDrawerToggle={() => setShellDrawerOpen((prev) => !prev)}
335
+ onSplitHorizontal={() => splitHorizontal(paneId)}
336
+ onSplitVertical={() => splitVertical(paneId)}
337
+ onClose={() => close(paneId)}
338
+ onDetach={handleDetach}
339
+ onReattach={handleReattach}
340
+ hasDetachedTmux={!!activeTab?.detachedTmux}
341
+ />
342
+ )}
343
+
344
+ {/* Content Area - Mobile: simple flex, Desktop: resizable panels */}
345
+ {isMobile ? (
346
+ <div
347
+ className="relative min-h-0 w-full flex-1"
348
+ onTouchStart={handleTouchStart}
349
+ onTouchEnd={handleTouchEnd}
350
+ >
351
+ {/* Terminals - one per tab */}
352
+ {paneData.tabs.map((tab) => {
353
+ const isActive = tab.id === activeTab?.id;
354
+ const savedState = sessionRegistry.getTerminalState(paneId, tab.id);
355
+
356
+ return (
357
+ <div
358
+ key={tab.id}
359
+ className={
360
+ viewMode === "terminal" && isActive
361
+ ? "h-full w-full"
362
+ : "hidden"
363
+ }
364
+ >
365
+ <Terminal
366
+ ref={getTerminalRef(tab.id)}
367
+ onConnected={getTerminalConnectedHandler(tab)}
368
+ onBeforeUnmount={(scrollState) => {
369
+ sessionRegistry.saveTerminalState(paneId, tab.id, {
370
+ scrollTop: scrollState.scrollTop,
371
+ scrollHeight: 0,
372
+ lastActivity: Date.now(),
373
+ cursorY: scrollState.cursorY,
374
+ });
375
+ }}
376
+ initialScrollState={
377
+ savedState
378
+ ? {
379
+ scrollTop: savedState.scrollTop,
380
+ cursorY: savedState.cursorY,
381
+ baseY: 0,
382
+ }
383
+ : undefined
384
+ }
385
+ />
386
+ </div>
387
+ );
388
+ })}
389
+
390
+ {/* Files */}
391
+ {session?.working_directory && (
392
+ <div className={viewMode === "files" ? "h-full" : "hidden"}>
393
+ <FileExplorer
394
+ workingDirectory={session.working_directory}
395
+ fileEditor={fileEditor}
396
+ />
397
+ </div>
398
+ )}
399
+
400
+ {/* Git - mobile only */}
401
+ {session?.working_directory && (
402
+ <div className={viewMode === "git" ? "h-full" : "hidden"}>
403
+ <GitPanel
404
+ workingDirectory={session.working_directory}
405
+ projectId={currentProject?.id}
406
+ repositories={projectRepositories}
407
+ />
408
+ </div>
409
+ )}
410
+
411
+ {/* Workers */}
412
+ {viewMode === "workers" && session && (
413
+ <ConductorPanel
414
+ conductorSessionId={session.id}
415
+ onAttachToWorker={(workerId) => {
416
+ setViewMode("terminal");
417
+ const worker = sessions.find((s) => s.id === workerId);
418
+ if (worker && terminalRef) {
419
+ const sessionName = `claude-${workerId}`;
420
+ terminalRef.sendInput("\x02d");
421
+ setTimeout(() => {
422
+ terminalRef?.sendInput("\x15");
423
+ setTimeout(() => {
424
+ terminalRef?.sendCommand(`tmux attach -t ${sessionName}`);
425
+ }, 50);
426
+ }, 100);
427
+ }
428
+ }}
429
+ />
430
+ )}
431
+ </div>
432
+ ) : (
433
+ <ResizablePanelGroup
434
+ orientation="horizontal"
435
+ className="min-h-0 flex-1"
436
+ >
437
+ {/* Left column: Main content + Shell drawer */}
438
+ <ResizablePanel defaultSize={gitDrawerOpen ? 70 : 100} minSize={20}>
439
+ <ResizablePanelGroup orientation="vertical" className="h-full">
440
+ {/* Main content */}
441
+ <ResizablePanel
442
+ defaultSize={shellDrawerOpen ? 70 : 100}
443
+ minSize={10}
444
+ >
445
+ <div className="relative h-full">
446
+ {/* Terminals - one per tab */}
447
+ {paneData.tabs.map((tab) => {
448
+ const isActive = tab.id === activeTab?.id;
449
+ const savedState = sessionRegistry.getTerminalState(
450
+ paneId,
451
+ tab.id
452
+ );
453
+
454
+ return (
455
+ <div
456
+ key={tab.id}
457
+ className={
458
+ viewMode === "terminal" && isActive
459
+ ? "h-full"
460
+ : "hidden"
461
+ }
462
+ >
463
+ <Terminal
464
+ ref={getTerminalRef(tab.id)}
465
+ onConnected={getTerminalConnectedHandler(tab)}
466
+ onBeforeUnmount={(scrollState) => {
467
+ sessionRegistry.saveTerminalState(paneId, tab.id, {
468
+ scrollTop: scrollState.scrollTop,
469
+ scrollHeight: 0,
470
+ lastActivity: Date.now(),
471
+ cursorY: scrollState.cursorY,
472
+ });
473
+ }}
474
+ initialScrollState={
475
+ savedState
476
+ ? {
477
+ scrollTop: savedState.scrollTop,
478
+ cursorY: savedState.cursorY,
479
+ baseY: 0,
480
+ }
481
+ : undefined
482
+ }
483
+ />
484
+ </div>
485
+ );
486
+ })}
487
+
488
+ {/* Files */}
489
+ {session?.working_directory && (
490
+ <div className={viewMode === "files" ? "h-full" : "hidden"}>
491
+ <FileExplorer
492
+ workingDirectory={session.working_directory}
493
+ fileEditor={fileEditor}
494
+ />
495
+ </div>
496
+ )}
497
+
498
+ {/* Workers */}
499
+ {viewMode === "workers" && session && (
500
+ <ConductorPanel
501
+ conductorSessionId={session.id}
502
+ onAttachToWorker={(workerId) => {
503
+ setViewMode("terminal");
504
+ const worker = sessions.find((s) => s.id === workerId);
505
+ if (worker && terminalRef) {
506
+ const sessionName = `claude-${workerId}`;
507
+ terminalRef.sendInput("\x02d");
508
+ setTimeout(() => {
509
+ terminalRef?.sendInput("\x15");
510
+ setTimeout(() => {
511
+ terminalRef?.sendCommand(
512
+ `tmux attach -t ${sessionName}`
513
+ );
514
+ }, 50);
515
+ }, 100);
516
+ }
517
+ }}
518
+ />
519
+ )}
520
+ </div>
521
+ </ResizablePanel>
522
+
523
+ {/* Shell drawer - under main content */}
524
+ {shellDrawerOpen && session?.working_directory && (
525
+ <>
526
+ <ResizablePanelHandle className="bg-border/30 hover:bg-primary/30 active:bg-primary/50 h-px cursor-row-resize transition-colors" />
527
+ <ResizablePanel defaultSize={30} minSize={10}>
528
+ <ShellDrawer
529
+ open={true}
530
+ onOpenChange={setShellDrawerOpen}
531
+ workingDirectory={session.working_directory}
532
+ />
533
+ </ResizablePanel>
534
+ </>
535
+ )}
536
+ </ResizablePanelGroup>
537
+ </ResizablePanel>
538
+
539
+ {/* Git drawer - right side, full height */}
540
+ {gitDrawerOpen && session?.working_directory && (
541
+ <>
542
+ <ResizablePanelHandle className="bg-border/30 hover:bg-primary/30 active:bg-primary/50 w-px cursor-col-resize transition-colors" />
543
+ <ResizablePanel defaultSize={30} minSize={10}>
544
+ <GitDrawer
545
+ open={true}
546
+ onOpenChange={setGitDrawerOpen}
547
+ workingDirectory={session.working_directory}
548
+ projectId={currentProject?.id}
549
+ repositories={projectRepositories}
550
+ />
551
+ </ResizablePanel>
552
+ </>
553
+ )}
554
+ </ResizablePanelGroup>
555
+ )}
556
+ </div>
557
+ );
558
+ });
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { Fragment } from "react";
4
+ import { Panel, Group, Separator } from "react-resizable-panels";
5
+ import type { PaneLayout as PaneLayoutType } from "@/lib/panes";
6
+ import { usePanes } from "@/contexts/PaneContext";
7
+
8
+ interface PaneLayoutProps {
9
+ layout: PaneLayoutType;
10
+ renderPane: (paneId: string) => React.ReactNode;
11
+ }
12
+
13
+ function LayoutRenderer({ layout, renderPane }: PaneLayoutProps) {
14
+ if (layout.type === "leaf") {
15
+ return <>{renderPane(layout.paneId)}</>;
16
+ }
17
+
18
+ const orientation = layout.direction;
19
+
20
+ return (
21
+ <Group orientation={orientation} className="h-full">
22
+ {layout.children.map((child, index) => (
23
+ <Fragment key={child.type === "leaf" ? child.paneId : index}>
24
+ <Panel
25
+ defaultSize={layout.sizes[index]}
26
+ minSize={15}
27
+ className="h-full"
28
+ >
29
+ <LayoutRenderer layout={child} renderPane={renderPane} />
30
+ </Panel>
31
+ {index < layout.children.length - 1 && (
32
+ <Separator
33
+ className={` ${orientation === "horizontal" ? "w-0.5 cursor-col-resize" : "h-0.5 cursor-row-resize"} bg-border hover:bg-primary/40 active:bg-primary/60 rounded-full transition-colors`}
34
+ />
35
+ )}
36
+ </Fragment>
37
+ ))}
38
+ </Group>
39
+ );
40
+ }
41
+
42
+ export function PaneLayout({
43
+ renderPane,
44
+ }: {
45
+ renderPane: (paneId: string) => React.ReactNode;
46
+ }) {
47
+ const { state, isMobile, focusedPaneId } = usePanes();
48
+
49
+ // On mobile: only render the focused pane (single pane mode)
50
+ if (isMobile) {
51
+ return <div className="h-full w-full">{renderPane(focusedPaneId)}</div>;
52
+ }
53
+
54
+ // On desktop: render full layout tree with splits
55
+ return (
56
+ <div className="h-full w-full">
57
+ <LayoutRenderer layout={state.layout} renderPane={renderPane} />
58
+ </div>
59
+ );
60
+ }