@adiontaegerron/claude-multi-terminal 1.0.3 → 1.2.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.
package/renderer.js CHANGED
@@ -6,8 +6,76 @@ const { FitAddon } = require('xterm-addon-fit');
6
6
  const terminals = new Map();
7
7
  let terminalCounter = 0;
8
8
 
9
+ // Command system
10
+ let selectedCommandIndex = -1;
11
+ let currentCommands = [];
12
+
13
+ // Command history
14
+ const commandHistory = [];
15
+ let historyIndex = -1;
16
+ let tempCurrentInput = '';
17
+ const MAX_HISTORY_SIZE = 50;
18
+
19
+ // Available commands
20
+ const availableCommands = [
21
+ {
22
+ name: '/send',
23
+ syntax: '/send <terminal> <command>',
24
+ description: 'Send command to specific terminal',
25
+ args: ['terminal', 'command']
26
+ },
27
+ {
28
+ name: '/send-all',
29
+ syntax: '/send-all <command>',
30
+ description: 'Send command to all terminals',
31
+ args: ['command']
32
+ },
33
+ {
34
+ name: '/list',
35
+ syntax: '/list',
36
+ description: 'List all terminals',
37
+ args: []
38
+ },
39
+ {
40
+ name: '/help',
41
+ syntax: '/help',
42
+ description: 'Show available commands',
43
+ args: []
44
+ },
45
+ {
46
+ name: '/create',
47
+ syntax: '/create [name]',
48
+ description: 'Create new terminal with optional name',
49
+ args: ['name?']
50
+ },
51
+ {
52
+ name: '/close',
53
+ syntax: '/close <terminal>',
54
+ description: 'Close specific terminal',
55
+ args: ['terminal']
56
+ },
57
+ {
58
+ name: '/return',
59
+ syntax: '/return <terminal>',
60
+ description: 'Submit current input in specified terminal (press Enter)',
61
+ args: ['terminal']
62
+ },
63
+ {
64
+ name: '/clear',
65
+ syntax: '/clear <terminal>',
66
+ description: 'Clear current input line in specified terminal (Ctrl+C)',
67
+ args: ['terminal']
68
+ },
69
+ {
70
+ name: '/interrupt',
71
+ syntax: '/interrupt <terminal>',
72
+ description: 'Interrupt current command in specified terminal (Ctrl+C)',
73
+ args: ['terminal']
74
+ }
75
+ ];
76
+
9
77
  // Create a new terminal instance
10
- async function createTerminal() {
78
+ async function createTerminal(customName = null) {
11
79
  terminalCounter++;
12
80
  const terminalId = `terminal-${terminalCounter}`;
13
81
 
@@ -21,7 +89,8 @@ async function createTerminal() {
21
89
  header.className = 'terminal-header';
22
90
 
23
91
  const title = document.createElement('span');
24
- title.textContent = `Terminal ${terminalCounter}`;
92
+ const terminalName = customName || `Terminal ${terminalCounter}`;
93
+ title.textContent = terminalName;
25
94
  title.className = 'terminal-title';
26
95
  title.style.cursor = 'pointer';
27
96
 
@@ -80,14 +149,14 @@ async function createTerminal() {
80
149
  fitAddon,
81
150
  element: terminalContainer,
82
151
  titleElement: title,
83
- name: `Terminal ${terminalCounter}`
152
+ name: terminalName
84
153
  });
85
154
 
86
155
  // Create terminal in main process
87
156
  await ipcRenderer.invoke('terminal:create', terminalId);
88
157
 
89
158
  // Add checkbox to chat sidebar
90
- addTerminalCheckbox(terminalId, `Terminal ${terminalCounter}`);
159
+ addTerminalCheckbox(terminalId, terminalName);
91
160
 
92
161
  // Auto-start Claude Code
93
162
  setTimeout(() => {
@@ -201,6 +270,7 @@ ipcRenderer.on('terminal:exit', (event, terminalId, exitCode) => {
201
270
  }
202
271
  });
203
272
 
273
+
204
274
  // Chat functionality
205
275
  function addTerminalCheckbox(terminalId, name) {
206
276
  const container = document.getElementById('terminal-checkboxes');
@@ -248,6 +318,32 @@ function sendMessage() {
248
318
  const message = input.value.trim();
249
319
  if (!message) return;
250
320
 
321
+ // Save to command history
322
+ if (commandHistory.length === 0 || commandHistory[commandHistory.length - 1] !== message) {
323
+ commandHistory.push(message);
324
+ if (commandHistory.length > MAX_HISTORY_SIZE) {
325
+ commandHistory.shift();
326
+ }
327
+ }
328
+ historyIndex = commandHistory.length;
329
+
330
+ // Hide command dropdown
331
+ hideCommandDropdown();
332
+
333
+ // Check if this is a slash command
334
+ if (message.startsWith('/')) {
335
+ executeCommand(message);
336
+ input.value = '';
337
+ return;
338
+ }
339
+
340
+ // Check if this is an @terminal command
341
+ if (message.startsWith('@')) {
342
+ executeAtCommand(message);
343
+ input.value = '';
344
+ return;
345
+ }
346
+
251
347
  // Check if default prompt should be prepended
252
348
  let fullMessage = message;
253
349
  const useDefaultPrompt = document.getElementById('use-default-prompt').checked;
@@ -267,16 +363,38 @@ function sendMessage() {
267
363
  // Add message to chat display
268
364
  addChatMessage('You', message);
269
365
 
366
+ // Check if plan mode is enabled
367
+ const usePlanMode = document.getElementById('use-plan-mode').checked;
368
+
270
369
  // Send to selected terminals
271
370
  selectedTerminals.forEach(terminalId => {
272
- // Send the plan mode instruction first with proper line endings
273
- ipcRenderer.invoke('terminal:write', terminalId, 'Please enter plan mode (press Shift+Tab twice)\r');
371
+ const termData = terminals.get(terminalId);
372
+ if (!termData) return;
274
373
 
275
- // Small delay to ensure the first command is processed
276
- setTimeout(() => {
277
- // Send the actual message with carriage return to execute
374
+ if (usePlanMode) {
375
+ // Send Shift+Tab twice to enter plan mode
376
+ // ESC[Z is the escape sequence for Shift+Tab
377
+ ipcRenderer.invoke('terminal:write', terminalId, '\x1b[Z\x1b[Z');
378
+
379
+ // Small delay to ensure plan mode is activated
380
+ setTimeout(() => {
381
+ // Send the actual message with carriage return to execute
382
+ ipcRenderer.invoke('terminal:write', terminalId, fullMessage + '\r');
383
+
384
+ // Auto-return after sending the message
385
+ setTimeout(() => {
386
+ executeReturnCommand([termData.name], true);
387
+ }, 150);
388
+ }, 300);
389
+ } else {
390
+ // Send the message directly to the terminal
278
391
  ipcRenderer.invoke('terminal:write', terminalId, fullMessage + '\r');
279
- }, 100);
392
+
393
+ // Auto-return after sending the message
394
+ setTimeout(() => {
395
+ executeReturnCommand([termData.name], true);
396
+ }, 150);
397
+ }
280
398
  });
281
399
 
282
400
  // Clear input
@@ -285,6 +403,8 @@ function sendMessage() {
285
403
 
286
404
  function addChatMessage(sender, message) {
287
405
  const messagesContainer = document.getElementById('chat-messages');
406
+ if (!messagesContainer) return; // Skip if no chat messages container
407
+
288
408
  const messageDiv = document.createElement('div');
289
409
  messageDiv.className = 'chat-message';
290
410
 
@@ -304,34 +424,710 @@ function addChatMessage(sender, message) {
304
424
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
305
425
  }
306
426
 
427
+ // Command System Functions
428
+ function parseCommand(message) {
429
+ const parts = message.split(' ');
430
+ const command = parts[0];
431
+ const args = parts.slice(1);
432
+
433
+ return { command, args, raw: message };
434
+ }
435
+
436
+ function findTerminal(identifier) {
437
+ // Try to find terminal by various methods
438
+
439
+ // 1. Try exact terminal ID match
440
+ if (terminals.has(identifier)) {
441
+ return { id: identifier, terminal: terminals.get(identifier) };
442
+ }
443
+
444
+ // 2. Try exact name match (case-insensitive)
445
+ for (const [id, termData] of terminals.entries()) {
446
+ if (termData.name && termData.name.toLowerCase() === identifier.toLowerCase()) {
447
+ return { id, terminal: termData };
448
+ }
449
+ }
450
+
451
+ // 3. Try partial name match
452
+ for (const [id, termData] of terminals.entries()) {
453
+ if (termData.name && termData.name.toLowerCase().includes(identifier.toLowerCase())) {
454
+ return { id, terminal: termData };
455
+ }
456
+ }
457
+
458
+ // 4. Try position/number match
459
+ const position = parseInt(identifier);
460
+ if (!isNaN(position) && position > 0) {
461
+ const terminalArray = Array.from(terminals.entries());
462
+ if (position <= terminalArray.length) {
463
+ const [id, termData] = terminalArray[position - 1];
464
+ return { id, terminal: termData };
465
+ }
466
+ }
467
+
468
+ return null;
469
+ }
470
+
471
+ function executeCommand(message) {
472
+ const { command, args } = parseCommand(message);
473
+
474
+ switch (command) {
475
+ case '/send':
476
+ executeSendCommand(args);
477
+ break;
478
+ case '/send-all':
479
+ executeSendAllCommand(args);
480
+ break;
481
+ case '/list':
482
+ executeListCommand();
483
+ break;
484
+ case '/help':
485
+ executeHelpCommand();
486
+ break;
487
+ case '/create':
488
+ executeCreateCommand(args);
489
+ break;
490
+ case '/close':
491
+ executeCloseCommand(args);
492
+ break;
493
+ case '/return':
494
+ executeReturnCommand(args);
495
+ break;
496
+ case '/clear':
497
+ case '/interrupt':
498
+ executeClearCommand(args);
499
+ break;
500
+ default:
501
+ addChatMessage('System', `Unknown command: ${command}. Type /help for available commands.`);
502
+ }
503
+ }
504
+
505
+ function executeSendCommand(args) {
506
+ if (args.length < 2) {
507
+ addChatMessage('System', 'Usage: /send <terminal> <command>');
508
+ return;
509
+ }
510
+
511
+ const terminalIdentifier = args[0];
512
+ const command = args.slice(1).join(' ');
513
+
514
+ const result = findTerminal(terminalIdentifier);
515
+ if (!result) {
516
+ const available = Array.from(terminals.values()).map(t => t.name).join(', ');
517
+ addChatMessage('System', `Terminal '${terminalIdentifier}' not found. Available: ${available}`);
518
+ return;
519
+ }
520
+
521
+ // Send command to terminal
522
+ ipcRenderer.invoke('terminal:write', result.id, command + '\r');
523
+ addChatMessage('System', `✅ Sent '${command}' to ${result.terminal.name}`);
524
+ }
525
+
526
+ function executeSendAllCommand(args) {
527
+ if (args.length === 0) {
528
+ addChatMessage('System', 'Usage: /send-all <command>');
529
+ return;
530
+ }
531
+
532
+ const command = args.join(' ');
533
+ let count = 0;
534
+
535
+ for (const [id, termData] of terminals.entries()) {
536
+ ipcRenderer.invoke('terminal:write', id, command + '\r');
537
+ count++;
538
+ }
539
+
540
+ addChatMessage('System', `✅ Sent '${command}' to ${count} terminals`);
541
+ }
542
+
543
+ function executeListCommand() {
544
+ const terminalList = Array.from(terminals.entries()).map(([id, termData], index) => {
545
+ return `${index + 1}. ${termData.name} (${id})`;
546
+ });
547
+
548
+ if (terminalList.length === 0) {
549
+ addChatMessage('System', 'No terminals available');
550
+ } else {
551
+ addChatMessage('System', 'Available terminals:\n' + terminalList.join('\n'));
552
+ }
553
+ }
554
+
555
+ function executeHelpCommand() {
556
+ const helpText = availableCommands.map(cmd =>
557
+ `${cmd.syntax} - ${cmd.description}`
558
+ ).join('\n');
559
+
560
+ const atHelp = '\n@terminal <command> - Send command directly to named terminal(s)\nExamples:\n @Tom ls -la - Send to single terminal\n @"My Server" status - Terminal with spaces in name\n @term1,term2,term3 pwd - Multiple terminals (comma-separated)\n @term1 @term2 date - Multiple terminals (space-separated)\n @all whoami - Send to all terminals';
561
+
562
+ addChatMessage('System', 'Available commands:\n' + helpText + atHelp);
563
+ }
564
+
565
+ function executeCreateCommand(args) {
566
+ const name = args.length > 0 ? args.join(' ') : undefined;
567
+ createTerminal(name);
568
+ addChatMessage('System', `✅ Creating new terminal${name ? ` named '${name}'` : ''}`);
569
+ }
570
+
571
+ function executeCloseCommand(args) {
572
+ if (args.length === 0) {
573
+ addChatMessage('System', 'Usage: /close <terminal>');
574
+ return;
575
+ }
576
+
577
+ const terminalIdentifier = args[0];
578
+ const result = findTerminal(terminalIdentifier);
579
+
580
+ if (!result) {
581
+ const available = Array.from(terminals.values()).map(t => t.name).join(', ');
582
+ addChatMessage('System', `Terminal '${terminalIdentifier}' not found. Available: ${available}`);
583
+ return;
584
+ }
585
+
586
+ closeTerminal(result.id);
587
+ addChatMessage('System', `✅ Closed terminal ${result.terminal.name}`);
588
+ }
589
+
590
+ function executeReturnCommand(args, silent = false) {
591
+ if (args.length === 0) {
592
+ if (!silent) addChatMessage('System', 'Usage: /return <terminal>');
593
+ return;
594
+ }
595
+
596
+ const terminalIdentifier = args[0];
597
+ const result = findTerminal(terminalIdentifier);
598
+
599
+ if (!result) {
600
+ if (!silent) {
601
+ const available = Array.from(terminals.values()).map(t => t.name).join(', ');
602
+ addChatMessage('System', `Terminal '${terminalIdentifier}' not found. Available: ${available}`);
603
+ }
604
+ return;
605
+ }
606
+
607
+ // Send carriage return to submit current input
608
+ ipcRenderer.invoke('terminal:write', result.id, '\r');
609
+ if (!silent) {
610
+ addChatMessage('System', `✅ Submitted input in ${result.terminal.name}`);
611
+ }
612
+ }
613
+
614
+ function executeClearCommand(args) {
615
+ if (args.length === 0) {
616
+ addChatMessage('System', 'Usage: /clear <terminal> or /interrupt <terminal>');
617
+ return;
618
+ }
619
+
620
+ const terminalIdentifier = args[0];
621
+ const result = findTerminal(terminalIdentifier);
622
+
623
+ if (!result) {
624
+ const available = Array.from(terminals.values()).map(t => t.name).join(', ');
625
+ addChatMessage('System', `Terminal '${terminalIdentifier}' not found. Available: ${available}`);
626
+ return;
627
+ }
628
+
629
+ // Send Ctrl+C to clear/interrupt
630
+ ipcRenderer.invoke('terminal:write', result.id, '\x03');
631
+ addChatMessage('System', `✅ Sent Ctrl+C to ${result.terminal.name}`);
632
+ }
633
+
634
+ function executeAtCommand(message) {
635
+ // Parse various @terminal command syntaxes:
636
+ // 1. Space-separated: "@term1 @term2 command"
637
+ // 2. Comma-separated: "@term1,term2,term3 command"
638
+ // 3. Single terminal: "@terminal command" or '@"terminal name" command'
639
+ // 4. All terminals: "@all command"
640
+
641
+ let targetTerminals = [];
642
+ let command = '';
643
+
644
+ // First, try to match space-separated terminals at the beginning
645
+ const spaceSeparatedRegex = /^(@(?:"[^"]+"|[^\s,]+)\s+)+(.+)$/;
646
+ const spaceSeparatedMatch = message.match(spaceSeparatedRegex);
647
+
648
+ if (spaceSeparatedMatch) {
649
+ // Extract all @terminal references
650
+ const terminalsStr = spaceSeparatedMatch[0].substring(0, spaceSeparatedMatch[0].length - spaceSeparatedMatch[2].length);
651
+ command = spaceSeparatedMatch[2].trim();
652
+
653
+ // Parse each @terminal reference
654
+ const terminalMatches = terminalsStr.matchAll(/@(?:"([^"]+)"|([^\s,]+))/g);
655
+ for (const match of terminalMatches) {
656
+ const terminalName = match[1] || match[2];
657
+ targetTerminals.push(terminalName.trim());
658
+ }
659
+ } else {
660
+ // Try single @terminal with possible comma-separated names
661
+ let match = message.match(/^@"([^"]+)"\s+(.+)$/); // Handle quoted names
662
+
663
+ if (!match) {
664
+ match = message.match(/^@(\S+)\s+(.+)$/); // Handle unquoted names (including comma-separated)
665
+ }
666
+
667
+ if (!match) {
668
+ addChatMessage('System', 'Usage: @terminal <command>\nExamples:\n @Tom ls -la\n @term1,term2 pwd\n @term1 @term2 date\n @all whoami');
669
+ return;
670
+ }
671
+
672
+ const terminalNameStr = match[1];
673
+ command = match[2];
674
+
675
+ // Check if it's @all
676
+ if (terminalNameStr.toLowerCase() === 'all') {
677
+ // Send to all terminals
678
+ const allTerminals = Array.from(terminals.entries());
679
+ if (allTerminals.length === 0) {
680
+ addChatMessage('System', 'No terminals available');
681
+ return;
682
+ }
683
+
684
+ const terminalNames = [];
685
+ for (const [id, termData] of allTerminals) {
686
+ ipcRenderer.invoke('terminal:write', id, command + '\r');
687
+ terminalNames.push(termData.name);
688
+ }
689
+
690
+ addChatMessage('System', `✅ Sent '${command}' to all ${allTerminals.length} terminals`);
691
+
692
+ // Auto-return to all terminals after a short delay
693
+ setTimeout(() => {
694
+ for (const name of terminalNames) {
695
+ executeReturnCommand([name], true);
696
+ }
697
+ }, 150);
698
+ return;
699
+ }
700
+
701
+ // Handle comma-separated terminal names
702
+ targetTerminals = terminalNameStr.split(',').map(name => name.trim());
703
+ }
704
+
705
+ // Process all target terminals
706
+ const foundTerminals = [];
707
+ const notFoundTerminals = [];
708
+
709
+ for (const terminalName of targetTerminals) {
710
+ const result = findTerminal(terminalName);
711
+ if (result) {
712
+ foundTerminals.push({ name: terminalName, id: result.id, terminal: result.terminal });
713
+ } else {
714
+ notFoundTerminals.push(terminalName);
715
+ }
716
+ }
717
+
718
+ // Report not found terminals
719
+ if (notFoundTerminals.length > 0) {
720
+ const available = Array.from(terminals.values()).map(t => t.name).join(', ');
721
+ addChatMessage('System', `Terminal(s) not found: ${notFoundTerminals.join(', ')}. Available: ${available}`);
722
+ }
723
+
724
+ // Send command to all found terminals
725
+ if (foundTerminals.length > 0) {
726
+ const sentToNames = [];
727
+ for (const { id, terminal } of foundTerminals) {
728
+ ipcRenderer.invoke('terminal:write', id, command + '\r');
729
+ sentToNames.push(terminal.name);
730
+ }
731
+ addChatMessage('System', `✅ Sent '${command}' to ${foundTerminals.length} terminal(s): ${sentToNames.join(', ')}`);
732
+
733
+ // Auto-return to all terminals after a short delay
734
+ setTimeout(() => {
735
+ for (const name of sentToNames) {
736
+ executeReturnCommand([name], true); // true = silent mode
737
+ }
738
+ }, 150);
739
+ }
740
+ }
741
+
742
+ // Dropdown Functions
743
+ function showCommandDropdown() {
744
+ const dropdown = document.getElementById('command-dropdown');
745
+ const input = document.getElementById('chat-input');
746
+ const inputValue = input.value;
747
+
748
+ // Filter commands based on current input
749
+ const searchTerm = inputValue.substring(1).toLowerCase(); // Remove the '/'
750
+ currentCommands = availableCommands.filter(cmd =>
751
+ cmd.name.substring(1).toLowerCase().startsWith(searchTerm)
752
+ );
753
+
754
+ if (currentCommands.length === 0) {
755
+ hideCommandDropdown();
756
+ return;
757
+ }
758
+
759
+ populateCommandDropdown();
760
+ dropdown.style.display = 'block';
761
+ selectedCommandIndex = -1;
762
+ }
763
+
764
+ function showTerminalDropdown(cursorPos = null) {
765
+ const dropdown = document.getElementById('command-dropdown');
766
+ const input = document.getElementById('chat-input');
767
+ const inputValue = input.value;
768
+ const pos = cursorPos !== null ? cursorPos : input.selectionStart;
769
+
770
+ // Find the @ symbol and extract the partial terminal name
771
+ let atPos = -1;
772
+ let searchTerm = '';
773
+
774
+ // Search backwards from cursor to find the @ symbol
775
+ for (let i = pos - 1; i >= 0; i--) {
776
+ if (inputValue[i] === '@') {
777
+ atPos = i;
778
+ // Extract text from @ to cursor position
779
+ searchTerm = inputValue.substring(i + 1, pos).toLowerCase();
780
+ break;
781
+ } else if (inputValue[i] === ' ' || inputValue[i] === ',') {
782
+ // Stop if we hit a space or comma before finding @
783
+ break;
784
+ }
785
+ }
786
+
787
+ // If no @ found or @ is not at a valid position, hide dropdown
788
+ if (atPos === -1) {
789
+ hideCommandDropdown();
790
+ return;
791
+ }
792
+
793
+ const availableTerminals = Array.from(terminals.entries()).map(([id, termData]) => ({
794
+ name: '@' + termData.name,
795
+ syntax: '@' + termData.name + ' <command>',
796
+ description: `Send command to ${termData.name}`,
797
+ id: id,
798
+ isTerminal: true,
799
+ terminalName: termData.name,
800
+ atPosition: atPos
801
+ }));
802
+
803
+ // Add @all option
804
+ availableTerminals.unshift({
805
+ name: '@all',
806
+ syntax: '@all <command>',
807
+ description: 'Send command to all terminals',
808
+ isTerminal: true,
809
+ terminalName: 'all',
810
+ atPosition: atPos
811
+ });
812
+
813
+ currentCommands = availableTerminals.filter(term =>
814
+ term.terminalName.toLowerCase().startsWith(searchTerm)
815
+ );
816
+
817
+ if (currentCommands.length === 0) {
818
+ hideCommandDropdown();
819
+ return;
820
+ }
821
+
822
+ populateCommandDropdown();
823
+ dropdown.style.display = 'block';
824
+ selectedCommandIndex = -1;
825
+ }
826
+
827
+ function hideCommandDropdown() {
828
+ const dropdown = document.getElementById('command-dropdown');
829
+ dropdown.style.display = 'none';
830
+ selectedCommandIndex = -1;
831
+ currentCommands = [];
832
+ }
833
+
834
+ function populateCommandDropdown() {
835
+ const commandList = document.getElementById('command-list');
836
+ commandList.innerHTML = '';
837
+
838
+ currentCommands.forEach((command, index) => {
839
+ const option = document.createElement('div');
840
+ option.className = 'command-option';
841
+ option.setAttribute('data-index', index);
842
+
843
+ const name = document.createElement('div');
844
+ name.className = 'command-name';
845
+ name.textContent = command.name;
846
+
847
+ const description = document.createElement('div');
848
+ description.className = 'command-description';
849
+ description.textContent = command.description;
850
+
851
+ const syntax = document.createElement('div');
852
+ syntax.className = 'command-syntax';
853
+ syntax.textContent = command.syntax;
854
+
855
+ option.appendChild(name);
856
+ option.appendChild(description);
857
+ option.appendChild(syntax);
858
+
859
+ option.addEventListener('click', () => {
860
+ selectCommand(index);
861
+ });
862
+
863
+ commandList.appendChild(option);
864
+ });
865
+ }
866
+
867
+ function selectCommand(index) {
868
+ if (index < 0 || index >= currentCommands.length) return;
869
+
870
+ const command = currentCommands[index];
871
+ const input = document.getElementById('chat-input');
872
+
873
+ // Check if this is a terminal selection with position info
874
+ if (command.isTerminal && command.atPosition !== undefined) {
875
+ const value = input.value;
876
+ const atPos = command.atPosition;
877
+
878
+ // Find the end of the partial terminal name
879
+ let endPos = input.selectionStart;
880
+
881
+ // Replace the partial terminal name with the selected one
882
+ const beforeAt = value.substring(0, atPos);
883
+ const afterCursor = value.substring(endPos);
884
+
885
+ // Insert the selected terminal name (without the @, as it's already there)
886
+ input.value = beforeAt + '@' + command.terminalName + ' ' + afterCursor;
887
+
888
+ // Position cursor after the inserted terminal name and space
889
+ const newCursorPos = atPos + command.terminalName.length + 2; // +2 for @ and space
890
+ input.setSelectionRange(newCursorPos, newCursorPos);
891
+ } else {
892
+ // Original behavior for slash commands
893
+ input.value = command.name + ' ';
894
+ input.setSelectionRange(input.value.length, input.value.length);
895
+ }
896
+
897
+ input.focus();
898
+ hideCommandDropdown();
899
+
900
+ // Trigger input event to update highlighting
901
+ input.dispatchEvent(new Event('input', { bubbles: true }));
902
+ }
903
+
904
+ function navigateDropdown(direction) {
905
+ if (currentCommands.length === 0) return;
906
+
907
+ // Update selected index
908
+ if (direction === 'down') {
909
+ selectedCommandIndex = Math.min(selectedCommandIndex + 1, currentCommands.length - 1);
910
+ } else if (direction === 'up') {
911
+ selectedCommandIndex = Math.max(selectedCommandIndex - 1, -1);
912
+ }
913
+
914
+ // Update visual selection
915
+ const options = document.querySelectorAll('.command-option');
916
+ options.forEach((option, index) => {
917
+ option.classList.toggle('selected', index === selectedCommandIndex);
918
+ });
919
+ }
920
+
921
+ function selectCurrentCommand() {
922
+ if (selectedCommandIndex >= 0 && selectedCommandIndex < currentCommands.length) {
923
+ selectCommand(selectedCommandIndex);
924
+ }
925
+ }
926
+
927
+ // Update overlay with syntax highlighting
928
+ function updateOverlayHighlighting() {
929
+ const input = document.getElementById('chat-input');
930
+ const overlay = document.getElementById('input-overlay');
931
+ const value = input.value;
932
+
933
+ // Parse and highlight text
934
+ let highlightedText = escapeHtml(value);
935
+
936
+ // Highlight @terminal commands
937
+ highlightedText = highlightedText.replace(
938
+ /@"([^"]+)"/g,
939
+ (match, terminalName) => {
940
+ const result = findTerminal(terminalName);
941
+ return result ? `@<span class="highlight-terminal">"${terminalName}"</span>` : match;
942
+ }
943
+ );
944
+
945
+ highlightedText = highlightedText.replace(
946
+ /@(\S+)/g,
947
+ (match, terminalName) => {
948
+ // Handle @all specially
949
+ if (terminalName.toLowerCase() === 'all') {
950
+ return `@<span class="highlight-terminal">${terminalName}</span>`;
951
+ }
952
+ // Handle comma-separated terminals
953
+ if (terminalName.includes(',')) {
954
+ const parts = terminalName.split(',');
955
+ const highlightedParts = parts.map((part, index) => {
956
+ const trimmedPart = part.trim();
957
+ const result = findTerminal(trimmedPart);
958
+ return (index > 0 ? ',' : '') + (result ? `<span class="highlight-terminal">${trimmedPart}</span>` : trimmedPart);
959
+ }).join('');
960
+ return '@' + highlightedParts;
961
+ }
962
+ // Single terminal
963
+ const result = findTerminal(terminalName);
964
+ return result ? `@<span class="highlight-terminal">${terminalName}</span>` : match;
965
+ }
966
+ );
967
+
968
+ // Highlight slash commands
969
+ highlightedText = highlightedText.replace(
970
+ /^(\/\w+)/gm,
971
+ (match, command) => {
972
+ const validCommand = availableCommands.find(cmd => cmd.name === command);
973
+ return validCommand ? `<span class="highlight-command">${command}</span>` : match;
974
+ }
975
+ );
976
+
977
+ overlay.innerHTML = highlightedText;
978
+
979
+ // Sync scroll position
980
+ overlay.scrollTop = input.scrollTop;
981
+ overlay.scrollLeft = input.scrollLeft;
982
+ }
983
+
984
+ function escapeHtml(text) {
985
+ const div = document.createElement('div');
986
+ div.textContent = text;
987
+ return div.innerHTML;
988
+ }
989
+
307
990
  // Initialize collapsible default prompt section
308
991
  function initCollapsible() {
309
992
  const toggle = document.querySelector('.collapse-toggle');
310
993
  const content = document.querySelector('.collapsible-content');
994
+ const header = document.querySelector('.collapsible-header');
311
995
 
312
- toggle.addEventListener('click', () => {
996
+ const toggleSection = (e) => {
997
+ // Prevent double-triggering when clicking the toggle button
998
+ if (e && e.target === toggle) {
999
+ e.stopPropagation();
1000
+ }
1001
+
313
1002
  const isExpanded = content.style.display !== 'none';
314
1003
  content.style.display = isExpanded ? 'none' : 'block';
315
1004
  toggle.textContent = isExpanded ? '▶' : '▼';
316
- });
1005
+ };
1006
+
1007
+ // Make the entire header clickable
1008
+ header.addEventListener('click', toggleSection);
317
1009
  }
318
1010
 
1011
+
319
1012
  // Initialize app
320
1013
  document.addEventListener('DOMContentLoaded', () => {
321
1014
  // New terminal button
322
- document.getElementById('new-terminal-btn').addEventListener('click', createTerminal);
1015
+ document.getElementById('new-terminal-btn').addEventListener('click', () => createTerminal());
1016
+
323
1017
 
324
1018
  // Send message button
325
1019
  document.getElementById('send-message-btn').addEventListener('click', sendMessage);
326
1020
 
327
- // Chat input - Enter key (Shift+Enter for new line)
1021
+ // Chat input - Enter key (Shift+Enter for new line) and dropdown navigation
328
1022
  document.getElementById('chat-input').addEventListener('keydown', (e) => {
1023
+ const dropdown = document.getElementById('command-dropdown');
1024
+ const isDropdownVisible = dropdown.style.display === 'block';
1025
+ const input = e.target;
1026
+
329
1027
  if (e.key === 'Enter' && !e.shiftKey) {
330
1028
  e.preventDefault();
331
- sendMessage();
1029
+ if (isDropdownVisible && selectedCommandIndex >= 0) {
1030
+ selectCurrentCommand();
1031
+ } else {
1032
+ sendMessage();
1033
+ }
1034
+ } else if (e.key === 'Escape' && isDropdownVisible) {
1035
+ e.preventDefault();
1036
+ hideCommandDropdown();
1037
+ } else if (e.key === 'ArrowDown' && isDropdownVisible) {
1038
+ e.preventDefault();
1039
+ navigateDropdown('down');
1040
+ } else if (e.key === 'ArrowUp' && isDropdownVisible) {
1041
+ e.preventDefault();
1042
+ navigateDropdown('up');
1043
+ } else if (e.key === 'Tab' && isDropdownVisible) {
1044
+ e.preventDefault();
1045
+ if (selectedCommandIndex >= 0) {
1046
+ selectCurrentCommand();
1047
+ } else if (currentCommands.length > 0) {
1048
+ // Auto-select first option if none selected
1049
+ selectCommand(0);
1050
+ }
1051
+ } else if (e.key === 'ArrowUp' && !isDropdownVisible && commandHistory.length > 0) {
1052
+ e.preventDefault();
1053
+ if (historyIndex === commandHistory.length) {
1054
+ tempCurrentInput = input.value;
1055
+ }
1056
+ if (historyIndex > 0) {
1057
+ historyIndex--;
1058
+ input.value = commandHistory[historyIndex];
1059
+ }
1060
+ } else if (e.key === 'ArrowDown' && !isDropdownVisible && commandHistory.length > 0) {
1061
+ e.preventDefault();
1062
+ if (historyIndex < commandHistory.length - 1) {
1063
+ historyIndex++;
1064
+ input.value = commandHistory[historyIndex];
1065
+ } else if (historyIndex === commandHistory.length - 1) {
1066
+ historyIndex = commandHistory.length;
1067
+ input.value = tempCurrentInput;
1068
+ }
332
1069
  }
333
1070
  });
334
1071
 
1072
+ // Chat input - Show dropdown on '/' and '@' and filter as user types
1073
+ document.getElementById('chat-input').addEventListener('input', (e) => {
1074
+ const input = e.target;
1075
+ const value = input.value;
1076
+ const cursorPos = input.selectionStart;
1077
+
1078
+ // Update overlay highlighting
1079
+ updateOverlayHighlighting();
1080
+
1081
+ // Check for slash commands (only at beginning and when cursor at end)
1082
+ if (cursorPos === value.length && value.startsWith('/') && value.length >= 1) {
1083
+ showCommandDropdown();
1084
+ return;
1085
+ }
1086
+
1087
+ // Check for @ at cursor position (can be anywhere in the input)
1088
+ if (cursorPos > 0) {
1089
+ // Look for @ at or before cursor position
1090
+ let foundAt = false;
1091
+ for (let i = cursorPos - 1; i >= 0; i--) {
1092
+ if (value[i] === '@') {
1093
+ foundAt = true;
1094
+ break;
1095
+ } else if (value[i] === ' ' || value[i] === ',') {
1096
+ // If we hit a space or comma, check if it's immediately followed by @
1097
+ if (i === cursorPos - 1 && value[cursorPos - 1] === '@') {
1098
+ foundAt = true;
1099
+ }
1100
+ break;
1101
+ }
1102
+ }
1103
+
1104
+ if (foundAt) {
1105
+ showTerminalDropdown(cursorPos);
1106
+ return;
1107
+ }
1108
+ }
1109
+
1110
+ hideCommandDropdown();
1111
+ });
1112
+
1113
+ // Hide dropdown when clicking outside
1114
+ document.addEventListener('click', (e) => {
1115
+ const dropdown = document.getElementById('command-dropdown');
1116
+ const input = document.getElementById('chat-input');
1117
+
1118
+ if (!dropdown.contains(e.target) && e.target !== input) {
1119
+ hideCommandDropdown();
1120
+ }
1121
+ });
1122
+
1123
+ // Sync overlay scroll with input
1124
+ document.getElementById('chat-input').addEventListener('scroll', () => {
1125
+ const input = document.getElementById('chat-input');
1126
+ const overlay = document.getElementById('input-overlay');
1127
+ overlay.scrollTop = input.scrollTop;
1128
+ overlay.scrollLeft = input.scrollLeft;
1129
+ });
1130
+
335
1131
  // Initialize collapsible
336
1132
  initCollapsible();
337
1133