@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/.claude/settings.local.json +3 -1
- package/README.md +32 -1
- package/index.html +42 -37
- package/main.js +16 -12
- package/package.json +1 -1
- package/renderer.js +811 -15
- package/style.css +236 -90
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
|
-
|
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:
|
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,
|
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
|
-
|
273
|
-
|
371
|
+
const termData = terminals.get(terminalId);
|
372
|
+
if (!termData) return;
|
274
373
|
|
275
|
-
|
276
|
-
|
277
|
-
//
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
|