@siteboon/claude-code-ui 1.8.2 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-BNGSzSdr.css +32 -0
- package/dist/assets/index-BZctMHnE.js +900 -0
- package/{index.html → dist/index.html} +4 -3
- package/package.json +6 -1
- package/server/database/auth.db +0 -0
- package/.env.example +0 -12
- package/.nvmrc +0 -1
- package/postcss.config.js +0 -6
- package/src/App.jsx +0 -751
- package/src/components/ChatInterface.jsx +0 -3485
- package/src/components/ClaudeLogo.jsx +0 -11
- package/src/components/ClaudeStatus.jsx +0 -107
- package/src/components/CodeEditor.jsx +0 -422
- package/src/components/CreateTaskModal.jsx +0 -88
- package/src/components/CursorLogo.jsx +0 -9
- package/src/components/DarkModeToggle.jsx +0 -35
- package/src/components/DiffViewer.jsx +0 -41
- package/src/components/ErrorBoundary.jsx +0 -73
- package/src/components/FileTree.jsx +0 -480
- package/src/components/GitPanel.jsx +0 -1283
- package/src/components/ImageViewer.jsx +0 -54
- package/src/components/LoginForm.jsx +0 -110
- package/src/components/MainContent.jsx +0 -577
- package/src/components/MicButton.jsx +0 -272
- package/src/components/MobileNav.jsx +0 -88
- package/src/components/NextTaskBanner.jsx +0 -695
- package/src/components/PRDEditor.jsx +0 -871
- package/src/components/ProtectedRoute.jsx +0 -44
- package/src/components/QuickSettingsPanel.jsx +0 -262
- package/src/components/Settings.jsx +0 -2023
- package/src/components/SetupForm.jsx +0 -135
- package/src/components/Shell.jsx +0 -663
- package/src/components/Sidebar.jsx +0 -1665
- package/src/components/StandaloneShell.jsx +0 -106
- package/src/components/TaskCard.jsx +0 -210
- package/src/components/TaskDetail.jsx +0 -406
- package/src/components/TaskIndicator.jsx +0 -108
- package/src/components/TaskList.jsx +0 -1054
- package/src/components/TaskMasterSetupWizard.jsx +0 -603
- package/src/components/TaskMasterStatus.jsx +0 -86
- package/src/components/TodoList.jsx +0 -91
- package/src/components/Tooltip.jsx +0 -91
- package/src/components/ui/badge.jsx +0 -31
- package/src/components/ui/button.jsx +0 -46
- package/src/components/ui/input.jsx +0 -19
- package/src/components/ui/scroll-area.jsx +0 -23
- package/src/contexts/AuthContext.jsx +0 -158
- package/src/contexts/TaskMasterContext.jsx +0 -324
- package/src/contexts/TasksSettingsContext.jsx +0 -95
- package/src/contexts/ThemeContext.jsx +0 -94
- package/src/contexts/WebSocketContext.jsx +0 -29
- package/src/hooks/useAudioRecorder.js +0 -109
- package/src/hooks/useVersionCheck.js +0 -39
- package/src/index.css +0 -822
- package/src/lib/utils.js +0 -6
- package/src/main.jsx +0 -10
- package/src/utils/api.js +0 -141
- package/src/utils/websocket.js +0 -109
- package/src/utils/whisper.js +0 -37
- package/tailwind.config.js +0 -63
- package/vite.config.js +0 -29
- /package/{public → dist}/convert-icons.md +0 -0
- /package/{public → dist}/favicon.png +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/generate-icons.js +0 -0
- /package/{public → dist}/icons/claude-ai-icon.svg +0 -0
- /package/{public → dist}/icons/cursor.svg +0 -0
- /package/{public → dist}/icons/generate-icons.md +0 -0
- /package/{public → dist}/icons/icon-128x128.png +0 -0
- /package/{public → dist}/icons/icon-128x128.svg +0 -0
- /package/{public → dist}/icons/icon-144x144.png +0 -0
- /package/{public → dist}/icons/icon-144x144.svg +0 -0
- /package/{public → dist}/icons/icon-152x152.png +0 -0
- /package/{public → dist}/icons/icon-152x152.svg +0 -0
- /package/{public → dist}/icons/icon-192x192.png +0 -0
- /package/{public → dist}/icons/icon-192x192.svg +0 -0
- /package/{public → dist}/icons/icon-384x384.png +0 -0
- /package/{public → dist}/icons/icon-384x384.svg +0 -0
- /package/{public → dist}/icons/icon-512x512.png +0 -0
- /package/{public → dist}/icons/icon-512x512.svg +0 -0
- /package/{public → dist}/icons/icon-72x72.png +0 -0
- /package/{public → dist}/icons/icon-72x72.svg +0 -0
- /package/{public → dist}/icons/icon-96x96.png +0 -0
- /package/{public → dist}/icons/icon-96x96.svg +0 -0
- /package/{public → dist}/icons/icon-template.svg +0 -0
- /package/{public → dist}/logo.svg +0 -0
- /package/{public → dist}/manifest.json +0 -0
- /package/{public → dist}/screenshots/cli-selection.png +0 -0
- /package/{public → dist}/screenshots/desktop-main.png +0 -0
- /package/{public → dist}/screenshots/mobile-chat.png +0 -0
- /package/{public → dist}/screenshots/tools-modal.png +0 -0
- /package/{public → dist}/sw.js +0 -0
package/src/components/Shell.jsx
DELETED
|
@@ -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;
|