@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,551 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState } from "react";
4
+ import {
5
+ Clipboard,
6
+ X,
7
+ Send,
8
+ Mic,
9
+ MicOff,
10
+ Paperclip,
11
+ FileText,
12
+ Plus,
13
+ Trash2,
14
+ MousePointer2,
15
+ Copy,
16
+ } from "lucide-react";
17
+ import { cn } from "@/lib/utils";
18
+ import { useSpeechRecognition } from "@/hooks/useSpeechRecognition";
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
+ ESC: "\x1b",
27
+ TAB: "\t",
28
+ CTRL_C: "\x03",
29
+ CTRL_D: "\x04",
30
+ CTRL_Z: "\x1a",
31
+ CTRL_L: "\x0c",
32
+ } as const;
33
+
34
+ interface TerminalToolbarProps {
35
+ onKeyPress: (key: string) => void;
36
+ onFilePicker?: () => void;
37
+ onCopy?: () => boolean; // Returns true if selection was copied
38
+ selectMode?: boolean;
39
+ onSelectModeChange?: (enabled: boolean) => void;
40
+ visible?: boolean;
41
+ }
42
+
43
+ interface Snippet {
44
+ id: string;
45
+ name: string;
46
+ content: string;
47
+ }
48
+
49
+ const SNIPPETS_STORAGE_KEY = "terminal-snippets";
50
+
51
+ const DEFAULT_SNIPPETS: Snippet[] = [
52
+ // Git shortcuts
53
+ { id: "default-1", name: "Git status", content: "git status" },
54
+ { id: "default-2", name: "Git diff", content: "git diff" },
55
+ { id: "default-3", name: "Git add all", content: "git add -A" },
56
+ { id: "default-4", name: "Git commit", content: 'git commit -m ""' },
57
+ { id: "default-5", name: "Git push", content: "git push" },
58
+ { id: "default-6", name: "Git pull", content: "git pull" },
59
+ // Claude Code prompts
60
+ { id: "default-7", name: "Continue", content: "continue" },
61
+ { id: "default-8", name: "Yes", content: "yes" },
62
+ { id: "default-9", name: "No", content: "no" },
63
+ {
64
+ id: "default-10",
65
+ name: "Explain this",
66
+ content: "explain what this code does",
67
+ },
68
+ { id: "default-11", name: "Fix errors", content: "fix the errors" },
69
+ {
70
+ id: "default-12",
71
+ name: "Run tests",
72
+ content: "run the tests and fix any failures",
73
+ },
74
+ {
75
+ id: "default-13",
76
+ name: "Commit changes",
77
+ content: "commit these changes with a descriptive message",
78
+ },
79
+ // Common commands
80
+ { id: "default-14", name: "List files", content: "ls -la" },
81
+ { id: "default-15", name: "NPM dev", content: "npm run dev" },
82
+ { id: "default-16", name: "NPM install", content: "npm install" },
83
+ ];
84
+
85
+ function getStoredSnippets(): Snippet[] {
86
+ if (typeof window === "undefined") return DEFAULT_SNIPPETS;
87
+ try {
88
+ const stored = localStorage.getItem(SNIPPETS_STORAGE_KEY);
89
+ if (!stored) {
90
+ // First time - save defaults
91
+ saveSnippets(DEFAULT_SNIPPETS);
92
+ return DEFAULT_SNIPPETS;
93
+ }
94
+ return JSON.parse(stored);
95
+ } catch {
96
+ return DEFAULT_SNIPPETS;
97
+ }
98
+ }
99
+
100
+ function saveSnippets(snippets: Snippet[]) {
101
+ localStorage.setItem(SNIPPETS_STORAGE_KEY, JSON.stringify(snippets));
102
+ }
103
+
104
+ // Snippets modal for saving/inserting common commands
105
+ function SnippetsModal({
106
+ open,
107
+ onClose,
108
+ onInsert,
109
+ }: {
110
+ open: boolean;
111
+ onClose: () => void;
112
+ onInsert: (content: string) => void;
113
+ }) {
114
+ const [snippets, setSnippets] = useState<Snippet[]>(() =>
115
+ getStoredSnippets()
116
+ );
117
+ const [isAdding, setIsAdding] = useState(false);
118
+ const [newName, setNewName] = useState("");
119
+ const [newContent, setNewContent] = useState("");
120
+
121
+ const handleAdd = () => {
122
+ if (newName.trim() && newContent.trim()) {
123
+ const newSnippet: Snippet = {
124
+ id: Date.now().toString(),
125
+ name: newName.trim(),
126
+ content: newContent.trim(),
127
+ };
128
+ const updated = [...snippets, newSnippet];
129
+ setSnippets(updated);
130
+ saveSnippets(updated);
131
+ setNewName("");
132
+ setNewContent("");
133
+ setIsAdding(false);
134
+ }
135
+ };
136
+
137
+ const handleDelete = (id: string) => {
138
+ const updated = snippets.filter((s) => s.id !== id);
139
+ setSnippets(updated);
140
+ saveSnippets(updated);
141
+ };
142
+
143
+ const handleInsert = (content: string) => {
144
+ onInsert(content);
145
+ onClose();
146
+ };
147
+
148
+ if (!open) return null;
149
+
150
+ return (
151
+ <div
152
+ className="fixed inset-0 z-50 flex items-end justify-center bg-black/50"
153
+ onClick={onClose}
154
+ >
155
+ <div
156
+ className="bg-background flex max-h-[70vh] w-full flex-col rounded-t-xl"
157
+ onClick={(e) => e.stopPropagation()}
158
+ >
159
+ {/* Header */}
160
+ <div className="border-border flex items-center justify-between border-b px-4 py-3">
161
+ <span className="text-sm font-medium">Snippets</span>
162
+ <div className="flex items-center gap-2">
163
+ <button
164
+ onClick={() => setIsAdding(!isAdding)}
165
+ className="hover:bg-muted rounded-md p-1.5"
166
+ >
167
+ <Plus className="h-5 w-5" />
168
+ </button>
169
+ <button
170
+ onClick={onClose}
171
+ className="hover:bg-muted rounded-md p-1.5"
172
+ >
173
+ <X className="h-5 w-5" />
174
+ </button>
175
+ </div>
176
+ </div>
177
+
178
+ {/* Add new snippet form */}
179
+ {isAdding && (
180
+ <div className="border-border bg-muted/50 border-b px-4 py-3">
181
+ <input
182
+ type="text"
183
+ value={newName}
184
+ onChange={(e) => setNewName(e.target.value)}
185
+ placeholder="Snippet name..."
186
+ className="bg-background focus:ring-primary mb-2 w-full rounded-lg px-3 py-2 text-sm focus:ring-2 focus:outline-none"
187
+ />
188
+ <textarea
189
+ value={newContent}
190
+ onChange={(e) => setNewContent(e.target.value)}
191
+ placeholder="Command or text..."
192
+ className="bg-background focus:ring-primary h-20 w-full resize-none rounded-lg px-3 py-2 font-mono text-sm focus:ring-2 focus:outline-none"
193
+ />
194
+ <button
195
+ onClick={handleAdd}
196
+ disabled={!newName.trim() || !newContent.trim()}
197
+ className="bg-primary text-primary-foreground mt-2 w-full rounded-lg py-2 font-medium disabled:opacity-50"
198
+ >
199
+ Save Snippet
200
+ </button>
201
+ </div>
202
+ )}
203
+
204
+ {/* Snippets list */}
205
+ <div className="flex-1 overflow-y-auto">
206
+ {snippets.length === 0 ? (
207
+ <div className="text-muted-foreground px-4 py-8 text-center text-sm">
208
+ No snippets yet. Tap + to add one.
209
+ </div>
210
+ ) : (
211
+ snippets.map((snippet) => (
212
+ <div
213
+ key={snippet.id}
214
+ className="border-border active:bg-muted flex items-center gap-2 border-b px-4 py-3"
215
+ >
216
+ <button
217
+ onClick={() => handleInsert(snippet.content)}
218
+ className="min-w-0 flex-1 text-left"
219
+ >
220
+ <div className="truncate text-sm font-medium">
221
+ {snippet.name}
222
+ </div>
223
+ <div className="text-muted-foreground truncate font-mono text-xs">
224
+ {snippet.content}
225
+ </div>
226
+ </button>
227
+ <button
228
+ onClick={() => handleDelete(snippet.id)}
229
+ className="hover:bg-destructive/20 text-muted-foreground hover:text-destructive rounded-md p-2"
230
+ >
231
+ <Trash2 className="h-4 w-4" />
232
+ </button>
233
+ </div>
234
+ ))
235
+ )}
236
+ </div>
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ // Paste modal for when clipboard API isn't available
243
+ function PasteModal({
244
+ open,
245
+ onClose,
246
+ onPaste,
247
+ }: {
248
+ open: boolean;
249
+ onClose: () => void;
250
+ onPaste: (text: string) => void;
251
+ }) {
252
+ const [text, setText] = useState("");
253
+
254
+ const handleSend = () => {
255
+ if (text) {
256
+ onPaste(text);
257
+ setText("");
258
+ onClose();
259
+ }
260
+ };
261
+
262
+ if (!open) return null;
263
+
264
+ return (
265
+ <div
266
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
267
+ onClick={onClose}
268
+ >
269
+ <div
270
+ className="bg-background w-[90%] max-w-md rounded-xl p-4"
271
+ onClick={(e) => e.stopPropagation()}
272
+ >
273
+ <div className="mb-3 flex items-center justify-between">
274
+ <span className="text-sm font-medium">Paste text</span>
275
+ <button onClick={onClose} className="hover:bg-muted rounded-md p-1">
276
+ <X className="h-5 w-5" />
277
+ </button>
278
+ </div>
279
+ <textarea
280
+ value={text}
281
+ onChange={(e) => setText(e.target.value)}
282
+ onPaste={(e) => {
283
+ const pasted = e.clipboardData?.getData("text");
284
+ if (pasted) {
285
+ e.preventDefault();
286
+ setText((prev) => prev + pasted);
287
+ }
288
+ }}
289
+ placeholder="Tap here, then long-press to paste..."
290
+ autoFocus
291
+ 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"
292
+ />
293
+ <button
294
+ onClick={handleSend}
295
+ disabled={!text}
296
+ 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"
297
+ >
298
+ <Send className="h-4 w-4" />
299
+ Send to Terminal
300
+ </button>
301
+ </div>
302
+ </div>
303
+ );
304
+ }
305
+
306
+ export function TerminalToolbar({
307
+ onKeyPress,
308
+ onFilePicker,
309
+ onCopy,
310
+ selectMode = false,
311
+ onSelectModeChange,
312
+ visible = true,
313
+ }: TerminalToolbarProps) {
314
+ const [showPasteModal, setShowPasteModal] = useState(false);
315
+ const [showSnippetsModal, setShowSnippetsModal] = useState(false);
316
+ const [shiftActive, setShiftActive] = useState(false);
317
+ const [copyFeedback, setCopyFeedback] = useState(false);
318
+
319
+ // Send text character-by-character to terminal
320
+ const sendText = useCallback(
321
+ (text: string) => {
322
+ for (const char of text) {
323
+ onKeyPress(char);
324
+ }
325
+ },
326
+ [onKeyPress]
327
+ );
328
+
329
+ const {
330
+ isListening,
331
+ isSupported: isMicSupported,
332
+ toggle: toggleMic,
333
+ } = useSpeechRecognition(sendText);
334
+
335
+ // Handle paste - try clipboard API first, fall back to modal
336
+ const handlePaste = useCallback(async () => {
337
+ try {
338
+ const text = await navigator.clipboard?.readText?.();
339
+ if (text) {
340
+ sendText(text);
341
+ return;
342
+ }
343
+ } catch {
344
+ // Clipboard API failed or unavailable
345
+ }
346
+ setShowPasteModal(true);
347
+ }, [sendText]);
348
+
349
+ // Handle copy with visual feedback
350
+ const handleCopy = useCallback(() => {
351
+ if (onCopy?.()) {
352
+ setCopyFeedback(true);
353
+ setTimeout(() => setCopyFeedback(false), 1000);
354
+ }
355
+ }, [onCopy]);
356
+
357
+ if (!visible) return null;
358
+
359
+ const buttons = [
360
+ { label: "Esc", key: SPECIAL_KEYS.ESC },
361
+ { label: "^C", key: SPECIAL_KEYS.CTRL_C, highlight: true },
362
+ { label: "Tab", key: SPECIAL_KEYS.TAB },
363
+ { label: "^D", key: SPECIAL_KEYS.CTRL_D },
364
+ { label: "←", key: SPECIAL_KEYS.LEFT },
365
+ { label: "→", key: SPECIAL_KEYS.RIGHT },
366
+ { label: "↑", key: SPECIAL_KEYS.UP },
367
+ { label: "↓", key: SPECIAL_KEYS.DOWN },
368
+ ];
369
+
370
+ return (
371
+ <>
372
+ <PasteModal
373
+ open={showPasteModal}
374
+ onClose={() => setShowPasteModal(false)}
375
+ onPaste={sendText}
376
+ />
377
+ <SnippetsModal
378
+ open={showSnippetsModal}
379
+ onClose={() => setShowSnippetsModal(false)}
380
+ onInsert={sendText}
381
+ />
382
+ <div
383
+ className="bg-background/95 border-border scrollbar-none flex items-center gap-1 overflow-x-auto border-t px-2 pt-1.5 pb-[max(0.375rem,env(safe-area-inset-bottom))] backdrop-blur"
384
+ onTouchEnd={(e) => e.stopPropagation()}
385
+ >
386
+ {/* Mic button */}
387
+ {isMicSupported && (
388
+ <button
389
+ type="button"
390
+ onMouseDown={(e) => e.preventDefault()}
391
+ onClick={(e) => {
392
+ e.preventDefault();
393
+ e.stopPropagation();
394
+ toggleMic();
395
+ }}
396
+ className={cn(
397
+ "flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
398
+ isListening
399
+ ? "animate-pulse bg-red-500 text-white"
400
+ : "bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground"
401
+ )}
402
+ >
403
+ {isListening ? (
404
+ <MicOff className="h-4 w-4" />
405
+ ) : (
406
+ <Mic className="h-4 w-4" />
407
+ )}
408
+ </button>
409
+ )}
410
+
411
+ {/* Paste button */}
412
+ <button
413
+ type="button"
414
+ onMouseDown={(e) => e.preventDefault()}
415
+ onClick={(e) => {
416
+ e.stopPropagation();
417
+ handlePaste();
418
+ }}
419
+ className="bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium"
420
+ >
421
+ <Clipboard className="h-4 w-4" />
422
+ </button>
423
+
424
+ {/* Select mode toggle */}
425
+ {onSelectModeChange && (
426
+ <button
427
+ type="button"
428
+ onMouseDown={(e) => e.preventDefault()}
429
+ onClick={(e) => {
430
+ e.stopPropagation();
431
+ onSelectModeChange(!selectMode);
432
+ }}
433
+ className={cn(
434
+ "flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
435
+ selectMode
436
+ ? "bg-primary text-primary-foreground"
437
+ : "bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground"
438
+ )}
439
+ >
440
+ <MousePointer2 className="h-4 w-4" />
441
+ </button>
442
+ )}
443
+
444
+ {/* Copy button - shown when in select mode */}
445
+ {selectMode && onCopy && (
446
+ <button
447
+ type="button"
448
+ onMouseDown={(e) => e.preventDefault()}
449
+ onClick={(e) => {
450
+ e.stopPropagation();
451
+ handleCopy();
452
+ }}
453
+ className={cn(
454
+ "flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
455
+ copyFeedback
456
+ ? "bg-green-500 text-white"
457
+ : "bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground"
458
+ )}
459
+ >
460
+ <Copy className="h-4 w-4" />
461
+ </button>
462
+ )}
463
+
464
+ {/* File picker button */}
465
+ {onFilePicker && (
466
+ <button
467
+ type="button"
468
+ onMouseDown={(e) => e.preventDefault()}
469
+ onClick={(e) => {
470
+ e.stopPropagation();
471
+ onFilePicker();
472
+ }}
473
+ className="bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium"
474
+ >
475
+ <Paperclip className="h-4 w-4" />
476
+ </button>
477
+ )}
478
+
479
+ {/* Snippets button */}
480
+ <button
481
+ type="button"
482
+ onMouseDown={(e) => e.preventDefault()}
483
+ onClick={(e) => {
484
+ e.stopPropagation();
485
+ setShowSnippetsModal(true);
486
+ }}
487
+ className="bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium"
488
+ >
489
+ <FileText className="h-4 w-4" />
490
+ </button>
491
+
492
+ {/* Divider */}
493
+ <div className="bg-border mx-1 h-6 w-px" />
494
+
495
+ {/* Shift toggle */}
496
+ <button
497
+ type="button"
498
+ onMouseDown={(e) => e.preventDefault()}
499
+ onClick={(e) => {
500
+ e.stopPropagation();
501
+ setShiftActive(!shiftActive);
502
+ }}
503
+ className={cn(
504
+ "flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
505
+ shiftActive
506
+ ? "bg-primary text-primary-foreground"
507
+ : "bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground"
508
+ )}
509
+ >
510
+
511
+ </button>
512
+
513
+ {/* Enter key - sends \n if shift active, \r otherwise */}
514
+ <button
515
+ type="button"
516
+ onMouseDown={(e) => e.preventDefault()}
517
+ onClick={(e) => {
518
+ e.stopPropagation();
519
+ onKeyPress(shiftActive ? "\n" : "\r");
520
+ setShiftActive(false);
521
+ }}
522
+ className="bg-secondary text-secondary-foreground active:bg-primary active:text-primary-foreground flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium"
523
+ >
524
+
525
+ </button>
526
+
527
+ {/* Special keys */}
528
+ {buttons.map((btn) => (
529
+ <button
530
+ type="button"
531
+ key={btn.label}
532
+ onMouseDown={(e) => e.preventDefault()}
533
+ onClick={(e) => {
534
+ e.stopPropagation();
535
+ onKeyPress(btn.key);
536
+ }}
537
+ className={cn(
538
+ "flex-shrink-0 rounded-md px-2.5 py-1.5 text-xs font-medium",
539
+ "active:bg-primary active:text-primary-foreground",
540
+ btn.highlight
541
+ ? "bg-red-500/20 text-red-500"
542
+ : "bg-secondary text-secondary-foreground"
543
+ )}
544
+ >
545
+ {btn.label}
546
+ </button>
547
+ ))}
548
+ </div>
549
+ </>
550
+ );
551
+ }