@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.
- package/LICENSE +21 -0
- package/README.md +123 -0
- package/app/api/claude/hidden/route.ts +66 -0
- package/app/api/claude/projects/[name]/sessions/route.ts +71 -0
- package/app/api/claude/projects/route.ts +44 -0
- package/app/api/code-search/available/route.ts +12 -0
- package/app/api/code-search/route.ts +47 -0
- package/app/api/dev-servers/[id]/logs/route.ts +23 -0
- package/app/api/dev-servers/[id]/restart/route.ts +20 -0
- package/app/api/dev-servers/[id]/route.ts +51 -0
- package/app/api/dev-servers/[id]/stop/route.ts +20 -0
- package/app/api/dev-servers/detect/route.ts +39 -0
- package/app/api/dev-servers/route.ts +48 -0
- package/app/api/exec/route.ts +60 -0
- package/app/api/files/content/route.ts +76 -0
- package/app/api/files/route.ts +37 -0
- package/app/api/files/upload-temp/route.ts +41 -0
- package/app/api/git/check/route.ts +54 -0
- package/app/api/git/clone/route.ts +99 -0
- package/app/api/git/commit/route.ts +75 -0
- package/app/api/git/discard/route.ts +38 -0
- package/app/api/git/file-content/route.ts +64 -0
- package/app/api/git/history/[hash]/diff/route.ts +38 -0
- package/app/api/git/history/[hash]/route.ts +34 -0
- package/app/api/git/history/route.ts +27 -0
- package/app/api/git/multi-status/route.ts +46 -0
- package/app/api/git/pr/route.ts +164 -0
- package/app/api/git/push/route.ts +64 -0
- package/app/api/git/stage/route.ts +40 -0
- package/app/api/git/status/route.ts +51 -0
- package/app/api/git/unstage/route.ts +46 -0
- package/app/api/groups/[...path]/route.ts +136 -0
- package/app/api/groups/route.ts +93 -0
- package/app/api/orchestrate/spawn/route.ts +45 -0
- package/app/api/orchestrate/workers/[id]/route.ts +89 -0
- package/app/api/orchestrate/workers/route.ts +31 -0
- package/app/api/projects/[id]/detect/route.ts +27 -0
- package/app/api/projects/[id]/dev-servers/[dsId]/route.ts +66 -0
- package/app/api/projects/[id]/dev-servers/route.ts +51 -0
- package/app/api/projects/[id]/repositories/[repoId]/route.ts +67 -0
- package/app/api/projects/[id]/repositories/route.ts +74 -0
- package/app/api/projects/[id]/route.ts +108 -0
- package/app/api/projects/detect/route.ts +33 -0
- package/app/api/projects/route.ts +59 -0
- package/app/api/sessions/[id]/claude-session/route.ts +42 -0
- package/app/api/sessions/[id]/fork/route.ts +74 -0
- package/app/api/sessions/[id]/mcp-config/route.ts +34 -0
- package/app/api/sessions/[id]/messages/route.ts +60 -0
- package/app/api/sessions/[id]/pr/route.ts +188 -0
- package/app/api/sessions/[id]/preview/route.ts +42 -0
- package/app/api/sessions/[id]/route.ts +229 -0
- package/app/api/sessions/[id]/send-keys/route.ts +119 -0
- package/app/api/sessions/[id]/summarize/route.ts +331 -0
- package/app/api/sessions/init-script/route.ts +84 -0
- package/app/api/sessions/route.ts +209 -0
- package/app/api/sessions/status/route.ts +237 -0
- package/app/api/system/route.ts +9 -0
- package/app/api/tmux/kill-all/route.ts +57 -0
- package/app/api/tmux/rename/route.ts +30 -0
- package/app/globals.css +174 -0
- package/app/icon.svg +11 -0
- package/app/layout.tsx +122 -0
- package/app/page.tsx +629 -0
- package/components/ChatMessage.tsx +65 -0
- package/components/ChatView.tsx +276 -0
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +195 -0
- package/components/ClaudeProjects/ClaudeProjectsSection.tsx +89 -0
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +100 -0
- package/components/ClaudeProjects/index.ts +1 -0
- package/components/CodeSearch/CodeSearchResults.tsx +177 -0
- package/components/ConductorPanel.tsx +256 -0
- package/components/DevServers/DevServerCard.tsx +311 -0
- package/components/DevServers/DevServersSection.tsx +91 -0
- package/components/DevServers/ServerLogsModal.tsx +151 -0
- package/components/DevServers/StartServerDialog.tsx +359 -0
- package/components/DevServers/index.ts +4 -0
- package/components/DiffViewer/DiffModal.tsx +151 -0
- package/components/DiffViewer/UnifiedDiff.tsx +185 -0
- package/components/DiffViewer/index.tsx +2 -0
- package/components/DirectoryPicker.tsx +355 -0
- package/components/FileExplorer/FileEditor.tsx +276 -0
- package/components/FileExplorer/FileTabs.tsx +118 -0
- package/components/FileExplorer/FileTree.tsx +214 -0
- package/components/FileExplorer/HtmlRenderer.tsx +16 -0
- package/components/FileExplorer/MarkdownRenderer.tsx +18 -0
- package/components/FileExplorer/index.tsx +520 -0
- package/components/FilePicker.tsx +339 -0
- package/components/FolderPicker.tsx +201 -0
- package/components/GitDrawer/FileEditDialog.tsx +400 -0
- package/components/GitDrawer/index.tsx +464 -0
- package/components/GitPanel/CommitForm.tsx +205 -0
- package/components/GitPanel/CommitHistory.tsx +174 -0
- package/components/GitPanel/CommitItem.tsx +196 -0
- package/components/GitPanel/FileChanges.tsx +414 -0
- package/components/GitPanel/GitPanelTabs.tsx +39 -0
- package/components/GitPanel/index.tsx +817 -0
- package/components/MessageInput.tsx +82 -0
- package/components/NewClaudeSessionDialog.tsx +166 -0
- package/components/NewSessionDialog/AdvancedSettings.tsx +78 -0
- package/components/NewSessionDialog/AgentSelector.tsx +37 -0
- package/components/NewSessionDialog/CreatingOverlay.tsx +94 -0
- package/components/NewSessionDialog/NewSessionDialog.types.ts +136 -0
- package/components/NewSessionDialog/ProjectSelector.tsx +146 -0
- package/components/NewSessionDialog/WorkingDirectoryInput.tsx +55 -0
- package/components/NewSessionDialog/WorktreeSection.tsx +92 -0
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +370 -0
- package/components/NewSessionDialog/index.tsx +106 -0
- package/components/NotificationSettings.tsx +127 -0
- package/components/PRCreationModal.tsx +272 -0
- package/components/Pane/DesktopTabBar.tsx +353 -0
- package/components/Pane/MobileTabBar.tsx +210 -0
- package/components/Pane/OpenInVSCode.tsx +69 -0
- package/components/Pane/PaneSkeletons.tsx +57 -0
- package/components/Pane/index.tsx +558 -0
- package/components/PaneLayout.tsx +60 -0
- package/components/Projects/DevServersSection.tsx +140 -0
- package/components/Projects/DirectoryField.tsx +92 -0
- package/components/Projects/NewProjectDialog.tsx +188 -0
- package/components/Projects/NewProjectDialog.types.ts +46 -0
- package/components/Projects/ProjectCard.tsx +276 -0
- package/components/Projects/ProjectSettingsDialog.tsx +811 -0
- package/components/Projects/hooks/useNewProjectForm.ts +249 -0
- package/components/Projects/index.ts +3 -0
- package/components/Providers.tsx +49 -0
- package/components/QuickSwitcher.tsx +306 -0
- package/components/SessionList/KillAllConfirm.tsx +46 -0
- package/components/SessionList/SelectionToolbar.tsx +164 -0
- package/components/SessionList/SessionList.types.ts +37 -0
- package/components/SessionList/SessionListHeader.tsx +71 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +269 -0
- package/components/SessionList/index.tsx +189 -0
- package/components/ShellDrawer/index.tsx +106 -0
- package/components/SidebarFooter.tsx +55 -0
- package/components/Terminal/KeybarToggleButton.tsx +45 -0
- package/components/Terminal/ScrollToBottomButton.tsx +32 -0
- package/components/Terminal/SearchBar.tsx +71 -0
- package/components/Terminal/TerminalToolbar.tsx +551 -0
- package/components/Terminal/VirtualKeyboard.tsx +711 -0
- package/components/Terminal/constants.ts +20 -0
- package/components/Terminal/hooks/index.ts +5 -0
- package/components/Terminal/hooks/resize-handlers.ts +140 -0
- package/components/Terminal/hooks/terminal-init.ts +151 -0
- package/components/Terminal/hooks/touch-scroll.ts +155 -0
- package/components/Terminal/hooks/useTerminalConnection.ts +282 -0
- package/components/Terminal/hooks/useTerminalConnection.types.ts +39 -0
- package/components/Terminal/hooks/useTerminalSearch.ts +103 -0
- package/components/Terminal/hooks/websocket-connection.ts +274 -0
- package/components/Terminal/index.tsx +320 -0
- package/components/ThemeToggle.tsx +168 -0
- package/components/TmuxSessions.tsx +132 -0
- package/components/ToolCallDisplay.tsx +71 -0
- package/components/WorkerCard.tsx +245 -0
- package/components/a/ABadge.tsx +115 -0
- package/components/a/AButton.tsx +163 -0
- package/components/a/ADialog.tsx +93 -0
- package/components/a/ADropdownMenu.tsx +279 -0
- package/components/a/AIconButton.tsx +190 -0
- package/components/a/ASheet.tsx +150 -0
- package/components/a/ATooltip.tsx +77 -0
- package/components/a/index.ts +64 -0
- package/components/mobile/SwipeSidebar.tsx +122 -0
- package/components/ui/badge.tsx +41 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/context-menu.tsx +197 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/dropdown-menu.tsx +257 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/scroll-area.tsx +52 -0
- package/components/ui/select.tsx +159 -0
- package/components/ui/skeleton.tsx +111 -0
- package/components/ui/switch.tsx +31 -0
- package/components/ui/textarea.tsx +21 -0
- package/components/ui/tooltip.tsx +32 -0
- package/components/views/DesktopView.tsx +244 -0
- package/components/views/MobileView.tsx +110 -0
- package/components/views/types.ts +75 -0
- package/contexts/PaneContext.tsx +336 -0
- package/data/claude/index.ts +9 -0
- package/data/claude/keys.ts +6 -0
- package/data/claude/queries.ts +120 -0
- package/data/claude/useClaudeUpdates.ts +37 -0
- package/data/code-search/index.ts +2 -0
- package/data/code-search/keys.ts +7 -0
- package/data/code-search/queries.ts +61 -0
- package/data/dev-servers/index.ts +8 -0
- package/data/dev-servers/keys.ts +4 -0
- package/data/dev-servers/queries.ts +104 -0
- package/data/files/index.ts +3 -0
- package/data/files/keys.ts +4 -0
- package/data/files/queries.ts +25 -0
- package/data/git/keys.ts +15 -0
- package/data/git/queries.ts +395 -0
- package/data/groups/index.ts +1 -0
- package/data/groups/mutations.ts +95 -0
- package/data/projects/index.ts +10 -0
- package/data/projects/keys.ts +4 -0
- package/data/projects/queries.ts +193 -0
- package/data/repositories/index.ts +7 -0
- package/data/repositories/keys.ts +5 -0
- package/data/repositories/queries.ts +122 -0
- package/data/sessions/index.ts +12 -0
- package/data/sessions/keys.ts +8 -0
- package/data/sessions/queries.ts +218 -0
- package/data/statuses/index.ts +1 -0
- package/data/statuses/queries.ts +69 -0
- package/hooks/useCopyToClipboard.ts +48 -0
- package/hooks/useDevServersManager.ts +73 -0
- package/hooks/useDirectoryBrowser.ts +90 -0
- package/hooks/useDrawerAnimation.ts +27 -0
- package/hooks/useFileDrop.ts +87 -0
- package/hooks/useFileEditor.ts +184 -0
- package/hooks/useGroups.ts +37 -0
- package/hooks/useHomePath.ts +34 -0
- package/hooks/useKeyRepeat.ts +55 -0
- package/hooks/useKeybarVisibility.ts +42 -0
- package/hooks/useNotifications.ts +257 -0
- package/hooks/useProjects.ts +53 -0
- package/hooks/useSessionStatuses.ts +30 -0
- package/hooks/useSessions.ts +86 -0
- package/hooks/useSpeechRecognition.ts +124 -0
- package/hooks/useViewport.ts +32 -0
- package/hooks/useViewportHeight.ts +50 -0
- package/lib/async-operations.ts +35 -0
- package/lib/banner.ts +81 -0
- package/lib/claude/jsonl-cache.ts +86 -0
- package/lib/claude/jsonl-reader.ts +271 -0
- package/lib/claude/process-manager.ts +278 -0
- package/lib/claude/stream-parser.ts +173 -0
- package/lib/claude/types.ts +154 -0
- package/lib/claude/watcher.ts +71 -0
- package/lib/client/session-registry.ts +111 -0
- package/lib/code-search.ts +121 -0
- package/lib/db/index.ts +48 -0
- package/lib/db/migrations.ts +45 -0
- package/lib/db/queries.ts +460 -0
- package/lib/db/schema.ts +114 -0
- package/lib/db/types.ts +92 -0
- package/lib/db.ts +2 -0
- package/lib/dev-servers.ts +509 -0
- package/lib/diff-parser.ts +221 -0
- package/lib/env-setup.ts +285 -0
- package/lib/file-upload.ts +34 -0
- package/lib/file-utils.ts +50 -0
- package/lib/files.ts +207 -0
- package/lib/git-history.ts +294 -0
- package/lib/git-status.ts +391 -0
- package/lib/git.ts +257 -0
- package/lib/mcp-config.ts +81 -0
- package/lib/multi-repo-git.ts +179 -0
- package/lib/notifications.ts +219 -0
- package/lib/orchestration.ts +448 -0
- package/lib/panes.ts +232 -0
- package/lib/ports.ts +97 -0
- package/lib/pr-generation.ts +307 -0
- package/lib/pr.ts +234 -0
- package/lib/projects.ts +578 -0
- package/lib/providers/registry.ts +70 -0
- package/lib/providers.ts +121 -0
- package/lib/query-client.ts +14 -0
- package/lib/rangeSelectionUtils.ts +65 -0
- package/lib/status-detector.ts +375 -0
- package/lib/terminal-themes.ts +265 -0
- package/lib/theme-config.ts +327 -0
- package/lib/utils.ts +6 -0
- package/lib/worktrees.ts +262 -0
- package/mcp/orchestration-server.ts +438 -0
- package/package.json +139 -0
- package/postcss.config.mjs +7 -0
- package/public/icon.svg +10 -0
- package/public/icons/icon-128x128.png +0 -0
- package/public/icons/icon-144x144.png +0 -0
- package/public/icons/icon-152x152.png +0 -0
- package/public/icons/icon-192x192.png +0 -0
- package/public/icons/icon-384x384.png +0 -0
- package/public/icons/icon-512x512.png +0 -0
- package/public/icons/icon-72x72.png +0 -0
- package/public/icons/icon-96x96.png +0 -0
- package/public/manifest.json +61 -0
- package/public/sw.js +64 -0
- package/scripts/agent-os +91 -0
- package/scripts/install.sh +48 -0
- package/scripts/lib/ai-clis.sh +132 -0
- package/scripts/lib/commands.sh +487 -0
- package/scripts/lib/common.sh +89 -0
- package/scripts/lib/prerequisites.sh +462 -0
- package/scripts/setup.sh +134 -0
- package/server.ts +155 -0
- package/stores/fileOpen.ts +26 -0
- package/stores/index.ts +1 -0
- package/stores/initialPrompt.ts +24 -0
- package/stores/sessionSelection.ts +48 -0
- package/styles/themes.css +603 -0
- 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
|
+
}
|