@siteboon/claude-code-ui 1.8.2 → 1.8.4

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 (92) hide show
  1. package/dist/assets/index-CeR_JfKq.js +895 -0
  2. package/dist/assets/index-Co7ALK3i.css +32 -0
  3. package/{index.html → dist/index.html} +2 -1
  4. package/package.json +6 -1
  5. package/server/database/auth.db +0 -0
  6. package/.env.example +0 -12
  7. package/.nvmrc +0 -1
  8. package/postcss.config.js +0 -6
  9. package/src/App.jsx +0 -751
  10. package/src/components/ChatInterface.jsx +0 -3485
  11. package/src/components/ClaudeLogo.jsx +0 -11
  12. package/src/components/ClaudeStatus.jsx +0 -107
  13. package/src/components/CodeEditor.jsx +0 -422
  14. package/src/components/CreateTaskModal.jsx +0 -88
  15. package/src/components/CursorLogo.jsx +0 -9
  16. package/src/components/DarkModeToggle.jsx +0 -35
  17. package/src/components/DiffViewer.jsx +0 -41
  18. package/src/components/ErrorBoundary.jsx +0 -73
  19. package/src/components/FileTree.jsx +0 -480
  20. package/src/components/GitPanel.jsx +0 -1283
  21. package/src/components/ImageViewer.jsx +0 -54
  22. package/src/components/LoginForm.jsx +0 -110
  23. package/src/components/MainContent.jsx +0 -577
  24. package/src/components/MicButton.jsx +0 -272
  25. package/src/components/MobileNav.jsx +0 -88
  26. package/src/components/NextTaskBanner.jsx +0 -695
  27. package/src/components/PRDEditor.jsx +0 -871
  28. package/src/components/ProtectedRoute.jsx +0 -44
  29. package/src/components/QuickSettingsPanel.jsx +0 -262
  30. package/src/components/Settings.jsx +0 -2023
  31. package/src/components/SetupForm.jsx +0 -135
  32. package/src/components/Shell.jsx +0 -663
  33. package/src/components/Sidebar.jsx +0 -1665
  34. package/src/components/StandaloneShell.jsx +0 -106
  35. package/src/components/TaskCard.jsx +0 -210
  36. package/src/components/TaskDetail.jsx +0 -406
  37. package/src/components/TaskIndicator.jsx +0 -108
  38. package/src/components/TaskList.jsx +0 -1054
  39. package/src/components/TaskMasterSetupWizard.jsx +0 -603
  40. package/src/components/TaskMasterStatus.jsx +0 -86
  41. package/src/components/TodoList.jsx +0 -91
  42. package/src/components/Tooltip.jsx +0 -91
  43. package/src/components/ui/badge.jsx +0 -31
  44. package/src/components/ui/button.jsx +0 -46
  45. package/src/components/ui/input.jsx +0 -19
  46. package/src/components/ui/scroll-area.jsx +0 -23
  47. package/src/contexts/AuthContext.jsx +0 -158
  48. package/src/contexts/TaskMasterContext.jsx +0 -324
  49. package/src/contexts/TasksSettingsContext.jsx +0 -95
  50. package/src/contexts/ThemeContext.jsx +0 -94
  51. package/src/contexts/WebSocketContext.jsx +0 -29
  52. package/src/hooks/useAudioRecorder.js +0 -109
  53. package/src/hooks/useVersionCheck.js +0 -39
  54. package/src/index.css +0 -822
  55. package/src/lib/utils.js +0 -6
  56. package/src/main.jsx +0 -10
  57. package/src/utils/api.js +0 -141
  58. package/src/utils/websocket.js +0 -109
  59. package/src/utils/whisper.js +0 -37
  60. package/tailwind.config.js +0 -63
  61. package/vite.config.js +0 -29
  62. /package/{public → dist}/convert-icons.md +0 -0
  63. /package/{public → dist}/favicon.png +0 -0
  64. /package/{public → dist}/favicon.svg +0 -0
  65. /package/{public → dist}/generate-icons.js +0 -0
  66. /package/{public → dist}/icons/claude-ai-icon.svg +0 -0
  67. /package/{public → dist}/icons/cursor.svg +0 -0
  68. /package/{public → dist}/icons/generate-icons.md +0 -0
  69. /package/{public → dist}/icons/icon-128x128.png +0 -0
  70. /package/{public → dist}/icons/icon-128x128.svg +0 -0
  71. /package/{public → dist}/icons/icon-144x144.png +0 -0
  72. /package/{public → dist}/icons/icon-144x144.svg +0 -0
  73. /package/{public → dist}/icons/icon-152x152.png +0 -0
  74. /package/{public → dist}/icons/icon-152x152.svg +0 -0
  75. /package/{public → dist}/icons/icon-192x192.png +0 -0
  76. /package/{public → dist}/icons/icon-192x192.svg +0 -0
  77. /package/{public → dist}/icons/icon-384x384.png +0 -0
  78. /package/{public → dist}/icons/icon-384x384.svg +0 -0
  79. /package/{public → dist}/icons/icon-512x512.png +0 -0
  80. /package/{public → dist}/icons/icon-512x512.svg +0 -0
  81. /package/{public → dist}/icons/icon-72x72.png +0 -0
  82. /package/{public → dist}/icons/icon-72x72.svg +0 -0
  83. /package/{public → dist}/icons/icon-96x96.png +0 -0
  84. /package/{public → dist}/icons/icon-96x96.svg +0 -0
  85. /package/{public → dist}/icons/icon-template.svg +0 -0
  86. /package/{public → dist}/logo.svg +0 -0
  87. /package/{public → dist}/manifest.json +0 -0
  88. /package/{public → dist}/screenshots/cli-selection.png +0 -0
  89. /package/{public → dist}/screenshots/desktop-main.png +0 -0
  90. /package/{public → dist}/screenshots/mobile-chat.png +0 -0
  91. /package/{public → dist}/screenshots/tools-modal.png +0 -0
  92. /package/{public → dist}/sw.js +0 -0
@@ -1,2023 +0,0 @@
1
- import { useState, useEffect } from 'react';
2
- import { Button } from './ui/button';
3
- import { Input } from './ui/input';
4
- import { Badge } from './ui/badge';
5
- import { X, Plus, Settings as SettingsIcon, Shield, AlertTriangle, Moon, Sun, Server, Edit3, Trash2, Globe, Terminal, Zap, FolderOpen, LogIn } from 'lucide-react';
6
- import { useTheme } from '../contexts/ThemeContext';
7
- import { useTasksSettings } from '../contexts/TasksSettingsContext';
8
- import StandaloneShell from './StandaloneShell';
9
- import ClaudeLogo from './ClaudeLogo';
10
- import CursorLogo from './CursorLogo';
11
-
12
- function Settings({ isOpen, onClose, projects = [] }) {
13
- const { isDarkMode, toggleDarkMode } = useTheme();
14
- const {
15
- tasksEnabled,
16
- setTasksEnabled,
17
- isTaskMasterInstalled,
18
- isTaskMasterReady,
19
- installationStatus,
20
- isCheckingInstallation
21
- } = useTasksSettings();
22
- const [allowedTools, setAllowedTools] = useState([]);
23
- const [disallowedTools, setDisallowedTools] = useState([]);
24
- const [newAllowedTool, setNewAllowedTool] = useState('');
25
- const [newDisallowedTool, setNewDisallowedTool] = useState('');
26
- const [skipPermissions, setSkipPermissions] = useState(false);
27
- const [isSaving, setIsSaving] = useState(false);
28
- const [saveStatus, setSaveStatus] = useState(null);
29
- const [projectSortOrder, setProjectSortOrder] = useState('name');
30
-
31
- const [mcpServers, setMcpServers] = useState([]);
32
- const [showMcpForm, setShowMcpForm] = useState(false);
33
- const [editingMcpServer, setEditingMcpServer] = useState(null);
34
- const [mcpFormData, setMcpFormData] = useState({
35
- name: '',
36
- type: 'stdio',
37
- scope: 'user',
38
- projectPath: '', // For local scope
39
- config: {
40
- command: '',
41
- args: [],
42
- env: {},
43
- url: '',
44
- headers: {},
45
- timeout: 30000
46
- },
47
- jsonInput: '', // For JSON import
48
- importMode: 'form' // 'form' or 'json'
49
- });
50
- const [mcpLoading, setMcpLoading] = useState(false);
51
- const [mcpTestResults, setMcpTestResults] = useState({});
52
- const [mcpServerTools, setMcpServerTools] = useState({});
53
- const [mcpToolsLoading, setMcpToolsLoading] = useState({});
54
- const [activeTab, setActiveTab] = useState('tools');
55
- const [jsonValidationError, setJsonValidationError] = useState('');
56
- const [toolsProvider, setToolsProvider] = useState('claude'); // 'claude' or 'cursor'
57
-
58
- // Cursor-specific states
59
- const [cursorAllowedCommands, setCursorAllowedCommands] = useState([]);
60
- const [cursorDisallowedCommands, setCursorDisallowedCommands] = useState([]);
61
- const [cursorSkipPermissions, setCursorSkipPermissions] = useState(false);
62
- const [newCursorCommand, setNewCursorCommand] = useState('');
63
- const [newCursorDisallowedCommand, setNewCursorDisallowedCommand] = useState('');
64
- const [cursorMcpServers, setCursorMcpServers] = useState([]);
65
-
66
- // Login modal states
67
- const [showLoginModal, setShowLoginModal] = useState(false);
68
- const [loginProvider, setLoginProvider] = useState(''); // 'claude' or 'cursor'
69
- const [selectedProject, setSelectedProject] = useState(null);
70
- // Common tool patterns for Claude
71
- const commonTools = [
72
- 'Bash(git log:*)',
73
- 'Bash(git diff:*)',
74
- 'Bash(git status:*)',
75
- 'Write',
76
- 'Read',
77
- 'Edit',
78
- 'Glob',
79
- 'Grep',
80
- 'MultiEdit',
81
- 'Task',
82
- 'TodoWrite',
83
- 'TodoRead',
84
- 'WebFetch',
85
- 'WebSearch'
86
- ];
87
-
88
- // Common shell commands for Cursor
89
- const commonCursorCommands = [
90
- 'Shell(ls)',
91
- 'Shell(mkdir)',
92
- 'Shell(cd)',
93
- 'Shell(cat)',
94
- 'Shell(echo)',
95
- 'Shell(git status)',
96
- 'Shell(git diff)',
97
- 'Shell(git log)',
98
- 'Shell(npm install)',
99
- 'Shell(npm run)',
100
- 'Shell(python)',
101
- 'Shell(node)'
102
- ];
103
-
104
- // Fetch Cursor MCP servers
105
- const fetchCursorMcpServers = async () => {
106
- try {
107
- const token = localStorage.getItem('auth-token');
108
- const response = await fetch('/api/cursor/mcp', {
109
- headers: {
110
- 'Authorization': `Bearer ${token}`,
111
- 'Content-Type': 'application/json'
112
- }
113
- });
114
-
115
- if (response.ok) {
116
- const data = await response.json();
117
- setCursorMcpServers(data.servers || []);
118
- } else {
119
- console.error('Failed to fetch Cursor MCP servers');
120
- }
121
- } catch (error) {
122
- console.error('Error fetching Cursor MCP servers:', error);
123
- }
124
- };
125
-
126
- // MCP API functions
127
- const fetchMcpServers = async () => {
128
- try {
129
- const token = localStorage.getItem('auth-token');
130
-
131
- // Try to read directly from config files for complete details
132
- const configResponse = await fetch('/api/mcp/config/read', {
133
- headers: {
134
- 'Authorization': `Bearer ${token}`,
135
- 'Content-Type': 'application/json'
136
- }
137
- });
138
-
139
- if (configResponse.ok) {
140
- const configData = await configResponse.json();
141
- if (configData.success && configData.servers) {
142
- setMcpServers(configData.servers);
143
- return;
144
- }
145
- }
146
-
147
- // Fallback to Claude CLI
148
- const cliResponse = await fetch('/api/mcp/cli/list', {
149
- headers: {
150
- 'Authorization': `Bearer ${token}`,
151
- 'Content-Type': 'application/json'
152
- }
153
- });
154
-
155
- if (cliResponse.ok) {
156
- const cliData = await cliResponse.json();
157
- if (cliData.success && cliData.servers) {
158
- // Convert CLI format to our format
159
- const servers = cliData.servers.map(server => ({
160
- id: server.name,
161
- name: server.name,
162
- type: server.type,
163
- scope: 'user',
164
- config: {
165
- command: server.command || '',
166
- args: server.args || [],
167
- env: server.env || {},
168
- url: server.url || '',
169
- headers: server.headers || {},
170
- timeout: 30000
171
- },
172
- created: new Date().toISOString(),
173
- updated: new Date().toISOString()
174
- }));
175
- setMcpServers(servers);
176
- return;
177
- }
178
- }
179
-
180
- // Final fallback to direct config reading
181
- const response = await fetch('/api/mcp/servers?scope=user', {
182
- headers: {
183
- 'Authorization': `Bearer ${token}`,
184
- 'Content-Type': 'application/json'
185
- }
186
- });
187
-
188
- if (response.ok) {
189
- const data = await response.json();
190
- setMcpServers(data.servers || []);
191
- } else {
192
- console.error('Failed to fetch MCP servers');
193
- }
194
- } catch (error) {
195
- console.error('Error fetching MCP servers:', error);
196
- }
197
- };
198
-
199
- const saveMcpServer = async (serverData) => {
200
- try {
201
- const token = localStorage.getItem('auth-token');
202
-
203
- if (editingMcpServer) {
204
- // For editing, remove old server and add new one
205
- await deleteMcpServer(editingMcpServer.id, 'user');
206
- }
207
-
208
- // Use Claude CLI to add the server
209
- const response = await fetch('/api/mcp/cli/add', {
210
- method: 'POST',
211
- headers: {
212
- 'Authorization': `Bearer ${token}`,
213
- 'Content-Type': 'application/json'
214
- },
215
- body: JSON.stringify({
216
- name: serverData.name,
217
- type: serverData.type,
218
- scope: serverData.scope,
219
- projectPath: serverData.projectPath,
220
- command: serverData.config?.command,
221
- args: serverData.config?.args || [],
222
- url: serverData.config?.url,
223
- headers: serverData.config?.headers || {},
224
- env: serverData.config?.env || {}
225
- })
226
- });
227
-
228
- if (response.ok) {
229
- const result = await response.json();
230
- if (result.success) {
231
- await fetchMcpServers(); // Refresh the list
232
- return true;
233
- } else {
234
- throw new Error(result.error || 'Failed to save server via Claude CLI');
235
- }
236
- } else {
237
- const error = await response.json();
238
- throw new Error(error.error || 'Failed to save server');
239
- }
240
- } catch (error) {
241
- console.error('Error saving MCP server:', error);
242
- throw error;
243
- }
244
- };
245
-
246
- const deleteMcpServer = async (serverId, scope = 'user') => {
247
- try {
248
- const token = localStorage.getItem('auth-token');
249
-
250
- // Use Claude CLI to remove the server with proper scope
251
- const response = await fetch(`/api/mcp/cli/remove/${serverId}?scope=${scope}`, {
252
- method: 'DELETE',
253
- headers: {
254
- 'Authorization': `Bearer ${token}`,
255
- 'Content-Type': 'application/json'
256
- }
257
- });
258
-
259
- if (response.ok) {
260
- const result = await response.json();
261
- if (result.success) {
262
- await fetchMcpServers(); // Refresh the list
263
- return true;
264
- } else {
265
- throw new Error(result.error || 'Failed to delete server via Claude CLI');
266
- }
267
- } else {
268
- const error = await response.json();
269
- throw new Error(error.error || 'Failed to delete server');
270
- }
271
- } catch (error) {
272
- console.error('Error deleting MCP server:', error);
273
- throw error;
274
- }
275
- };
276
-
277
- const testMcpServer = async (serverId, scope = 'user') => {
278
- try {
279
- const token = localStorage.getItem('auth-token');
280
- const response = await fetch(`/api/mcp/servers/${serverId}/test?scope=${scope}`, {
281
- method: 'POST',
282
- headers: {
283
- 'Authorization': `Bearer ${token}`,
284
- 'Content-Type': 'application/json'
285
- }
286
- });
287
-
288
- if (response.ok) {
289
- const data = await response.json();
290
- return data.testResult;
291
- } else {
292
- const error = await response.json();
293
- throw new Error(error.error || 'Failed to test server');
294
- }
295
- } catch (error) {
296
- console.error('Error testing MCP server:', error);
297
- throw error;
298
- }
299
- };
300
-
301
-
302
- const discoverMcpTools = async (serverId, scope = 'user') => {
303
- try {
304
- const token = localStorage.getItem('auth-token');
305
- const response = await fetch(`/api/mcp/servers/${serverId}/tools?scope=${scope}`, {
306
- method: 'POST',
307
- headers: {
308
- 'Authorization': `Bearer ${token}`,
309
- 'Content-Type': 'application/json'
310
- }
311
- });
312
-
313
- if (response.ok) {
314
- const data = await response.json();
315
- return data.toolsResult;
316
- } else {
317
- const error = await response.json();
318
- throw new Error(error.error || 'Failed to discover tools');
319
- }
320
- } catch (error) {
321
- console.error('Error discovering MCP tools:', error);
322
- throw error;
323
- }
324
- };
325
-
326
- useEffect(() => {
327
- if (isOpen) {
328
- loadSettings();
329
- }
330
- }, [isOpen]);
331
-
332
- const loadSettings = async () => {
333
- try {
334
-
335
- // Load Claude settings from localStorage
336
- const savedSettings = localStorage.getItem('claude-settings');
337
-
338
- if (savedSettings) {
339
- const settings = JSON.parse(savedSettings);
340
- setAllowedTools(settings.allowedTools || []);
341
- setDisallowedTools(settings.disallowedTools || []);
342
- setSkipPermissions(settings.skipPermissions || false);
343
- setProjectSortOrder(settings.projectSortOrder || 'name');
344
- } else {
345
- // Set defaults
346
- setAllowedTools([]);
347
- setDisallowedTools([]);
348
- setSkipPermissions(false);
349
- setProjectSortOrder('name');
350
- }
351
-
352
- // Load Cursor settings from localStorage
353
- const savedCursorSettings = localStorage.getItem('cursor-tools-settings');
354
-
355
- if (savedCursorSettings) {
356
- const cursorSettings = JSON.parse(savedCursorSettings);
357
- setCursorAllowedCommands(cursorSettings.allowedCommands || []);
358
- setCursorDisallowedCommands(cursorSettings.disallowedCommands || []);
359
- setCursorSkipPermissions(cursorSettings.skipPermissions || false);
360
- } else {
361
- // Set Cursor defaults
362
- setCursorAllowedCommands([]);
363
- setCursorDisallowedCommands([]);
364
- setCursorSkipPermissions(false);
365
- }
366
-
367
- // Load MCP servers from API
368
- await fetchMcpServers();
369
-
370
- // Load Cursor MCP servers
371
- await fetchCursorMcpServers();
372
- } catch (error) {
373
- console.error('Error loading tool settings:', error);
374
- // Set defaults on error
375
- setAllowedTools([]);
376
- setDisallowedTools([]);
377
- setSkipPermissions(false);
378
- setProjectSortOrder('name');
379
- }
380
- };
381
-
382
- // Login handlers
383
- const handleClaudeLogin = () => {
384
- setLoginProvider('claude');
385
- setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
386
- setShowLoginModal(true);
387
- };
388
-
389
- const handleCursorLogin = () => {
390
- setLoginProvider('cursor');
391
- setSelectedProject(projects?.[0] || { name: 'default', fullPath: process.cwd() });
392
- setShowLoginModal(true);
393
- };
394
-
395
- const handleLoginComplete = (exitCode) => {
396
- if (exitCode === 0) {
397
- // Login successful - could show a success message here
398
- }
399
- setShowLoginModal(false);
400
- };
401
-
402
- const saveSettings = () => {
403
- setIsSaving(true);
404
- setSaveStatus(null);
405
-
406
- try {
407
- // Save Claude settings
408
- const claudeSettings = {
409
- allowedTools,
410
- disallowedTools,
411
- skipPermissions,
412
- projectSortOrder,
413
- lastUpdated: new Date().toISOString()
414
- };
415
-
416
- // Save Cursor settings
417
- const cursorSettings = {
418
- allowedCommands: cursorAllowedCommands,
419
- disallowedCommands: cursorDisallowedCommands,
420
- skipPermissions: cursorSkipPermissions,
421
- lastUpdated: new Date().toISOString()
422
- };
423
-
424
- // Save to localStorage
425
- localStorage.setItem('claude-settings', JSON.stringify(claudeSettings));
426
- localStorage.setItem('cursor-tools-settings', JSON.stringify(cursorSettings));
427
-
428
- setSaveStatus('success');
429
-
430
- setTimeout(() => {
431
- onClose();
432
- }, 1000);
433
- } catch (error) {
434
- console.error('Error saving tool settings:', error);
435
- setSaveStatus('error');
436
- } finally {
437
- setIsSaving(false);
438
- }
439
- };
440
-
441
- const addAllowedTool = (tool) => {
442
- if (tool && !allowedTools.includes(tool)) {
443
- setAllowedTools([...allowedTools, tool]);
444
- setNewAllowedTool('');
445
- }
446
- };
447
-
448
- const removeAllowedTool = (tool) => {
449
- setAllowedTools(allowedTools.filter(t => t !== tool));
450
- };
451
-
452
- const addDisallowedTool = (tool) => {
453
- if (tool && !disallowedTools.includes(tool)) {
454
- setDisallowedTools([...disallowedTools, tool]);
455
- setNewDisallowedTool('');
456
- }
457
- };
458
-
459
- const removeDisallowedTool = (tool) => {
460
- setDisallowedTools(disallowedTools.filter(t => t !== tool));
461
- };
462
-
463
- // MCP form handling functions
464
- const resetMcpForm = () => {
465
- setMcpFormData({
466
- name: '',
467
- type: 'stdio',
468
- scope: 'user', // Default to user scope
469
- projectPath: '',
470
- config: {
471
- command: '',
472
- args: [],
473
- env: {},
474
- url: '',
475
- headers: {},
476
- timeout: 30000
477
- },
478
- jsonInput: '',
479
- importMode: 'form'
480
- });
481
- setEditingMcpServer(null);
482
- setShowMcpForm(false);
483
- setJsonValidationError('');
484
- };
485
-
486
- const openMcpForm = (server = null) => {
487
- if (server) {
488
- setEditingMcpServer(server);
489
- setMcpFormData({
490
- name: server.name,
491
- type: server.type,
492
- scope: server.scope,
493
- projectPath: server.projectPath || '',
494
- config: { ...server.config },
495
- raw: server.raw, // Store raw config for display
496
- importMode: 'form', // Always use form mode when editing
497
- jsonInput: ''
498
- });
499
- } else {
500
- resetMcpForm();
501
- }
502
- setShowMcpForm(true);
503
- };
504
-
505
- const handleMcpSubmit = async (e) => {
506
- e.preventDefault();
507
-
508
- setMcpLoading(true);
509
-
510
- try {
511
- if (mcpFormData.importMode === 'json') {
512
- // Use JSON import endpoint
513
- const token = localStorage.getItem('auth-token');
514
- const response = await fetch('/api/mcp/cli/add-json', {
515
- method: 'POST',
516
- headers: {
517
- 'Authorization': `Bearer ${token}`,
518
- 'Content-Type': 'application/json'
519
- },
520
- body: JSON.stringify({
521
- name: mcpFormData.name,
522
- jsonConfig: mcpFormData.jsonInput,
523
- scope: mcpFormData.scope,
524
- projectPath: mcpFormData.projectPath
525
- })
526
- });
527
-
528
- if (response.ok) {
529
- const result = await response.json();
530
- if (result.success) {
531
- await fetchMcpServers(); // Refresh the list
532
- resetMcpForm();
533
- setSaveStatus('success');
534
- } else {
535
- throw new Error(result.error || 'Failed to add server via JSON');
536
- }
537
- } else {
538
- const error = await response.json();
539
- throw new Error(error.error || 'Failed to add server');
540
- }
541
- } else {
542
- // Use regular form-based save
543
- await saveMcpServer(mcpFormData);
544
- resetMcpForm();
545
- setSaveStatus('success');
546
- }
547
- } catch (error) {
548
- alert(`Error: ${error.message}`);
549
- setSaveStatus('error');
550
- } finally {
551
- setMcpLoading(false);
552
- }
553
- };
554
-
555
- const handleMcpDelete = async (serverId, scope) => {
556
- if (confirm('Are you sure you want to delete this MCP server?')) {
557
- try {
558
- await deleteMcpServer(serverId, scope);
559
- setSaveStatus('success');
560
- } catch (error) {
561
- alert(`Error: ${error.message}`);
562
- setSaveStatus('error');
563
- }
564
- }
565
- };
566
-
567
- const handleMcpTest = async (serverId, scope) => {
568
- try {
569
- setMcpTestResults({ ...mcpTestResults, [serverId]: { loading: true } });
570
- const result = await testMcpServer(serverId, scope);
571
- setMcpTestResults({ ...mcpTestResults, [serverId]: result });
572
- } catch (error) {
573
- setMcpTestResults({
574
- ...mcpTestResults,
575
- [serverId]: {
576
- success: false,
577
- message: error.message,
578
- details: []
579
- }
580
- });
581
- }
582
- };
583
-
584
- const handleMcpToolsDiscovery = async (serverId, scope) => {
585
- try {
586
- setMcpToolsLoading({ ...mcpToolsLoading, [serverId]: true });
587
- const result = await discoverMcpTools(serverId, scope);
588
- setMcpServerTools({ ...mcpServerTools, [serverId]: result });
589
- } catch (error) {
590
- setMcpServerTools({
591
- ...mcpServerTools,
592
- [serverId]: {
593
- success: false,
594
- tools: [],
595
- resources: [],
596
- prompts: []
597
- }
598
- });
599
- } finally {
600
- setMcpToolsLoading({ ...mcpToolsLoading, [serverId]: false });
601
- }
602
- };
603
-
604
- const updateMcpConfig = (key, value) => {
605
- setMcpFormData(prev => ({
606
- ...prev,
607
- config: {
608
- ...prev.config,
609
- [key]: value
610
- }
611
- }));
612
- };
613
-
614
-
615
- const getTransportIcon = (type) => {
616
- switch (type) {
617
- case 'stdio': return <Terminal className="w-4 h-4" />;
618
- case 'sse': return <Zap className="w-4 h-4" />;
619
- case 'http': return <Globe className="w-4 h-4" />;
620
- default: return <Server className="w-4 h-4" />;
621
- }
622
- };
623
-
624
- if (!isOpen) return null;
625
-
626
- return (
627
- <div className="modal-backdrop fixed inset-0 flex items-center justify-center z-[100] md:p-4 bg-background/95">
628
- <div className="bg-background border border-border md:rounded-lg shadow-xl w-full md:max-w-4xl h-full md:h-[90vh] flex flex-col">
629
- <div className="flex items-center justify-between p-4 md:p-6 border-b border-border flex-shrink-0">
630
- <div className="flex items-center gap-3">
631
- <SettingsIcon className="w-5 h-5 md:w-6 md:h-6 text-blue-600" />
632
- <h2 className="text-lg md:text-xl font-semibold text-foreground">
633
- Settings
634
- </h2>
635
- </div>
636
- <Button
637
- variant="ghost"
638
- size="sm"
639
- onClick={onClose}
640
- className="text-muted-foreground hover:text-foreground touch-manipulation"
641
- >
642
- <X className="w-5 h-5" />
643
- </Button>
644
- </div>
645
-
646
- <div className="flex-1 overflow-y-auto">
647
- {/* Tab Navigation */}
648
- <div className="border-b border-border">
649
- <div className="flex px-4 md:px-6">
650
- <button
651
- onClick={() => setActiveTab('tools')}
652
- className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
653
- activeTab === 'tools'
654
- ? 'border-blue-600 text-blue-600 dark:text-blue-400'
655
- : 'border-transparent text-muted-foreground hover:text-foreground'
656
- }`}
657
- >
658
- Tools
659
- </button>
660
- <button
661
- onClick={() => setActiveTab('appearance')}
662
- className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
663
- activeTab === 'appearance'
664
- ? 'border-blue-600 text-blue-600 dark:text-blue-400'
665
- : 'border-transparent text-muted-foreground hover:text-foreground'
666
- }`}
667
- >
668
- Appearance
669
- </button>
670
- <button
671
- onClick={() => setActiveTab('tasks')}
672
- className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
673
- activeTab === 'tasks'
674
- ? 'border-blue-600 text-blue-600 dark:text-blue-400'
675
- : 'border-transparent text-muted-foreground hover:text-foreground'
676
- }`}
677
- >
678
- Tasks
679
- </button>
680
- </div>
681
- </div>
682
-
683
- <div className="p-4 md:p-6 space-y-6 md:space-y-8 pb-safe-area-inset-bottom">
684
-
685
- {/* Appearance Tab */}
686
- {activeTab === 'appearance' && (
687
- <div className="space-y-6 md:space-y-8">
688
- {activeTab === 'appearance' && (
689
- <div className="space-y-6 md:space-y-8">
690
- {/* Theme Settings */}
691
- <div className="space-y-4">
692
- <div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
693
- <div className="flex items-center justify-between">
694
- <div>
695
- <div className="font-medium text-foreground">
696
- Dark Mode
697
- </div>
698
- <div className="text-sm text-muted-foreground">
699
- Toggle between light and dark themes
700
- </div>
701
- </div>
702
- <button
703
- onClick={toggleDarkMode}
704
- className="relative inline-flex h-8 w-14 items-center rounded-full bg-gray-200 dark:bg-gray-700 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
705
- role="switch"
706
- aria-checked={isDarkMode}
707
- aria-label="Toggle dark mode"
708
- >
709
- <span className="sr-only">Toggle dark mode</span>
710
- <span
711
- className={`${
712
- isDarkMode ? 'translate-x-7' : 'translate-x-1'
713
- } inline-block h-6 w-6 transform rounded-full bg-white shadow-lg transition-transform duration-200 flex items-center justify-center`}
714
- >
715
- {isDarkMode ? (
716
- <Moon className="w-3.5 h-3.5 text-gray-700" />
717
- ) : (
718
- <Sun className="w-3.5 h-3.5 text-yellow-500" />
719
- )}
720
- </span>
721
- </button>
722
- </div>
723
- </div>
724
- </div>
725
-
726
- {/* Project Sorting */}
727
- <div className="space-y-4">
728
- <div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
729
- <div className="flex items-center justify-between">
730
- <div>
731
- <div className="font-medium text-foreground">
732
- Project Sorting
733
- </div>
734
- <div className="text-sm text-muted-foreground">
735
- How projects are ordered in the sidebar
736
- </div>
737
- </div>
738
- <select
739
- value={projectSortOrder}
740
- onChange={(e) => setProjectSortOrder(e.target.value)}
741
- className="text-sm bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2 w-32"
742
- >
743
- <option value="name">Alphabetical</option>
744
- <option value="date">Recent Activity</option>
745
- </select>
746
- </div>
747
- </div>
748
- </div>
749
- </div>
750
- )}
751
-
752
- </div>
753
- )}
754
-
755
- {/* Tools Tab */}
756
- {activeTab === 'tools' && (
757
- <div className="space-y-6 md:space-y-8">
758
-
759
- {/* Provider Tabs */}
760
- <div className="border-b border-gray-300 dark:border-gray-600">
761
- <div className="flex gap-4">
762
- <button
763
- onClick={() => setToolsProvider('claude')}
764
- className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
765
- toolsProvider === 'claude'
766
- ? 'border-blue-600 text-blue-600 dark:text-blue-400'
767
- : 'border-transparent text-muted-foreground hover:text-foreground'
768
- }`}
769
- >
770
- <div className="flex items-center gap-2">
771
- <ClaudeLogo className="w-4 h-4" />
772
- <span>Claude</span>
773
- </div>
774
- </button>
775
- <button
776
- onClick={() => setToolsProvider('cursor')}
777
- className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
778
- toolsProvider === 'cursor'
779
- ? 'border-purple-600 text-purple-600 dark:text-purple-400'
780
- : 'border-transparent text-muted-foreground hover:text-foreground'
781
- }`}
782
- >
783
- <div className="flex items-center gap-2">
784
- <CursorLogo className="w-4 h-4" />
785
- <span>Cursor</span>
786
- </div>
787
- </button>
788
- </div>
789
- </div>
790
-
791
- {/* Claude Content */}
792
- {toolsProvider === 'claude' && (
793
- <div className="space-y-6 md:space-y-8">
794
-
795
- {/* Skip Permissions */}
796
- <div className="space-y-4">
797
- <div className="flex items-center gap-3">
798
- <AlertTriangle className="w-5 h-5 text-orange-500" />
799
- <h3 className="text-lg font-medium text-foreground">
800
- Permission Settings
801
- </h3>
802
- </div>
803
- <div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
804
- <label className="flex items-center gap-3">
805
- <input
806
- type="checkbox"
807
- checked={skipPermissions}
808
- onChange={(e) => setSkipPermissions(e.target.checked)}
809
- className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
810
- />
811
- <div>
812
- <div className="font-medium text-orange-900 dark:text-orange-100">
813
- Skip permission prompts (use with caution)
814
- </div>
815
- <div className="text-sm text-orange-700 dark:text-orange-300">
816
- Equivalent to --dangerously-skip-permissions flag
817
- </div>
818
- </div>
819
- </label>
820
- </div>
821
- </div>
822
-
823
- {/* Claude Login */}
824
- <div className="space-y-4">
825
- <div className="flex items-center gap-3">
826
- <LogIn className="w-5 h-5 text-blue-500" />
827
- <h3 className="text-lg font-medium text-foreground">
828
- Authentication
829
- </h3>
830
- </div>
831
- <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
832
- <div className="flex items-center justify-between">
833
- <div>
834
- <div className="font-medium text-blue-900 dark:text-blue-100">
835
- Claude CLI Login
836
- </div>
837
- <div className="text-sm text-blue-700 dark:text-blue-300">
838
- Sign in to your Claude account to enable AI features
839
- </div>
840
- </div>
841
- <Button
842
- onClick={handleClaudeLogin}
843
- className="bg-blue-600 hover:bg-blue-700 text-white"
844
- size="sm"
845
- >
846
- <LogIn className="w-4 h-4 mr-2" />
847
- Login
848
- </Button>
849
- </div>
850
- </div>
851
- </div>
852
-
853
- {/* Allowed Tools */}
854
- <div className="space-y-4">
855
- <div className="flex items-center gap-3">
856
- <Shield className="w-5 h-5 text-green-500" />
857
- <h3 className="text-lg font-medium text-foreground">
858
- Allowed Tools
859
- </h3>
860
- </div>
861
- <p className="text-sm text-muted-foreground">
862
- Tools that are automatically allowed without prompting for permission
863
- </p>
864
-
865
- <div className="flex flex-col sm:flex-row gap-2">
866
- <Input
867
- value={newAllowedTool}
868
- onChange={(e) => setNewAllowedTool(e.target.value)}
869
- placeholder='e.g., "Bash(git log:*)" or "Write"'
870
- onKeyPress={(e) => {
871
- if (e.key === 'Enter') {
872
- addAllowedTool(newAllowedTool);
873
- }
874
- }}
875
- className="flex-1 h-10 touch-manipulation"
876
- style={{ fontSize: '16px' }}
877
- />
878
- <Button
879
- onClick={() => addAllowedTool(newAllowedTool)}
880
- disabled={!newAllowedTool}
881
- size="sm"
882
- className="h-10 px-4 touch-manipulation"
883
- >
884
- <Plus className="w-4 h-4 mr-2 sm:mr-0" />
885
- <span className="sm:hidden">Add Tool</span>
886
- </Button>
887
- </div>
888
-
889
- {/* Common tools quick add */}
890
- <div className="space-y-2">
891
- <p className="text-sm font-medium text-gray-700 dark:text-gray-300">
892
- Quick add common tools:
893
- </p>
894
- <div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-2">
895
- {commonTools.map(tool => (
896
- <Button
897
- key={tool}
898
- variant="outline"
899
- size="sm"
900
- onClick={() => addAllowedTool(tool)}
901
- disabled={allowedTools.includes(tool)}
902
- className="text-xs h-8 touch-manipulation truncate"
903
- >
904
- {tool}
905
- </Button>
906
- ))}
907
- </div>
908
- </div>
909
-
910
- <div className="space-y-2">
911
- {allowedTools.map(tool => (
912
- <div key={tool} className="flex items-center justify-between bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
913
- <span className="font-mono text-sm text-green-800 dark:text-green-200">
914
- {tool}
915
- </span>
916
- <Button
917
- variant="ghost"
918
- size="sm"
919
- onClick={() => removeAllowedTool(tool)}
920
- className="text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
921
- >
922
- <X className="w-4 h-4" />
923
- </Button>
924
- </div>
925
- ))}
926
- {allowedTools.length === 0 && (
927
- <div className="text-center py-8 text-gray-500 dark:text-gray-400">
928
- No allowed tools configured
929
- </div>
930
- )}
931
- </div>
932
- </div>
933
-
934
- {/* Disallowed Tools */}
935
- <div className="space-y-4">
936
- <div className="flex items-center gap-3">
937
- <AlertTriangle className="w-5 h-5 text-red-500" />
938
- <h3 className="text-lg font-medium text-foreground">
939
- Disallowed Tools
940
- </h3>
941
- </div>
942
- <p className="text-sm text-muted-foreground">
943
- Tools that are automatically blocked without prompting for permission
944
- </p>
945
-
946
- <div className="flex flex-col sm:flex-row gap-2">
947
- <Input
948
- value={newDisallowedTool}
949
- onChange={(e) => setNewDisallowedTool(e.target.value)}
950
- placeholder='e.g., "Bash(rm:*)" or "Write"'
951
- onKeyPress={(e) => {
952
- if (e.key === 'Enter') {
953
- addDisallowedTool(newDisallowedTool);
954
- }
955
- }}
956
- className="flex-1 h-10 touch-manipulation"
957
- style={{ fontSize: '16px' }}
958
- />
959
- <Button
960
- onClick={() => addDisallowedTool(newDisallowedTool)}
961
- disabled={!newDisallowedTool}
962
- size="sm"
963
- className="h-10 px-4 touch-manipulation"
964
- >
965
- <Plus className="w-4 h-4 mr-2 sm:mr-0" />
966
- <span className="sm:hidden">Add Tool</span>
967
- </Button>
968
- </div>
969
-
970
- <div className="space-y-2">
971
- {disallowedTools.map(tool => (
972
- <div key={tool} className="flex items-center justify-between bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
973
- <span className="font-mono text-sm text-red-800 dark:text-red-200">
974
- {tool}
975
- </span>
976
- <Button
977
- variant="ghost"
978
- size="sm"
979
- onClick={() => removeDisallowedTool(tool)}
980
- className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
981
- >
982
- <X className="w-4 h-4" />
983
- </Button>
984
- </div>
985
- ))}
986
- {disallowedTools.length === 0 && (
987
- <div className="text-center py-8 text-gray-500 dark:text-gray-400">
988
- No disallowed tools configured
989
- </div>
990
- )}
991
- </div>
992
- </div>
993
-
994
- {/* Help Section */}
995
- <div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
996
- <h4 className="font-medium text-blue-900 dark:text-blue-100 mb-2">
997
- Tool Pattern Examples:
998
- </h4>
999
- <ul className="text-sm text-blue-800 dark:text-blue-200 space-y-1">
1000
- <li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git log:*)"</code> - Allow all git log commands</li>
1001
- <li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(git diff:*)"</code> - Allow all git diff commands</li>
1002
- <li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Write"</code> - Allow all Write tool usage</li>
1003
- <li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Read"</code> - Allow all Read tool usage</li>
1004
- <li><code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">"Bash(rm:*)"</code> - Block all rm commands (dangerous)</li>
1005
- </ul>
1006
- </div>
1007
-
1008
- {/* MCP Server Management */}
1009
- <div className="space-y-4">
1010
- <div className="flex items-center gap-3">
1011
- <Server className="w-5 h-5 text-purple-500" />
1012
- <h3 className="text-lg font-medium text-foreground">
1013
- MCP Servers
1014
- </h3>
1015
- </div>
1016
- <div className="space-y-2">
1017
- <p className="text-sm text-muted-foreground">
1018
- Model Context Protocol servers provide additional tools and data sources to Claude
1019
- </p>
1020
- </div>
1021
-
1022
- <div className="flex justify-between items-center">
1023
- <Button
1024
- onClick={() => openMcpForm()}
1025
- className="bg-purple-600 hover:bg-purple-700 text-white"
1026
- size="sm"
1027
- >
1028
- <Plus className="w-4 h-4 mr-2" />
1029
- Add MCP Server
1030
- </Button>
1031
- </div>
1032
-
1033
- {/* MCP Servers List */}
1034
- <div className="space-y-2">
1035
- {mcpServers.map(server => (
1036
- <div key={server.id} className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
1037
- <div className="flex items-start justify-between">
1038
- <div className="flex-1">
1039
- <div className="flex items-center gap-2 mb-2">
1040
- {getTransportIcon(server.type)}
1041
- <span className="font-medium text-foreground">{server.name}</span>
1042
- <Badge variant="outline" className="text-xs">
1043
- {server.type}
1044
- </Badge>
1045
- <Badge variant="outline" className="text-xs">
1046
- {server.scope === 'local' ? '📁 local' : server.scope === 'user' ? '👤 user' : server.scope}
1047
- </Badge>
1048
- {server.projectPath && (
1049
- <Badge variant="outline" className="text-xs bg-purple-50 dark:bg-purple-900/20" title={server.projectPath}>
1050
- {server.projectPath.split('/').pop()}
1051
- </Badge>
1052
- )}
1053
- </div>
1054
-
1055
- <div className="text-sm text-muted-foreground space-y-1">
1056
- {server.type === 'stdio' && server.config.command && (
1057
- <div>Command: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.command}</code></div>
1058
- )}
1059
- {(server.type === 'sse' || server.type === 'http') && server.config.url && (
1060
- <div>URL: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.url}</code></div>
1061
- )}
1062
- {server.config.args && server.config.args.length > 0 && (
1063
- <div>Args: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{server.config.args.join(' ')}</code></div>
1064
- )}
1065
- {server.config.env && Object.keys(server.config.env).length > 0 && (
1066
- <div>Environment: <code className="bg-gray-100 dark:bg-gray-800 px-1 rounded text-xs">{Object.entries(server.config.env).map(([k, v]) => `${k}=${v}`).join(', ')}</code></div>
1067
- )}
1068
- {server.raw && (
1069
- <details className="mt-2">
1070
- <summary className="cursor-pointer text-xs text-muted-foreground hover:text-foreground">View full config</summary>
1071
- <pre className="mt-1 text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-x-auto">
1072
- {JSON.stringify(server.raw, null, 2)}
1073
- </pre>
1074
- </details>
1075
- )}
1076
- </div>
1077
-
1078
- {/* Test Results */}
1079
- {mcpTestResults[server.id] && (
1080
- <div className={`mt-2 p-2 rounded text-xs ${
1081
- mcpTestResults[server.id].success
1082
- ? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200'
1083
- : 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200'
1084
- }`}>
1085
- <div className="font-medium">{mcpTestResults[server.id].message}</div>
1086
- {mcpTestResults[server.id].details && mcpTestResults[server.id].details.length > 0 && (
1087
- <ul className="mt-1 space-y-0.5">
1088
- {mcpTestResults[server.id].details.map((detail, i) => (
1089
- <li key={i}>• {detail}</li>
1090
- ))}
1091
- </ul>
1092
- )}
1093
- </div>
1094
- )}
1095
-
1096
- {/* Tools Discovery Results */}
1097
- {mcpServerTools[server.id] && (
1098
- <div className="mt-2 p-2 rounded text-xs bg-blue-50 dark:bg-blue-900/20 text-blue-800 dark:text-blue-200 border border-blue-200 dark:border-blue-800">
1099
- <div className="font-medium mb-2">Available Tools & Resources</div>
1100
-
1101
- {mcpServerTools[server.id].tools && mcpServerTools[server.id].tools.length > 0 && (
1102
- <div className="mb-2">
1103
- <div className="font-medium text-xs mb-1">Tools ({mcpServerTools[server.id].tools.length}):</div>
1104
- <ul className="space-y-0.5">
1105
- {mcpServerTools[server.id].tools.map((tool, i) => (
1106
- <li key={i} className="flex items-start gap-1">
1107
- <span className="text-blue-400 mt-0.5">•</span>
1108
- <div>
1109
- <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">{tool.name}</code>
1110
- {tool.description && tool.description !== 'No description provided' && (
1111
- <span className="ml-1 text-xs opacity-75">- {tool.description}</span>
1112
- )}
1113
- </div>
1114
- </li>
1115
- ))}
1116
- </ul>
1117
- </div>
1118
- )}
1119
-
1120
- {mcpServerTools[server.id].resources && mcpServerTools[server.id].resources.length > 0 && (
1121
- <div className="mb-2">
1122
- <div className="font-medium text-xs mb-1">Resources ({mcpServerTools[server.id].resources.length}):</div>
1123
- <ul className="space-y-0.5">
1124
- {mcpServerTools[server.id].resources.map((resource, i) => (
1125
- <li key={i} className="flex items-start gap-1">
1126
- <span className="text-blue-400 mt-0.5">•</span>
1127
- <div>
1128
- <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">{resource.name}</code>
1129
- {resource.description && resource.description !== 'No description provided' && (
1130
- <span className="ml-1 text-xs opacity-75">- {resource.description}</span>
1131
- )}
1132
- </div>
1133
- </li>
1134
- ))}
1135
- </ul>
1136
- </div>
1137
- )}
1138
-
1139
- {mcpServerTools[server.id].prompts && mcpServerTools[server.id].prompts.length > 0 && (
1140
- <div>
1141
- <div className="font-medium text-xs mb-1">Prompts ({mcpServerTools[server.id].prompts.length}):</div>
1142
- <ul className="space-y-0.5">
1143
- {mcpServerTools[server.id].prompts.map((prompt, i) => (
1144
- <li key={i} className="flex items-start gap-1">
1145
- <span className="text-blue-400 mt-0.5">•</span>
1146
- <div>
1147
- <code className="bg-blue-100 dark:bg-blue-800 px-1 rounded">{prompt.name}</code>
1148
- {prompt.description && prompt.description !== 'No description provided' && (
1149
- <span className="ml-1 text-xs opacity-75">- {prompt.description}</span>
1150
- )}
1151
- </div>
1152
- </li>
1153
- ))}
1154
- </ul>
1155
- </div>
1156
- )}
1157
-
1158
- {(!mcpServerTools[server.id].tools || mcpServerTools[server.id].tools.length === 0) &&
1159
- (!mcpServerTools[server.id].resources || mcpServerTools[server.id].resources.length === 0) &&
1160
- (!mcpServerTools[server.id].prompts || mcpServerTools[server.id].prompts.length === 0) && (
1161
- <div className="text-xs opacity-75">No tools, resources, or prompts discovered</div>
1162
- )}
1163
- </div>
1164
- )}
1165
- </div>
1166
-
1167
- <div className="flex items-center gap-2 ml-4">
1168
- <Button
1169
- onClick={() => openMcpForm(server)}
1170
- variant="ghost"
1171
- size="sm"
1172
- className="text-gray-600 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
1173
- title="Edit server"
1174
- >
1175
- <Edit3 className="w-4 h-4" />
1176
- </Button>
1177
- <Button
1178
- onClick={() => handleMcpDelete(server.id, server.scope)}
1179
- variant="ghost"
1180
- size="sm"
1181
- className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
1182
- title="Delete server"
1183
- >
1184
- <Trash2 className="w-4 h-4" />
1185
- </Button>
1186
- </div>
1187
- </div>
1188
- </div>
1189
- ))}
1190
- {mcpServers.length === 0 && (
1191
- <div className="text-center py-8 text-gray-500 dark:text-gray-400">
1192
- No MCP servers configured
1193
- </div>
1194
- )}
1195
- </div>
1196
- </div>
1197
-
1198
- {/* MCP Server Form Modal */}
1199
- {showMcpForm && (
1200
- <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-[110] p-4">
1201
- <div className="bg-background border border-border rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
1202
- <div className="flex items-center justify-between p-4 border-b border-border">
1203
- <h3 className="text-lg font-medium text-foreground">
1204
- {editingMcpServer ? 'Edit MCP Server' : 'Add MCP Server'}
1205
- </h3>
1206
- <Button variant="ghost" size="sm" onClick={resetMcpForm}>
1207
- <X className="w-4 h-4" />
1208
- </Button>
1209
- </div>
1210
-
1211
- <form onSubmit={handleMcpSubmit} className="p-4 space-y-4">
1212
-
1213
- {!editingMcpServer && (
1214
- <div className="flex gap-2 mb-4">
1215
- <button
1216
- type="button"
1217
- onClick={() => setMcpFormData(prev => ({...prev, importMode: 'form'}))}
1218
- className={`px-4 py-2 rounded-lg font-medium transition-colors ${
1219
- mcpFormData.importMode === 'form'
1220
- ? 'bg-blue-600 text-white'
1221
- : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
1222
- }`}
1223
- >
1224
- Form Input
1225
- </button>
1226
- <button
1227
- type="button"
1228
- onClick={() => setMcpFormData(prev => ({...prev, importMode: 'json'}))}
1229
- className={`px-4 py-2 rounded-lg font-medium transition-colors ${
1230
- mcpFormData.importMode === 'json'
1231
- ? 'bg-blue-600 text-white'
1232
- : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
1233
- }`}
1234
- >
1235
- JSON Import
1236
- </button>
1237
- </div>
1238
- )}
1239
-
1240
- {/* Show current scope when editing */}
1241
- {mcpFormData.importMode === 'form' && editingMcpServer && (
1242
- <div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-3">
1243
- <label className="block text-sm font-medium text-foreground mb-2">
1244
- Scope
1245
- </label>
1246
- <div className="flex items-center gap-2">
1247
- {mcpFormData.scope === 'user' ? <Globe className="w-4 h-4" /> : <FolderOpen className="w-4 h-4" />}
1248
- <span className="text-sm">
1249
- {mcpFormData.scope === 'user' ? 'User (Global)' : 'Project (Local)'}
1250
- </span>
1251
- {mcpFormData.scope === 'local' && mcpFormData.projectPath && (
1252
- <span className="text-xs text-muted-foreground">
1253
- - {mcpFormData.projectPath}
1254
- </span>
1255
- )}
1256
- </div>
1257
- <p className="text-xs text-muted-foreground mt-2">
1258
- Scope cannot be changed when editing an existing server
1259
- </p>
1260
- </div>
1261
- )}
1262
-
1263
- {/* Scope Selection - Moved to top, disabled when editing */}
1264
- {mcpFormData.importMode === 'form' && !editingMcpServer && (
1265
- <div className="space-y-4">
1266
- <div>
1267
- <label className="block text-sm font-medium text-foreground mb-2">
1268
- Scope *
1269
- </label>
1270
- <div className="flex gap-2">
1271
- <button
1272
- type="button"
1273
- onClick={() => setMcpFormData(prev => ({...prev, scope: 'user', projectPath: ''}))}
1274
- className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
1275
- mcpFormData.scope === 'user'
1276
- ? 'bg-blue-600 text-white'
1277
- : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
1278
- }`}
1279
- >
1280
- <div className="flex items-center justify-center gap-2">
1281
- <Globe className="w-4 h-4" />
1282
- <span>User (Global)</span>
1283
- </div>
1284
- </button>
1285
- <button
1286
- type="button"
1287
- onClick={() => setMcpFormData(prev => ({...prev, scope: 'local'}))}
1288
- className={`flex-1 px-4 py-2 rounded-lg font-medium transition-colors ${
1289
- mcpFormData.scope === 'local'
1290
- ? 'bg-blue-600 text-white'
1291
- : 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
1292
- }`}
1293
- >
1294
- <div className="flex items-center justify-center gap-2">
1295
- <FolderOpen className="w-4 h-4" />
1296
- <span>Project (Local)</span>
1297
- </div>
1298
- </button>
1299
- </div>
1300
- <p className="text-xs text-muted-foreground mt-2">
1301
- {mcpFormData.scope === 'user'
1302
- ? 'User scope: Available across all projects on your machine'
1303
- : 'Local scope: Only available in the selected project'
1304
- }
1305
- </p>
1306
- </div>
1307
-
1308
- {/* Project Selection for Local Scope */}
1309
- {mcpFormData.scope === 'local' && !editingMcpServer && (
1310
- <div>
1311
- <label className="block text-sm font-medium text-foreground mb-2">
1312
- Project *
1313
- </label>
1314
- <select
1315
- value={mcpFormData.projectPath}
1316
- onChange={(e) => setMcpFormData(prev => ({...prev, projectPath: e.target.value}))}
1317
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
1318
- required={mcpFormData.scope === 'local'}
1319
- >
1320
- <option value="">Select a project...</option>
1321
- {projects.map(project => (
1322
- <option key={project.name} value={project.path || project.fullPath}>
1323
- {project.displayName || project.name}
1324
- </option>
1325
- ))}
1326
- </select>
1327
- {mcpFormData.projectPath && (
1328
- <p className="text-xs text-muted-foreground mt-1">
1329
- Path: {mcpFormData.projectPath}
1330
- </p>
1331
- )}
1332
- </div>
1333
- )}
1334
- </div>
1335
- )}
1336
-
1337
- {/* Basic Info */}
1338
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
1339
- <div className={mcpFormData.importMode === 'json' ? 'md:col-span-2' : ''}>
1340
- <label className="block text-sm font-medium text-foreground mb-2">
1341
- Server Name *
1342
- </label>
1343
- <Input
1344
- value={mcpFormData.name}
1345
- onChange={(e) => {
1346
- setMcpFormData(prev => ({...prev, name: e.target.value}));
1347
- }}
1348
- placeholder="my-server"
1349
- required
1350
- />
1351
- </div>
1352
-
1353
- {mcpFormData.importMode === 'form' && (
1354
- <div>
1355
- <label className="block text-sm font-medium text-foreground mb-2">
1356
- Transport Type *
1357
- </label>
1358
- <select
1359
- value={mcpFormData.type}
1360
- onChange={(e) => {
1361
- setMcpFormData(prev => ({...prev, type: e.target.value}));
1362
- }}
1363
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
1364
- >
1365
- <option value="stdio">stdio</option>
1366
- <option value="sse">SSE</option>
1367
- <option value="http">HTTP</option>
1368
- </select>
1369
- </div>
1370
- )}
1371
- </div>
1372
-
1373
-
1374
- {/* Show raw configuration details when editing */}
1375
- {editingMcpServer && mcpFormData.raw && mcpFormData.importMode === 'form' && (
1376
- <div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
1377
- <h4 className="text-sm font-medium text-foreground mb-2">
1378
- Configuration Details (from {editingMcpServer.scope === 'global' ? '~/.claude.json' : 'project config'})
1379
- </h4>
1380
- <pre className="text-xs bg-gray-100 dark:bg-gray-800 p-3 rounded overflow-x-auto">
1381
- {JSON.stringify(mcpFormData.raw, null, 2)}
1382
- </pre>
1383
- </div>
1384
- )}
1385
-
1386
- {/* JSON Import Mode */}
1387
- {mcpFormData.importMode === 'json' && (
1388
- <div className="space-y-4">
1389
- <div>
1390
- <label className="block text-sm font-medium text-foreground mb-2">
1391
- JSON Configuration *
1392
- </label>
1393
- <textarea
1394
- value={mcpFormData.jsonInput}
1395
- onChange={(e) => {
1396
- setMcpFormData(prev => ({...prev, jsonInput: e.target.value}));
1397
- // Validate JSON as user types
1398
- try {
1399
- if (e.target.value.trim()) {
1400
- const parsed = JSON.parse(e.target.value);
1401
- // Basic validation
1402
- if (!parsed.type) {
1403
- setJsonValidationError('Missing required field: type');
1404
- } else if (parsed.type === 'stdio' && !parsed.command) {
1405
- setJsonValidationError('stdio type requires a command field');
1406
- } else if ((parsed.type === 'http' || parsed.type === 'sse') && !parsed.url) {
1407
- setJsonValidationError(`${parsed.type} type requires a url field`);
1408
- } else {
1409
- setJsonValidationError('');
1410
- }
1411
- }
1412
- } catch (err) {
1413
- if (e.target.value.trim()) {
1414
- setJsonValidationError('Invalid JSON format');
1415
- } else {
1416
- setJsonValidationError('');
1417
- }
1418
- }
1419
- }}
1420
- className={`w-full px-3 py-2 border ${jsonValidationError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 font-mono text-sm`}
1421
- rows="8"
1422
- placeholder={'{\n "type": "stdio",\n "command": "/path/to/server",\n "args": ["--api-key", "abc123"],\n "env": {\n "CACHE_DIR": "/tmp"\n }\n}'}
1423
- required
1424
- />
1425
- {jsonValidationError && (
1426
- <p className="text-xs text-red-500 mt-1">{jsonValidationError}</p>
1427
- )}
1428
- <p className="text-xs text-muted-foreground mt-2">
1429
- Paste your MCP server configuration in JSON format. Example formats:
1430
- <br />• stdio: {`{"type":"stdio","command":"npx","args":["@upstash/context7-mcp"]}`}
1431
- <br />• http/sse: {`{"type":"http","url":"https://api.example.com/mcp"}`}
1432
- </p>
1433
- </div>
1434
- </div>
1435
- )}
1436
-
1437
- {/* Transport-specific Config - Only show in form mode */}
1438
- {mcpFormData.importMode === 'form' && mcpFormData.type === 'stdio' && (
1439
- <div className="space-y-4">
1440
- <div>
1441
- <label className="block text-sm font-medium text-foreground mb-2">
1442
- Command *
1443
- </label>
1444
- <Input
1445
- value={mcpFormData.config.command}
1446
- onChange={(e) => updateMcpConfig('command', e.target.value)}
1447
- placeholder="/path/to/mcp-server"
1448
- required
1449
- />
1450
- </div>
1451
-
1452
- <div>
1453
- <label className="block text-sm font-medium text-foreground mb-2">
1454
- Arguments (one per line)
1455
- </label>
1456
- <textarea
1457
- value={Array.isArray(mcpFormData.config.args) ? mcpFormData.config.args.join('\n') : ''}
1458
- onChange={(e) => updateMcpConfig('args', e.target.value.split('\n').filter(arg => arg.trim()))}
1459
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
1460
- rows="3"
1461
- placeholder="--api-key&#10;abc123"
1462
- />
1463
- </div>
1464
- </div>
1465
- )}
1466
-
1467
- {mcpFormData.importMode === 'form' && (mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
1468
- <div>
1469
- <label className="block text-sm font-medium text-foreground mb-2">
1470
- URL *
1471
- </label>
1472
- <Input
1473
- value={mcpFormData.config.url}
1474
- onChange={(e) => updateMcpConfig('url', e.target.value)}
1475
- placeholder="https://api.example.com/mcp"
1476
- type="url"
1477
- required
1478
- />
1479
- </div>
1480
- )}
1481
-
1482
- {/* Environment Variables - Only show in form mode */}
1483
- {mcpFormData.importMode === 'form' && (
1484
- <div>
1485
- <label className="block text-sm font-medium text-foreground mb-2">
1486
- Environment Variables (KEY=value, one per line)
1487
- </label>
1488
- <textarea
1489
- value={Object.entries(mcpFormData.config.env || {}).map(([k, v]) => `${k}=${v}`).join('\n')}
1490
- onChange={(e) => {
1491
- const env = {};
1492
- e.target.value.split('\n').forEach(line => {
1493
- const [key, ...valueParts] = line.split('=');
1494
- if (key && key.trim()) {
1495
- env[key.trim()] = valueParts.join('=').trim();
1496
- }
1497
- });
1498
- updateMcpConfig('env', env);
1499
- }}
1500
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
1501
- rows="3"
1502
- placeholder="API_KEY=your-key&#10;DEBUG=true"
1503
- />
1504
- </div>
1505
- )}
1506
-
1507
- {mcpFormData.importMode === 'form' && (mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
1508
- <div>
1509
- <label className="block text-sm font-medium text-foreground mb-2">
1510
- Headers (KEY=value, one per line)
1511
- </label>
1512
- <textarea
1513
- value={Object.entries(mcpFormData.config.headers || {}).map(([k, v]) => `${k}=${v}`).join('\n')}
1514
- onChange={(e) => {
1515
- const headers = {};
1516
- e.target.value.split('\n').forEach(line => {
1517
- const [key, ...valueParts] = line.split('=');
1518
- if (key && key.trim()) {
1519
- headers[key.trim()] = valueParts.join('=').trim();
1520
- }
1521
- });
1522
- updateMcpConfig('headers', headers);
1523
- }}
1524
- className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
1525
- rows="3"
1526
- placeholder="Authorization=Bearer token&#10;X-API-Key=your-key"
1527
- />
1528
- </div>
1529
- )}
1530
-
1531
-
1532
- <div className="flex justify-end gap-2 pt-4">
1533
- <Button type="button" variant="outline" onClick={resetMcpForm}>
1534
- Cancel
1535
- </Button>
1536
- <Button
1537
- type="submit"
1538
- disabled={mcpLoading}
1539
- className="bg-purple-600 hover:bg-purple-700 disabled:opacity-50"
1540
- >
1541
- {mcpLoading ? 'Saving...' : (editingMcpServer ? 'Update Server' : 'Add Server')}
1542
- </Button>
1543
- </div>
1544
- </form>
1545
- </div>
1546
- </div>
1547
- )}
1548
- </div>
1549
- )}
1550
-
1551
- {/* Cursor Content */}
1552
- {toolsProvider === 'cursor' && (
1553
- <div className="space-y-6 md:space-y-8">
1554
-
1555
- {/* Skip Permissions for Cursor */}
1556
- <div className="space-y-4">
1557
- <div className="flex items-center gap-3">
1558
- <AlertTriangle className="w-5 h-5 text-orange-500" />
1559
- <h3 className="text-lg font-medium text-foreground">
1560
- Cursor Permission Settings
1561
- </h3>
1562
- </div>
1563
- <div className="bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
1564
- <label className="flex items-center gap-3">
1565
- <input
1566
- type="checkbox"
1567
- checked={cursorSkipPermissions}
1568
- onChange={(e) => setCursorSkipPermissions(e.target.checked)}
1569
- className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500"
1570
- />
1571
- <div>
1572
- <div className="font-medium text-orange-900 dark:text-orange-100">
1573
- Skip permission prompts (use with caution)
1574
- </div>
1575
- <div className="text-sm text-orange-700 dark:text-orange-300">
1576
- Equivalent to -f flag in Cursor CLI
1577
- </div>
1578
- </div>
1579
- </label>
1580
- </div>
1581
- </div>
1582
-
1583
- {/* Cursor Login */}
1584
- <div className="space-y-4">
1585
- <div className="flex items-center gap-3">
1586
- <LogIn className="w-5 h-5 text-purple-500" />
1587
- <h3 className="text-lg font-medium text-foreground">
1588
- Authentication
1589
- </h3>
1590
- </div>
1591
- <div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
1592
- <div className="flex items-center justify-between">
1593
- <div>
1594
- <div className="font-medium text-purple-900 dark:text-purple-100">
1595
- Cursor CLI Login
1596
- </div>
1597
- <div className="text-sm text-purple-700 dark:text-purple-300">
1598
- Sign in to your Cursor account to enable AI features
1599
- </div>
1600
- </div>
1601
- <Button
1602
- onClick={handleCursorLogin}
1603
- className="bg-purple-600 hover:bg-purple-700 text-white"
1604
- size="sm"
1605
- >
1606
- <LogIn className="w-4 h-4 mr-2" />
1607
- Login
1608
- </Button>
1609
- </div>
1610
- </div>
1611
- </div>
1612
-
1613
- {/* Allowed Shell Commands */}
1614
- <div className="space-y-4">
1615
- <div className="flex items-center gap-3">
1616
- <Shield className="w-5 h-5 text-green-500" />
1617
- <h3 className="text-lg font-medium text-foreground">
1618
- Allowed Shell Commands
1619
- </h3>
1620
- </div>
1621
- <p className="text-sm text-muted-foreground">
1622
- Shell commands that are automatically allowed without prompting for permission
1623
- </p>
1624
-
1625
- <div className="flex flex-col sm:flex-row gap-2">
1626
- <Input
1627
- value={newCursorCommand}
1628
- onChange={(e) => setNewCursorCommand(e.target.value)}
1629
- placeholder='e.g., "Shell(ls)" or "Shell(git status)"'
1630
- onKeyPress={(e) => {
1631
- if (e.key === 'Enter') {
1632
- if (newCursorCommand && !cursorAllowedCommands.includes(newCursorCommand)) {
1633
- setCursorAllowedCommands([...cursorAllowedCommands, newCursorCommand]);
1634
- setNewCursorCommand('');
1635
- }
1636
- }
1637
- }}
1638
- className="flex-1 h-10 touch-manipulation"
1639
- style={{ fontSize: '16px' }}
1640
- />
1641
- <Button
1642
- onClick={() => {
1643
- if (newCursorCommand && !cursorAllowedCommands.includes(newCursorCommand)) {
1644
- setCursorAllowedCommands([...cursorAllowedCommands, newCursorCommand]);
1645
- setNewCursorCommand('');
1646
- }
1647
- }}
1648
- disabled={!newCursorCommand}
1649
- size="sm"
1650
- className="h-10 px-4 touch-manipulation"
1651
- >
1652
- <Plus className="w-4 h-4 mr-2 sm:mr-0" />
1653
- <span className="sm:hidden">Add Command</span>
1654
- </Button>
1655
- </div>
1656
-
1657
- {/* Common commands quick add */}
1658
- <div className="space-y-2">
1659
- <p className="text-sm font-medium text-gray-700 dark:text-gray-300">
1660
- Quick add common commands:
1661
- </p>
1662
- <div className="grid grid-cols-2 sm:flex sm:flex-wrap gap-2">
1663
- {commonCursorCommands.map(cmd => (
1664
- <Button
1665
- key={cmd}
1666
- variant="outline"
1667
- size="sm"
1668
- onClick={() => {
1669
- if (!cursorAllowedCommands.includes(cmd)) {
1670
- setCursorAllowedCommands([...cursorAllowedCommands, cmd]);
1671
- }
1672
- }}
1673
- disabled={cursorAllowedCommands.includes(cmd)}
1674
- className="text-xs h-8 touch-manipulation truncate"
1675
- >
1676
- {cmd}
1677
- </Button>
1678
- ))}
1679
- </div>
1680
- </div>
1681
-
1682
- <div className="space-y-2">
1683
- {cursorAllowedCommands.map(cmd => (
1684
- <div key={cmd} className="flex items-center justify-between bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
1685
- <span className="font-mono text-sm text-green-800 dark:text-green-200">
1686
- {cmd}
1687
- </span>
1688
- <Button
1689
- variant="ghost"
1690
- size="sm"
1691
- onClick={() => setCursorAllowedCommands(cursorAllowedCommands.filter(c => c !== cmd))}
1692
- className="text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
1693
- >
1694
- <X className="w-4 h-4" />
1695
- </Button>
1696
- </div>
1697
- ))}
1698
- {cursorAllowedCommands.length === 0 && (
1699
- <div className="text-center py-8 text-gray-500 dark:text-gray-400">
1700
- No allowed shell commands configured
1701
- </div>
1702
- )}
1703
- </div>
1704
- </div>
1705
-
1706
- {/* Disallowed Shell Commands */}
1707
- <div className="space-y-4">
1708
- <div className="flex items-center gap-3">
1709
- <Shield className="w-5 h-5 text-red-500" />
1710
- <h3 className="text-lg font-medium text-foreground">
1711
- Disallowed Shell Commands
1712
- </h3>
1713
- </div>
1714
- <p className="text-sm text-muted-foreground">
1715
- Shell commands that should always be denied
1716
- </p>
1717
-
1718
- <div className="flex flex-col sm:flex-row gap-2">
1719
- <Input
1720
- value={newCursorDisallowedCommand}
1721
- onChange={(e) => setNewCursorDisallowedCommand(e.target.value)}
1722
- placeholder='e.g., "Shell(rm -rf)" or "Shell(sudo)"'
1723
- onKeyPress={(e) => {
1724
- if (e.key === 'Enter') {
1725
- if (newCursorDisallowedCommand && !cursorDisallowedCommands.includes(newCursorDisallowedCommand)) {
1726
- setCursorDisallowedCommands([...cursorDisallowedCommands, newCursorDisallowedCommand]);
1727
- setNewCursorDisallowedCommand('');
1728
- }
1729
- }
1730
- }}
1731
- className="flex-1 h-10 touch-manipulation"
1732
- style={{ fontSize: '16px' }}
1733
- />
1734
- <Button
1735
- onClick={() => {
1736
- if (newCursorDisallowedCommand && !cursorDisallowedCommands.includes(newCursorDisallowedCommand)) {
1737
- setCursorDisallowedCommands([...cursorDisallowedCommands, newCursorDisallowedCommand]);
1738
- setNewCursorDisallowedCommand('');
1739
- }
1740
- }}
1741
- disabled={!newCursorDisallowedCommand}
1742
- size="sm"
1743
- className="h-10 px-4 touch-manipulation"
1744
- >
1745
- <Plus className="w-4 h-4 mr-2 sm:mr-0" />
1746
- <span className="sm:hidden">Add Command</span>
1747
- </Button>
1748
- </div>
1749
-
1750
- <div className="space-y-2">
1751
- {cursorDisallowedCommands.map(cmd => (
1752
- <div key={cmd} className="flex items-center justify-between bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
1753
- <span className="font-mono text-sm text-red-800 dark:text-red-200">
1754
- {cmd}
1755
- </span>
1756
- <Button
1757
- variant="ghost"
1758
- size="sm"
1759
- onClick={() => setCursorDisallowedCommands(cursorDisallowedCommands.filter(c => c !== cmd))}
1760
- className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
1761
- >
1762
- <X className="w-4 h-4" />
1763
- </Button>
1764
- </div>
1765
- ))}
1766
- {cursorDisallowedCommands.length === 0 && (
1767
- <div className="text-center py-8 text-gray-500 dark:text-gray-400">
1768
- No disallowed shell commands configured
1769
- </div>
1770
- )}
1771
- </div>
1772
- </div>
1773
-
1774
- {/* Help Section */}
1775
- <div className="bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800 rounded-lg p-4">
1776
- <h4 className="font-medium text-purple-900 dark:text-purple-100 mb-2">
1777
- Cursor Shell Command Examples:
1778
- </h4>
1779
- <ul className="text-sm text-purple-800 dark:text-purple-200 space-y-1">
1780
- <li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(ls)"</code> - Allow ls command</li>
1781
- <li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(git status)"</code> - Allow git status command</li>
1782
- <li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"Shell(mkdir)"</code> - Allow mkdir command</li>
1783
- <li><code className="bg-purple-100 dark:bg-purple-800 px-1 rounded">"-f"</code> flag - Skip all permission prompts (dangerous)</li>
1784
- </ul>
1785
- </div>
1786
- </div>
1787
- )}
1788
- </div>
1789
- )}
1790
-
1791
- {/* Tasks Tab */}
1792
- {activeTab === 'tasks' && (
1793
- <div className="space-y-6 md:space-y-8">
1794
- {/* Installation Status Check */}
1795
- {isCheckingInstallation ? (
1796
- <div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
1797
- <div className="flex items-center gap-3">
1798
- <div className="animate-spin w-5 h-5 border-2 border-blue-600 border-t-transparent rounded-full"></div>
1799
- <span className="text-sm text-muted-foreground">Checking TaskMaster installation...</span>
1800
- </div>
1801
- </div>
1802
- ) : (
1803
- <>
1804
- {/* TaskMaster Not Installed Warning */}
1805
- {!isTaskMasterInstalled && (
1806
- <div className="bg-orange-50 dark:bg-orange-950/50 border border-orange-200 dark:border-orange-800 rounded-lg p-4">
1807
- <div className="flex items-start gap-3">
1808
- <div className="w-8 h-8 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
1809
- <svg className="w-4 h-4 text-orange-600 dark:text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1810
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
1811
- </svg>
1812
- </div>
1813
- <div className="flex-1">
1814
- <div className="font-medium text-orange-900 dark:text-orange-100 mb-2">
1815
- TaskMaster AI CLI Not Installed
1816
- </div>
1817
- <div className="text-sm text-orange-800 dark:text-orange-200 space-y-3">
1818
- <p>TaskMaster CLI is required to use task management features. Install it to get started:</p>
1819
-
1820
- <div className="bg-orange-100 dark:bg-orange-900/50 rounded-lg p-3 font-mono text-sm">
1821
- <code>npm install -g task-master-ai</code>
1822
- <a
1823
- href="https://github.com/eyaltoledano/claude-task-master"
1824
- target="_blank"
1825
- rel="noopener noreferrer"
1826
- className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
1827
- >
1828
- <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
1829
- <path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
1830
- </svg>
1831
- View on GitHub
1832
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1833
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
1834
- </svg>
1835
- </a>
1836
- </div>
1837
-
1838
- <div className="space-y-2">
1839
- <p className="font-medium">After installation:</p>
1840
- <ol className="list-decimal list-inside space-y-1 text-xs">
1841
- <li>Restart this application</li>
1842
- <li>TaskMaster features will automatically become available</li>
1843
- <li>Use <code className="bg-orange-100 dark:bg-orange-800 px-1 rounded">task-master init</code> in your project directory</li>
1844
- </ol>
1845
- </div>
1846
- </div>
1847
- </div>
1848
- </div>
1849
- </div>
1850
- )}
1851
-
1852
- {/* TaskMaster Settings */}
1853
- <div className="space-y-4">
1854
- <div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
1855
- <div className="flex items-center justify-between">
1856
- <div>
1857
- <div className="font-medium text-foreground">
1858
- Enable TaskMaster Integration
1859
- </div>
1860
- <div className="text-sm text-muted-foreground mt-1">
1861
- Show TaskMaster tasks, banners, and sidebar indicators across the interface
1862
- </div>
1863
- {!isTaskMasterInstalled && (
1864
- <div className="text-xs text-orange-600 dark:text-orange-400 mt-1">
1865
- TaskMaster CLI must be installed first
1866
- </div>
1867
- )}
1868
- </div>
1869
- <label className="relative inline-flex items-center cursor-pointer">
1870
- <input
1871
- type="checkbox"
1872
- checked={tasksEnabled}
1873
- onChange={(e) => setTasksEnabled(e.target.checked)}
1874
- disabled={!isTaskMasterInstalled}
1875
- className="sr-only peer"
1876
- />
1877
- <div className={`w-11 h-6 ${!isTaskMasterInstalled ? 'bg-gray-300 dark:bg-gray-600' : 'bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800'} rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600`}></div>
1878
- </label>
1879
- </div>
1880
- </div>
1881
-
1882
- {/* TaskMaster Information */}
1883
- <div className="bg-blue-50 dark:bg-blue-950/50 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
1884
- <div className="flex items-start gap-3">
1885
- <div className="w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
1886
- <Zap className="w-4 h-4 text-blue-600 dark:text-blue-400" />
1887
- </div>
1888
- <div>
1889
- <div className="font-medium text-blue-900 dark:text-blue-100 mb-2">
1890
- 🎯 About TaskMaster AI
1891
- </div>
1892
- <div className="text-sm text-blue-800 dark:text-blue-200 space-y-2">
1893
- <p><strong>AI-Powered Task Management:</strong> Break complex projects into manageable subtasks with AI assistance</p>
1894
- <p><strong>PRD:</strong> Generate structured tasks from Product Requirements Documents</p>
1895
- <p><strong>Dependency Tracking:</strong> Understand task relationships and execution order</p>
1896
- <p><strong>Progress Visualization:</strong> Kanban boards, and detailed task views</p>
1897
- </div>
1898
- </div>
1899
-
1900
- {/* GitHub Link and Resources */}
1901
- <div className="mt-4 pt-4 border-t border-blue-200 dark:border-blue-700">
1902
- <div className="flex items-start gap-3">
1903
- <div className="w-6 h-6 bg-blue-100 dark:bg-blue-800 rounded-full flex items-center justify-center flex-shrink-0 mt-0.5">
1904
- <svg className="w-3 h-3 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
1905
- <path fillRule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 00-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clipRule="evenodd" />
1906
- </svg>
1907
- </div>
1908
- <div className="flex-1">
1909
- <div className="font-medium text-blue-900 dark:text-blue-100 mb-2">
1910
- 📚 Learn More & Tutorial
1911
- </div>
1912
- <div className="text-sm text-blue-800 dark:text-blue-200 space-y-2">
1913
- <p>TaskMaster AI (aka <strong>claude-task-master</strong> ) is an advanced AI-powered task management system built for developers.</p>
1914
- <div className="flex flex-col gap-2">
1915
- <a
1916
- href="https://github.com/eyaltoledano/claude-task-master"
1917
- target="_blank"
1918
- rel="noopener noreferrer"
1919
- className="inline-flex items-center gap-2 text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 font-medium"
1920
- >
1921
- <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
1922
- <path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" />
1923
- </svg>
1924
- View on GitHub
1925
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1926
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
1927
- </svg>
1928
- </a>
1929
- <p className="text-xs text-blue-700 dark:text-blue-300">
1930
- Find documentation, setup guides, and examples for advanced TaskMaster workflows
1931
- </p>
1932
- </div>
1933
- </div>
1934
- </div>
1935
- </div>
1936
- </div>
1937
- </div>
1938
- </div>
1939
- </div>
1940
- </>
1941
- )}
1942
- </div>
1943
- )}
1944
- </div>
1945
- </div>
1946
-
1947
- <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-4 md:p-6 border-t border-border flex-shrink-0 gap-3 pb-safe-area-inset-bottom">
1948
- <div className="flex items-center justify-center sm:justify-start gap-2 order-2 sm:order-1">
1949
- {saveStatus === 'success' && (
1950
- <div className="text-green-600 dark:text-green-400 text-sm flex items-center gap-1">
1951
- <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
1952
- <path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
1953
- </svg>
1954
- Settings saved successfully!
1955
- </div>
1956
- )}
1957
- {saveStatus === 'error' && (
1958
- <div className="text-red-600 dark:text-red-400 text-sm flex items-center gap-1">
1959
- <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
1960
- <path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
1961
- </svg>
1962
- Failed to save settings
1963
- </div>
1964
- )}
1965
- </div>
1966
- <div className="flex items-center gap-3 order-1 sm:order-2">
1967
- <Button
1968
- variant="outline"
1969
- onClick={onClose}
1970
- disabled={isSaving}
1971
- className="flex-1 sm:flex-none h-10 touch-manipulation"
1972
- >
1973
- Cancel
1974
- </Button>
1975
- <Button
1976
- onClick={saveSettings}
1977
- disabled={isSaving}
1978
- className="flex-1 sm:flex-none h-10 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 touch-manipulation"
1979
- >
1980
- {isSaving ? (
1981
- <div className="flex items-center gap-2">
1982
- <div className="w-4 h-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
1983
- Saving...
1984
- </div>
1985
- ) : (
1986
- 'Save Settings'
1987
- )}
1988
- </Button>
1989
- </div>
1990
- </div>
1991
- </div>
1992
-
1993
- {/* Login Modal */}
1994
- {showLoginModal && (
1995
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 max-md:items-stretch max-md:justify-stretch">
1996
- <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl h-3/4 flex flex-col md:max-w-4xl md:h-3/4 md:rounded-lg md:m-4 max-md:max-w-none max-md:h-full max-md:rounded-none max-md:m-0">
1997
- <div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
1998
- <h3 className="text-lg font-semibold text-gray-900 dark:text-white">
1999
- {loginProvider === 'claude' ? 'Claude CLI Login' : 'Cursor CLI Login'}
2000
- </h3>
2001
- <button
2002
- onClick={() => setShowLoginModal(false)}
2003
- className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
2004
- >
2005
- <X className="w-6 h-6" />
2006
- </button>
2007
- </div>
2008
- <div className="flex-1 overflow-hidden">
2009
- <StandaloneShell
2010
- project={selectedProject}
2011
- command={loginProvider === 'claude' ? 'claude /login' : 'cursor-agent login'}
2012
- onComplete={handleLoginComplete}
2013
- showHeader={false}
2014
- />
2015
- </div>
2016
- </div>
2017
- </div>
2018
- )}
2019
- </div>
2020
- );
2021
- }
2022
-
2023
- export default Settings;