shell-mirror 1.0.0

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.
@@ -0,0 +1,867 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Terminal Mirror</title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
+ <style>
7
+ html, body {
8
+ margin: 0;
9
+ padding: 0;
10
+ height: 100vh;
11
+ width: 100vw;
12
+ overflow: hidden;
13
+ background-color: #000;
14
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
15
+ }
16
+ #auth-container {
17
+ display: flex;
18
+ justify-content: center;
19
+ align-items: center;
20
+ height: 100%;
21
+ }
22
+ #login-btn {
23
+ padding: 15px 30px;
24
+ font-size: 18px;
25
+ color: white;
26
+ background-color: #4285F4;
27
+ border: none;
28
+ border-radius: 5px;
29
+ cursor: pointer;
30
+ }
31
+ #app-container {
32
+ display: none; /* Initially hidden */
33
+ flex-direction: column;
34
+ width: 100%;
35
+ height: 100%;
36
+ }
37
+ #terminal-container {
38
+ flex-grow: 1;
39
+ width: 100%;
40
+ overflow: hidden;
41
+ position: relative;
42
+ }
43
+ #connection-status {
44
+ position: absolute;
45
+ top: 10px;
46
+ right: 10px;
47
+ padding: 5px 10px;
48
+ border-radius: 15px;
49
+ font-size: 12px;
50
+ font-weight: bold;
51
+ z-index: 1000;
52
+ }
53
+ .status-connected {
54
+ background-color: #4CAF50;
55
+ color: white;
56
+ }
57
+ .status-connecting {
58
+ background-color: #FF9800;
59
+ color: white;
60
+ }
61
+ .status-disconnected {
62
+ background-color: #F44336;
63
+ color: white;
64
+ }
65
+ #keyboard-bar {
66
+ flex-shrink: 0;
67
+ width: 100%;
68
+ background-color: #222;
69
+ display: flex;
70
+ justify-content: space-around;
71
+ padding: 8px 0;
72
+ box-sizing: border-box;
73
+ }
74
+ .key-btn {
75
+ color: white;
76
+ background-color: #444;
77
+ border: 1px solid #666;
78
+ border-radius: 5px;
79
+ font-size: 16px;
80
+ padding: 10px 12px;
81
+ margin: 0 2px;
82
+ touch-action: manipulation;
83
+ -webkit-tap-highlight-color: transparent;
84
+ }
85
+ </style>
86
+ </head>
87
+ <body>
88
+ <div id="auth-container">
89
+ <button id="login-btn">Login with Google</button>
90
+ </div>
91
+
92
+ <div id="app-container">
93
+ <div id="terminal-container">
94
+ <div id="connection-status" class="status-connecting">Connecting...</div>
95
+ </div>
96
+ <div id="keyboard-bar">
97
+ <button class="key-btn" data-key-name="ESC">Esc</button>
98
+ <button class="key-btn" data-key-name="TAB">Tab</button>
99
+ <button class="key-btn" data-key-name="ARROW_UP">▲</button>
100
+ <button class="key-btn" data-key-name="ARROW_DOWN">▼</button>
101
+ <button class="key-btn" data-key-name="ARROW_LEFT">◀</button>
102
+ <button class="key-btn" data-key-name="ARROW_RIGHT">▶</button>
103
+ <button class="key-btn" data-key-name="ENTER">Enter</button>
104
+ </div>
105
+ </div>
106
+
107
+ <!-- Dynamically load xterm scripts only after authentication -->
108
+ <script>
109
+ const authContainer = document.getElementById('auth-container');
110
+ const appContainer = document.getElementById('app-container');
111
+ const loginBtn = document.getElementById('login-btn');
112
+
113
+ loginBtn.addEventListener('click', () => {
114
+ window.location.href = '/php-backend/api/auth-login.php';
115
+ });
116
+
117
+ // Check authentication status on page load
118
+ fetch('/php-backend/api/auth-status.php', {
119
+ credentials: 'same-origin',
120
+ headers: {
121
+ 'Accept': 'application/json',
122
+ 'Content-Type': 'application/json'
123
+ }
124
+ })
125
+ .then(res => {
126
+ if (!res.ok) {
127
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
128
+ }
129
+ return res.json();
130
+ })
131
+ .then(data => {
132
+ console.log('Auth status response:', data);
133
+ if (data.success && data.data && data.data.authenticated) {
134
+ console.log('✅ User is authenticated, loading terminal...');
135
+ authContainer.style.display = 'none';
136
+ appContainer.style.display = 'flex';
137
+ console.log('📦 Loading xterm scripts...');
138
+ loadXtermScripts(loadTerminal);
139
+ } else {
140
+ console.log('❌ User not authenticated, showing login form');
141
+ }
142
+ })
143
+ .catch(error => {
144
+ console.error('Auth status check failed:', error);
145
+ console.log('Falling back to login form due to auth check failure');
146
+ });
147
+
148
+ function loadXtermScripts(callback) {
149
+ console.log('🔄 Starting to load xterm CSS...');
150
+ const xtermCss = document.createElement('link');
151
+ xtermCss.rel = 'stylesheet';
152
+ xtermCss.href = 'https://unpkg.com/xterm@5.3.0/css/xterm.css';
153
+ document.head.appendChild(xtermCss);
154
+
155
+ console.log('🔄 Starting to load xterm JS...');
156
+ const xtermJs = document.createElement('script');
157
+ xtermJs.src = 'https://unpkg.com/xterm@5.3.0/lib/xterm.js';
158
+ document.head.appendChild(xtermJs);
159
+
160
+ xtermJs.onload = () => {
161
+ console.log('✅ Xterm JS loaded, loading fit addon...');
162
+ const fitAddonJs = document.createElement('script');
163
+ fitAddonJs.src = 'https://unpkg.com/@xterm/addon-fit@0.10.0/lib/addon-fit.js';
164
+ document.head.appendChild(fitAddonJs);
165
+ fitAddonJs.onload = () => {
166
+ console.log('✅ Fit addon loaded, initializing terminal...');
167
+ callback();
168
+ };
169
+ fitAddonJs.onerror = (error) => {
170
+ console.error('❌ Failed to load fit addon:', error);
171
+ };
172
+ };
173
+ xtermJs.onerror = (error) => {
174
+ console.error('❌ Failed to load xterm JS:', error);
175
+ };
176
+ }
177
+
178
+ function loadTerminal() {
179
+ console.log('🚀 Initializing terminal...');
180
+ const termContainer = document.getElementById('terminal-container');
181
+ const connectionStatus = document.getElementById('connection-status');
182
+
183
+ if (!termContainer) {
184
+ console.error('❌ Terminal container not found!');
185
+ return;
186
+ }
187
+
188
+ if (!connectionStatus) {
189
+ console.error('❌ Connection status element not found!');
190
+ return;
191
+ }
192
+
193
+ console.log('📦 Creating Terminal instance...');
194
+ const term = new Terminal({
195
+ cursorBlink: true,
196
+ convertEol: true,
197
+ fontSize: 14,
198
+ });
199
+ const fitAddon = new FitAddon.FitAddon();
200
+ term.loadAddon(fitAddon);
201
+ term.open(termContainer);
202
+
203
+ const keyMap = {
204
+ ARROW_UP: "\u001b[A",
205
+ ARROW_DOWN: "\u001b[B",
206
+ ARROW_RIGHT: "\u001b[C",
207
+ ARROW_LEFT: "\u001b[D",
208
+ TAB: "\u0009",
209
+ ESC: "\u001b",
210
+ ENTER: "\r"
211
+ };
212
+
213
+ let sessionId = 'terminal_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
214
+ let currentCommand = '';
215
+ let commandHistory = [];
216
+ let historyIndex = -1;
217
+ let tabCompletions = [];
218
+ let tabCompletionIndex = -1;
219
+ let lastTabPrefix = '';
220
+
221
+ // Performance enhancement variables
222
+ let commandCache = new Map();
223
+ let directoryCache = new Map();
224
+ let currentWorkingDir = '~';
225
+ let isExecuting = false;
226
+ let executionQueue = [];
227
+ let pendingInput = ''; // Buffer input during execution
228
+
229
+ function updateConnectionStatus(status, message) {
230
+ connectionStatus.className = `status-${status}`;
231
+
232
+ // Add cache info to status
233
+ const cacheSize = commandCache.size;
234
+ const cacheInfo = cacheSize > 0 ? ` (${cacheSize} cached)` : '';
235
+ connectionStatus.textContent = message + cacheInfo;
236
+ }
237
+
238
+ function initializeTerminal() {
239
+ console.log('🚀 Initializing terminal...');
240
+ updateConnectionStatus('connected', 'Connected');
241
+
242
+ // Load cached state
243
+ loadCachedState();
244
+
245
+ // Show welcome message
246
+ term.write('\x1b[32mTerminal Mirror - Ready\x1b[0m\r\n');
247
+ term.write('Commands will route to available Mac agents or web server.\r\n');
248
+ term.write('High-performance mode: Caching enabled for instant responses.\r\n\r\n');
249
+
250
+ // Show initial prompt
251
+ console.log('📝 Showing initial prompt, current dir:', currentWorkingDir);
252
+ showPrompt();
253
+
254
+ setTimeout(() => {
255
+ fitAndResize();
256
+ term.focus();
257
+ console.log('✅ Terminal focused and ready');
258
+
259
+ // Start background pre-caching
260
+ startBackgroundCaching();
261
+ }, 100);
262
+ }
263
+
264
+ function loadCachedState() {
265
+ // Restore working directory
266
+ const savedCwd = localStorage.getItem('terminal_cwd');
267
+ if (savedCwd) {
268
+ currentWorkingDir = savedCwd;
269
+ }
270
+
271
+ // Restore command history
272
+ const savedHistory = localStorage.getItem('terminal_history');
273
+ if (savedHistory) {
274
+ try {
275
+ commandHistory = JSON.parse(savedHistory);
276
+ historyIndex = commandHistory.length;
277
+ } catch (e) {
278
+ console.warn('Failed to restore command history');
279
+ }
280
+ }
281
+
282
+ // Store user info for instant whoami
283
+ if (window.userInfo) {
284
+ localStorage.setItem('terminal_user_info', JSON.stringify(window.userInfo));
285
+ }
286
+ }
287
+
288
+ function startBackgroundCaching() {
289
+ // Pre-cache current directory listing
290
+ preCacheDirectoryContents();
291
+
292
+ // Cache common system info
293
+ setTimeout(() => {
294
+ ['whoami', 'uname -a'].forEach(cmd => {
295
+ if (!commandCache.has(`${currentWorkingDir}:${cmd}`)) {
296
+ executeServerCommand(cmd, true);
297
+ }
298
+ });
299
+ }, 500);
300
+
301
+ // Set up periodic cache refresh
302
+ setInterval(() => {
303
+ refreshStaleCache();
304
+ }, 60000); // Every minute
305
+ }
306
+
307
+ function refreshStaleCache() {
308
+ const now = Date.now();
309
+ const staleThreshold = 300000; // 5 minutes
310
+
311
+ for (const [key, entry] of commandCache.entries()) {
312
+ if (now - entry.timestamp > staleThreshold) {
313
+ // Refresh stale entries in background
314
+ const [dir, command] = key.split(':');
315
+ if (dir === currentWorkingDir && (command.startsWith('ls') || command === 'pwd')) {
316
+ executeServerCommand(command, true);
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ function saveState() {
323
+ // Save command history
324
+ localStorage.setItem('terminal_history', JSON.stringify(commandHistory.slice(-100))); // Keep last 100 commands
325
+
326
+ // Save working directory
327
+ localStorage.setItem('terminal_cwd', currentWorkingDir);
328
+ }
329
+
330
+ function checkMacAgents() {
331
+ return fetch('/php-backend/api/agents-list.php')
332
+ .then(response => response.json())
333
+ .then(data => {
334
+ if (data.success && data.data.agents && data.data.agents.length > 0) {
335
+ const onlineAgents = data.data.agents.filter(agent => agent.status === 'online');
336
+ if (onlineAgents.length > 0) {
337
+ const agent = onlineAgents[0];
338
+ return {
339
+ hasMacAgents: true,
340
+ agentName: agent.machineName || agent.agentId,
341
+ ownerName: agent.ownerEmail,
342
+ agentId: agent.agentId
343
+ };
344
+ }
345
+ }
346
+ return { hasMacAgents: false };
347
+ })
348
+ .catch(error => {
349
+ console.error('Mac agent check failed:', error);
350
+ return { hasMacAgents: false };
351
+ });
352
+ }
353
+
354
+ function showPrompt() {
355
+ // Show performance-enhanced prompt with current directory
356
+ let shortDir = currentWorkingDir;
357
+
358
+ // Simplify directory display
359
+ try {
360
+ if (shortDir.length > 20) {
361
+ const parts = shortDir.split('/');
362
+ shortDir = '.../' + parts[parts.length - 1];
363
+ }
364
+ } catch (e) {
365
+ shortDir = '~';
366
+ }
367
+
368
+ term.write('\x1b[36m' + shortDir + ' $\x1b[0m ');
369
+ }
370
+
371
+ function executeCommand(command) {
372
+ const trimmedCommand = command.trim();
373
+
374
+ if (!trimmedCommand) {
375
+ term.write('\r\n');
376
+ showPrompt();
377
+ return;
378
+ }
379
+
380
+ // Log the exact command being executed
381
+ console.log('🎯 Executing exact command:', JSON.stringify(trimmedCommand));
382
+
383
+ // Add to history immediately
384
+ commandHistory.push(trimmedCommand);
385
+ historyIndex = commandHistory.length;
386
+
387
+ // Save state for persistence
388
+ saveState();
389
+
390
+ // Set executing state to prevent input bleeding
391
+ isExecuting = true;
392
+ pendingInput = '';
393
+
394
+ // Move to new line (preserve the typed command line)
395
+ term.write('\r\n');
396
+
397
+ // Check for instant commands first
398
+ const instantResult = executeInstantCommand(trimmedCommand);
399
+ if (instantResult !== null) {
400
+ if (instantResult) {
401
+ term.write(instantResult);
402
+ }
403
+ term.write('\r\n');
404
+ isExecuting = false; // Re-enable input
405
+ showPrompt();
406
+ processPendingInput();
407
+ return;
408
+ }
409
+
410
+ // Check cache for recently executed commands
411
+ const cacheKey = `${currentWorkingDir}:${trimmedCommand}`;
412
+ if (commandCache.has(cacheKey)) {
413
+ const cachedResult = commandCache.get(cacheKey);
414
+ const cacheAge = Date.now() - cachedResult.timestamp;
415
+
416
+ // Use cache if less than 30 seconds old for most commands
417
+ // or 5 minutes for directory listings
418
+ const maxAge = trimmedCommand.startsWith('ls') ? 300000 : 30000;
419
+
420
+ if (cacheAge < maxAge) {
421
+ term.write('\x1b[90m[Cached result - ' + Math.round(cacheAge/1000) + 's old]\x1b[0m\r\n');
422
+ if (cachedResult.output) {
423
+ term.write(cachedResult.output.replace(/\n/g, '\r\n'));
424
+ }
425
+ term.write('\r\n');
426
+ isExecuting = false; // Re-enable input
427
+ showPrompt();
428
+ processPendingInput();
429
+
430
+ // Refresh cache in background
431
+ setTimeout(() => executeServerCommand(trimmedCommand, true), 100);
432
+ return;
433
+ }
434
+ }
435
+
436
+ // Show execution status without cluttering terminal
437
+ updateConnectionStatus('connecting', 'Executing...');
438
+
439
+ // Execute on server
440
+ executeServerCommand(trimmedCommand, false);
441
+ }
442
+
443
+ function executeInstantCommand(command) {
444
+ const cmd = command.trim().toLowerCase();
445
+ const now = new Date();
446
+
447
+ switch (cmd) {
448
+ case 'pwd':
449
+ return currentWorkingDir;
450
+ case 'date':
451
+ return now.toString();
452
+ case 'whoami':
453
+ // Return cached user info if available
454
+ const userInfo = localStorage.getItem('terminal_user_info');
455
+ if (userInfo) {
456
+ return JSON.parse(userInfo).username || 'user';
457
+ }
458
+ break;
459
+ case 'clear':
460
+ term.clear();
461
+ return '';
462
+ case 'history':
463
+ return commandHistory.map((cmd, i) => `${i + 1} ${cmd}`).join('\n');
464
+ }
465
+
466
+ return null; // Not an instant command
467
+ }
468
+
469
+ function executeServerCommand(command, isBackgroundRefresh = false) {
470
+ const startTime = Date.now();
471
+
472
+ fetch('/php-backend/api/terminal-execute.php', {
473
+ method: 'POST',
474
+ headers: {
475
+ 'Content-Type': 'application/json',
476
+ },
477
+ body: JSON.stringify({
478
+ command: command,
479
+ session_id: sessionId
480
+ })
481
+ })
482
+ .then(response => response.json())
483
+ .then(data => {
484
+ const responseTime = Date.now() - startTime;
485
+
486
+ if (!isBackgroundRefresh) {
487
+ updateConnectionStatus('connected', 'Connected');
488
+ }
489
+
490
+ if (data.success) {
491
+ const output = data.data.output || '';
492
+
493
+ // Cache the result
494
+ const cacheKey = `${currentWorkingDir}:${command}`;
495
+ commandCache.set(cacheKey, {
496
+ output: output,
497
+ timestamp: Date.now(),
498
+ responseTime: responseTime
499
+ });
500
+
501
+ // Update working directory if cd command
502
+ updateWorkingDirectory(command, output);
503
+
504
+ // Pre-cache directory contents for navigation commands
505
+ if (command.startsWith('cd ')) {
506
+ preCacheDirectoryContents();
507
+ }
508
+
509
+ if (!isBackgroundRefresh) {
510
+ // Show execution source info
511
+ if (data.data.mac_agent) {
512
+ term.write('\x1b[90m[Mac: ' + (data.data.agent_id || 'unknown') + ' - ' + responseTime + 'ms]\x1b[0m\r\n');
513
+ } else {
514
+ term.write('\x1b[90m[Server - ' + responseTime + 'ms]\x1b[0m\r\n');
515
+ }
516
+
517
+ if (output) {
518
+ const formattedOutput = output.replace(/\n/g, '\r\n');
519
+ term.write(formattedOutput);
520
+ }
521
+ term.write('\r\n');
522
+ isExecuting = false; // Re-enable input
523
+ showPrompt();
524
+ processPendingInput();
525
+ }
526
+ } else {
527
+ if (!isBackgroundRefresh) {
528
+ term.write('\x1b[31m❌ Error: ' + (data.message || 'Command execution failed') + '\x1b[0m\r\n');
529
+ isExecuting = false; // Re-enable input
530
+ showPrompt();
531
+ processPendingInput();
532
+ }
533
+ }
534
+ })
535
+ .catch(error => {
536
+ if (!isBackgroundRefresh) {
537
+ updateConnectionStatus('disconnected', 'Error');
538
+ console.error('Command execution failed:', error);
539
+ term.write('\x1b[31m🚫 Connection error. Please try again.\x1b[0m\r\n');
540
+ isExecuting = false; // Re-enable input
541
+ showPrompt();
542
+ processPendingInput();
543
+ }
544
+ });
545
+ }
546
+
547
+ function updateWorkingDirectory(command, output) {
548
+ if (command.startsWith('cd ')) {
549
+ // Only update directory if cd command was successful
550
+ if (output && output.includes('Changed directory to:')) {
551
+ const match = output.match(/Changed directory to:\s*(.+)/);
552
+ if (match) {
553
+ currentWorkingDir = match[1].trim();
554
+ localStorage.setItem('terminal_cwd', currentWorkingDir);
555
+ console.log('🗂️ Updated working directory to:', currentWorkingDir);
556
+ return;
557
+ }
558
+ }
559
+
560
+ // Check if command failed - do NOT update directory
561
+ if (output && (output.includes('no such file or directory') ||
562
+ output.includes('Not a directory') ||
563
+ output.includes('Permission denied') ||
564
+ output.includes('cd:'))) {
565
+ console.log('❌ cd command failed, keeping current directory:', currentWorkingDir);
566
+ return; // Don't update directory on failure
567
+ }
568
+
569
+ // Only fallback to manual construction if no output (shouldn't happen with Mac agent)
570
+ console.warn('⚠️ No output from cd command, using fallback logic');
571
+ const target = command.substring(3).trim();
572
+ if (target === '' || target === '~') {
573
+ currentWorkingDir = '~';
574
+ localStorage.setItem('terminal_cwd', currentWorkingDir);
575
+ console.log('🗂️ Updated working directory to home:', currentWorkingDir);
576
+ }
577
+ // Don't attempt relative path construction without confirmation
578
+
579
+ } else if (command === 'pwd' && output) {
580
+ currentWorkingDir = output.trim();
581
+ localStorage.setItem('terminal_cwd', currentWorkingDir);
582
+ console.log('🗂️ Updated working directory from pwd:', currentWorkingDir);
583
+ }
584
+ }
585
+
586
+ function preCacheDirectoryContents() {
587
+ // Pre-cache ls results for current directory
588
+ setTimeout(() => {
589
+ ['ls', 'ls -la', 'ls -l'].forEach(lsCmd => {
590
+ const cacheKey = `${currentWorkingDir}:${lsCmd}`;
591
+ if (!commandCache.has(cacheKey)) {
592
+ executeServerCommand(lsCmd, true);
593
+ }
594
+ });
595
+ }, 200);
596
+ }
597
+
598
+ function handleSpecialKeys(sequence) {
599
+ switch (sequence) {
600
+ case keyMap.ARROW_UP:
601
+ // History up
602
+ if (historyIndex > 0) {
603
+ historyIndex--;
604
+ replaceCurrentCommand(commandHistory[historyIndex] || '');
605
+ resetTabCompletion();
606
+ }
607
+ break;
608
+ case keyMap.ARROW_DOWN:
609
+ // History down
610
+ if (historyIndex < commandHistory.length - 1) {
611
+ historyIndex++;
612
+ replaceCurrentCommand(commandHistory[historyIndex] || '');
613
+ } else {
614
+ historyIndex = commandHistory.length;
615
+ replaceCurrentCommand('');
616
+ }
617
+ resetTabCompletion();
618
+ break;
619
+ case keyMap.ENTER:
620
+ executeCommand(currentCommand);
621
+ currentCommand = '';
622
+ break;
623
+ case keyMap.TAB:
624
+ handleTabCompletion();
625
+ break;
626
+ default:
627
+ return false; // Not handled
628
+ }
629
+ return true; // Handled
630
+ }
631
+
632
+ function replaceCurrentCommand(newCommand) {
633
+ // Clear current line
634
+ term.write('\x1b[2K\r'); // Clear line and return to start
635
+ showPrompt();
636
+ term.write(newCommand);
637
+ currentCommand = newCommand;
638
+ console.log('🔄 Command replaced with:', JSON.stringify(newCommand));
639
+ }
640
+
641
+ function handleTabCompletion() {
642
+ // Reset tab completion if command changed
643
+ const words = currentCommand.split(' ');
644
+ const currentWord = words[words.length - 1] || '';
645
+
646
+ if (lastTabPrefix !== currentWord) {
647
+ tabCompletions = [];
648
+ tabCompletionIndex = -1;
649
+ lastTabPrefix = currentWord;
650
+ }
651
+
652
+ if (tabCompletions.length === 0) {
653
+ // First tab - get completions from server
654
+ getTabCompletions(currentWord).then(completions => {
655
+ if (completions.length === 0) {
656
+ // No completions available
657
+ return;
658
+ } else if (completions.length === 1) {
659
+ // Single completion - auto-complete immediately
660
+ completeCurrentWord(completions[0]);
661
+ } else {
662
+ // Multiple completions - start cycling
663
+ tabCompletions = completions;
664
+ tabCompletionIndex = 0;
665
+ completeCurrentWord(completions[0]);
666
+ }
667
+ });
668
+ } else {
669
+ // Subsequent tabs - cycle through completions
670
+ tabCompletionIndex = (tabCompletionIndex + 1) % tabCompletions.length;
671
+ completeCurrentWord(tabCompletions[tabCompletionIndex]);
672
+ }
673
+ }
674
+
675
+ function completeCurrentWord(completion) {
676
+ const words = currentCommand.split(' ');
677
+ words[words.length - 1] = completion;
678
+ const newCommand = words.join(' ');
679
+
680
+ // Clear current line and write new command
681
+ term.write('\x1b[2K\r');
682
+ showPrompt();
683
+ term.write(newCommand);
684
+ currentCommand = newCommand;
685
+ console.log('🔤 Tab completed to:', JSON.stringify(newCommand));
686
+ }
687
+
688
+ function getTabCompletions(prefix) {
689
+ // Try to get completions from cache first
690
+ const cachedCompletions = getCachedCompletions(prefix);
691
+ if (cachedCompletions.length > 0) {
692
+ return Promise.resolve(cachedCompletions);
693
+ }
694
+
695
+ // Fallback to server request
696
+ return fetch('/php-backend/api/terminal-complete.php', {
697
+ method: 'POST',
698
+ headers: {
699
+ 'Content-Type': 'application/json',
700
+ },
701
+ body: JSON.stringify({
702
+ prefix: prefix,
703
+ command: currentCommand,
704
+ session_id: sessionId
705
+ })
706
+ })
707
+ .then(response => response.json())
708
+ .then(data => {
709
+ if (data.success && data.data && data.data.completions) {
710
+ return data.data.completions;
711
+ }
712
+ return [];
713
+ })
714
+ .catch(error => {
715
+ console.error('Tab completion failed:', error);
716
+ return getCachedCompletions(prefix); // Fallback to cache on error
717
+ });
718
+ }
719
+
720
+ function getCachedCompletions(prefix) {
721
+ const words = currentCommand.split(' ');
722
+ const isFirstWord = words.length <= 1;
723
+
724
+ if (isFirstWord) {
725
+ // Command completion
726
+ const commands = ['ls', 'cd', 'pwd', 'cat', 'echo', 'grep', 'find', 'ps', 'df', 'du', 'uname', 'date', 'which', 'whoami', 'clear', 'history', 'mkdir', 'rmdir', 'rm', 'cp', 'mv', 'chmod', 'chown', 'head', 'tail', 'less', 'more', 'wc', 'sort', 'uniq'];
727
+ return commands.filter(cmd => cmd.startsWith(prefix.toLowerCase()));
728
+ } else {
729
+ // File/directory completion from cached ls results
730
+ const lsCacheKey = `${currentWorkingDir}:ls -la`;
731
+ if (commandCache.has(lsCacheKey)) {
732
+ const lsOutput = commandCache.get(lsCacheKey).output;
733
+ const files = parseLsOutput(lsOutput);
734
+ return files.filter(file => file.startsWith(prefix));
735
+ }
736
+ }
737
+
738
+ return [];
739
+ }
740
+
741
+ function parseLsOutput(lsOutput) {
742
+ if (!lsOutput) return [];
743
+
744
+ const lines = lsOutput.split('\n');
745
+ const files = [];
746
+
747
+ for (const line of lines) {
748
+ const trimmed = line.trim();
749
+ if (!trimmed || trimmed.startsWith('total')) continue;
750
+
751
+ // Parse ls -la format: permissions user group size date filename
752
+ const parts = trimmed.split(/\s+/);
753
+ if (parts.length >= 9) {
754
+ const filename = parts.slice(8).join(' ');
755
+ if (filename !== '.' && filename !== '..') {
756
+ files.push(filename);
757
+ }
758
+ }
759
+ }
760
+
761
+ return files;
762
+ }
763
+
764
+ function resetTabCompletion() {
765
+ tabCompletions = [];
766
+ tabCompletionIndex = -1;
767
+ lastTabPrefix = '';
768
+ }
769
+
770
+ function fitAndResize() {
771
+ appContainer.style.height = `${window.visualViewport.height}px`;
772
+ fitAddon.fit();
773
+ }
774
+
775
+ // Initialize terminal
776
+ initializeTerminal();
777
+
778
+ // Terminal input handling
779
+ term.onData((data) => {
780
+ try {
781
+ // If command is executing, buffer the input instead of processing it
782
+ if (isExecuting) {
783
+ pendingInput += data;
784
+ console.log('⏳ Buffering input during execution:', JSON.stringify(data));
785
+ return;
786
+ }
787
+
788
+ // Handle special key sequences
789
+ if (handleSpecialKeys(data)) {
790
+ return;
791
+ }
792
+
793
+ // Handle regular characters
794
+ if (data === '\x7f') { // Backspace
795
+ if (currentCommand.length > 0) {
796
+ currentCommand = currentCommand.slice(0, -1);
797
+ term.write('\b \b');
798
+ resetTabCompletion();
799
+ }
800
+ } else if (data === '\r') { // Enter
801
+ console.log('📤 Executing command:', JSON.stringify(currentCommand));
802
+ executeCommand(currentCommand);
803
+ clearCurrentCommand();
804
+ } else if (data === '\t') { // Tab key
805
+ handleTabCompletion();
806
+ } else if (data.charCodeAt(0) >= 32) { // Printable characters
807
+ currentCommand += data;
808
+ term.write(data);
809
+ console.log('⌨️ Current command buffer:', JSON.stringify(currentCommand));
810
+ // Reset tab completion when user types
811
+ resetTabCompletion();
812
+ }
813
+ } catch (error) {
814
+ console.error('Terminal input error:', error);
815
+ term.write('\r\n\x1b[31mInput error occurred\x1b[0m\r\n');
816
+ clearCurrentCommand();
817
+ showPrompt();
818
+ }
819
+ });
820
+
821
+ function processPendingInput() {
822
+ if (pendingInput) {
823
+ console.log('🔄 Processing pending input:', JSON.stringify(pendingInput));
824
+ // Process buffered input character by character
825
+ for (let i = 0; i < pendingInput.length; i++) {
826
+ const char = pendingInput[i];
827
+ if (char.charCodeAt(0) >= 32) { // Only printable characters
828
+ currentCommand += char;
829
+ term.write(char);
830
+ }
831
+ }
832
+ pendingInput = '';
833
+ }
834
+ }
835
+
836
+ function clearCurrentCommand() {
837
+ currentCommand = '';
838
+ console.log('🧹 Command buffer cleared');
839
+ }
840
+
841
+ // Virtual keyboard handling
842
+ document.getElementById('keyboard-bar').addEventListener('click', (e) => {
843
+ if (e.target.classList.contains('key-btn')) {
844
+ e.preventDefault();
845
+ const keyName = e.target.dataset.keyName;
846
+ const sequence = keyMap[keyName];
847
+ if (sequence) {
848
+ handleSpecialKeys(sequence);
849
+ }
850
+ term.focus();
851
+ }
852
+ });
853
+
854
+ // Focus terminal on body click
855
+ document.body.addEventListener('click', (e) => {
856
+ if (e.target.classList.contains('key-btn')) return;
857
+ term.focus();
858
+ }, true);
859
+
860
+ // Handle window resize
861
+ window.visualViewport.addEventListener('resize', fitAndResize);
862
+
863
+ term.focus();
864
+ }
865
+ </script>
866
+ </body>
867
+ </html>