@adiontaegerron/claude-multi-terminal 1.1.0 → 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/README.md +32 -1
- package/index.html +9 -5
- package/package.json +1 -1
- package/renderer.js +352 -26
- package/style.css +46 -10
package/README.md
CHANGED
@@ -9,7 +9,14 @@ A multi-terminal editor for coordinating multiple Claude Code instances. Run mul
|
|
9
9
|
- **Multi-Terminal Support**: Run multiple Claude Code instances in a 2x2 grid layout
|
10
10
|
- **Unified Chat Interface**: Send commands to selected terminals from a single chat interface
|
11
11
|
- **Terminal Selection**: Choose which terminals receive your commands with checkboxes
|
12
|
+
- **Multi-@ Terminal Commands**: Send commands to multiple terminals with various syntaxes:
|
13
|
+
- `@all command` - Send to all terminals
|
14
|
+
- `@term1,term2,term3 command` - Comma-separated terminals
|
15
|
+
- `@term1 @term2 command` - Space-separated terminals
|
16
|
+
- **Smart Autocomplete**: Get terminal suggestions while typing @ anywhere in your message
|
17
|
+
- **Slash Commands**: Built-in commands for terminal management
|
12
18
|
- **Default Prompt**: Set a default prompt to prepend to all messages
|
19
|
+
- **Plan Mode Support**: Automatically enters plan mode with Shift+Tab
|
13
20
|
- **Auto-Start Claude**: Each terminal automatically starts Claude Code
|
14
21
|
- **Rename Terminals**: Double-click terminal titles to rename them
|
15
22
|
- **Responsive Layout**: Adapts to different screen sizes
|
@@ -49,12 +56,36 @@ This will open the multi-terminal editor with all terminals starting in your cur
|
|
49
56
|
4. **Send Commands**: Type in the chat input and click "Send" or press Enter
|
50
57
|
5. **Use Default Prompt**: Expand the "Default Prompt" section to set a prompt that's prepended to all messages
|
51
58
|
|
59
|
+
### Slash Commands
|
60
|
+
|
61
|
+
- `/send <terminal> <command>` - Send command to specific terminal
|
62
|
+
- `/send-all <command>` or `/broadcast <command>` - Send to all terminals
|
63
|
+
- `/list` - List all terminals
|
64
|
+
- `/create [name]` - Create new terminal with optional name
|
65
|
+
- `/close <terminal>` - Close specific terminal
|
66
|
+
- `/return <terminal>` - Submit current input in terminal (press Enter)
|
67
|
+
- `/clear <terminal>` or `/interrupt <terminal>` - Send Ctrl+C to terminal
|
68
|
+
- `/help` - Show available commands
|
69
|
+
|
70
|
+
### @ Terminal Commands
|
71
|
+
|
72
|
+
Send commands directly to terminals using @ syntax:
|
73
|
+
|
74
|
+
- `@Terminal1 ls -la` - Send to single terminal
|
75
|
+
- `@"My Terminal" pwd` - Terminal with spaces in name
|
76
|
+
- `@term1,term2,term3 date` - Multiple terminals (comma-separated)
|
77
|
+
- `@term1 @term2 @term3 whoami` - Multiple terminals (space-separated)
|
78
|
+
- `@all echo "Hello"` - Send to all terminals
|
79
|
+
|
80
|
+
**Autocomplete**: Type `@` anywhere to get terminal suggestions!
|
81
|
+
|
52
82
|
### Tips
|
53
83
|
|
54
84
|
- Each terminal automatically starts Claude Code when created
|
55
|
-
-
|
85
|
+
- Plan mode is activated automatically using Shift+Tab
|
56
86
|
- Double-click terminal titles to rename them for better organization
|
57
87
|
- The application uses your current directory as the working directory for all terminals
|
88
|
+
- Use multi-@ commands to coordinate multiple Claude instances efficiently
|
58
89
|
|
59
90
|
## Development
|
60
91
|
|
package/index.html
CHANGED
@@ -36,18 +36,22 @@
|
|
36
36
|
</div>
|
37
37
|
</div>
|
38
38
|
|
39
|
+
<div class="plan-mode-row">
|
40
|
+
<label class="plan-mode-option">
|
41
|
+
<input type="checkbox" id="use-plan-mode" checked />
|
42
|
+
<span>Send in Plan Mode</span>
|
43
|
+
</label>
|
44
|
+
</div>
|
45
|
+
|
39
46
|
<div class="input-row">
|
40
47
|
<div class="input-wrapper">
|
41
|
-
<
|
48
|
+
<div id="input-overlay" class="input-overlay"></div>
|
49
|
+
<textarea id="chat-input" placeholder="Type your message, / for commands, or @terminal for direct send..."></textarea>
|
42
50
|
<div id="command-dropdown" class="command-dropdown" style="display: none;">
|
43
51
|
<div id="command-list" class="command-list">
|
44
52
|
<!-- Command options will be populated here -->
|
45
53
|
</div>
|
46
54
|
</div>
|
47
|
-
<label class="plan-mode-option">
|
48
|
-
<input type="checkbox" id="use-plan-mode" checked />
|
49
|
-
<span>Use Plan Mode</span>
|
50
|
-
</label>
|
51
55
|
</div>
|
52
56
|
<button id="send-message-btn">Send</button>
|
53
57
|
</div>
|
package/package.json
CHANGED
package/renderer.js
CHANGED
@@ -337,6 +337,13 @@ function sendMessage() {
|
|
337
337
|
return;
|
338
338
|
}
|
339
339
|
|
340
|
+
// Check if this is an @terminal command
|
341
|
+
if (message.startsWith('@')) {
|
342
|
+
executeAtCommand(message);
|
343
|
+
input.value = '';
|
344
|
+
return;
|
345
|
+
}
|
346
|
+
|
340
347
|
// Check if default prompt should be prepended
|
341
348
|
let fullMessage = message;
|
342
349
|
const useDefaultPrompt = document.getElementById('use-default-prompt').checked;
|
@@ -361,18 +368,32 @@ function sendMessage() {
|
|
361
368
|
|
362
369
|
// Send to selected terminals
|
363
370
|
selectedTerminals.forEach(terminalId => {
|
371
|
+
const termData = terminals.get(terminalId);
|
372
|
+
if (!termData) return;
|
373
|
+
|
364
374
|
if (usePlanMode) {
|
365
|
-
// Send
|
366
|
-
|
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');
|
367
378
|
|
368
|
-
// Small delay to ensure
|
379
|
+
// Small delay to ensure plan mode is activated
|
369
380
|
setTimeout(() => {
|
370
381
|
// Send the actual message with carriage return to execute
|
371
382
|
ipcRenderer.invoke('terminal:write', terminalId, fullMessage + '\r');
|
372
|
-
|
383
|
+
|
384
|
+
// Auto-return after sending the message
|
385
|
+
setTimeout(() => {
|
386
|
+
executeReturnCommand([termData.name], true);
|
387
|
+
}, 150);
|
388
|
+
}, 300);
|
373
389
|
} else {
|
374
390
|
// Send the message directly to the terminal
|
375
391
|
ipcRenderer.invoke('terminal:write', terminalId, fullMessage + '\r');
|
392
|
+
|
393
|
+
// Auto-return after sending the message
|
394
|
+
setTimeout(() => {
|
395
|
+
executeReturnCommand([termData.name], true);
|
396
|
+
}, 150);
|
376
397
|
}
|
377
398
|
});
|
378
399
|
|
@@ -536,7 +557,9 @@ function executeHelpCommand() {
|
|
536
557
|
`${cmd.syntax} - ${cmd.description}`
|
537
558
|
).join('\n');
|
538
559
|
|
539
|
-
|
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);
|
540
563
|
}
|
541
564
|
|
542
565
|
function executeCreateCommand(args) {
|
@@ -564,9 +587,9 @@ function executeCloseCommand(args) {
|
|
564
587
|
addChatMessage('System', `✅ Closed terminal ${result.terminal.name}`);
|
565
588
|
}
|
566
589
|
|
567
|
-
function executeReturnCommand(args) {
|
590
|
+
function executeReturnCommand(args, silent = false) {
|
568
591
|
if (args.length === 0) {
|
569
|
-
addChatMessage('System', 'Usage: /return <terminal>');
|
592
|
+
if (!silent) addChatMessage('System', 'Usage: /return <terminal>');
|
570
593
|
return;
|
571
594
|
}
|
572
595
|
|
@@ -574,14 +597,18 @@ function executeReturnCommand(args) {
|
|
574
597
|
const result = findTerminal(terminalIdentifier);
|
575
598
|
|
576
599
|
if (!result) {
|
577
|
-
|
578
|
-
|
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
|
+
}
|
579
604
|
return;
|
580
605
|
}
|
581
606
|
|
582
607
|
// Send carriage return to submit current input
|
583
608
|
ipcRenderer.invoke('terminal:write', result.id, '\r');
|
584
|
-
|
609
|
+
if (!silent) {
|
610
|
+
addChatMessage('System', `✅ Submitted input in ${result.terminal.name}`);
|
611
|
+
}
|
585
612
|
}
|
586
613
|
|
587
614
|
function executeClearCommand(args) {
|
@@ -604,6 +631,114 @@ function executeClearCommand(args) {
|
|
604
631
|
addChatMessage('System', `✅ Sent Ctrl+C to ${result.terminal.name}`);
|
605
632
|
}
|
606
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
|
+
|
607
742
|
// Dropdown Functions
|
608
743
|
function showCommandDropdown() {
|
609
744
|
const dropdown = document.getElementById('command-dropdown');
|
@@ -626,6 +761,69 @@ function showCommandDropdown() {
|
|
626
761
|
selectedCommandIndex = -1;
|
627
762
|
}
|
628
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
|
+
|
629
827
|
function hideCommandDropdown() {
|
630
828
|
const dropdown = document.getElementById('command-dropdown');
|
631
829
|
dropdown.style.display = 'none';
|
@@ -672,14 +870,35 @@ function selectCommand(index) {
|
|
672
870
|
const command = currentCommands[index];
|
673
871
|
const input = document.getElementById('chat-input');
|
674
872
|
|
675
|
-
//
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
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
|
+
}
|
681
896
|
|
897
|
+
input.focus();
|
682
898
|
hideCommandDropdown();
|
899
|
+
|
900
|
+
// Trigger input event to update highlighting
|
901
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
683
902
|
}
|
684
903
|
|
685
904
|
function navigateDropdown(direction) {
|
@@ -705,16 +924,88 @@ function selectCurrentCommand() {
|
|
705
924
|
}
|
706
925
|
}
|
707
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
|
+
|
708
990
|
// Initialize collapsible default prompt section
|
709
991
|
function initCollapsible() {
|
710
992
|
const toggle = document.querySelector('.collapse-toggle');
|
711
993
|
const content = document.querySelector('.collapsible-content');
|
994
|
+
const header = document.querySelector('.collapsible-header');
|
712
995
|
|
713
|
-
|
996
|
+
const toggleSection = (e) => {
|
997
|
+
// Prevent double-triggering when clicking the toggle button
|
998
|
+
if (e && e.target === toggle) {
|
999
|
+
e.stopPropagation();
|
1000
|
+
}
|
1001
|
+
|
714
1002
|
const isExpanded = content.style.display !== 'none';
|
715
1003
|
content.style.display = isExpanded ? 'none' : 'block';
|
716
1004
|
toggle.textContent = isExpanded ? '▶' : '▼';
|
717
|
-
}
|
1005
|
+
};
|
1006
|
+
|
1007
|
+
// Make the entire header clickable
|
1008
|
+
header.addEventListener('click', toggleSection);
|
718
1009
|
}
|
719
1010
|
|
720
1011
|
|
@@ -753,6 +1044,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
753
1044
|
e.preventDefault();
|
754
1045
|
if (selectedCommandIndex >= 0) {
|
755
1046
|
selectCurrentCommand();
|
1047
|
+
} else if (currentCommands.length > 0) {
|
1048
|
+
// Auto-select first option if none selected
|
1049
|
+
selectCommand(0);
|
756
1050
|
}
|
757
1051
|
} else if (e.key === 'ArrowUp' && !isDropdownVisible && commandHistory.length > 0) {
|
758
1052
|
e.preventDefault();
|
@@ -775,21 +1069,45 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
775
1069
|
}
|
776
1070
|
});
|
777
1071
|
|
778
|
-
// Chat input - Show dropdown on '/' and filter as user types
|
1072
|
+
// Chat input - Show dropdown on '/' and '@' and filter as user types
|
779
1073
|
document.getElementById('chat-input').addEventListener('input', (e) => {
|
780
1074
|
const input = e.target;
|
781
1075
|
const value = input.value;
|
782
1076
|
const cursorPos = input.selectionStart;
|
783
1077
|
|
784
|
-
//
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
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) {
|
789
1083
|
showCommandDropdown();
|
790
|
-
|
791
|
-
|
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
|
+
}
|
792
1108
|
}
|
1109
|
+
|
1110
|
+
hideCommandDropdown();
|
793
1111
|
});
|
794
1112
|
|
795
1113
|
// Hide dropdown when clicking outside
|
@@ -802,6 +1120,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
802
1120
|
}
|
803
1121
|
});
|
804
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
|
+
|
805
1131
|
// Initialize collapsible
|
806
1132
|
initCollapsible();
|
807
1133
|
|
package/style.css
CHANGED
@@ -312,15 +312,39 @@ body {
|
|
312
312
|
|
313
313
|
/* Removed - Mode selector is now integrated into input area */
|
314
314
|
|
315
|
+
/* Input Overlay for Syntax Highlighting */
|
316
|
+
.input-overlay {
|
317
|
+
position: absolute;
|
318
|
+
top: 0;
|
319
|
+
left: 0;
|
320
|
+
right: 0;
|
321
|
+
min-height: 80px;
|
322
|
+
max-height: 200px;
|
323
|
+
padding: 10px 12px;
|
324
|
+
border-radius: 4px;
|
325
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
326
|
+
font-size: 14px;
|
327
|
+
line-height: 1.5;
|
328
|
+
color: transparent;
|
329
|
+
background: transparent;
|
330
|
+
pointer-events: none;
|
331
|
+
z-index: 1;
|
332
|
+
white-space: pre-wrap;
|
333
|
+
word-wrap: break-word;
|
334
|
+
overflow: hidden;
|
335
|
+
}
|
336
|
+
|
315
337
|
/* Chat Input - Full Width */
|
316
338
|
#chat-input {
|
339
|
+
position: relative;
|
340
|
+
z-index: 2;
|
317
341
|
width: 100%;
|
318
342
|
min-height: 80px;
|
319
343
|
max-height: 200px;
|
320
344
|
background-color: #3c3c3c;
|
321
345
|
border: 1px solid #555;
|
322
346
|
color: white;
|
323
|
-
padding: 10px 12px
|
347
|
+
padding: 10px 12px;
|
324
348
|
border-radius: 4px;
|
325
349
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
326
350
|
font-size: 14px;
|
@@ -334,26 +358,38 @@ body {
|
|
334
358
|
box-shadow: 0 0 0 2px rgba(14, 99, 156, 0.2);
|
335
359
|
}
|
336
360
|
|
361
|
+
/* Syntax highlighting styles */
|
362
|
+
.highlight-terminal {
|
363
|
+
color: #4CAF50;
|
364
|
+
}
|
365
|
+
|
366
|
+
.highlight-command {
|
367
|
+
color: #FF9800;
|
368
|
+
}
|
369
|
+
|
370
|
+
/* Plan Mode Row */
|
371
|
+
.plan-mode-row {
|
372
|
+
margin-bottom: 8px;
|
373
|
+
}
|
374
|
+
|
337
375
|
/* Plan Mode Option */
|
338
376
|
.plan-mode-option {
|
339
|
-
position: absolute;
|
340
|
-
bottom: 10px;
|
341
|
-
left: 12px;
|
342
377
|
display: flex;
|
343
378
|
align-items: center;
|
344
|
-
gap:
|
345
|
-
font-size:
|
346
|
-
color: #
|
379
|
+
gap: 8px;
|
380
|
+
font-size: 14px;
|
381
|
+
color: #cccccc;
|
347
382
|
cursor: pointer;
|
383
|
+
padding: 4px 0;
|
348
384
|
}
|
349
385
|
|
350
386
|
.plan-mode-option:hover {
|
351
|
-
color: #
|
387
|
+
color: #ffffff;
|
352
388
|
}
|
353
389
|
|
354
390
|
.plan-mode-option input[type="checkbox"] {
|
355
|
-
width:
|
356
|
-
height:
|
391
|
+
width: 16px;
|
392
|
+
height: 16px;
|
357
393
|
cursor: pointer;
|
358
394
|
}
|
359
395
|
|