@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
package/app/page.tsx ADDED
@@ -0,0 +1,629 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback, useRef } from "react";
4
+
5
+ // Debug log buffer - persists even if console is closed
6
+ const debugLogs: string[] = [];
7
+ const MAX_DEBUG_LOGS = 100;
8
+
9
+ function debugLog(message: string) {
10
+ const timestamp = new Date().toISOString().split("T")[1].slice(0, 12);
11
+ const entry = `[${timestamp}] ${message}`;
12
+ debugLogs.push(entry);
13
+ if (debugLogs.length > MAX_DEBUG_LOGS) debugLogs.shift();
14
+ console.log(`[ClaudeDeck] ${message}`);
15
+ }
16
+
17
+ // Expose to window for debugging
18
+ if (typeof window !== "undefined") {
19
+ (window as unknown as { agentOSLogs: () => void }).agentOSLogs = () => {
20
+ console.log("=== ClaudeDeck Debug Logs ===");
21
+ debugLogs.forEach((log) => console.log(log));
22
+ console.log("=== End Logs ===");
23
+ };
24
+ }
25
+ import { PaneProvider, usePanes } from "@/contexts/PaneContext";
26
+ import { Pane } from "@/components/Pane";
27
+ import { useNotifications } from "@/hooks/useNotifications";
28
+ import { useViewport } from "@/hooks/useViewport";
29
+ import { useViewportHeight } from "@/hooks/useViewportHeight";
30
+ import { useSessions } from "@/hooks/useSessions";
31
+ import { useProjects } from "@/hooks/useProjects";
32
+ import { useDevServersManager } from "@/hooks/useDevServersManager";
33
+ import { useSessionStatuses } from "@/hooks/useSessionStatuses";
34
+ import type { Session } from "@/lib/db";
35
+ import type { TerminalHandle } from "@/components/Terminal";
36
+ import { getProvider } from "@/lib/providers";
37
+ import { DesktopView } from "@/components/views/DesktopView";
38
+ import { MobileView } from "@/components/views/MobileView";
39
+ import { getPendingPrompt, clearPendingPrompt } from "@/stores/initialPrompt";
40
+ import { NewClaudeSessionDialog } from "@/components/NewClaudeSessionDialog";
41
+ import { useClaudeProjectsQuery } from "@/data/claude";
42
+
43
+ function HomeContent() {
44
+ // UI State
45
+ const [sidebarOpen, setSidebarOpen] = useState(true);
46
+ const [showNewSessionDialog, setShowNewSessionDialog] = useState(false);
47
+ const [newSessionProjectId, setNewSessionProjectId] = useState<string | null>(
48
+ null
49
+ );
50
+ const [showNotificationSettings, setShowNotificationSettings] =
51
+ useState(false);
52
+ const [showQuickSwitcher, setShowQuickSwitcher] = useState(false);
53
+ const [copiedSessionId, setCopiedSessionId] = useState(false);
54
+ const terminalRefs = useRef<Map<string, TerminalHandle>>(new Map());
55
+
56
+ // Pane context
57
+ const { focusedPaneId, attachSession, getActiveTab, addTab } = usePanes();
58
+ const focusedActiveTab = getActiveTab(focusedPaneId);
59
+ const { isMobile, isHydrated } = useViewport();
60
+
61
+ // Data hooks
62
+ const { sessions, fetchSessions } = useSessions();
63
+ const { projects, fetchProjects } = useProjects();
64
+ const { data: claudeProjects } = useClaudeProjectsQuery();
65
+ const {
66
+ startDevServerProjectId,
67
+ setStartDevServerProjectId,
68
+ startDevServer,
69
+ createDevServer,
70
+ } = useDevServersManager();
71
+
72
+ // Helper to get init script command from API
73
+ const getInitScriptCommand = useCallback(
74
+ async (agentCommand: string): Promise<string> => {
75
+ try {
76
+ const res = await fetch("/api/sessions/init-script", {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify({ agentCommand }),
80
+ });
81
+ const data = await res.json();
82
+ return data.command || agentCommand;
83
+ } catch {
84
+ return agentCommand;
85
+ }
86
+ },
87
+ []
88
+ );
89
+
90
+ // Set CSS variable for viewport height (handles mobile keyboard)
91
+ useViewportHeight();
92
+
93
+ // Terminal ref management
94
+ const registerTerminalRef = useCallback(
95
+ (paneId: string, tabId: string, ref: TerminalHandle | null) => {
96
+ const key = `${paneId}:${tabId}`;
97
+ if (ref) {
98
+ terminalRefs.current.set(key, ref);
99
+ debugLog(
100
+ `Terminal registered: ${key}, total refs: ${terminalRefs.current.size}`
101
+ );
102
+ } else {
103
+ terminalRefs.current.delete(key);
104
+ debugLog(
105
+ `Terminal unregistered: ${key}, total refs: ${terminalRefs.current.size}`
106
+ );
107
+ }
108
+ },
109
+ []
110
+ );
111
+
112
+ // Get terminal for a pane, with fallback to first available
113
+ const getTerminalWithFallback = useCallback(():
114
+ | { terminal: TerminalHandle; paneId: string; tabId: string }
115
+ | undefined => {
116
+ debugLog(
117
+ `getTerminalWithFallback called, total refs: ${terminalRefs.current.size}, focusedPaneId: ${focusedPaneId}`
118
+ );
119
+
120
+ // Try focused pane first
121
+ const activeTab = getActiveTab(focusedPaneId);
122
+ debugLog(`activeTab for focused pane: ${activeTab?.id || "null"}`);
123
+
124
+ if (activeTab) {
125
+ const key = `${focusedPaneId}:${activeTab.id}`;
126
+ const terminal = terminalRefs.current.get(key);
127
+ debugLog(
128
+ `Looking for terminal at key "${key}": ${terminal ? "found" : "not found"}`
129
+ );
130
+ if (terminal) {
131
+ return { terminal, paneId: focusedPaneId, tabId: activeTab.id };
132
+ }
133
+ }
134
+
135
+ // Fallback to first available terminal
136
+ const firstEntry = terminalRefs.current.entries().next().value;
137
+ if (firstEntry) {
138
+ const [key, terminal] = firstEntry as [string, TerminalHandle];
139
+ const [paneId, tabId] = key.split(":");
140
+ debugLog(`Using fallback terminal: ${key}`);
141
+ return { terminal, paneId, tabId };
142
+ }
143
+
144
+ debugLog(
145
+ `NO TERMINAL FOUND. Available keys: ${Array.from(terminalRefs.current.keys()).join(", ") || "none"}`
146
+ );
147
+ return undefined;
148
+ }, [focusedPaneId, getActiveTab]);
149
+
150
+ // Build tmux command for a session
151
+ const buildSessionCommand = useCallback(
152
+ async (
153
+ session: Session
154
+ ): Promise<{ sessionName: string; cwd: string; command: string }> => {
155
+ const provider = getProvider(session.agent_type || "claude");
156
+ const sessionName = session.tmux_name || `${provider.id}-${session.id}`;
157
+ const cwd = session.working_directory?.replace("~", "$HOME") || "$HOME";
158
+
159
+ // TODO: Add explicit "Enable Orchestration" toggle that creates .mcp.json
160
+ // for conductor sessions. Removed auto-creation because it pollutes projects
161
+ // with .mcp.json files that aren't in their .gitignore.
162
+ // See: /api/sessions/[id]/mcp-config, lib/mcp-config.ts
163
+
164
+ // Get parent session ID for forking
165
+ let parentSessionId: string | null = null;
166
+ if (!session.claude_session_id && session.parent_session_id) {
167
+ const parentSession = sessions.find(
168
+ (s) => s.id === session.parent_session_id
169
+ );
170
+ parentSessionId = parentSession?.claude_session_id || null;
171
+ }
172
+
173
+ // Check for pending initial prompt
174
+ const initialPrompt = getPendingPrompt(session.id);
175
+ if (initialPrompt) {
176
+ clearPendingPrompt(session.id);
177
+ }
178
+
179
+ const flags = provider.buildFlags({
180
+ sessionId: session.claude_session_id,
181
+ parentSessionId,
182
+ autoApprove: session.auto_approve,
183
+ model: session.model,
184
+ initialPrompt: initialPrompt || undefined,
185
+ });
186
+ const flagsStr = flags.join(" ");
187
+
188
+ const agentCmd = `${provider.command} ${flagsStr}`;
189
+ const command = await getInitScriptCommand(agentCmd);
190
+
191
+ return { sessionName, cwd, command };
192
+ },
193
+ [sessions, getInitScriptCommand]
194
+ );
195
+
196
+ // Attach a session to a terminal
197
+ const runSessionInTerminal = useCallback(
198
+ (
199
+ terminal: TerminalHandle,
200
+ paneId: string,
201
+ session: Session,
202
+ sessionInfo: { sessionName: string; cwd: string; command: string }
203
+ ) => {
204
+ const { sessionName, cwd, command } = sessionInfo;
205
+ const tmuxNew = command
206
+ ? `tmux new -s ${sessionName} -c "${cwd}" "${command}"`
207
+ : `tmux new -s ${sessionName} -c "${cwd}"`;
208
+ terminal.sendCommand(
209
+ `tmux set -g mouse on 2>/dev/null; tmux set -g set-clipboard on 2>/dev/null; tmux attach -t ${sessionName} 2>/dev/null || ${tmuxNew}`
210
+ );
211
+ attachSession(paneId, session.id, sessionName);
212
+ terminal.focus();
213
+ },
214
+ [attachSession]
215
+ );
216
+
217
+ // Attach session to terminal
218
+ const attachToSession = useCallback(
219
+ async (session: Session) => {
220
+ const terminalInfo = getTerminalWithFallback();
221
+ if (!terminalInfo) {
222
+ debugLog(
223
+ `ERROR: No terminal available to attach session: ${session.name}`
224
+ );
225
+ alert(
226
+ `[ClaudeDeck Debug] No terminal available!\n\nRun agentOSLogs() in console to see debug logs.`
227
+ );
228
+ return;
229
+ }
230
+
231
+ const { terminal, paneId } = terminalInfo;
232
+ const activeTab = getActiveTab(paneId);
233
+ const isInTmux = !!activeTab?.attachedTmux;
234
+
235
+ if (isInTmux) {
236
+ terminal.sendInput("\x02d");
237
+ }
238
+
239
+ setTimeout(
240
+ () => {
241
+ terminal.sendInput("\x03");
242
+ setTimeout(async () => {
243
+ const sessionInfo = await buildSessionCommand(session);
244
+ runSessionInTerminal(terminal, paneId, session, sessionInfo);
245
+ }, 50);
246
+ },
247
+ isInTmux ? 100 : 0
248
+ );
249
+ },
250
+ [
251
+ getTerminalWithFallback,
252
+ getActiveTab,
253
+ buildSessionCommand,
254
+ runSessionInTerminal,
255
+ ]
256
+ );
257
+
258
+ // Open session in new tab
259
+ const openSessionInNewTab = useCallback(
260
+ (session: Session) => {
261
+ const existingKeys = new Set(terminalRefs.current.keys());
262
+ addTab(focusedPaneId);
263
+
264
+ let attempts = 0;
265
+ const maxAttempts = 20;
266
+
267
+ const waitForNewTerminal = () => {
268
+ attempts++;
269
+
270
+ for (const key of terminalRefs.current.keys()) {
271
+ if (!existingKeys.has(key) && key.startsWith(`${focusedPaneId}:`)) {
272
+ const terminal = terminalRefs.current.get(key);
273
+ if (terminal) {
274
+ buildSessionCommand(session).then((sessionInfo) => {
275
+ runSessionInTerminal(
276
+ terminal,
277
+ focusedPaneId,
278
+ session,
279
+ sessionInfo
280
+ );
281
+ });
282
+ return;
283
+ }
284
+ }
285
+ }
286
+
287
+ if (attempts < maxAttempts) {
288
+ setTimeout(waitForNewTerminal, 50);
289
+ } else {
290
+ debugLog(`Failed to find new terminal after ${maxAttempts} attempts`);
291
+ }
292
+ };
293
+
294
+ setTimeout(waitForNewTerminal, 50);
295
+ },
296
+ [addTab, focusedPaneId, buildSessionCommand, runSessionInTerminal]
297
+ );
298
+
299
+ const resumeClaudeSession = useCallback(
300
+ (
301
+ claudeSessionId: string,
302
+ cwd: string,
303
+ sessionName?: string,
304
+ projectName?: string
305
+ ) => {
306
+ const terminalInfo = getTerminalWithFallback();
307
+ if (!terminalInfo) return;
308
+
309
+ const { terminal, paneId } = terminalInfo;
310
+ const activeTab = getActiveTab(paneId);
311
+ const isInTmux = !!activeTab?.attachedTmux;
312
+
313
+ const tmuxName = `claude-${claudeSessionId}`;
314
+ const tmuxCmd = [
315
+ `tmux kill-session -t ${tmuxName} 2>/dev/null;`,
316
+ `tmux new -s ${tmuxName} -c "${cwd}" "claude --resume ${claudeSessionId} || claude --continue"`,
317
+ ].join(" ");
318
+
319
+ if (isInTmux) {
320
+ terminal.sendInput("\x02d");
321
+ }
322
+
323
+ setTimeout(
324
+ () => {
325
+ terminal.sendInput("\x03");
326
+ setTimeout(() => {
327
+ terminal.sendCommand(tmuxCmd);
328
+ attachSession(
329
+ paneId,
330
+ claudeSessionId,
331
+ tmuxName,
332
+ sessionName,
333
+ projectName,
334
+ cwd
335
+ );
336
+ terminal.focus();
337
+ }, 50);
338
+ },
339
+ isInTmux ? 100 : 0
340
+ );
341
+ },
342
+ [getTerminalWithFallback, getActiveTab, attachSession]
343
+ );
344
+
345
+ const [newSessionPending, setNewSessionPending] = useState<{
346
+ cwd: string;
347
+ projectName: string;
348
+ } | null>(null);
349
+
350
+ const newClaudeSession = useCallback((cwd?: string, projectName?: string) => {
351
+ setNewSessionPending({ cwd: cwd || "~", projectName: projectName || "" });
352
+ }, []);
353
+
354
+ const handleNewClaudeSessionConfirm = useCallback(
355
+ (name: string, overrideCwd?: string, overrideProject?: string) => {
356
+ if (!newSessionPending) return;
357
+ setNewSessionPending(null);
358
+
359
+ const terminalInfo = getTerminalWithFallback();
360
+ if (!terminalInfo) return;
361
+
362
+ const { terminal, paneId } = terminalInfo;
363
+ const activeTab = getActiveTab(paneId);
364
+ const isInTmux = !!activeTab?.attachedTmux;
365
+ const id = Math.random().toString(36).slice(2) + Date.now().toString(36);
366
+ const tmuxName = `claude-new-${id}`;
367
+ const cwd = overrideCwd || newSessionPending.cwd;
368
+ const projectName = overrideProject || newSessionPending.projectName;
369
+ const tmuxCmd = `tmux new -s ${tmuxName} -c "${cwd}" "claude"`;
370
+
371
+ if (isInTmux) {
372
+ terminal.sendInput("\x02d");
373
+ }
374
+
375
+ setTimeout(
376
+ () => {
377
+ terminal.sendInput("\x03");
378
+ setTimeout(() => {
379
+ terminal.sendCommand(tmuxCmd);
380
+ attachSession(paneId, id, tmuxName, name, projectName, cwd);
381
+ terminal.focus();
382
+ }, 50);
383
+ },
384
+ isInTmux ? 100 : 0
385
+ );
386
+
387
+ if (isMobile) setSidebarOpen(false);
388
+ },
389
+ [
390
+ newSessionPending,
391
+ getTerminalWithFallback,
392
+ getActiveTab,
393
+ attachSession,
394
+ isMobile,
395
+ ]
396
+ );
397
+
398
+ // Notification click handler
399
+ const handleNotificationClick = useCallback(
400
+ (sessionId: string) => {
401
+ const session = sessions.find((s) => s.id === sessionId);
402
+ if (session) {
403
+ attachToSession(session);
404
+ }
405
+ },
406
+ [sessions, attachToSession]
407
+ );
408
+
409
+ // Notifications
410
+ const {
411
+ settings: notificationSettings,
412
+ checkStateChanges,
413
+ updateSettings,
414
+ requestPermission,
415
+ permissionGranted,
416
+ } = useNotifications({ onSessionClick: handleNotificationClick });
417
+
418
+ // Session statuses
419
+ const { sessionStatuses } = useSessionStatuses({
420
+ sessions,
421
+ activeSessionId: focusedActiveTab?.sessionId,
422
+ checkStateChanges,
423
+ });
424
+
425
+ // Set initial sidebar state based on viewport (only after hydration)
426
+ useEffect(() => {
427
+ if (isHydrated && !isMobile) setSidebarOpen(true);
428
+ }, [isMobile, isHydrated]);
429
+
430
+ // Keyboard shortcut: Cmd+K to open quick switcher
431
+ useEffect(() => {
432
+ const handleKeyDown = (e: KeyboardEvent) => {
433
+ if ((e.metaKey || e.ctrlKey) && e.key === "k") {
434
+ e.preventDefault();
435
+ setShowQuickSwitcher(true);
436
+ }
437
+ };
438
+ window.addEventListener("keydown", handleKeyDown);
439
+ return () => window.removeEventListener("keydown", handleKeyDown);
440
+ }, []);
441
+
442
+ // Session selection handler
443
+ const handleSelectSession = useCallback(
444
+ (sessionId: string) => {
445
+ debugLog(`handleSelectSession called for: ${sessionId}`);
446
+ const session = sessions.find((s) => s.id === sessionId);
447
+ if (session) {
448
+ debugLog(`Found session: ${session.name}, calling attachToSession`);
449
+ attachToSession(session);
450
+ } else {
451
+ debugLog(
452
+ `Session not found in sessions array (length: ${sessions.length})`
453
+ );
454
+ }
455
+ },
456
+ [sessions, attachToSession]
457
+ );
458
+
459
+ // Pane renderer
460
+ const renderPane = useCallback(
461
+ (paneId: string) => (
462
+ <Pane
463
+ key={paneId}
464
+ paneId={paneId}
465
+ sessions={sessions}
466
+ projects={projects}
467
+ onRegisterTerminal={registerTerminalRef}
468
+ onMenuClick={isMobile ? () => setSidebarOpen(true) : undefined}
469
+ onSelectSession={handleSelectSession}
470
+ onResumeClaudeSession={resumeClaudeSession}
471
+ />
472
+ ),
473
+ [
474
+ sessions,
475
+ projects,
476
+ registerTerminalRef,
477
+ isMobile,
478
+ handleSelectSession,
479
+ resumeClaudeSession,
480
+ ]
481
+ );
482
+
483
+ // New session in project handler
484
+ const handleNewSessionInProject = useCallback((projectId: string) => {
485
+ setNewSessionProjectId(projectId);
486
+ setShowNewSessionDialog(true);
487
+ }, []);
488
+
489
+ // Session created handler (shared between desktop/mobile)
490
+ const handleSessionCreated = useCallback(
491
+ async (sessionId: string) => {
492
+ setShowNewSessionDialog(false);
493
+ setNewSessionProjectId(null);
494
+ await fetchSessions();
495
+
496
+ const res = await fetch(`/api/sessions/${sessionId}`);
497
+ const data = await res.json();
498
+ if (!data.session) return;
499
+
500
+ setTimeout(() => attachToSession(data.session), 100);
501
+ },
502
+ [fetchSessions, attachToSession]
503
+ );
504
+
505
+ // Project created handler (shared between desktop/mobile)
506
+ const handleCreateProject = useCallback(
507
+ async (
508
+ name: string,
509
+ workingDirectory: string,
510
+ agentType?: string
511
+ ): Promise<string | null> => {
512
+ const res = await fetch("/api/projects", {
513
+ method: "POST",
514
+ headers: { "Content-Type": "application/json" },
515
+ body: JSON.stringify({ name, workingDirectory, agentType }),
516
+ });
517
+ const data = await res.json();
518
+ if (data.project) {
519
+ await fetchProjects();
520
+ return data.project.id;
521
+ }
522
+ return null;
523
+ },
524
+ [fetchProjects]
525
+ );
526
+
527
+ // Open terminal in project handler (shell session, not AI agent)
528
+ const handleOpenTerminal = useCallback(
529
+ async (projectId: string) => {
530
+ const project = projects.find((p) => p.id === projectId);
531
+ if (!project) return;
532
+
533
+ // Create a shell session with the project's working directory
534
+ const res = await fetch("/api/sessions", {
535
+ method: "POST",
536
+ headers: { "Content-Type": "application/json" },
537
+ body: JSON.stringify({
538
+ name: `${project.name} Terminal`,
539
+ workingDirectory: project.working_directory || "~",
540
+ agentType: "shell",
541
+ projectId,
542
+ }),
543
+ });
544
+
545
+ const data = await res.json();
546
+ if (!data.session) return;
547
+
548
+ await fetchSessions();
549
+
550
+ // Small delay to ensure state updates, then attach
551
+ setTimeout(() => {
552
+ attachToSession(data.session);
553
+ }, 100);
554
+ },
555
+ [projects, fetchSessions, attachToSession]
556
+ );
557
+
558
+ // Active session and dev server project
559
+ const activeSession = sessions.find(
560
+ (s) => s.id === focusedActiveTab?.sessionId
561
+ );
562
+ const startDevServerProject = startDevServerProjectId
563
+ ? (projects.find((p) => p.id === startDevServerProjectId) ?? null)
564
+ : null;
565
+
566
+ // View props
567
+ const viewProps = {
568
+ sessions,
569
+ projects,
570
+ sessionStatuses,
571
+ sidebarOpen,
572
+ setSidebarOpen,
573
+ activeSession,
574
+ focusedActiveTab,
575
+ copiedSessionId,
576
+ setCopiedSessionId,
577
+ showNewSessionDialog,
578
+ setShowNewSessionDialog,
579
+ newSessionProjectId,
580
+ showNotificationSettings,
581
+ setShowNotificationSettings,
582
+ showQuickSwitcher,
583
+ setShowQuickSwitcher,
584
+ notificationSettings,
585
+ permissionGranted,
586
+ updateSettings,
587
+ requestPermission,
588
+ attachToSession,
589
+ openSessionInNewTab,
590
+ handleNewSessionInProject,
591
+ handleOpenTerminal,
592
+ handleSessionCreated,
593
+ handleCreateProject,
594
+ handleStartDevServer: startDevServer,
595
+ handleCreateDevServer: createDevServer,
596
+ startDevServerProject,
597
+ setStartDevServerProjectId,
598
+ newClaudeSession,
599
+ resumeClaudeSession,
600
+ renderPane,
601
+ };
602
+
603
+ const view = isMobile ? (
604
+ <MobileView {...viewProps} />
605
+ ) : (
606
+ <DesktopView {...viewProps} />
607
+ );
608
+
609
+ return (
610
+ <>
611
+ {view}
612
+ <NewClaudeSessionDialog
613
+ open={!!newSessionPending}
614
+ projectName={newSessionPending?.projectName || ""}
615
+ projects={claudeProjects}
616
+ onClose={() => setNewSessionPending(null)}
617
+ onConfirm={handleNewClaudeSessionConfirm}
618
+ />
619
+ </>
620
+ );
621
+ }
622
+
623
+ export default function Home() {
624
+ return (
625
+ <PaneProvider>
626
+ <HomeContent />
627
+ </PaneProvider>
628
+ );
629
+ }
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { User, Bot } from "lucide-react";
5
+
6
+ interface ChatMessageProps {
7
+ role: "user" | "assistant";
8
+ content: string;
9
+ timestamp: string;
10
+ isStreaming?: boolean;
11
+ }
12
+
13
+ export function ChatMessage({
14
+ role,
15
+ content,
16
+ timestamp,
17
+ isStreaming,
18
+ }: ChatMessageProps) {
19
+ const isUser = role === "user";
20
+
21
+ return (
22
+ <div
23
+ className={cn(
24
+ "flex gap-3 rounded-lg p-4",
25
+ isUser ? "bg-muted/50" : "bg-card"
26
+ )}
27
+ >
28
+ <div
29
+ className={cn(
30
+ "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full",
31
+ isUser ? "bg-primary" : "bg-muted"
32
+ )}
33
+ >
34
+ {isUser ? (
35
+ <User className="text-primary-foreground h-4 w-4" />
36
+ ) : (
37
+ <Bot className="text-muted-foreground h-4 w-4" />
38
+ )}
39
+ </div>
40
+
41
+ <div className="min-w-0 flex-1">
42
+ <div className="mb-1 flex items-center gap-2">
43
+ <span className="text-sm font-medium">
44
+ {isUser ? "You" : "Claude"}
45
+ </span>
46
+ <span className="text-muted-foreground text-xs">
47
+ {new Date(timestamp).toLocaleTimeString()}
48
+ </span>
49
+ {isStreaming && (
50
+ <span className="text-primary animate-pulse text-xs">
51
+ streaming...
52
+ </span>
53
+ )}
54
+ </div>
55
+
56
+ <div className="text-sm break-words whitespace-pre-wrap">
57
+ {content}
58
+ {isStreaming && (
59
+ <span className="bg-primary ml-0.5 inline-block h-4 w-2 animate-pulse" />
60
+ )}
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }