@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,320 @@
1
+ "use client";
2
+
3
+ import {
4
+ useRef,
5
+ forwardRef,
6
+ useImperativeHandle,
7
+ useCallback,
8
+ useState,
9
+ useMemo,
10
+ } from "react";
11
+ import { useTheme } from "next-themes";
12
+ import "@xterm/xterm/css/xterm.css";
13
+ import { Paperclip, WifiOff, Upload, Loader2 } from "lucide-react";
14
+ import { cn } from "@/lib/utils";
15
+ import { SearchBar } from "./SearchBar";
16
+ import { ScrollToBottomButton } from "./ScrollToBottomButton";
17
+ import { TerminalToolbar } from "./TerminalToolbar";
18
+ import { useTerminalConnection, useTerminalSearch } from "./hooks";
19
+ import type { TerminalScrollState } from "./hooks";
20
+ import { useViewport } from "@/hooks/useViewport";
21
+ import { useFileDrop } from "@/hooks/useFileDrop";
22
+ import { uploadFileToTemp } from "@/lib/file-upload";
23
+ import { FilePicker } from "@/components/FilePicker";
24
+
25
+ export type { TerminalScrollState };
26
+
27
+ export interface TerminalHandle {
28
+ sendCommand: (command: string) => void;
29
+ sendInput: (data: string) => void;
30
+ focus: () => void;
31
+ getScrollState: () => TerminalScrollState | null;
32
+ restoreScrollState: (state: TerminalScrollState) => void;
33
+ }
34
+
35
+ interface TerminalProps {
36
+ onConnected?: () => void;
37
+ onDisconnected?: () => void;
38
+ onBeforeUnmount?: (scrollState: TerminalScrollState) => void;
39
+ initialScrollState?: TerminalScrollState;
40
+ /** Show image picker button (default: true) */
41
+ showImageButton?: boolean;
42
+ }
43
+
44
+ export const Terminal = forwardRef<TerminalHandle, TerminalProps>(
45
+ function Terminal(
46
+ {
47
+ onConnected,
48
+ onDisconnected,
49
+ onBeforeUnmount,
50
+ initialScrollState,
51
+ showImageButton = true,
52
+ },
53
+ ref
54
+ ) {
55
+ const terminalRef = useRef<HTMLDivElement>(null);
56
+ const containerRef = useRef<HTMLDivElement>(null);
57
+ const { isMobile } = useViewport();
58
+ const { theme: currentTheme, resolvedTheme } = useTheme();
59
+ const [showFilePicker, setShowFilePicker] = useState(false);
60
+ const [selectMode, setSelectMode] = useState(false);
61
+ const [isUploading, setIsUploading] = useState(false);
62
+
63
+ // Use the full theme string (e.g., "dark-purple") for terminal theming
64
+ const terminalTheme = useMemo(() => {
65
+ // For system theme, use the resolved theme
66
+ if (currentTheme === "system") {
67
+ return resolvedTheme || "dark";
68
+ }
69
+ return currentTheme || "dark";
70
+ }, [currentTheme, resolvedTheme]);
71
+
72
+ const {
73
+ connectionState,
74
+ isAtBottom,
75
+ xtermRef,
76
+ searchAddonRef,
77
+ scrollToBottom,
78
+ copySelection,
79
+ sendInput,
80
+ sendCommand,
81
+ focus,
82
+ getScrollState,
83
+ restoreScrollState,
84
+ reconnect,
85
+ } = useTerminalConnection({
86
+ terminalRef,
87
+ onConnected,
88
+ onDisconnected,
89
+ onBeforeUnmount,
90
+ initialScrollState,
91
+ isMobile,
92
+ theme: terminalTheme,
93
+ selectMode,
94
+ });
95
+
96
+ const {
97
+ searchVisible,
98
+ searchQuery,
99
+ setSearchQuery,
100
+ searchInputRef,
101
+ closeSearch,
102
+ findNext,
103
+ findPrevious,
104
+ } = useTerminalSearch(searchAddonRef, xtermRef);
105
+
106
+ // Handle image selection - paste file path into terminal
107
+ const handleImageSelect = useCallback(
108
+ (filePath: string) => {
109
+ sendInput(filePath);
110
+ setShowFilePicker(false);
111
+ focus();
112
+ },
113
+ [sendInput, focus]
114
+ );
115
+
116
+ // Handle file drop - upload and insert path into terminal
117
+ const handleFileDrop = useCallback(
118
+ async (file: File) => {
119
+ setIsUploading(true);
120
+ try {
121
+ const path = await uploadFileToTemp(file);
122
+ if (path) {
123
+ sendInput(path);
124
+ focus();
125
+ }
126
+ } catch (err) {
127
+ console.error("Failed to upload file:", err);
128
+ } finally {
129
+ setIsUploading(false);
130
+ }
131
+ },
132
+ [sendInput, focus]
133
+ );
134
+
135
+ // Drag and drop for file uploads
136
+ const { isDragging, dragHandlers } = useFileDrop(
137
+ containerRef,
138
+ handleFileDrop,
139
+ { disabled: isUploading || showFilePicker }
140
+ );
141
+
142
+ // Expose imperative methods
143
+ useImperativeHandle(ref, () => ({
144
+ sendCommand,
145
+ sendInput,
146
+ focus,
147
+ getScrollState,
148
+ restoreScrollState,
149
+ }));
150
+
151
+ // Extract terminal text for select mode overlay
152
+ const terminalText = useMemo(() => {
153
+ if (!selectMode || !xtermRef.current) return "";
154
+
155
+ const term = xtermRef.current;
156
+ const buffer = term.buffer.active;
157
+ const startRow = Math.max(0, buffer.baseY - 500);
158
+ const endRow = buffer.baseY + term.rows;
159
+ const lines: string[] = [];
160
+
161
+ for (let i = startRow; i < endRow; i++) {
162
+ const line = buffer.getLine(i);
163
+ if (line) lines.push(line.translateToString(true));
164
+ }
165
+
166
+ return lines.join("\n");
167
+ }, [selectMode, xtermRef]);
168
+
169
+ return (
170
+ <div
171
+ ref={containerRef}
172
+ className="bg-background flex flex-col overflow-hidden"
173
+ style={{
174
+ position: "relative",
175
+ width: "100%",
176
+ height: "100%",
177
+ }}
178
+ {...dragHandlers}
179
+ >
180
+ {/* Search Bar */}
181
+ <SearchBar
182
+ ref={searchInputRef}
183
+ visible={searchVisible}
184
+ query={searchQuery}
185
+ onQueryChange={setSearchQuery}
186
+ onFindNext={findNext}
187
+ onFindPrevious={findPrevious}
188
+ onClose={closeSearch}
189
+ />
190
+
191
+ {/* Terminal container - NO padding! FitAddon reads offsetHeight which includes padding */}
192
+ <div
193
+ ref={terminalRef}
194
+ className={cn(
195
+ "terminal-container min-h-0 w-full flex-1 overflow-hidden",
196
+ selectMode && "ring-primary ring-2 ring-inset",
197
+ isDragging && "ring-primary ring-2 ring-inset"
198
+ )}
199
+ onClick={focus}
200
+ onTouchStart={selectMode ? (e) => e.stopPropagation() : undefined}
201
+ onTouchEnd={selectMode ? (e) => e.stopPropagation() : undefined}
202
+ />
203
+
204
+ {/* Select mode overlay - shows terminal text in a selectable format */}
205
+ {selectMode && (
206
+ <div
207
+ className="bg-background absolute inset-0 z-40 flex flex-col"
208
+ onTouchStart={(e) => e.stopPropagation()}
209
+ onTouchEnd={(e) => e.stopPropagation()}
210
+ >
211
+ <div className="bg-primary text-primary-foreground flex items-center justify-between px-3 py-2 text-xs font-medium">
212
+ <span>Select text below, then tap Copy</span>
213
+ <button
214
+ onClick={() => setSelectMode(false)}
215
+ className="bg-primary-foreground/20 rounded px-2 py-0.5 text-xs"
216
+ >
217
+ Done
218
+ </button>
219
+ </div>
220
+ <pre
221
+ className="flex-1 overflow-auto p-3 font-mono text-xs break-all whitespace-pre-wrap select-text"
222
+ style={{
223
+ userSelect: "text",
224
+ WebkitUserSelect: "text",
225
+ }}
226
+ >
227
+ {terminalText}
228
+ </pre>
229
+ </div>
230
+ )}
231
+
232
+ {/* Drag and drop overlay */}
233
+ {isDragging && (
234
+ <div className="bg-primary/10 pointer-events-none absolute inset-0 z-30 flex items-center justify-center">
235
+ <div className="border-primary bg-background/90 rounded-lg border px-6 py-4 text-center shadow-lg">
236
+ <Upload className="text-primary mx-auto mb-2 h-8 w-8" />
237
+ <p className="text-sm font-medium">Drop file to upload</p>
238
+ </div>
239
+ </div>
240
+ )}
241
+
242
+ {/* Upload in progress overlay */}
243
+ {isUploading && (
244
+ <div className="bg-background/50 pointer-events-none absolute inset-0 z-30 flex items-center justify-center">
245
+ <div className="bg-background rounded-lg border px-6 py-4 text-center shadow-lg">
246
+ <Loader2 className="text-primary mx-auto mb-2 h-6 w-6 animate-spin" />
247
+ <p className="text-sm">Uploading file...</p>
248
+ </div>
249
+ </div>
250
+ )}
251
+
252
+ {/* File picker button - desktop only, for agent terminals */}
253
+ {!isMobile && showImageButton && (
254
+ <button
255
+ onClick={() => setShowFilePicker(true)}
256
+ className="bg-secondary hover:bg-accent absolute top-3 right-3 z-40 flex h-9 w-9 items-center justify-center rounded-full shadow-lg transition-all"
257
+ title="Attach file"
258
+ >
259
+ <Paperclip className="h-4 w-4" />
260
+ </button>
261
+ )}
262
+
263
+ {/* Image picker modal */}
264
+ {showFilePicker && (
265
+ <FilePicker
266
+ initialPath="~"
267
+ onSelect={handleImageSelect}
268
+ onClose={() => setShowFilePicker(false)}
269
+ />
270
+ )}
271
+
272
+ {/* Scroll to bottom button */}
273
+ <ScrollToBottomButton visible={!isAtBottom} onClick={scrollToBottom} />
274
+
275
+ {/* Mobile: Toolbar with special keys (native keyboard handles text) */}
276
+ {isMobile && (
277
+ <TerminalToolbar
278
+ onKeyPress={sendInput}
279
+ onFilePicker={() => setShowFilePicker(true)}
280
+ onCopy={copySelection}
281
+ selectMode={selectMode}
282
+ onSelectModeChange={setSelectMode}
283
+ visible={true}
284
+ />
285
+ )}
286
+
287
+ {/* Connection status overlays */}
288
+ {connectionState === "connecting" && (
289
+ <div className="bg-background absolute inset-0 z-20 flex flex-col items-center justify-center gap-3">
290
+ <div className="bg-primary h-2 w-2 animate-pulse rounded-full" />
291
+ <span className="text-muted-foreground text-sm">Connecting...</span>
292
+ </div>
293
+ )}
294
+
295
+ {connectionState === "reconnecting" && (
296
+ <div className="absolute top-4 left-4 flex items-center gap-2 rounded bg-amber-500/20 px-2 py-1 text-xs text-amber-400">
297
+ <div className="h-2 w-2 animate-pulse rounded-full bg-amber-500" />
298
+ Reconnecting...
299
+ </div>
300
+ )}
301
+
302
+ {/* Disconnected overlay - shows tap to reconnect button */}
303
+ {connectionState === "disconnected" && (
304
+ <button
305
+ onClick={reconnect}
306
+ className="bg-background/80 active:bg-background/90 absolute inset-0 z-30 flex flex-col items-center justify-center gap-3 backdrop-blur-sm transition-all"
307
+ >
308
+ <WifiOff className="text-muted-foreground h-8 w-8" />
309
+ <span className="text-foreground text-sm font-medium">
310
+ Connection lost
311
+ </span>
312
+ <span className="bg-primary text-primary-foreground rounded-full px-4 py-2 text-sm font-medium">
313
+ Tap to reconnect
314
+ </span>
315
+ </button>
316
+ )}
317
+ </div>
318
+ );
319
+ }
320
+ );
@@ -0,0 +1,168 @@
1
+ "use client";
2
+
3
+ import { Moon, Sun, Monitor, Check, Palette } from "lucide-react";
4
+ import { useTheme } from "next-themes";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuContent,
9
+ DropdownMenuItem,
10
+ DropdownMenuSeparator,
11
+ DropdownMenuLabel,
12
+ DropdownMenuTrigger,
13
+ DropdownMenuSub,
14
+ DropdownMenuSubContent,
15
+ DropdownMenuSubTrigger,
16
+ } from "@/components/ui/dropdown-menu";
17
+ import {
18
+ DARK_THEMES,
19
+ LIGHT_THEMES,
20
+ parseTheme,
21
+ buildTheme,
22
+ type DarkThemeVariant,
23
+ type LightThemeVariant,
24
+ } from "@/lib/theme-config";
25
+
26
+ export function ThemeToggle() {
27
+ const { theme, setTheme } = useTheme();
28
+ const { mode, variant } = parseTheme(theme || "system");
29
+
30
+ return (
31
+ <DropdownMenu>
32
+ <DropdownMenuTrigger asChild>
33
+ <Button variant="ghost" size="icon" className="h-8 w-8">
34
+ <Palette className="h-4 w-4" />
35
+ <span className="sr-only">Change theme</span>
36
+ </Button>
37
+ </DropdownMenuTrigger>
38
+ <DropdownMenuContent align="end" className="w-56">
39
+ {/* Light Themes */}
40
+ <DropdownMenuSub>
41
+ <DropdownMenuSubTrigger>
42
+ <Sun className="mr-2 h-4 w-4" />
43
+ <span>Light</span>
44
+ {mode === "light" && (
45
+ <Check className="text-primary ml-auto h-4 w-4" />
46
+ )}
47
+ </DropdownMenuSubTrigger>
48
+ <DropdownMenuSubContent className="max-h-[50vh] w-56 overflow-y-auto">
49
+ <DropdownMenuLabel className="text-muted-foreground text-xs">
50
+ Choose your light theme
51
+ </DropdownMenuLabel>
52
+ <DropdownMenuSeparator />
53
+ {LIGHT_THEMES.map((lightTheme) => {
54
+ const Icon = lightTheme.icon;
55
+ const isActive = mode === "light" && variant === lightTheme.id;
56
+ return (
57
+ <DropdownMenuItem
58
+ key={lightTheme.id}
59
+ onClick={() =>
60
+ setTheme(
61
+ buildTheme("light", lightTheme.id as LightThemeVariant)
62
+ )
63
+ }
64
+ className="cursor-pointer"
65
+ >
66
+ <Icon className="mr-2 h-4 w-4 flex-shrink-0" />
67
+ <div className="flex flex-1 flex-col gap-0.5">
68
+ <div className="flex items-center justify-between">
69
+ <span className="text-sm font-medium">
70
+ {lightTheme.label}
71
+ </span>
72
+ {isActive && <Check className="text-primary h-4 w-4" />}
73
+ </div>
74
+ <span className="text-muted-foreground text-xs">
75
+ {lightTheme.description}
76
+ </span>
77
+ {/* Color preview */}
78
+ <div className="mt-1 flex gap-1">
79
+ <div
80
+ className="border-border/50 h-3 w-8 rounded-sm border"
81
+ style={{
82
+ backgroundColor: lightTheme.preview.background,
83
+ }}
84
+ />
85
+ <div
86
+ className="border-border/50 h-3 w-8 rounded-sm border"
87
+ style={{ backgroundColor: lightTheme.preview.accent }}
88
+ />
89
+ </div>
90
+ </div>
91
+ </DropdownMenuItem>
92
+ );
93
+ })}
94
+ </DropdownMenuSubContent>
95
+ </DropdownMenuSub>
96
+
97
+ {/* Dark Themes */}
98
+ <DropdownMenuSub>
99
+ <DropdownMenuSubTrigger>
100
+ <Moon className="mr-2 h-4 w-4" />
101
+ <span>Dark</span>
102
+ {mode === "dark" && (
103
+ <Check className="text-primary ml-auto h-4 w-4" />
104
+ )}
105
+ </DropdownMenuSubTrigger>
106
+ <DropdownMenuSubContent className="max-h-[50vh] w-56 overflow-y-auto">
107
+ <DropdownMenuLabel className="text-muted-foreground text-xs">
108
+ Choose your dark theme
109
+ </DropdownMenuLabel>
110
+ <DropdownMenuSeparator />
111
+ {DARK_THEMES.map((darkTheme) => {
112
+ const Icon = darkTheme.icon;
113
+ const isActive = mode === "dark" && variant === darkTheme.id;
114
+ return (
115
+ <DropdownMenuItem
116
+ key={darkTheme.id}
117
+ onClick={() =>
118
+ setTheme(
119
+ buildTheme("dark", darkTheme.id as DarkThemeVariant)
120
+ )
121
+ }
122
+ className="cursor-pointer"
123
+ >
124
+ <Icon className="mr-2 h-4 w-4 flex-shrink-0" />
125
+ <div className="flex flex-1 flex-col gap-0.5">
126
+ <div className="flex items-center justify-between">
127
+ <span className="text-sm font-medium">
128
+ {darkTheme.label}
129
+ </span>
130
+ {isActive && <Check className="text-primary h-4 w-4" />}
131
+ </div>
132
+ <span className="text-muted-foreground text-xs">
133
+ {darkTheme.description}
134
+ </span>
135
+ {/* Color preview */}
136
+ <div className="mt-1 flex gap-1">
137
+ <div
138
+ className="border-border/50 h-3 w-8 rounded-sm border"
139
+ style={{
140
+ backgroundColor: darkTheme.preview.background,
141
+ }}
142
+ />
143
+ <div
144
+ className="border-border/50 h-3 w-8 rounded-sm border"
145
+ style={{ backgroundColor: darkTheme.preview.accent }}
146
+ />
147
+ </div>
148
+ </div>
149
+ </DropdownMenuItem>
150
+ );
151
+ })}
152
+ </DropdownMenuSubContent>
153
+ </DropdownMenuSub>
154
+
155
+ <DropdownMenuSeparator />
156
+
157
+ {/* System Theme */}
158
+ <DropdownMenuItem onClick={() => setTheme("system")}>
159
+ <Monitor className="mr-2 h-4 w-4" />
160
+ <span>System</span>
161
+ {mode === "system" && (
162
+ <Check className="text-primary ml-auto h-4 w-4" />
163
+ )}
164
+ </DropdownMenuItem>
165
+ </DropdownMenuContent>
166
+ </DropdownMenu>
167
+ );
168
+ }
@@ -0,0 +1,132 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useCallback } from "react";
4
+ import { Button } from "./ui/button";
5
+ import { Badge } from "./ui/badge";
6
+ import { RefreshCw, Terminal, MonitorUp } from "lucide-react";
7
+ import { cn } from "@/lib/utils";
8
+
9
+ interface TmuxSession {
10
+ name: string;
11
+ windows: number;
12
+ created: string;
13
+ attached: boolean;
14
+ }
15
+
16
+ interface TmuxSessionsProps {
17
+ onAttach: (sessionName: string) => void;
18
+ }
19
+
20
+ export function TmuxSessions({ onAttach }: TmuxSessionsProps) {
21
+ const [sessions, setSessions] = useState<TmuxSession[]>([]);
22
+ const [loading, setLoading] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+
25
+ const fetchSessions = useCallback(async () => {
26
+ setLoading(true);
27
+ setError(null);
28
+ try {
29
+ const res = await fetch("/api/exec", {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({
33
+ command:
34
+ "tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}' 2>/dev/null || echo ''",
35
+ }),
36
+ });
37
+ const data = await res.json();
38
+
39
+ if (data.success && data.output.trim()) {
40
+ const parsed = data.output
41
+ .trim()
42
+ .split("\n")
43
+ .filter((line: string) => line.includes("|"))
44
+ .map((line: string) => {
45
+ const [name, windows, created, attached] = line.split("|");
46
+ return {
47
+ name,
48
+ windows: parseInt(windows),
49
+ created: new Date(parseInt(created) * 1000).toLocaleString(),
50
+ attached: attached === "1",
51
+ };
52
+ });
53
+ setSessions(parsed);
54
+ } else {
55
+ setSessions([]);
56
+ }
57
+ } catch (err) {
58
+ console.error("Failed to fetch tmux sessions:", err);
59
+ setError("Failed to load");
60
+ setSessions([]);
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ }, []);
65
+
66
+ useEffect(() => {
67
+ fetchSessions();
68
+ // Refresh every 30 seconds
69
+ const interval = setInterval(fetchSessions, 30000);
70
+ return () => clearInterval(interval);
71
+ }, [fetchSessions]);
72
+
73
+ if (sessions.length === 0 && !loading && !error) {
74
+ return null; // Don't show section if no tmux sessions
75
+ }
76
+
77
+ return (
78
+ <div className="border-border border-b">
79
+ <div className="flex items-center justify-between px-4 py-2">
80
+ <div className="flex items-center gap-2">
81
+ <Terminal className="text-muted-foreground h-4 w-4" />
82
+ <span className="text-muted-foreground text-xs font-medium tracking-wider uppercase">
83
+ Tmux Sessions
84
+ </span>
85
+ </div>
86
+ <Button
87
+ variant="ghost"
88
+ size="icon-sm"
89
+ onClick={fetchSessions}
90
+ disabled={loading}
91
+ className="h-6 w-6"
92
+ >
93
+ <RefreshCw className={cn("h-3 w-3", loading && "animate-spin")} />
94
+ </Button>
95
+ </div>
96
+
97
+ <div className="space-y-1 px-4 pb-3">
98
+ {error && <p className="text-destructive text-xs">{error}</p>}
99
+ {sessions.map((session) => (
100
+ <button
101
+ key={session.name}
102
+ onClick={() => onAttach(session.name)}
103
+ className={cn(
104
+ "flex w-full items-center justify-between rounded-md p-2 text-left transition-colors",
105
+ "hover:bg-primary/10 border",
106
+ session.attached
107
+ ? "border-primary/50 bg-primary/5"
108
+ : "border-transparent"
109
+ )}
110
+ >
111
+ <div className="flex min-w-0 items-center gap-2">
112
+ <MonitorUp className="text-primary h-4 w-4 flex-shrink-0" />
113
+ <span className="truncate text-sm font-medium">
114
+ {session.name}
115
+ </span>
116
+ </div>
117
+ <div className="flex flex-shrink-0 items-center gap-2">
118
+ <span className="text-muted-foreground text-xs">
119
+ {session.windows}w
120
+ </span>
121
+ {session.attached && (
122
+ <Badge variant="success" className="px-1 py-0 text-[10px]">
123
+ attached
124
+ </Badge>
125
+ )}
126
+ </div>
127
+ </button>
128
+ ))}
129
+ </div>
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { Wrench, Check, X, Loader2 } from "lucide-react";
5
+
6
+ interface ToolCallDisplayProps {
7
+ name: string;
8
+ input: Record<string, unknown>;
9
+ output?: string;
10
+ status: "pending" | "running" | "completed" | "error";
11
+ }
12
+
13
+ export function ToolCallDisplay({
14
+ name,
15
+ input,
16
+ output,
17
+ status,
18
+ }: ToolCallDisplayProps) {
19
+ const StatusIcon = {
20
+ pending: Loader2,
21
+ running: Loader2,
22
+ completed: Check,
23
+ error: X,
24
+ }[status];
25
+
26
+ return (
27
+ <div className="border-border my-2 ml-11 overflow-hidden rounded-lg border">
28
+ {/* Header */}
29
+ <div
30
+ className={cn(
31
+ "flex items-center gap-2 px-3 py-2",
32
+ status === "error" ? "bg-destructive/10" : "bg-muted/50"
33
+ )}
34
+ >
35
+ <Wrench className="text-muted-foreground h-4 w-4" />
36
+ <span className="font-mono text-sm">{name}</span>
37
+ <StatusIcon
38
+ className={cn(
39
+ "ml-auto h-4 w-4",
40
+ status === "running" && "animate-spin text-yellow-400",
41
+ status === "pending" && "text-muted-foreground animate-spin",
42
+ status === "completed" && "text-primary",
43
+ status === "error" && "text-destructive"
44
+ )}
45
+ />
46
+ </div>
47
+
48
+ {/* Input */}
49
+ <details className="group">
50
+ <summary className="text-muted-foreground hover:text-foreground cursor-pointer px-3 py-1 text-xs">
51
+ Input
52
+ </summary>
53
+ <pre className="text-muted-foreground bg-background overflow-x-auto px-3 py-2 text-xs">
54
+ {JSON.stringify(input, null, 2)}
55
+ </pre>
56
+ </details>
57
+
58
+ {/* Output */}
59
+ {output && (
60
+ <details className="group" open={status === "completed"}>
61
+ <summary className="text-muted-foreground hover:text-foreground cursor-pointer px-3 py-1 text-xs">
62
+ Output
63
+ </summary>
64
+ <pre className="text-muted-foreground bg-background max-h-48 overflow-x-auto px-3 py-2 text-xs">
65
+ {output.length > 1000 ? output.slice(0, 1000) + "..." : output}
66
+ </pre>
67
+ </details>
68
+ )}
69
+ </div>
70
+ );
71
+ }