@siteboon/claude-code-ui 1.8.2

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 (106) hide show
  1. package/.env.example +12 -0
  2. package/.nvmrc +1 -0
  3. package/LICENSE +675 -0
  4. package/README.md +275 -0
  5. package/index.html +48 -0
  6. package/package.json +84 -0
  7. package/postcss.config.js +6 -0
  8. package/public/convert-icons.md +53 -0
  9. package/public/favicon.png +0 -0
  10. package/public/favicon.svg +9 -0
  11. package/public/generate-icons.js +49 -0
  12. package/public/icons/claude-ai-icon.svg +1 -0
  13. package/public/icons/cursor.svg +1 -0
  14. package/public/icons/generate-icons.md +19 -0
  15. package/public/icons/icon-128x128.png +0 -0
  16. package/public/icons/icon-128x128.svg +12 -0
  17. package/public/icons/icon-144x144.png +0 -0
  18. package/public/icons/icon-144x144.svg +12 -0
  19. package/public/icons/icon-152x152.png +0 -0
  20. package/public/icons/icon-152x152.svg +12 -0
  21. package/public/icons/icon-192x192.png +0 -0
  22. package/public/icons/icon-192x192.svg +12 -0
  23. package/public/icons/icon-384x384.png +0 -0
  24. package/public/icons/icon-384x384.svg +12 -0
  25. package/public/icons/icon-512x512.png +0 -0
  26. package/public/icons/icon-512x512.svg +12 -0
  27. package/public/icons/icon-72x72.png +0 -0
  28. package/public/icons/icon-72x72.svg +12 -0
  29. package/public/icons/icon-96x96.png +0 -0
  30. package/public/icons/icon-96x96.svg +12 -0
  31. package/public/icons/icon-template.svg +12 -0
  32. package/public/logo.svg +9 -0
  33. package/public/manifest.json +61 -0
  34. package/public/screenshots/cli-selection.png +0 -0
  35. package/public/screenshots/desktop-main.png +0 -0
  36. package/public/screenshots/mobile-chat.png +0 -0
  37. package/public/screenshots/tools-modal.png +0 -0
  38. package/public/sw.js +49 -0
  39. package/server/claude-cli.js +391 -0
  40. package/server/cursor-cli.js +250 -0
  41. package/server/database/db.js +86 -0
  42. package/server/database/init.sql +16 -0
  43. package/server/index.js +1167 -0
  44. package/server/middleware/auth.js +80 -0
  45. package/server/projects.js +1063 -0
  46. package/server/routes/auth.js +135 -0
  47. package/server/routes/cursor.js +794 -0
  48. package/server/routes/git.js +823 -0
  49. package/server/routes/mcp-utils.js +48 -0
  50. package/server/routes/mcp.js +552 -0
  51. package/server/routes/taskmaster.js +1971 -0
  52. package/server/utils/mcp-detector.js +198 -0
  53. package/server/utils/taskmaster-websocket.js +129 -0
  54. package/src/App.jsx +751 -0
  55. package/src/components/ChatInterface.jsx +3485 -0
  56. package/src/components/ClaudeLogo.jsx +11 -0
  57. package/src/components/ClaudeStatus.jsx +107 -0
  58. package/src/components/CodeEditor.jsx +422 -0
  59. package/src/components/CreateTaskModal.jsx +88 -0
  60. package/src/components/CursorLogo.jsx +9 -0
  61. package/src/components/DarkModeToggle.jsx +35 -0
  62. package/src/components/DiffViewer.jsx +41 -0
  63. package/src/components/ErrorBoundary.jsx +73 -0
  64. package/src/components/FileTree.jsx +480 -0
  65. package/src/components/GitPanel.jsx +1283 -0
  66. package/src/components/ImageViewer.jsx +54 -0
  67. package/src/components/LoginForm.jsx +110 -0
  68. package/src/components/MainContent.jsx +577 -0
  69. package/src/components/MicButton.jsx +272 -0
  70. package/src/components/MobileNav.jsx +88 -0
  71. package/src/components/NextTaskBanner.jsx +695 -0
  72. package/src/components/PRDEditor.jsx +871 -0
  73. package/src/components/ProtectedRoute.jsx +44 -0
  74. package/src/components/QuickSettingsPanel.jsx +262 -0
  75. package/src/components/Settings.jsx +2023 -0
  76. package/src/components/SetupForm.jsx +135 -0
  77. package/src/components/Shell.jsx +663 -0
  78. package/src/components/Sidebar.jsx +1665 -0
  79. package/src/components/StandaloneShell.jsx +106 -0
  80. package/src/components/TaskCard.jsx +210 -0
  81. package/src/components/TaskDetail.jsx +406 -0
  82. package/src/components/TaskIndicator.jsx +108 -0
  83. package/src/components/TaskList.jsx +1054 -0
  84. package/src/components/TaskMasterSetupWizard.jsx +603 -0
  85. package/src/components/TaskMasterStatus.jsx +86 -0
  86. package/src/components/TodoList.jsx +91 -0
  87. package/src/components/Tooltip.jsx +91 -0
  88. package/src/components/ui/badge.jsx +31 -0
  89. package/src/components/ui/button.jsx +46 -0
  90. package/src/components/ui/input.jsx +19 -0
  91. package/src/components/ui/scroll-area.jsx +23 -0
  92. package/src/contexts/AuthContext.jsx +158 -0
  93. package/src/contexts/TaskMasterContext.jsx +324 -0
  94. package/src/contexts/TasksSettingsContext.jsx +95 -0
  95. package/src/contexts/ThemeContext.jsx +94 -0
  96. package/src/contexts/WebSocketContext.jsx +29 -0
  97. package/src/hooks/useAudioRecorder.js +109 -0
  98. package/src/hooks/useVersionCheck.js +39 -0
  99. package/src/index.css +822 -0
  100. package/src/lib/utils.js +6 -0
  101. package/src/main.jsx +10 -0
  102. package/src/utils/api.js +141 -0
  103. package/src/utils/websocket.js +109 -0
  104. package/src/utils/whisper.js +37 -0
  105. package/tailwind.config.js +63 -0
  106. package/vite.config.js +29 -0
@@ -0,0 +1,272 @@
1
+ import React, { useState, useEffect, useRef } from 'react';
2
+ import { Mic, Loader2, Brain } from 'lucide-react';
3
+ import { transcribeWithWhisper } from '../utils/whisper';
4
+
5
+ export function MicButton({ onTranscript, className = '' }) {
6
+ const [state, setState] = useState('idle'); // idle, recording, transcribing, processing
7
+ const [error, setError] = useState(null);
8
+ const [isSupported, setIsSupported] = useState(true);
9
+
10
+ const mediaRecorderRef = useRef(null);
11
+ const streamRef = useRef(null);
12
+ const chunksRef = useRef([]);
13
+ const lastTapRef = useRef(0);
14
+
15
+ // Check microphone support on mount
16
+ useEffect(() => {
17
+ const checkSupport = () => {
18
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
19
+ setIsSupported(false);
20
+ setError('Microphone not supported. Please use HTTPS or a modern browser.');
21
+ return;
22
+ }
23
+
24
+ // Additional check for secure context
25
+ if (location.protocol !== 'https:' && location.hostname !== 'localhost') {
26
+ setIsSupported(false);
27
+ setError('Microphone requires HTTPS. Please use a secure connection.');
28
+ return;
29
+ }
30
+
31
+ setIsSupported(true);
32
+ setError(null);
33
+ };
34
+
35
+ checkSupport();
36
+ }, []);
37
+
38
+ // Start recording
39
+ const startRecording = async () => {
40
+ try {
41
+ console.log('Starting recording...');
42
+ setError(null);
43
+ chunksRef.current = [];
44
+
45
+ // Check if getUserMedia is available
46
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
47
+ throw new Error('Microphone access not available. Please use HTTPS or a supported browser.');
48
+ }
49
+
50
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
51
+ streamRef.current = stream;
52
+
53
+ const mimeType = MediaRecorder.isTypeSupported('audio/webm') ? 'audio/webm' : 'audio/mp4';
54
+ const recorder = new MediaRecorder(stream, { mimeType });
55
+ mediaRecorderRef.current = recorder;
56
+
57
+ recorder.ondataavailable = (e) => {
58
+ if (e.data.size > 0) {
59
+ chunksRef.current.push(e.data);
60
+ }
61
+ };
62
+
63
+ recorder.onstop = async () => {
64
+ console.log('Recording stopped, creating blob...');
65
+ const blob = new Blob(chunksRef.current, { type: mimeType });
66
+
67
+ // Clean up stream
68
+ if (streamRef.current) {
69
+ streamRef.current.getTracks().forEach(track => track.stop());
70
+ streamRef.current = null;
71
+ }
72
+
73
+ // Start transcribing
74
+ setState('transcribing');
75
+
76
+ // Check if we're in an enhancement mode
77
+ const whisperMode = window.localStorage.getItem('whisperMode') || 'default';
78
+ const isEnhancementMode = whisperMode === 'prompt' || whisperMode === 'vibe' || whisperMode === 'instructions' || whisperMode === 'architect';
79
+
80
+ // Set up a timer to switch to processing state for enhancement modes
81
+ let processingTimer;
82
+ if (isEnhancementMode) {
83
+ processingTimer = setTimeout(() => {
84
+ setState('processing');
85
+ }, 2000); // Switch to processing after 2 seconds
86
+ }
87
+
88
+ try {
89
+ const text = await transcribeWithWhisper(blob);
90
+ if (text && onTranscript) {
91
+ onTranscript(text);
92
+ }
93
+ } catch (err) {
94
+ console.error('Transcription error:', err);
95
+ setError(err.message);
96
+ } finally {
97
+ if (processingTimer) {
98
+ clearTimeout(processingTimer);
99
+ }
100
+ setState('idle');
101
+ }
102
+ };
103
+
104
+ recorder.start();
105
+ setState('recording');
106
+ console.log('Recording started successfully');
107
+ } catch (err) {
108
+ console.error('Failed to start recording:', err);
109
+
110
+ // Provide specific error messages based on error type
111
+ let errorMessage = 'Microphone access failed';
112
+
113
+ if (err.name === 'NotAllowedError') {
114
+ errorMessage = 'Microphone access denied. Please allow microphone permissions.';
115
+ } else if (err.name === 'NotFoundError') {
116
+ errorMessage = 'No microphone found. Please check your audio devices.';
117
+ } else if (err.name === 'NotSupportedError') {
118
+ errorMessage = 'Microphone not supported by this browser.';
119
+ } else if (err.name === 'NotReadableError') {
120
+ errorMessage = 'Microphone is being used by another application.';
121
+ } else if (err.message.includes('HTTPS')) {
122
+ errorMessage = err.message;
123
+ }
124
+
125
+ setError(errorMessage);
126
+ setState('idle');
127
+ }
128
+ };
129
+
130
+ // Stop recording
131
+ const stopRecording = () => {
132
+ console.log('Stopping recording...');
133
+ if (mediaRecorderRef.current && mediaRecorderRef.current.state === 'recording') {
134
+ mediaRecorderRef.current.stop();
135
+ // Don't set state here - let the onstop handler do it
136
+ } else {
137
+ // If recorder isn't in recording state, force cleanup
138
+ console.log('Recorder not in recording state, forcing cleanup');
139
+ if (streamRef.current) {
140
+ streamRef.current.getTracks().forEach(track => track.stop());
141
+ streamRef.current = null;
142
+ }
143
+ setState('idle');
144
+ }
145
+ };
146
+
147
+ // Handle button click
148
+ const handleClick = (e) => {
149
+ // Prevent double firing on mobile
150
+ if (e) {
151
+ e.preventDefault();
152
+ e.stopPropagation();
153
+ }
154
+
155
+ // Don't proceed if microphone is not supported
156
+ if (!isSupported) {
157
+ return;
158
+ }
159
+
160
+ // Debounce for mobile double-tap issue
161
+ const now = Date.now();
162
+ if (now - lastTapRef.current < 300) {
163
+ console.log('Ignoring rapid tap');
164
+ return;
165
+ }
166
+ lastTapRef.current = now;
167
+
168
+ console.log('Button clicked, current state:', state);
169
+
170
+ if (state === 'idle') {
171
+ startRecording();
172
+ } else if (state === 'recording') {
173
+ stopRecording();
174
+ }
175
+ // Do nothing if transcribing or processing
176
+ };
177
+
178
+ // Clean up on unmount
179
+ useEffect(() => {
180
+ return () => {
181
+ if (streamRef.current) {
182
+ streamRef.current.getTracks().forEach(track => track.stop());
183
+ }
184
+ };
185
+ }, []);
186
+
187
+ // Button appearance based on state
188
+ const getButtonAppearance = () => {
189
+ if (!isSupported) {
190
+ return {
191
+ icon: <Mic className="w-5 h-5" />,
192
+ className: 'bg-gray-400 cursor-not-allowed',
193
+ disabled: true
194
+ };
195
+ }
196
+
197
+ switch (state) {
198
+ case 'recording':
199
+ return {
200
+ icon: <Mic className="w-5 h-5 text-white" />,
201
+ className: 'bg-red-500 hover:bg-red-600 animate-pulse',
202
+ disabled: false
203
+ };
204
+ case 'transcribing':
205
+ return {
206
+ icon: <Loader2 className="w-5 h-5 animate-spin" />,
207
+ className: 'bg-blue-500 hover:bg-blue-600',
208
+ disabled: true
209
+ };
210
+ case 'processing':
211
+ return {
212
+ icon: <Brain className="w-5 h-5 animate-pulse" />,
213
+ className: 'bg-purple-500 hover:bg-purple-600',
214
+ disabled: true
215
+ };
216
+ default: // idle
217
+ return {
218
+ icon: <Mic className="w-5 h-5" />,
219
+ className: 'bg-gray-700 hover:bg-gray-600',
220
+ disabled: false
221
+ };
222
+ }
223
+ };
224
+
225
+ const { icon, className: buttonClass, disabled } = getButtonAppearance();
226
+
227
+ return (
228
+ <div className="relative">
229
+ <button
230
+ type="button"
231
+ style={{
232
+ backgroundColor: state === 'recording' ? '#ef4444' :
233
+ state === 'transcribing' ? '#3b82f6' :
234
+ state === 'processing' ? '#a855f7' :
235
+ '#374151'
236
+ }}
237
+ className={`
238
+ flex items-center justify-center
239
+ w-12 h-12 rounded-full
240
+ text-white transition-all duration-200
241
+ focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500
242
+ dark:ring-offset-gray-800
243
+ touch-action-manipulation
244
+ ${disabled ? 'cursor-not-allowed opacity-75' : 'cursor-pointer'}
245
+ ${state === 'recording' ? 'animate-pulse' : ''}
246
+ hover:opacity-90
247
+ ${className}
248
+ `}
249
+ onClick={handleClick}
250
+ disabled={disabled}
251
+ >
252
+ {icon}
253
+ </button>
254
+
255
+ {error && (
256
+ <div className="absolute top-full mt-2 left-1/2 transform -translate-x-1/2
257
+ bg-red-500 text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10
258
+ animate-fade-in">
259
+ {error}
260
+ </div>
261
+ )}
262
+
263
+ {state === 'recording' && (
264
+ <div className="absolute -inset-1 rounded-full border-2 border-red-500 animate-ping pointer-events-none" />
265
+ )}
266
+
267
+ {state === 'processing' && (
268
+ <div className="absolute -inset-1 rounded-full border-2 border-purple-500 animate-ping pointer-events-none" />
269
+ )}
270
+ </div>
271
+ );
272
+ }
@@ -0,0 +1,88 @@
1
+ import React from 'react';
2
+ import { MessageSquare, Folder, Terminal, GitBranch, Globe, CheckSquare } from 'lucide-react';
3
+ import { useTasksSettings } from '../contexts/TasksSettingsContext';
4
+
5
+ function MobileNav({ activeTab, setActiveTab, isInputFocused }) {
6
+ const { tasksEnabled } = useTasksSettings();
7
+ // Detect dark mode
8
+ const isDarkMode = document.documentElement.classList.contains('dark');
9
+ const navItems = [
10
+ {
11
+ id: 'chat',
12
+ icon: MessageSquare,
13
+ onClick: () => setActiveTab('chat')
14
+ },
15
+ {
16
+ id: 'shell',
17
+ icon: Terminal,
18
+ onClick: () => setActiveTab('shell')
19
+ },
20
+ {
21
+ id: 'files',
22
+ icon: Folder,
23
+ onClick: () => setActiveTab('files')
24
+ },
25
+ {
26
+ id: 'git',
27
+ icon: GitBranch,
28
+ onClick: () => setActiveTab('git')
29
+ },
30
+ // Conditionally add tasks tab if enabled
31
+ ...(tasksEnabled ? [{
32
+ id: 'tasks',
33
+ icon: CheckSquare,
34
+ onClick: () => setActiveTab('tasks')
35
+ }] : [])
36
+ ];
37
+
38
+ return (
39
+ <>
40
+ <style>
41
+ {`
42
+ .mobile-nav-container {
43
+ background-color: ${isDarkMode ? '#1f2937' : '#ffffff'} !important;
44
+ }
45
+ .mobile-nav-container:hover {
46
+ background-color: ${isDarkMode ? '#1f2937' : '#ffffff'} !important;
47
+ }
48
+ `}
49
+ </style>
50
+ <div
51
+ className={`mobile-nav-container fixed bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 z-50 ios-bottom-safe transform transition-transform duration-300 ease-in-out shadow-lg ${
52
+ isInputFocused ? 'translate-y-full' : 'translate-y-0'
53
+ }`}
54
+ >
55
+ <div className="flex items-center justify-around py-1">
56
+ {navItems.map((item) => {
57
+ const Icon = item.icon;
58
+ const isActive = activeTab === item.id;
59
+
60
+ return (
61
+ <button
62
+ key={item.id}
63
+ onClick={item.onClick}
64
+ onTouchStart={(e) => {
65
+ e.preventDefault();
66
+ item.onClick();
67
+ }}
68
+ className={`flex items-center justify-center p-2 rounded-lg min-h-[40px] min-w-[40px] relative touch-manipulation ${
69
+ isActive
70
+ ? 'text-blue-600 dark:text-blue-400'
71
+ : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
72
+ }`}
73
+ aria-label={item.id}
74
+ >
75
+ <Icon className="w-5 h-5" />
76
+ {isActive && (
77
+ <div className="absolute top-0 left-1/2 transform -translate-x-1/2 w-6 h-0.5 bg-blue-600 dark:bg-blue-400 rounded-full" />
78
+ )}
79
+ </button>
80
+ );
81
+ })}
82
+ </div>
83
+ </div>
84
+ </>
85
+ );
86
+ }
87
+
88
+ export default MobileNav;