@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,711 @@
1
+ "use client";
2
+
3
+ import { useState, useCallback, useRef, useEffect, memo } from "react";
4
+ import {
5
+ ChevronUp,
6
+ ChevronDown,
7
+ ChevronLeft,
8
+ ChevronRight,
9
+ ImagePlus,
10
+ Mic,
11
+ MicOff,
12
+ Clipboard,
13
+ X,
14
+ Send,
15
+ } from "lucide-react";
16
+ import { cn } from "@/lib/utils";
17
+ import { useSpeechRecognition } from "@/hooks/useSpeechRecognition";
18
+ import { useKeyRepeat } from "@/hooks/useKeyRepeat";
19
+
20
+ // ANSI escape sequences
21
+ const SPECIAL_KEYS = {
22
+ UP: "\x1b[A",
23
+ DOWN: "\x1b[B",
24
+ LEFT: "\x1b[D",
25
+ RIGHT: "\x1b[C",
26
+ ENTER: "\r",
27
+ ESC: "\x1b",
28
+ TAB: "\t",
29
+ BACKSPACE: "\x7f",
30
+ CTRL_C: "\x03",
31
+ CTRL_D: "\x04",
32
+ CTRL_Z: "\x1a",
33
+ CTRL_L: "\x0c",
34
+ } as const;
35
+
36
+ // Keyboard layouts
37
+ const ROWS = {
38
+ numbers: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
39
+ numbersShift: ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"],
40
+ row1: ["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"],
41
+ row2: ["a", "s", "d", "f", "g", "h", "j", "k", "l"],
42
+ row3: ["z", "x", "c", "v", "b", "n", "m"],
43
+ symbols: ["-", "/", ":", ";", "(", ")", "$", "&", "@", '"'],
44
+ symbolsMore: [".", ",", "?", "!", "'", "`", "~", "=", "+", "*"],
45
+ };
46
+
47
+ type KeyboardMode = "quick" | "abc" | "num";
48
+
49
+ interface VirtualKeyboardProps {
50
+ onKeyPress: (key: string) => void;
51
+ onImagePick?: () => void;
52
+ visible?: boolean;
53
+ }
54
+
55
+ // Track last touch time globally to prevent duplicate events from touch->mouse emulation
56
+ let lastTouchTime = 0;
57
+
58
+ // Event delegation handler - finds the key from data attribute and fires callback
59
+ function createKeyboardHandler(onKey: (key: string) => void) {
60
+ const handleEvent = (e: TouchEvent | MouseEvent) => {
61
+ // Find the button with data-key attribute
62
+ const target = e.target as HTMLElement;
63
+ const button = target.closest("[data-key]") as HTMLElement | null;
64
+ if (!button) return;
65
+
66
+ const key = button.getAttribute("data-key");
67
+ if (!key) return;
68
+
69
+ e.preventDefault();
70
+
71
+ // Prevent duplicate from touch->mouse emulation
72
+ if (e.type === "touchstart") {
73
+ lastTouchTime = Date.now();
74
+ } else if (e.type === "mousedown" && Date.now() - lastTouchTime < 500) {
75
+ return;
76
+ }
77
+
78
+ onKey(key);
79
+ };
80
+
81
+ return handleEvent;
82
+ }
83
+
84
+ // Simple key button - no individual handlers, uses event delegation
85
+ // Memoized to prevent re-renders when parent state changes (like shift)
86
+ const Key = memo(function Key({
87
+ char,
88
+ dataKey,
89
+ className,
90
+ }: {
91
+ char: string;
92
+ dataKey?: string;
93
+ className?: string;
94
+ }) {
95
+ return (
96
+ <button
97
+ data-key={dataKey ?? char}
98
+ className={cn(
99
+ "flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-sm font-medium",
100
+ "bg-secondary text-secondary-foreground",
101
+ "active:bg-primary active:text-primary-foreground",
102
+ "min-w-[32px] select-none",
103
+ className
104
+ )}
105
+ >
106
+ {char}
107
+ </button>
108
+ );
109
+ });
110
+
111
+ // Fast button for special keys (uses event delegation via data-key)
112
+ function FastKey({
113
+ dataKey,
114
+ className,
115
+ children,
116
+ }: {
117
+ dataKey: string;
118
+ className?: string;
119
+ children: React.ReactNode;
120
+ }) {
121
+ return (
122
+ <button data-key={dataKey} className={className}>
123
+ {children}
124
+ </button>
125
+ );
126
+ }
127
+
128
+ // Fast button with direct handler (for shortcuts bar which is outside main keyboard delegation)
129
+ function FastButton({
130
+ onPress,
131
+ className,
132
+ children,
133
+ }: {
134
+ onPress: () => void;
135
+ className?: string;
136
+ children: React.ReactNode;
137
+ }) {
138
+ const handleTouchStart = (e: React.TouchEvent) => {
139
+ e.preventDefault();
140
+ lastTouchTime = Date.now();
141
+ onPress();
142
+ };
143
+
144
+ const handleMouseDown = (e: React.MouseEvent) => {
145
+ if (Date.now() - lastTouchTime < 500) return;
146
+ e.preventDefault();
147
+ onPress();
148
+ };
149
+
150
+ return (
151
+ <button
152
+ onTouchStart={handleTouchStart}
153
+ onMouseDown={handleMouseDown}
154
+ onContextMenu={(e) => e.preventDefault()}
155
+ className={className}
156
+ >
157
+ {children}
158
+ </button>
159
+ );
160
+ }
161
+
162
+ // Paste modal for when clipboard API isn't available
163
+ function PasteModal({
164
+ open,
165
+ onClose,
166
+ onPaste,
167
+ }: {
168
+ open: boolean;
169
+ onClose: () => void;
170
+ onPaste: (text: string) => void;
171
+ }) {
172
+ const [text, setText] = useState("");
173
+ const inputRef = useRef<HTMLTextAreaElement>(null);
174
+
175
+ // Focus input when modal opens
176
+ useCallback(() => {
177
+ if (open && inputRef.current) {
178
+ inputRef.current.focus();
179
+ }
180
+ }, [open]);
181
+
182
+ const handleSend = () => {
183
+ if (text) {
184
+ onPaste(text);
185
+ setText("");
186
+ onClose();
187
+ }
188
+ };
189
+
190
+ if (!open) return null;
191
+
192
+ return (
193
+ <div
194
+ className="fixed inset-0 z-50 flex items-end justify-center bg-black/50"
195
+ onClick={onClose}
196
+ >
197
+ <div
198
+ className="bg-background w-full max-w-lg rounded-t-xl p-4 pb-[calc(1rem+env(safe-area-inset-bottom))]"
199
+ onClick={(e) => e.stopPropagation()}
200
+ >
201
+ <div className="mb-3 flex items-center justify-between">
202
+ <span className="text-sm font-medium">Paste text</span>
203
+ <button onClick={onClose} className="hover:bg-muted rounded-md p-1">
204
+ <X className="h-5 w-5" />
205
+ </button>
206
+ </div>
207
+ <textarea
208
+ ref={inputRef}
209
+ value={text}
210
+ onChange={(e) => setText(e.target.value)}
211
+ onPaste={(e) => {
212
+ // Handle paste event directly
213
+ const pasted = e.clipboardData?.getData("text");
214
+ if (pasted) {
215
+ e.preventDefault();
216
+ setText((prev) => prev + pasted);
217
+ }
218
+ }}
219
+ placeholder="Tap here, then long-press to paste..."
220
+ autoFocus
221
+ inputMode="text"
222
+ className="bg-muted focus:ring-primary h-24 w-full resize-none rounded-lg px-3 py-2 text-sm focus:ring-2 focus:outline-none"
223
+ />
224
+ <button
225
+ onClick={handleSend}
226
+ disabled={!text}
227
+ className="bg-primary text-primary-foreground mt-3 flex w-full items-center justify-center gap-2 rounded-lg py-2.5 font-medium disabled:opacity-50"
228
+ >
229
+ <Send className="h-4 w-4" />
230
+ Send to Terminal
231
+ </button>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ // Terminal shortcuts bar - common keys for terminal interaction
238
+ function TerminalShortcutsBar({
239
+ onKeyPress,
240
+ isListening,
241
+ onMicToggle,
242
+ isMicSupported,
243
+ }: {
244
+ onKeyPress: (key: string) => void;
245
+ isListening?: boolean;
246
+ onMicToggle?: () => void;
247
+ isMicSupported?: boolean;
248
+ }) {
249
+ const [showPasteModal, setShowPasteModal] = useState(false);
250
+
251
+ const shortcuts = [
252
+ { label: "Esc", key: SPECIAL_KEYS.ESC },
253
+ { label: "^C", key: SPECIAL_KEYS.CTRL_C, highlight: true },
254
+ { label: "Tab", key: SPECIAL_KEYS.TAB },
255
+ { label: "^D", key: SPECIAL_KEYS.CTRL_D },
256
+ { label: "^Z", key: SPECIAL_KEYS.CTRL_Z },
257
+ { label: "^L", key: SPECIAL_KEYS.CTRL_L },
258
+ { label: "↑", key: SPECIAL_KEYS.UP },
259
+ { label: "↓", key: SPECIAL_KEYS.DOWN },
260
+ ];
261
+
262
+ // Handle paste - try clipboard API first, fall back to modal
263
+ const handlePaste = useCallback(async () => {
264
+ try {
265
+ if (navigator.clipboard?.readText) {
266
+ const text = await navigator.clipboard.readText();
267
+ if (text) {
268
+ for (const char of text) {
269
+ onKeyPress(char);
270
+ }
271
+ return;
272
+ }
273
+ }
274
+ } catch {
275
+ // Clipboard API failed, show modal
276
+ }
277
+ // Fall back to modal
278
+ setShowPasteModal(true);
279
+ }, [onKeyPress]);
280
+
281
+ // Handle paste from modal
282
+ const handleModalPaste = useCallback(
283
+ (text: string) => {
284
+ for (const char of text) {
285
+ onKeyPress(char);
286
+ }
287
+ },
288
+ [onKeyPress]
289
+ );
290
+
291
+ return (
292
+ <>
293
+ <PasteModal
294
+ open={showPasteModal}
295
+ onClose={() => setShowPasteModal(false)}
296
+ onPaste={handleModalPaste}
297
+ />
298
+ <div className="scrollbar-none flex items-center gap-1.5 overflow-x-auto px-2 py-1.5">
299
+ {/* Paste button */}
300
+ <FastButton
301
+ onPress={handlePaste}
302
+ className="bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground flex-shrink-0 touch-manipulation rounded-md px-3 py-1.5 text-xs font-medium select-none"
303
+ >
304
+ <Clipboard className="h-4 w-4" />
305
+ </FastButton>
306
+ {/* Mic button - always visible when supported */}
307
+ {isMicSupported && onMicToggle && (
308
+ <FastButton
309
+ onPress={onMicToggle}
310
+ className={cn(
311
+ "flex-shrink-0 touch-manipulation rounded-md px-3 py-1.5 text-xs font-medium select-none",
312
+ isListening
313
+ ? "animate-pulse bg-red-500 text-white"
314
+ : "bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground"
315
+ )}
316
+ >
317
+ {isListening ? (
318
+ <MicOff className="h-4 w-4" />
319
+ ) : (
320
+ <Mic className="h-4 w-4" />
321
+ )}
322
+ </FastButton>
323
+ )}
324
+ {shortcuts.map((shortcut) => (
325
+ <FastButton
326
+ key={shortcut.label}
327
+ onPress={() => onKeyPress(shortcut.key)}
328
+ className={cn(
329
+ "flex-shrink-0 touch-manipulation rounded-md px-3 py-1.5 text-xs font-medium select-none",
330
+ "active:bg-primary active:text-primary-foreground",
331
+ shortcut.highlight
332
+ ? "bg-red-500/20 text-red-500"
333
+ : "bg-secondary text-secondary-foreground"
334
+ )}
335
+ >
336
+ {shortcut.label}
337
+ </FastButton>
338
+ ))}
339
+ </div>
340
+ </>
341
+ );
342
+ }
343
+
344
+ export function VirtualKeyboard({
345
+ onKeyPress,
346
+ onImagePick,
347
+ visible = true,
348
+ }: VirtualKeyboardProps) {
349
+ const [mode, setMode] = useState<KeyboardMode>("abc");
350
+ const [shifted, setShifted] = useState(false);
351
+ const keyboardRef = useRef<HTMLDivElement>(null);
352
+
353
+ // Speech recognition - send transcript directly to terminal
354
+ const handleTranscript = useCallback(
355
+ (text: string) => {
356
+ for (const char of text) {
357
+ onKeyPress(char);
358
+ }
359
+ },
360
+ [onKeyPress]
361
+ );
362
+
363
+ const {
364
+ isListening,
365
+ isSupported: isMicSupported,
366
+ toggle: toggleMic,
367
+ } = useSpeechRecognition(handleTranscript);
368
+
369
+ // Key repeat for backspace
370
+ const handleBackspace = useCallback(() => {
371
+ onKeyPress(SPECIAL_KEYS.BACKSPACE);
372
+ }, [onKeyPress]);
373
+ const { startRepeat: startBackspace, stopRepeat: stopBackspace } =
374
+ useKeyRepeat(handleBackspace);
375
+
376
+ // Event delegation - attach once, handle all keys
377
+ useEffect(() => {
378
+ const el = keyboardRef.current;
379
+ if (!el) return;
380
+
381
+ const handleKey = (key: string) => {
382
+ // Handle special keys
383
+ if (key === "SHIFT") {
384
+ setShifted((s) => !s);
385
+ return;
386
+ }
387
+ if (key === "MODE_ABC") {
388
+ setMode("abc");
389
+ return;
390
+ }
391
+ if (key === "MODE_NUM") {
392
+ setMode("num");
393
+ return;
394
+ }
395
+ if (key === "MODE_QUICK") {
396
+ setMode("quick");
397
+ return;
398
+ }
399
+ if (key === "SPACE") {
400
+ onKeyPress(" ");
401
+ return;
402
+ }
403
+ if (key === "ENTER") {
404
+ onKeyPress(SPECIAL_KEYS.ENTER);
405
+ return;
406
+ }
407
+ if (key === "LEFT") {
408
+ onKeyPress(SPECIAL_KEYS.LEFT);
409
+ return;
410
+ }
411
+ if (key === "RIGHT") {
412
+ onKeyPress(SPECIAL_KEYS.RIGHT);
413
+ return;
414
+ }
415
+ if (key === "UP") {
416
+ onKeyPress(SPECIAL_KEYS.UP);
417
+ return;
418
+ }
419
+ if (key === "DOWN") {
420
+ onKeyPress(SPECIAL_KEYS.DOWN);
421
+ return;
422
+ }
423
+ if (key === "IMAGE" && onImagePick) {
424
+ onImagePick();
425
+ return;
426
+ }
427
+
428
+ // Regular character - apply shift if needed
429
+ const char = shifted ? key.toUpperCase() : key;
430
+ onKeyPress(char);
431
+ if (shifted) setShifted(false);
432
+ };
433
+
434
+ const handler = createKeyboardHandler(handleKey);
435
+
436
+ el.addEventListener("touchstart", handler, { passive: false });
437
+ el.addEventListener("mousedown", handler);
438
+ el.addEventListener("contextmenu", (e) => e.preventDefault());
439
+
440
+ return () => {
441
+ el.removeEventListener("touchstart", handler);
442
+ el.removeEventListener("mousedown", handler);
443
+ };
444
+ }, [onKeyPress, shifted, onImagePick]);
445
+
446
+ if (!visible) return null;
447
+
448
+ // Quick mode - just essential terminal keys
449
+ if (mode === "quick") {
450
+ return (
451
+ <div
452
+ ref={keyboardRef}
453
+ className="bg-background flex flex-col select-none"
454
+ >
455
+ {/* Terminal shortcuts */}
456
+ <TerminalShortcutsBar
457
+ onKeyPress={onKeyPress}
458
+ isListening={isListening}
459
+ onMicToggle={toggleMic}
460
+ isMicSupported={isMicSupported}
461
+ />
462
+
463
+ <div className="flex flex-col gap-1.5 px-2 py-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
464
+ {/* Mode tabs + common keys */}
465
+ <div className="flex gap-1.5">
466
+ <FastKey
467
+ dataKey="MODE_ABC"
468
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
469
+ >
470
+ ABC
471
+ </FastKey>
472
+ <FastKey
473
+ dataKey="MODE_NUM"
474
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
475
+ >
476
+ 123
477
+ </FastKey>
478
+ {onImagePick && (
479
+ <FastKey
480
+ dataKey="IMAGE"
481
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
482
+ >
483
+ <ImagePlus className="h-5 w-5" />
484
+ </FastKey>
485
+ )}
486
+ <div className="flex-1" />
487
+ <button
488
+ onTouchStart={startBackspace}
489
+ onTouchEnd={stopBackspace}
490
+ onTouchCancel={stopBackspace}
491
+ onMouseDown={startBackspace}
492
+ onMouseUp={stopBackspace}
493
+ onMouseLeave={stopBackspace}
494
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[56px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
495
+ >
496
+
497
+ </button>
498
+ </div>
499
+
500
+ {/* Arrow keys + Enter */}
501
+ <div className="flex gap-1.5">
502
+ <FastKey
503
+ dataKey="LEFT"
504
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
505
+ >
506
+ <ChevronLeft className="h-6 w-6" />
507
+ </FastKey>
508
+ <div className="flex flex-col gap-1">
509
+ <FastKey
510
+ dataKey="UP"
511
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[20px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
512
+ >
513
+ <ChevronUp className="h-4 w-4" />
514
+ </FastKey>
515
+ <FastKey
516
+ dataKey="DOWN"
517
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[20px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
518
+ >
519
+ <ChevronDown className="h-4 w-4" />
520
+ </FastKey>
521
+ </div>
522
+ <FastKey
523
+ dataKey="RIGHT"
524
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[44px] touch-manipulation items-center justify-center rounded-md select-none"
525
+ >
526
+ <ChevronRight className="h-6 w-6" />
527
+ </FastKey>
528
+ <div className="flex-1" />
529
+ <Key
530
+ char="⏎"
531
+ dataKey="ENTER"
532
+ className="bg-primary/30 text-primary w-[68px]"
533
+ />
534
+ </div>
535
+ </div>
536
+ </div>
537
+ );
538
+ }
539
+
540
+ // ABC mode - full QWERTY
541
+ if (mode === "abc") {
542
+ return (
543
+ <div
544
+ ref={keyboardRef}
545
+ className="bg-background flex flex-col select-none"
546
+ >
547
+ {/* Terminal shortcuts */}
548
+ <TerminalShortcutsBar
549
+ onKeyPress={onKeyPress}
550
+ isListening={isListening}
551
+ onMicToggle={toggleMic}
552
+ isMicSupported={isMicSupported}
553
+ />
554
+
555
+ <div className="flex flex-col gap-1.5 px-2 py-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
556
+ {/* QWERTY rows */}
557
+ <div className="flex gap-1">
558
+ {ROWS.row1.map((char) => (
559
+ <Key
560
+ key={char}
561
+ char={shifted ? char.toUpperCase() : char}
562
+ dataKey={char}
563
+ />
564
+ ))}
565
+ </div>
566
+ <div className="flex gap-1 px-4">
567
+ {ROWS.row2.map((char) => (
568
+ <Key
569
+ key={char}
570
+ char={shifted ? char.toUpperCase() : char}
571
+ dataKey={char}
572
+ />
573
+ ))}
574
+ </div>
575
+ <div className="flex gap-1">
576
+ <FastKey
577
+ dataKey="SHIFT"
578
+ className={cn(
579
+ "flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none",
580
+ shifted
581
+ ? "bg-primary/30 text-primary active:bg-primary active:text-primary-foreground"
582
+ : "bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground"
583
+ )}
584
+ >
585
+
586
+ </FastKey>
587
+ {ROWS.row3.map((char) => (
588
+ <Key
589
+ key={char}
590
+ char={shifted ? char.toUpperCase() : char}
591
+ dataKey={char}
592
+ />
593
+ ))}
594
+ <button
595
+ onTouchStart={startBackspace}
596
+ onTouchEnd={stopBackspace}
597
+ onTouchCancel={stopBackspace}
598
+ onMouseDown={startBackspace}
599
+ onMouseUp={stopBackspace}
600
+ onMouseLeave={stopBackspace}
601
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
602
+ >
603
+
604
+ </button>
605
+ </div>
606
+
607
+ {/* Bottom row */}
608
+ <div className="flex gap-1">
609
+ <FastKey
610
+ dataKey="MODE_QUICK"
611
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[56px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
612
+ >
613
+ ^C
614
+ </FastKey>
615
+ <FastKey
616
+ dataKey="MODE_NUM"
617
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
618
+ >
619
+ 123
620
+ </FastKey>
621
+ <FastKey
622
+ dataKey="SPACE"
623
+ className="bg-secondary text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-sm select-none"
624
+ >
625
+ space
626
+ </FastKey>
627
+ <FastKey
628
+ dataKey="ENTER"
629
+ className="bg-primary/30 text-primary active:bg-primary active:text-primary-foreground flex h-[44px] w-[68px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
630
+ >
631
+
632
+ </FastKey>
633
+ </div>
634
+ </div>
635
+ </div>
636
+ );
637
+ }
638
+
639
+ // Num mode - numbers and symbols
640
+ return (
641
+ <div ref={keyboardRef} className="bg-background flex flex-col select-none">
642
+ {/* Terminal shortcuts */}
643
+ <TerminalShortcutsBar
644
+ onKeyPress={onKeyPress}
645
+ isListening={isListening}
646
+ onMicToggle={toggleMic}
647
+ isMicSupported={isMicSupported}
648
+ />
649
+
650
+ <div className="flex flex-col gap-1.5 px-2 py-2 pb-[calc(0.5rem+env(safe-area-inset-bottom))]">
651
+ {/* Number row */}
652
+ <div className="flex gap-1">
653
+ {ROWS.numbers.map((char) => (
654
+ <Key key={char} char={char} />
655
+ ))}
656
+ </div>
657
+
658
+ {/* Symbols rows */}
659
+ <div className="flex gap-1">
660
+ {ROWS.symbols.map((char) => (
661
+ <Key key={char} char={char} />
662
+ ))}
663
+ </div>
664
+ <div className="flex gap-1">
665
+ {ROWS.symbolsMore.map((char) => (
666
+ <Key key={char} char={char} />
667
+ ))}
668
+ </div>
669
+
670
+ {/* Bottom row */}
671
+ <div className="flex gap-1">
672
+ <FastKey
673
+ dataKey="MODE_QUICK"
674
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[56px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
675
+ >
676
+ ^C
677
+ </FastKey>
678
+ <FastKey
679
+ dataKey="MODE_ABC"
680
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-xs font-medium select-none"
681
+ >
682
+ ABC
683
+ </FastKey>
684
+ <FastKey
685
+ dataKey="SPACE"
686
+ className="bg-secondary text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] flex-1 touch-manipulation items-center justify-center rounded-md text-sm select-none"
687
+ >
688
+ space
689
+ </FastKey>
690
+ <button
691
+ onTouchStart={startBackspace}
692
+ onTouchEnd={stopBackspace}
693
+ onTouchCancel={stopBackspace}
694
+ onMouseDown={startBackspace}
695
+ onMouseUp={stopBackspace}
696
+ onMouseLeave={stopBackspace}
697
+ className="bg-muted text-muted-foreground active:bg-primary active:text-primary-foreground flex h-[44px] w-[48px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
698
+ >
699
+
700
+ </button>
701
+ <FastKey
702
+ dataKey="ENTER"
703
+ className="bg-primary/30 text-primary active:bg-primary active:text-primary-foreground flex h-[44px] w-[68px] touch-manipulation items-center justify-center rounded-md text-sm font-medium select-none"
704
+ >
705
+
706
+ </FastKey>
707
+ </div>
708
+ </div>
709
+ </div>
710
+ );
711
+ }