@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,663 +0,0 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
- import { Terminal } from 'xterm';
3
- import { FitAddon } from 'xterm-addon-fit';
4
- import { ClipboardAddon } from '@xterm/addon-clipboard';
5
- import { WebglAddon } from '@xterm/addon-webgl';
6
- import 'xterm/css/xterm.css';
7
-
8
- // CSS to remove xterm focus outline
9
- const xtermStyles = `
10
- .xterm .xterm-screen {
11
- outline: none !important;
12
- }
13
- .xterm:focus .xterm-screen {
14
- outline: none !important;
15
- }
16
- .xterm-screen:focus {
17
- outline: none !important;
18
- }
19
- `;
20
-
21
- // Inject styles
22
- if (typeof document !== 'undefined') {
23
- const styleSheet = document.createElement('style');
24
- styleSheet.type = 'text/css';
25
- styleSheet.innerText = xtermStyles;
26
- document.head.appendChild(styleSheet);
27
- }
28
-
29
- // Global store for shell sessions to persist across tab switches
30
- const shellSessions = new Map();
31
-
32
- function Shell({ selectedProject, selectedSession, isActive, initialCommand, isPlainShell = false, onProcessComplete }) {
33
- const terminalRef = useRef(null);
34
- const terminal = useRef(null);
35
- const fitAddon = useRef(null);
36
- const ws = useRef(null);
37
- const [isConnected, setIsConnected] = useState(false);
38
- const [isInitialized, setIsInitialized] = useState(false);
39
- const [isRestarting, setIsRestarting] = useState(false);
40
- const [lastSessionId, setLastSessionId] = useState(null);
41
- const [isConnecting, setIsConnecting] = useState(false);
42
-
43
- // Connect to shell function
44
- const connectToShell = () => {
45
- if (!isInitialized || isConnected || isConnecting) return;
46
-
47
- setIsConnecting(true);
48
-
49
- // Start the WebSocket connection
50
- connectWebSocket();
51
- };
52
-
53
- // Disconnect from shell function
54
- const disconnectFromShell = () => {
55
-
56
- if (ws.current) {
57
- ws.current.close();
58
- ws.current = null;
59
- }
60
-
61
- // Clear terminal content completely
62
- if (terminal.current) {
63
- terminal.current.clear();
64
- terminal.current.write('\x1b[2J\x1b[H'); // Clear screen and move cursor to home
65
- }
66
-
67
- setIsConnected(false);
68
- setIsConnecting(false);
69
- };
70
-
71
- // Restart shell function
72
- const restartShell = () => {
73
- setIsRestarting(true);
74
-
75
- // Clear ALL session storage for this project to force fresh start
76
- const sessionKeys = Array.from(shellSessions.keys()).filter(key =>
77
- key.includes(selectedProject.name)
78
- );
79
- sessionKeys.forEach(key => shellSessions.delete(key));
80
-
81
-
82
- // Close existing WebSocket
83
- if (ws.current) {
84
- ws.current.close();
85
- ws.current = null;
86
- }
87
-
88
- // Clear and dispose existing terminal
89
- if (terminal.current) {
90
-
91
- // Dispose terminal immediately without writing text
92
- terminal.current.dispose();
93
- terminal.current = null;
94
- fitAddon.current = null;
95
- }
96
-
97
- // Reset states
98
- setIsConnected(false);
99
- setIsInitialized(false);
100
-
101
-
102
- // Force re-initialization after cleanup
103
- setTimeout(() => {
104
- setIsRestarting(false);
105
- }, 200);
106
- };
107
-
108
- // Watch for session changes and restart shell
109
- useEffect(() => {
110
- const currentSessionId = selectedSession?.id || null;
111
-
112
-
113
- // Disconnect when session changes (user will need to manually reconnect)
114
- if (lastSessionId !== null && lastSessionId !== currentSessionId && isInitialized) {
115
-
116
- // Disconnect from current shell
117
- disconnectFromShell();
118
-
119
- // Clear stored sessions for this project
120
- const allKeys = Array.from(shellSessions.keys());
121
- allKeys.forEach(key => {
122
- if (key.includes(selectedProject.name)) {
123
- shellSessions.delete(key);
124
- }
125
- });
126
- }
127
-
128
- setLastSessionId(currentSessionId);
129
- }, [selectedSession?.id, isInitialized]);
130
-
131
- // Initialize terminal when component mounts
132
- useEffect(() => {
133
-
134
- if (!terminalRef.current || !selectedProject || isRestarting) {
135
- return;
136
- }
137
-
138
- // Create session key for this project/session combination
139
- const sessionKey = selectedSession?.id || `project-${selectedProject.name}`;
140
-
141
- // Check if we have an existing session
142
- const existingSession = shellSessions.get(sessionKey);
143
- if (existingSession && !terminal.current) {
144
-
145
- try {
146
- // Reuse existing terminal
147
- terminal.current = existingSession.terminal;
148
- fitAddon.current = existingSession.fitAddon;
149
- ws.current = existingSession.ws;
150
- setIsConnected(existingSession.isConnected);
151
-
152
- // Reattach to DOM - dispose existing element first if needed
153
- if (terminal.current.element && terminal.current.element.parentNode) {
154
- terminal.current.element.parentNode.removeChild(terminal.current.element);
155
- }
156
-
157
- terminal.current.open(terminalRef.current);
158
-
159
- setTimeout(() => {
160
- if (fitAddon.current) {
161
- fitAddon.current.fit();
162
- // Send terminal size to backend after reattaching
163
- if (ws.current && ws.current.readyState === WebSocket.OPEN) {
164
- ws.current.send(JSON.stringify({
165
- type: 'resize',
166
- cols: terminal.current.cols,
167
- rows: terminal.current.rows
168
- }));
169
- }
170
- }
171
- }, 100);
172
-
173
- setIsInitialized(true);
174
- return;
175
- } catch (error) {
176
- // Clear the broken session and continue to create a new one
177
- shellSessions.delete(sessionKey);
178
- terminal.current = null;
179
- fitAddon.current = null;
180
- ws.current = null;
181
- }
182
- }
183
-
184
- if (terminal.current) {
185
- return;
186
- }
187
-
188
-
189
- // Initialize new terminal
190
- terminal.current = new Terminal({
191
- cursorBlink: true,
192
- fontSize: 14,
193
- fontFamily: 'Menlo, Monaco, "Courier New", monospace',
194
- allowProposedApi: true, // Required for clipboard addon
195
- allowTransparency: false,
196
- convertEol: true,
197
- scrollback: 10000,
198
- tabStopWidth: 4,
199
- // Enable full color support
200
- windowsMode: false,
201
- macOptionIsMeta: true,
202
- macOptionClickForcesSelection: false,
203
- // Enhanced theme with full 16-color ANSI support + true colors
204
- theme: {
205
- // Basic colors
206
- background: '#1e1e1e',
207
- foreground: '#d4d4d4',
208
- cursor: '#ffffff',
209
- cursorAccent: '#1e1e1e',
210
- selection: '#264f78',
211
- selectionForeground: '#ffffff',
212
-
213
- // Standard ANSI colors (0-7)
214
- black: '#000000',
215
- red: '#cd3131',
216
- green: '#0dbc79',
217
- yellow: '#e5e510',
218
- blue: '#2472c8',
219
- magenta: '#bc3fbc',
220
- cyan: '#11a8cd',
221
- white: '#e5e5e5',
222
-
223
- // Bright ANSI colors (8-15)
224
- brightBlack: '#666666',
225
- brightRed: '#f14c4c',
226
- brightGreen: '#23d18b',
227
- brightYellow: '#f5f543',
228
- brightBlue: '#3b8eea',
229
- brightMagenta: '#d670d6',
230
- brightCyan: '#29b8db',
231
- brightWhite: '#ffffff',
232
-
233
- // Extended colors for better Claude output
234
- extendedAnsi: [
235
- // 16-color palette extension for 256-color support
236
- '#000000', '#800000', '#008000', '#808000',
237
- '#000080', '#800080', '#008080', '#c0c0c0',
238
- '#808080', '#ff0000', '#00ff00', '#ffff00',
239
- '#0000ff', '#ff00ff', '#00ffff', '#ffffff'
240
- ]
241
- }
242
- });
243
-
244
- fitAddon.current = new FitAddon();
245
- const clipboardAddon = new ClipboardAddon();
246
- const webglAddon = new WebglAddon();
247
-
248
- terminal.current.loadAddon(fitAddon.current);
249
- terminal.current.loadAddon(clipboardAddon);
250
-
251
- try {
252
- terminal.current.loadAddon(webglAddon);
253
- } catch (error) {
254
- }
255
-
256
- terminal.current.open(terminalRef.current);
257
-
258
- // Wait for terminal to be fully rendered, then fit
259
- setTimeout(() => {
260
- if (fitAddon.current) {
261
- fitAddon.current.fit();
262
- }
263
- }, 50);
264
-
265
- // Add keyboard shortcuts for copy/paste
266
- terminal.current.attachCustomKeyEventHandler((event) => {
267
- // Ctrl+C or Cmd+C for copy (when text is selected)
268
- if ((event.ctrlKey || event.metaKey) && event.key === 'c' && terminal.current.hasSelection()) {
269
- document.execCommand('copy');
270
- return false;
271
- }
272
-
273
- // Ctrl+V or Cmd+V for paste
274
- if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
275
- navigator.clipboard.readText().then(text => {
276
- if (ws.current && ws.current.readyState === WebSocket.OPEN) {
277
- ws.current.send(JSON.stringify({
278
- type: 'input',
279
- data: text
280
- }));
281
- }
282
- }).catch(err => {
283
- // Failed to read clipboard
284
- });
285
- return false;
286
- }
287
-
288
- return true;
289
- });
290
-
291
- // Ensure terminal takes full space and notify backend of size
292
- setTimeout(() => {
293
- if (fitAddon.current) {
294
- fitAddon.current.fit();
295
- // Send terminal size to backend after fitting
296
- if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) {
297
- ws.current.send(JSON.stringify({
298
- type: 'resize',
299
- cols: terminal.current.cols,
300
- rows: terminal.current.rows
301
- }));
302
- }
303
- }
304
- }, 100);
305
-
306
- setIsInitialized(true);
307
-
308
- // Handle terminal input
309
- terminal.current.onData((data) => {
310
- if (ws.current && ws.current.readyState === WebSocket.OPEN) {
311
- ws.current.send(JSON.stringify({
312
- type: 'input',
313
- data: data
314
- }));
315
- }
316
- });
317
-
318
- // Add resize observer to handle container size changes
319
- const resizeObserver = new ResizeObserver(() => {
320
- if (fitAddon.current && terminal.current) {
321
- setTimeout(() => {
322
- fitAddon.current.fit();
323
- // Send updated terminal size to backend after resize
324
- if (ws.current && ws.current.readyState === WebSocket.OPEN) {
325
- ws.current.send(JSON.stringify({
326
- type: 'resize',
327
- cols: terminal.current.cols,
328
- rows: terminal.current.rows
329
- }));
330
- }
331
- }, 50);
332
- }
333
- });
334
-
335
- if (terminalRef.current) {
336
- resizeObserver.observe(terminalRef.current);
337
- }
338
-
339
- return () => {
340
- resizeObserver.disconnect();
341
-
342
- // Store session for reuse instead of disposing
343
- if (terminal.current && selectedProject) {
344
- const sessionKey = selectedSession?.id || `project-${selectedProject.name}`;
345
-
346
- try {
347
- shellSessions.set(sessionKey, {
348
- terminal: terminal.current,
349
- fitAddon: fitAddon.current,
350
- ws: ws.current,
351
- isConnected: isConnected
352
- });
353
-
354
- } catch (error) {
355
- }
356
- }
357
- };
358
- }, [terminalRef.current, selectedProject, selectedSession, isRestarting]);
359
-
360
- // Fit terminal when tab becomes active
361
- useEffect(() => {
362
- if (!isActive || !isInitialized) return;
363
-
364
- // Fit terminal when tab becomes active and notify backend
365
- setTimeout(() => {
366
- if (fitAddon.current) {
367
- fitAddon.current.fit();
368
- // Send terminal size to backend after tab activation
369
- if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) {
370
- ws.current.send(JSON.stringify({
371
- type: 'resize',
372
- cols: terminal.current.cols,
373
- rows: terminal.current.rows
374
- }));
375
- }
376
- }
377
- }, 100);
378
- }, [isActive, isInitialized]);
379
-
380
- // WebSocket connection function (called manually)
381
- const connectWebSocket = async () => {
382
- if (isConnecting || isConnected) return;
383
-
384
- try {
385
- // Get authentication token
386
- const token = localStorage.getItem('auth-token');
387
- if (!token) {
388
- console.error('No authentication token found for Shell WebSocket connection');
389
- return;
390
- }
391
-
392
- // Fetch server configuration to get the correct WebSocket URL
393
- let wsBaseUrl;
394
- try {
395
- const configResponse = await fetch('/api/config', {
396
- headers: {
397
- 'Authorization': `Bearer ${token}`
398
- }
399
- });
400
- const config = await configResponse.json();
401
- wsBaseUrl = config.wsUrl;
402
-
403
- // If the config returns localhost but we're not on localhost, use current host but with API server port
404
- if (wsBaseUrl.includes('localhost') && !window.location.hostname.includes('localhost')) {
405
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
406
- // For development, API server is typically on port 3002 when Vite is on 3001
407
- const apiPort = window.location.port === '3001' ? '3002' : window.location.port;
408
- wsBaseUrl = `${protocol}//${window.location.hostname}:${apiPort}`;
409
- }
410
- } catch (error) {
411
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
412
- // For development, API server is typically on port 3002 when Vite is on 3001
413
- const apiPort = window.location.port === '3001' ? '3002' : window.location.port;
414
- wsBaseUrl = `${protocol}//${window.location.hostname}:${apiPort}`;
415
- }
416
-
417
- // Include token in WebSocket URL as query parameter
418
- const wsUrl = `${wsBaseUrl}/shell?token=${encodeURIComponent(token)}`;
419
-
420
- ws.current = new WebSocket(wsUrl);
421
-
422
- ws.current.onopen = () => {
423
- setIsConnected(true);
424
- setIsConnecting(false);
425
-
426
- // Wait for terminal to be ready, then fit and send dimensions
427
- setTimeout(() => {
428
- if (fitAddon.current && terminal.current) {
429
- // Force a fit to ensure proper dimensions
430
- fitAddon.current.fit();
431
-
432
- // Wait a bit more for fit to complete, then send dimensions
433
- setTimeout(() => {
434
- const initPayload = {
435
- type: 'init',
436
- projectPath: selectedProject.fullPath || selectedProject.path,
437
- sessionId: isPlainShell ? null : selectedSession?.id,
438
- hasSession: isPlainShell ? false : !!selectedSession,
439
- provider: isPlainShell ? 'plain-shell' : (selectedSession?.__provider || 'claude'),
440
- cols: terminal.current.cols,
441
- rows: terminal.current.rows,
442
- initialCommand: initialCommand,
443
- isPlainShell: isPlainShell
444
- };
445
-
446
- console.log('Shell init payload:', initPayload);
447
-
448
- ws.current.send(JSON.stringify(initPayload));
449
-
450
- // Also send resize message immediately after init
451
- setTimeout(() => {
452
- if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) {
453
- ws.current.send(JSON.stringify({
454
- type: 'resize',
455
- cols: terminal.current.cols,
456
- rows: terminal.current.rows
457
- }));
458
- }
459
- }, 100);
460
- }, 50);
461
- }
462
- }, 200);
463
- };
464
-
465
- ws.current.onmessage = (event) => {
466
- try {
467
- const data = JSON.parse(event.data);
468
- if (data.type === 'output') {
469
- // Check for URLs in the output and make them clickable
470
- const urlRegex = /(https?:\/\/[^\s\x1b\x07]+)/g;
471
- let output = data.data;
472
-
473
- // Find URLs in the text (excluding ANSI escape sequences)
474
- const urls = [];
475
- let match;
476
- while ((match = urlRegex.exec(output.replace(/\x1b\[[0-9;]*m/g, ''))) !== null) {
477
- urls.push(match[1]);
478
- }
479
-
480
- if (isPlainShell && onProcessComplete) {
481
- const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, ''); // Remove ANSI codes
482
- if (cleanOutput.includes('Process exited with code 0')) {
483
- onProcessComplete(0); // Success
484
- } else if (cleanOutput.match(/Process exited with code (\d+)/)) {
485
- const exitCode = parseInt(cleanOutput.match(/Process exited with code (\d+)/)[1]);
486
- if (exitCode !== 0) {
487
- onProcessComplete(exitCode); // Error
488
- }
489
- }
490
- }
491
-
492
- // If URLs found, log them for potential opening
493
-
494
- terminal.current.write(output);
495
- } else if (data.type === 'url_open') {
496
- // Handle explicit URL opening requests from server
497
- window.open(data.url, '_blank');
498
- }
499
- } catch (error) {
500
- }
501
- };
502
-
503
- ws.current.onclose = (event) => {
504
- setIsConnected(false);
505
- setIsConnecting(false);
506
-
507
- // Clear terminal content when connection closes
508
- if (terminal.current) {
509
- terminal.current.clear();
510
- terminal.current.write('\x1b[2J\x1b[H'); // Clear screen and move cursor to home
511
- }
512
-
513
- // Don't auto-reconnect anymore - user must manually connect
514
- };
515
-
516
- ws.current.onerror = (error) => {
517
- setIsConnected(false);
518
- setIsConnecting(false);
519
- };
520
- } catch (error) {
521
- setIsConnected(false);
522
- setIsConnecting(false);
523
- }
524
- };
525
-
526
-
527
- if (!selectedProject) {
528
- return (
529
- <div className="h-full flex items-center justify-center">
530
- <div className="text-center text-gray-500 dark:text-gray-400">
531
- <div className="w-16 h-16 mx-auto mb-4 bg-gray-100 dark:bg-gray-800 rounded-full flex items-center justify-center">
532
- <svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
533
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v14a2 2 0 002 2z" />
534
- </svg>
535
- </div>
536
- <h3 className="text-lg font-semibold mb-2">Select a Project</h3>
537
- <p>Choose a project to open an interactive shell in that directory</p>
538
- </div>
539
- </div>
540
- );
541
- }
542
-
543
- return (
544
- <div className="h-full flex flex-col bg-gray-900 w-full">
545
- {/* Header */}
546
- <div className="flex-shrink-0 bg-gray-800 border-b border-gray-700 px-4 py-2">
547
- <div className="flex items-center justify-between">
548
- <div className="flex items-center space-x-2">
549
- <div className={`w-2 h-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />
550
- {selectedSession && (() => {
551
- const displaySessionName = selectedSession.__provider === 'cursor'
552
- ? (selectedSession.name || 'Untitled Session')
553
- : (selectedSession.summary || 'New Session');
554
- return (
555
- <span className="text-xs text-blue-300">
556
- ({displaySessionName.slice(0, 30)}...)
557
- </span>
558
- );
559
- })()}
560
- {!selectedSession && (
561
- <span className="text-xs text-gray-400">(New Session)</span>
562
- )}
563
- {!isInitialized && (
564
- <span className="text-xs text-yellow-400">(Initializing...)</span>
565
- )}
566
- {isRestarting && (
567
- <span className="text-xs text-blue-400">(Restarting...)</span>
568
- )}
569
- </div>
570
- <div className="flex items-center space-x-3">
571
- {isConnected && (
572
- <button
573
- onClick={disconnectFromShell}
574
- className="px-3 py-1 text-xs bg-red-600 text-white rounded hover:bg-red-700 flex items-center space-x-1"
575
- title="Disconnect from shell"
576
- >
577
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
578
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
579
- </svg>
580
- <span>Disconnect</span>
581
- </button>
582
- )}
583
-
584
- <button
585
- onClick={restartShell}
586
- disabled={isRestarting || isConnected}
587
- className="text-xs text-gray-400 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-1"
588
- title="Restart Shell (disconnect first)"
589
- >
590
- <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
591
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
592
- </svg>
593
- <span>Restart</span>
594
- </button>
595
- </div>
596
- </div>
597
- </div>
598
-
599
- {/* Terminal */}
600
- <div className="flex-1 p-2 overflow-hidden relative">
601
- <div ref={terminalRef} className="h-full w-full focus:outline-none" style={{ outline: 'none' }} />
602
-
603
- {/* Loading state */}
604
- {!isInitialized && (
605
- <div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90">
606
- <div className="text-white">Loading terminal...</div>
607
- </div>
608
- )}
609
-
610
- {/* Connect button when not connected */}
611
- {isInitialized && !isConnected && !isConnecting && (
612
- <div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
613
- <div className="text-center max-w-sm w-full">
614
- <button
615
- onClick={connectToShell}
616
- className="px-6 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center justify-center space-x-2 text-base font-medium w-full sm:w-auto"
617
- title="Connect to shell"
618
- >
619
- <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
620
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
621
- </svg>
622
- <span>Continue in Shell</span>
623
- </button>
624
- <p className="text-gray-400 text-sm mt-3 px-2">
625
- {isPlainShell ?
626
- `Run ${initialCommand || 'command'} in ${selectedProject.displayName}` :
627
- selectedSession ?
628
- (() => {
629
- const displaySessionName = selectedSession.__provider === 'cursor'
630
- ? (selectedSession.name || 'Untitled Session')
631
- : (selectedSession.summary || 'New Session');
632
- return `Resume session: ${displaySessionName.slice(0, 50)}...`;
633
- })() :
634
- 'Start a new Claude session'
635
- }
636
- </p>
637
- </div>
638
- </div>
639
- )}
640
-
641
- {/* Connecting state */}
642
- {isConnecting && (
643
- <div className="absolute inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 p-4">
644
- <div className="text-center max-w-sm w-full">
645
- <div className="flex items-center justify-center space-x-3 text-yellow-400">
646
- <div className="w-6 h-6 animate-spin rounded-full border-2 border-yellow-400 border-t-transparent"></div>
647
- <span className="text-base font-medium">Connecting to shell...</span>
648
- </div>
649
- <p className="text-gray-400 text-sm mt-3 px-2">
650
- {isPlainShell ?
651
- `Running ${initialCommand || 'command'} in ${selectedProject.displayName}` :
652
- `Starting Claude CLI in ${selectedProject.displayName}`
653
- }
654
- </p>
655
- </div>
656
- </div>
657
- )}
658
- </div>
659
- </div>
660
- );
661
- }
662
-
663
- export default Shell;