@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,20 @@
1
+ /**
2
+ * Terminal constants and theme configuration
3
+ */
4
+
5
+ import { getTerminalTheme, type TerminalTheme } from "@/lib/terminal-themes";
6
+
7
+ // Reconnection constants
8
+ export const WS_RECONNECT_BASE_DELAY = 1000; // 1 second
9
+ export const WS_RECONNECT_MAX_DELAY = 30000; // 30 seconds
10
+ export const WS_INACTIVITY_TIMEOUT = 45000; // 45 seconds - force reconnect if no data
11
+
12
+ // Get terminal theme for current app theme
13
+ export function getTerminalThemeForApp(theme: string): TerminalTheme {
14
+ return getTerminalTheme(theme);
15
+ }
16
+
17
+ // Legacy exports for compatibility - default dark themes
18
+ export const TERMINAL_THEME_DARK = getTerminalTheme("dark");
19
+ export const TERMINAL_THEME_LIGHT = getTerminalTheme("light");
20
+ export const TERMINAL_THEME = TERMINAL_THEME_DARK;
@@ -0,0 +1,5 @@
1
+ "use client";
2
+
3
+ export { useTerminalConnection } from "./useTerminalConnection";
4
+ export { useTerminalSearch } from "./useTerminalSearch";
5
+ export type { TerminalScrollState } from "./useTerminalConnection";
@@ -0,0 +1,140 @@
1
+ "use client";
2
+
3
+ import type { Terminal as XTerm } from "@xterm/xterm";
4
+ import type { FitAddon } from "@xterm/addon-fit";
5
+
6
+ interface ResizeHandlersConfig {
7
+ term: XTerm;
8
+ fitAddon: FitAddon;
9
+ containerRef: React.RefObject<HTMLDivElement | null>;
10
+ isMobile: boolean;
11
+ sendResize: (cols: number, rows: number) => void;
12
+ }
13
+
14
+ export function setupResizeHandlers(config: ResizeHandlersConfig): () => void {
15
+ const { term, fitAddon, containerRef, isMobile, sendResize } = config;
16
+
17
+ let resizeTimeout: NodeJS.Timeout | null = null;
18
+ let fitTimeouts: NodeJS.Timeout[] = [];
19
+ const mqListeners: { mq: MediaQueryList; handler: () => void }[] = [];
20
+ let resizeObserver: ResizeObserver | null = null;
21
+
22
+ // Workaround for FitAddon bug: it reserves 14px for scrollbar even when hidden
23
+ // FitAddon uses `overviewRuler?.width || 14` which doesn't work with 0 (falsy)
24
+ // On mobile we hide the scrollbar via CSS, so manually expand xterm-screen to full width
25
+ const fixMobileScrollbarWidth = () => {
26
+ if (!isMobile || !containerRef.current) return;
27
+
28
+ const xtermScreen = containerRef.current.querySelector(
29
+ ".xterm-screen"
30
+ ) as HTMLElement | null;
31
+ if (xtermScreen) {
32
+ const containerWidth = containerRef.current.clientWidth;
33
+ xtermScreen.style.width = `${containerWidth}px`;
34
+ }
35
+ };
36
+
37
+ const doFit = () => {
38
+ // Clear any pending fit timeouts
39
+ fitTimeouts.forEach(clearTimeout);
40
+ fitTimeouts = [];
41
+
42
+ // On mobile, save scroll position before fit to prevent keyboard open/close scroll
43
+ const savedScrollLine = isMobile ? term.buffer.active.viewportY : null;
44
+
45
+ const restoreScroll = () => {
46
+ if (savedScrollLine !== null) {
47
+ term.scrollToLine(savedScrollLine);
48
+ }
49
+ };
50
+
51
+ requestAnimationFrame(() => {
52
+ // First fit - immediate
53
+ fitAddon.fit();
54
+ fixMobileScrollbarWidth();
55
+ restoreScroll();
56
+ sendResize(term.cols, term.rows);
57
+
58
+ // Second fit - after 100ms (handles most delayed layout updates)
59
+ fitTimeouts.push(
60
+ setTimeout(() => {
61
+ fitAddon.fit();
62
+ fixMobileScrollbarWidth();
63
+ restoreScroll();
64
+ sendResize(term.cols, term.rows);
65
+ }, 100)
66
+ );
67
+
68
+ // Third fit - after 250ms (handles slow layout updates, e.g., DevTools toggle)
69
+ fitTimeouts.push(
70
+ setTimeout(() => {
71
+ fitAddon.fit();
72
+ fixMobileScrollbarWidth();
73
+ restoreScroll();
74
+ sendResize(term.cols, term.rows);
75
+ }, 250)
76
+ );
77
+ });
78
+ };
79
+
80
+ const handleResize = () => {
81
+ if (resizeTimeout) clearTimeout(resizeTimeout);
82
+ resizeTimeout = setTimeout(doFit, isMobile ? 100 : 50);
83
+ };
84
+
85
+ // Window resize
86
+ window.addEventListener("resize", handleResize);
87
+
88
+ // Media query listeners for Chrome DevTools mobile toggle
89
+ const mediaQueries = [
90
+ "(max-width: 640px)",
91
+ "(max-width: 768px)",
92
+ "(max-width: 1024px)",
93
+ ];
94
+ mediaQueries.forEach((query) => {
95
+ const mq = window.matchMedia(query);
96
+ const handler = () => handleResize();
97
+ mq.addEventListener("change", handler);
98
+ mqListeners.push({ mq, handler });
99
+ });
100
+
101
+ // Handle orientation change on mobile
102
+ if (isMobile && "orientation" in screen) {
103
+ screen.orientation.addEventListener("change", handleResize);
104
+ }
105
+
106
+ // Handle visual viewport changes (for mobile keyboard)
107
+ if (isMobile && window.visualViewport) {
108
+ window.visualViewport.addEventListener("resize", handleResize);
109
+ }
110
+
111
+ // ResizeObserver for container changes
112
+ if (containerRef.current) {
113
+ resizeObserver = new ResizeObserver(() => handleResize());
114
+ resizeObserver.observe(containerRef.current);
115
+ }
116
+
117
+ // Return cleanup function
118
+ return () => {
119
+ if (resizeTimeout) clearTimeout(resizeTimeout);
120
+ fitTimeouts.forEach(clearTimeout);
121
+
122
+ window.removeEventListener("resize", handleResize);
123
+
124
+ mqListeners.forEach(({ mq, handler }) => {
125
+ mq.removeEventListener("change", handler);
126
+ });
127
+
128
+ if (isMobile && "orientation" in screen) {
129
+ screen.orientation.removeEventListener("change", handleResize);
130
+ }
131
+
132
+ if (isMobile && window.visualViewport) {
133
+ window.visualViewport.removeEventListener("resize", handleResize);
134
+ }
135
+
136
+ if (resizeObserver) {
137
+ resizeObserver.disconnect();
138
+ }
139
+ };
140
+ }
@@ -0,0 +1,151 @@
1
+ "use client";
2
+
3
+ import { Terminal as XTerm } from "@xterm/xterm";
4
+ import { FitAddon } from "@xterm/addon-fit";
5
+ import { WebLinksAddon } from "@xterm/addon-web-links";
6
+ import { SearchAddon } from "@xterm/addon-search";
7
+ import { CanvasAddon } from "@xterm/addon-canvas";
8
+ import { getTerminalThemeForApp } from "../constants";
9
+
10
+ export interface TerminalInstance {
11
+ term: XTerm;
12
+ fitAddon: FitAddon;
13
+ searchAddon: SearchAddon;
14
+ cleanup: () => void;
15
+ }
16
+
17
+ export function createTerminal(
18
+ container: HTMLElement,
19
+ isMobile: boolean,
20
+ theme: string
21
+ ): TerminalInstance {
22
+ const fontSize = isMobile ? 11 : 14;
23
+ const terminalTheme = getTerminalThemeForApp(theme || "dark");
24
+
25
+ const term = new XTerm({
26
+ cursorBlink: true,
27
+ fontSize,
28
+ fontFamily:
29
+ '"JetBrains Mono", "Fira Code", Menlo, Monaco, "Courier New", monospace',
30
+ fontWeight: "400",
31
+ fontWeightBold: "600",
32
+ letterSpacing: 0,
33
+ lineHeight: isMobile ? 1.15 : 1.2,
34
+ scrollback: 15000,
35
+ scrollSensitivity: isMobile ? 3 : 1,
36
+ fastScrollSensitivity: 5,
37
+ smoothScrollDuration: 100,
38
+ cursorStyle: "bar",
39
+ cursorWidth: 2,
40
+ allowProposedApi: true,
41
+ theme: terminalTheme,
42
+ });
43
+
44
+ const fitAddon = new FitAddon();
45
+ const searchAddon = new SearchAddon();
46
+
47
+ term.loadAddon(fitAddon);
48
+ term.loadAddon(new WebLinksAddon());
49
+ term.loadAddon(searchAddon);
50
+ term.open(container);
51
+ term.loadAddon(new CanvasAddon());
52
+ fitAddon.fit();
53
+
54
+ // Helper to copy text to clipboard with fallback
55
+ const copyToClipboard = (text: string) => {
56
+ if (navigator.clipboard?.writeText) {
57
+ navigator.clipboard.writeText(text).catch(() => {
58
+ // Fallback if clipboard API fails
59
+ execCommandCopy(text);
60
+ });
61
+ } else {
62
+ // Fallback for non-secure contexts
63
+ execCommandCopy(text);
64
+ }
65
+ };
66
+
67
+ const execCommandCopy = (text: string) => {
68
+ const textarea = document.createElement("textarea");
69
+ textarea.value = text;
70
+ textarea.style.position = "fixed";
71
+ textarea.style.opacity = "0";
72
+ document.body.appendChild(textarea);
73
+ textarea.select();
74
+ document.execCommand("copy");
75
+ document.body.removeChild(textarea);
76
+ };
77
+
78
+ // Handle Cmd+A and Cmd+C via document event listener (more reliable than attachCustomKeyEventHandler)
79
+ const handleKeyDown = (event: KeyboardEvent) => {
80
+ // Only handle when terminal is focused (xterm creates its textarea inside the container)
81
+ if (!container.contains(document.activeElement)) return;
82
+
83
+ const key = event.key.toLowerCase();
84
+
85
+ // Cmd+A (macOS) / Ctrl+A for select all
86
+ if ((event.metaKey || event.ctrlKey) && key === "a") {
87
+ event.preventDefault();
88
+ event.stopPropagation();
89
+ term.selectAll();
90
+ return;
91
+ }
92
+
93
+ // Cmd+C (macOS) / Ctrl+C for copy when text is selected
94
+ if ((event.metaKey || event.ctrlKey) && key === "c") {
95
+ const selection = term.getSelection();
96
+ if (selection) {
97
+ event.preventDefault();
98
+ event.stopPropagation();
99
+ copyToClipboard(selection);
100
+ }
101
+ }
102
+ };
103
+
104
+ // Use capture phase to intercept before browser default
105
+ document.addEventListener("keydown", handleKeyDown, true);
106
+
107
+ // Handle OSC 52 (clipboard) sequences from tmux
108
+ const osc52Disposable = term.parser.registerOscHandler(52, (data) => {
109
+ const parts = data.split(";");
110
+ if (parts.length >= 2) {
111
+ const base64 = parts[parts.length - 1];
112
+ if (base64 && base64 !== "?") {
113
+ try {
114
+ const text = atob(base64);
115
+ copyToClipboard(text);
116
+ } catch {}
117
+ }
118
+ }
119
+ return true;
120
+ });
121
+
122
+ const cleanup = () => {
123
+ document.removeEventListener("keydown", handleKeyDown, true);
124
+ osc52Disposable.dispose();
125
+ };
126
+
127
+ return { term, fitAddon, searchAddon, cleanup };
128
+ }
129
+
130
+ export function updateTerminalForMobile(
131
+ term: XTerm,
132
+ fitAddon: FitAddon,
133
+ isMobile: boolean,
134
+ sendResize: (cols: number, rows: number) => void
135
+ ): void {
136
+ const newFontSize = isMobile ? 11 : 14;
137
+ const newLineHeight = isMobile ? 1.15 : 1.2;
138
+
139
+ if (term.options.fontSize !== newFontSize) {
140
+ term.options.fontSize = newFontSize;
141
+ term.options.lineHeight = newLineHeight;
142
+ term.refresh(0, term.rows - 1);
143
+ fitAddon.fit();
144
+ sendResize(term.cols, term.rows);
145
+ }
146
+ }
147
+
148
+ export function updateTerminalTheme(term: XTerm, theme: string): void {
149
+ const terminalTheme = getTerminalThemeForApp(theme || "dark");
150
+ term.options.theme = terminalTheme;
151
+ }
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import type { Terminal as XTerm } from "@xterm/xterm";
4
+ import type { RefObject } from "react";
5
+
6
+ interface TouchScrollConfig {
7
+ term: XTerm;
8
+ selectModeRef: RefObject<boolean>;
9
+ }
10
+
11
+ export function setupTouchScroll(config: TouchScrollConfig): () => void {
12
+ const { term, selectModeRef } = config;
13
+
14
+ if (!term.element) return () => {};
15
+
16
+ let touchElement: HTMLElement | null = null;
17
+ let handleTouchStart: ((e: TouchEvent) => void) | null = null;
18
+ let handleTouchMove: ((e: TouchEvent) => void) | null = null;
19
+ let handleTouchEnd: (() => void) | null = null;
20
+ let handleTouchCancel: (() => void) | null = null;
21
+ let setupTimeout: NodeJS.Timeout | null = null;
22
+
23
+ const setupTouchScrollInner = () => {
24
+ const xtermScreen = term.element?.querySelector(
25
+ ".xterm-screen"
26
+ ) as HTMLElement | null;
27
+ if (!xtermScreen) {
28
+ setupTimeout = setTimeout(setupTouchScrollInner, 50);
29
+ return;
30
+ }
31
+
32
+ xtermScreen.style.touchAction = "none";
33
+ xtermScreen.style.userSelect = "none";
34
+ (
35
+ xtermScreen.style as CSSStyleDeclaration & { webkitUserSelect?: string }
36
+ ).webkitUserSelect = "none";
37
+
38
+ const canvases = xtermScreen.querySelectorAll("canvas");
39
+ canvases.forEach((canvas) => {
40
+ (canvas as HTMLElement).style.touchAction = "none";
41
+ });
42
+
43
+ // Prevent native scroll on the xterm-viewport and scrollable-element wrappers
44
+ // which can intercept touch events on mobile before they reach .xterm-screen
45
+ const viewport = term.element?.querySelector(
46
+ ".xterm-viewport"
47
+ ) as HTMLElement | null;
48
+ if (viewport) {
49
+ viewport.style.touchAction = "none";
50
+ viewport.style.overflowY = "hidden";
51
+ }
52
+ const scrollableEl = term.element?.querySelector(
53
+ ".xterm-scrollable-element"
54
+ ) as HTMLElement | null;
55
+ if (scrollableEl) {
56
+ scrollableEl.style.touchAction = "none";
57
+ }
58
+
59
+ let startX = 0;
60
+ let startY = 0;
61
+ let lastY = 0;
62
+ let scrollDirection: "vertical" | "horizontal" | null = null;
63
+ let scrollAccumulator = 0;
64
+
65
+ handleTouchStart = (e: TouchEvent) => {
66
+ if (selectModeRef.current || e.touches.length === 0) return;
67
+ const touch = e.touches[0];
68
+ startX = touch.clientX;
69
+ startY = touch.clientY;
70
+ lastY = touch.clientY;
71
+ scrollDirection = null;
72
+ scrollAccumulator = 0;
73
+ };
74
+
75
+ handleTouchMove = (e: TouchEvent) => {
76
+ if (selectModeRef.current || e.touches.length === 0) return;
77
+ if (scrollDirection === null && startX === 0 && startY === 0) return;
78
+
79
+ const touch = e.touches[0];
80
+
81
+ if (scrollDirection === null) {
82
+ const deltaX = Math.abs(touch.clientX - startX);
83
+ const deltaY = Math.abs(touch.clientY - startY);
84
+ if (deltaX > 8 || deltaY > 8) {
85
+ scrollDirection = deltaX > deltaY ? "horizontal" : "vertical";
86
+ }
87
+ }
88
+
89
+ if (scrollDirection === "horizontal") return;
90
+
91
+ if (scrollDirection === "vertical") {
92
+ e.preventDefault();
93
+ e.stopPropagation();
94
+ }
95
+
96
+ const deltaY = touch.clientY - lastY;
97
+ lastY = touch.clientY;
98
+
99
+ if (Math.abs(deltaY) < 1) return;
100
+
101
+ scrollAccumulator += deltaY;
102
+ const step = 20;
103
+
104
+ while (Math.abs(scrollAccumulator) >= step) {
105
+ const wheelDelta = scrollAccumulator > 0 ? -100 : 100;
106
+ const syntheticWheel = new WheelEvent("wheel", {
107
+ deltaY: wheelDelta,
108
+ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
109
+ bubbles: true,
110
+ cancelable: true,
111
+ clientX: touch.clientX,
112
+ clientY: touch.clientY,
113
+ });
114
+ xtermScreen.dispatchEvent(syntheticWheel);
115
+ scrollAccumulator += scrollAccumulator > 0 ? -step : step;
116
+ }
117
+ };
118
+
119
+ handleTouchEnd = () => {
120
+ startX = 0;
121
+ startY = 0;
122
+ lastY = 0;
123
+ scrollDirection = null;
124
+ scrollAccumulator = 0;
125
+ };
126
+ handleTouchCancel = handleTouchEnd;
127
+
128
+ xtermScreen.addEventListener("touchstart", handleTouchStart, {
129
+ passive: true,
130
+ });
131
+ xtermScreen.addEventListener("touchmove", handleTouchMove, {
132
+ passive: false,
133
+ });
134
+ xtermScreen.addEventListener("touchend", handleTouchEnd);
135
+ xtermScreen.addEventListener("touchcancel", handleTouchCancel);
136
+
137
+ touchElement = xtermScreen;
138
+ };
139
+
140
+ setupTouchScrollInner();
141
+
142
+ return () => {
143
+ if (setupTimeout) clearTimeout(setupTimeout);
144
+ if (touchElement) {
145
+ if (handleTouchStart)
146
+ touchElement.removeEventListener("touchstart", handleTouchStart);
147
+ if (handleTouchMove)
148
+ touchElement.removeEventListener("touchmove", handleTouchMove);
149
+ if (handleTouchEnd)
150
+ touchElement.removeEventListener("touchend", handleTouchEnd);
151
+ if (handleTouchCancel)
152
+ touchElement.removeEventListener("touchcancel", handleTouchCancel);
153
+ }
154
+ };
155
+ }