@adiontaegerron/claude-multi-terminal 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,13 +7,33 @@ A multi-terminal editor for coordinating multiple Claude Code instances. Run mul
7
7
  ## Features
8
8
 
9
9
  - **Multi-Terminal Support**: Run multiple Claude Code instances in a 2x2 grid layout
10
- - **Unified Chat Interface**: Send commands to selected terminals from a single chat interface
11
- - **Terminal Selection**: Choose which terminals receive your commands with checkboxes
10
+ - **Unified Chat Interface**: Send commands to terminals using @ syntax
11
+ - **Multi-@ Terminal Commands**: Target terminals with flexible syntax:
12
+ - `@all command` - Send to all terminals
13
+ - `@term1,term2,term3 command` - Comma-separated terminals
14
+ - `@term1 @term2 command` - Space-separated terminals
15
+ - **Mixed @/ Commands**: Combine terminal targeting with slash commands:
16
+ - `@term1 /clear` - Run slash command on specific terminal
17
+ - `@term1 @term2 /list` - Multiple terminals with slash commands
18
+ - `@all /interrupt` - Slash commands with @all
19
+ - **Smart Autocomplete**: Get suggestions while typing @ or / anywhere in your message
20
+ - **Slash Commands**: Built-in commands for terminal management (works anywhere)
12
21
  - **Default Prompt**: Set a default prompt to prepend to all messages
22
+ - **Plan Mode Support**: Automatically enters plan mode with Shift+Tab
13
23
  - **Auto-Start Claude**: Each terminal automatically starts Claude Code
14
24
  - **Rename Terminals**: Double-click terminal titles to rename them
15
25
  - **Responsive Layout**: Adapts to different screen sizes
16
26
 
27
+ ## Breaking Changes in v2.0
28
+
29
+ - **Removed checkbox selection system** - All commands must now use @ syntax
30
+ - **No default terminal selection** - You must explicitly specify terminals with @
31
+ - **Plain text without @ or / will show an error** - Every command needs a target
32
+
33
+ To migrate from v1.x:
34
+ - Instead of selecting checkboxes and typing a command, use: `@term1 @term2 command`
35
+ - To send to all terminals, use: `@all command`
36
+
17
37
  ## Installation
18
38
 
19
39
  ### From npm
@@ -45,16 +65,42 @@ This will open the multi-terminal editor with all terminals starting in your cur
45
65
 
46
66
  1. **Start the Application**: Run `claude-multi` in your project directory
47
67
  2. **Create Terminals**: Click "New Terminal" to add more Claude instances (starts with one)
48
- 3. **Select Terminals**: Use checkboxes in the chat sidebar to select which terminals receive commands
49
- 4. **Send Commands**: Type in the chat input and click "Send" or press Enter
68
+ 3. **Target Terminals**: Use @ syntax to specify which terminals receive commands
69
+ 4. **Send Commands**: Type your command with @ targeting and press Enter
50
70
  5. **Use Default Prompt**: Expand the "Default Prompt" section to set a prompt that's prepended to all messages
51
71
 
72
+ **Important**: Version 2.0 removes checkbox selection. All commands must now use @ syntax to target terminals.
73
+
74
+ ### Slash Commands
75
+
76
+ - `/send <terminal> <command>` - Send command to specific terminal
77
+ - `/send-all <command>` or `/broadcast <command>` - Send to all terminals
78
+ - `/list` - List all terminals
79
+ - `/create [name]` - Create new terminal with optional name
80
+ - `/close <terminal>` - Close specific terminal
81
+ - `/return <terminal>` - Submit current input in terminal (press Enter)
82
+ - `/clear <terminal>` or `/interrupt <terminal>` - Send Ctrl+C to terminal
83
+ - `/help` - Show available commands
84
+
85
+ ### @ Terminal Commands
86
+
87
+ Send commands directly to terminals using @ syntax:
88
+
89
+ - `@Terminal1 ls -la` - Send to single terminal
90
+ - `@"My Terminal" pwd` - Terminal with spaces in name
91
+ - `@term1,term2,term3 date` - Multiple terminals (comma-separated)
92
+ - `@term1 @term2 @term3 whoami` - Multiple terminals (space-separated)
93
+ - `@all echo "Hello"` - Send to all terminals
94
+
95
+ **Autocomplete**: Type `@` anywhere to get terminal suggestions!
96
+
52
97
  ### Tips
53
98
 
54
99
  - Each terminal automatically starts Claude Code when created
55
- - Messages include a plan mode instruction by default
100
+ - Plan mode is activated automatically using Shift+Tab
56
101
  - Double-click terminal titles to rename them for better organization
57
102
  - The application uses your current directory as the working directory for all terminals
103
+ - Use multi-@ commands to coordinate multiple Claude instances efficiently
58
104
 
59
105
  ## Development
60
106
 
package/index.html CHANGED
@@ -14,13 +14,6 @@
14
14
 
15
15
  <div id="toolbar-extended">
16
16
  <div class="controls-row">
17
- <div id="terminal-selector" class="control-section">
18
- <label>Send to:</label>
19
- <div id="terminal-checkboxes">
20
- <!-- Terminal checkboxes will be added here dynamically -->
21
- </div>
22
- </div>
23
-
24
17
  <div id="default-prompt-section" class="control-section collapsible">
25
18
  <div class="collapsible-header">
26
19
  <button class="collapse-toggle">▶</button>
@@ -36,18 +29,22 @@
36
29
  </div>
37
30
  </div>
38
31
 
32
+ <div class="plan-mode-row">
33
+ <label class="plan-mode-option">
34
+ <input type="checkbox" id="use-plan-mode" checked />
35
+ <span>Send in Plan Mode</span>
36
+ </label>
37
+ </div>
38
+
39
39
  <div class="input-row">
40
40
  <div class="input-wrapper">
41
- <textarea id="chat-input" placeholder="Type your message or / for commands..."></textarea>
41
+ <div id="input-overlay" class="input-overlay"></div>
42
+ <textarea id="chat-input" placeholder="Type your message, / for commands, or @terminal for direct send..."></textarea>
42
43
  <div id="command-dropdown" class="command-dropdown" style="display: none;">
43
44
  <div id="command-list" class="command-list">
44
45
  <!-- Command options will be populated here -->
45
46
  </div>
46
47
  </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
48
  </div>
52
49
  <button id="send-message-btn">Send</button>
53
50
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adiontaegerron/claude-multi-terminal",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Multi-terminal editor for coordinating multiple Claude Code instances",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer.js CHANGED
@@ -155,9 +155,6 @@ async function createTerminal(customName = null) {
155
155
  // Create terminal in main process
156
156
  await ipcRenderer.invoke('terminal:create', terminalId);
157
157
 
158
- // Add checkbox to chat sidebar
159
- addTerminalCheckbox(terminalId, terminalName);
160
-
161
158
  // Auto-start Claude Code
162
159
  setTimeout(() => {
163
160
  ipcRenderer.invoke('terminal:write', terminalId, 'claude\r');
@@ -191,7 +188,6 @@ async function createTerminal(customName = null) {
191
188
  if (newName && newName !== title.textContent) {
192
189
  title.textContent = newName;
193
190
  terminals.get(terminalId).name = newName;
194
- updateTerminalCheckbox(terminalId, newName);
195
191
  }
196
192
  title.style.display = 'inline';
197
193
  renameInput.style.display = 'none';
@@ -249,9 +245,6 @@ async function closeTerminal(terminalId) {
249
245
  terminal.xterm.dispose();
250
246
  terminal.element.remove();
251
247
  terminals.delete(terminalId);
252
-
253
- // Remove checkbox from chat
254
- removeTerminalCheckbox(terminalId);
255
248
  }
256
249
  }
257
250
 
@@ -270,47 +263,83 @@ ipcRenderer.on('terminal:exit', (event, terminalId, exitCode) => {
270
263
  }
271
264
  });
272
265
 
273
-
274
- // Chat functionality
275
- function addTerminalCheckbox(terminalId, name) {
276
- const container = document.getElementById('terminal-checkboxes');
277
-
278
- const label = document.createElement('label');
279
- label.className = 'terminal-checkbox-label';
280
- label.setAttribute('data-terminal-id', terminalId);
281
-
282
- const checkbox = document.createElement('input');
283
- checkbox.type = 'checkbox';
284
- checkbox.className = 'terminal-selector-checkbox';
285
- checkbox.value = terminalId;
286
- checkbox.checked = true; // Default to checked
287
-
288
- const span = document.createElement('span');
289
- span.textContent = name;
290
-
291
- label.appendChild(checkbox);
292
- label.appendChild(span);
293
- container.appendChild(label);
294
- }
295
-
296
- function updateTerminalCheckbox(terminalId, newName) {
297
- const label = document.querySelector(`[data-terminal-id="${terminalId}"]`);
298
- if (label) {
299
- const span = label.querySelector('span');
300
- span.textContent = newName;
301
- }
302
- }
303
-
304
- function removeTerminalCheckbox(terminalId) {
305
- const label = document.querySelector(`[data-terminal-id="${terminalId}"]`);
306
- if (label) {
307
- label.remove();
266
+ // Parse complex commands that mix @ terminals and / commands
267
+ function parseComplexCommand(message) {
268
+ const commands = [];
269
+ let currentPos = 0;
270
+
271
+ while (currentPos < message.length) {
272
+ // Skip whitespace
273
+ while (currentPos < message.length && message[currentPos] === ' ') {
274
+ currentPos++;
275
+ }
276
+
277
+ if (currentPos >= message.length) break;
278
+
279
+ // Check if we're starting with @ terminals
280
+ const terminals = [];
281
+ while (currentPos < message.length && message[currentPos] === '@') {
282
+ // Parse @terminal or @"terminal name"
283
+ let terminalName = '';
284
+ currentPos++; // Skip @
285
+
286
+ if (currentPos < message.length && message[currentPos] === '"') {
287
+ // Quoted terminal name
288
+ currentPos++; // Skip opening quote
289
+ while (currentPos < message.length && message[currentPos] !== '"') {
290
+ terminalName += message[currentPos];
291
+ currentPos++;
292
+ }
293
+ if (currentPos < message.length) currentPos++; // Skip closing quote
294
+ } else {
295
+ // Unquoted terminal name (can include commas for multiple terminals)
296
+ while (currentPos < message.length &&
297
+ message[currentPos] !== ' ' &&
298
+ message[currentPos] !== '@' &&
299
+ message[currentPos] !== '/') {
300
+ terminalName += message[currentPos];
301
+ currentPos++;
302
+ }
303
+ }
304
+
305
+ // Handle comma-separated terminals
306
+ if (terminalName.includes(',')) {
307
+ terminals.push(...terminalName.split(',').map(t => t.trim()).filter(t => t));
308
+ } else if (terminalName) {
309
+ terminals.push(terminalName);
310
+ }
311
+
312
+ // Skip whitespace after terminal
313
+ while (currentPos < message.length && message[currentPos] === ' ') {
314
+ currentPos++;
315
+ }
316
+ }
317
+
318
+ // Now parse the command (either /command or regular command)
319
+ let command = '';
320
+ let commandStart = currentPos;
321
+
322
+ // Find the end of this command (next @ or end of string)
323
+ while (currentPos < message.length) {
324
+ if (message[currentPos] === '@' &&
325
+ (currentPos === 0 || message[currentPos - 1] === ' ')) {
326
+ // Found start of next terminal specification
327
+ break;
328
+ }
329
+ currentPos++;
330
+ }
331
+
332
+ command = message.substring(commandStart, currentPos).trim();
333
+
334
+ if (terminals.length > 0 && command) {
335
+ commands.push({ terminals, command });
336
+ } else if (command && !terminals.length) {
337
+ // Command without specific terminals (use all or selected)
338
+ commands.push({ terminals: [], command });
339
+ }
308
340
  }
309
- }
310
-
311
- function getSelectedTerminals() {
312
- const checkboxes = document.querySelectorAll('.terminal-selector-checkbox:checked');
313
- return Array.from(checkboxes).map(cb => cb.value);
341
+
342
+ return commands;
314
343
  }
315
344
 
316
345
  function sendMessage() {
@@ -330,26 +359,11 @@ function sendMessage() {
330
359
  // Hide command dropdown
331
360
  hideCommandDropdown();
332
361
 
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 default prompt should be prepended
341
- let fullMessage = message;
342
- const useDefaultPrompt = document.getElementById('use-default-prompt').checked;
343
- const defaultPrompt = document.getElementById('default-prompt-text').value.trim();
344
-
345
- if (useDefaultPrompt && defaultPrompt) {
346
- fullMessage = defaultPrompt + ' ' + message;
347
- }
362
+ // Parse the complex command
363
+ const parsedCommands = parseComplexCommand(message);
348
364
 
349
- // Get selected terminals
350
- const selectedTerminals = getSelectedTerminals();
351
- if (selectedTerminals.length === 0) {
352
- alert('Please select at least one terminal');
365
+ if (parsedCommands.length === 0) {
366
+ addChatMessage('System', 'Invalid command format. Use @terminal command or /command syntax.');
353
367
  return;
354
368
  }
355
369
 
@@ -358,23 +372,107 @@ function sendMessage() {
358
372
 
359
373
  // Check if plan mode is enabled
360
374
  const usePlanMode = document.getElementById('use-plan-mode').checked;
375
+ const useDefaultPrompt = document.getElementById('use-default-prompt').checked;
376
+ const defaultPrompt = document.getElementById('default-prompt-text').value.trim();
361
377
 
362
- // Send to selected terminals
363
- selectedTerminals.forEach(terminalId => {
364
- if (usePlanMode) {
365
- // Send the plan mode instruction first with proper line endings
366
- ipcRenderer.invoke('terminal:write', terminalId, 'Please enter plan mode (press Shift+Tab twice)\r');
367
-
368
- // Small delay to ensure the first command is processed
369
- setTimeout(() => {
370
- // Send the actual message with carriage return to execute
371
- ipcRenderer.invoke('terminal:write', terminalId, fullMessage + '\r');
372
- }, 100);
378
+ // Execute each parsed command
379
+ for (const { terminals: targetTerminals, command } of parsedCommands) {
380
+ // Apply default prompt if needed (only for non-slash commands)
381
+ let finalCommand = command;
382
+ if (useDefaultPrompt && defaultPrompt && !command.startsWith('/')) {
383
+ finalCommand = defaultPrompt + ' ' + command;
384
+ }
385
+
386
+ // Determine which terminals to use
387
+ let terminalsToUse = [];
388
+ if (targetTerminals.length === 0) {
389
+ // No specific terminals specified
390
+ if (command.startsWith('/')) {
391
+ // Slash command without terminals - execute normally
392
+ executeCommand(command);
393
+ continue;
394
+ } else {
395
+ // Regular command without terminals - error
396
+ addChatMessage('System', 'Please specify terminals using @terminal syntax.');
397
+ continue;
398
+ }
399
+ }
400
+
401
+ // Process terminal specifications
402
+ for (const terminalSpec of targetTerminals) {
403
+ if (terminalSpec.toLowerCase() === 'all') {
404
+ // @all - use all terminals
405
+ terminalsToUse = Array.from(terminals.keys());
406
+ break;
407
+ } else {
408
+ // Find specific terminal
409
+ const result = findTerminal(terminalSpec);
410
+ if (result) {
411
+ terminalsToUse.push(result.id);
412
+ } else {
413
+ addChatMessage('System', `Terminal '${terminalSpec}' not found.`);
414
+ }
415
+ }
416
+ }
417
+
418
+ // Execute command on all target terminals
419
+ if (command.startsWith('/')) {
420
+ // Slash command - execute for each terminal
421
+ for (const terminalId of terminalsToUse) {
422
+ const termData = terminals.get(terminalId);
423
+ if (!termData) continue;
424
+
425
+ // Parse and execute the slash command with terminal context
426
+ const { command: slashCmd, args } = parseCommand(command);
427
+
428
+ // Modify args to include terminal reference if needed
429
+ switch (slashCmd) {
430
+ case '/send':
431
+ case '/clear':
432
+ case '/interrupt':
433
+ case '/close':
434
+ case '/return':
435
+ // These commands take a terminal as first arg
436
+ // Replace with current terminal
437
+ executeCommand(`${slashCmd} ${termData.name} ${args.slice(1).join(' ')}`.trim());
438
+ break;
439
+ default:
440
+ // Other commands execute as-is
441
+ executeCommand(command);
442
+ break;
443
+ }
444
+ }
373
445
  } else {
374
- // Send the message directly to the terminal
375
- ipcRenderer.invoke('terminal:write', terminalId, fullMessage + '\r');
446
+ // Regular command - send to terminals
447
+ for (const terminalId of terminalsToUse) {
448
+ const termData = terminals.get(terminalId);
449
+ if (!termData) continue;
450
+
451
+ if (usePlanMode) {
452
+ // Send Shift+Tab twice to enter plan mode
453
+ ipcRenderer.invoke('terminal:write', terminalId, '\x1b[Z\x1b[Z');
454
+
455
+ // Small delay to ensure plan mode is activated
456
+ setTimeout(() => {
457
+ ipcRenderer.invoke('terminal:write', terminalId, finalCommand + '\r');
458
+
459
+ // Auto-return after sending the message
460
+ setTimeout(() => {
461
+ executeReturnCommand([termData.name], true);
462
+ }, 150);
463
+ }, 300);
464
+ } else {
465
+ // Send the message directly to the terminal
466
+ ipcRenderer.invoke('terminal:write', terminalId, finalCommand + '\r');
467
+
468
+ // Auto-return after sending the message
469
+ setTimeout(() => {
470
+ executeReturnCommand([termData.name], true);
471
+ }, 150);
472
+ }
473
+ }
376
474
  }
377
- });
475
+ }
378
476
 
379
477
  // Clear input
380
478
  input.value = '';
@@ -536,7 +634,9 @@ function executeHelpCommand() {
536
634
  `${cmd.syntax} - ${cmd.description}`
537
635
  ).join('\n');
538
636
 
539
- addChatMessage('System', 'Available commands:\n' + helpText);
637
+ 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';
638
+
639
+ addChatMessage('System', 'Available commands:\n' + helpText + atHelp);
540
640
  }
541
641
 
542
642
  function executeCreateCommand(args) {
@@ -564,9 +664,9 @@ function executeCloseCommand(args) {
564
664
  addChatMessage('System', `✅ Closed terminal ${result.terminal.name}`);
565
665
  }
566
666
 
567
- function executeReturnCommand(args) {
667
+ function executeReturnCommand(args, silent = false) {
568
668
  if (args.length === 0) {
569
- addChatMessage('System', 'Usage: /return <terminal>');
669
+ if (!silent) addChatMessage('System', 'Usage: /return <terminal>');
570
670
  return;
571
671
  }
572
672
 
@@ -574,14 +674,18 @@ function executeReturnCommand(args) {
574
674
  const result = findTerminal(terminalIdentifier);
575
675
 
576
676
  if (!result) {
577
- const available = Array.from(terminals.values()).map(t => t.name).join(', ');
578
- addChatMessage('System', `Terminal '${terminalIdentifier}' not found. Available: ${available}`);
677
+ if (!silent) {
678
+ const available = Array.from(terminals.values()).map(t => t.name).join(', ');
679
+ addChatMessage('System', `Terminal '${terminalIdentifier}' not found. Available: ${available}`);
680
+ }
579
681
  return;
580
682
  }
581
683
 
582
684
  // Send carriage return to submit current input
583
685
  ipcRenderer.invoke('terminal:write', result.id, '\r');
584
- addChatMessage('System', `✅ Submitted input in ${result.terminal.name}`);
686
+ if (!silent) {
687
+ addChatMessage('System', `✅ Submitted input in ${result.terminal.name}`);
688
+ }
585
689
  }
586
690
 
587
691
  function executeClearCommand(args) {
@@ -604,16 +708,214 @@ function executeClearCommand(args) {
604
708
  addChatMessage('System', `✅ Sent Ctrl+C to ${result.terminal.name}`);
605
709
  }
606
710
 
711
+ function executeAtCommand(message) {
712
+ // Parse various @terminal command syntaxes:
713
+ // 1. Space-separated: "@term1 @term2 command"
714
+ // 2. Comma-separated: "@term1,term2,term3 command"
715
+ // 3. Single terminal: "@terminal command" or '@"terminal name" command'
716
+ // 4. All terminals: "@all command"
717
+
718
+ let targetTerminals = [];
719
+ let command = '';
720
+
721
+ // First, try to match space-separated terminals at the beginning
722
+ const spaceSeparatedRegex = /^(@(?:"[^"]+"|[^\s,]+)\s+)+(.+)$/;
723
+ const spaceSeparatedMatch = message.match(spaceSeparatedRegex);
724
+
725
+ if (spaceSeparatedMatch) {
726
+ // Extract all @terminal references
727
+ const terminalsStr = spaceSeparatedMatch[0].substring(0, spaceSeparatedMatch[0].length - spaceSeparatedMatch[2].length);
728
+ command = spaceSeparatedMatch[2].trim();
729
+
730
+ // Parse each @terminal reference
731
+ const terminalMatches = terminalsStr.matchAll(/@(?:"([^"]+)"|([^\s,]+))/g);
732
+ for (const match of terminalMatches) {
733
+ const terminalName = match[1] || match[2];
734
+ targetTerminals.push(terminalName.trim());
735
+ }
736
+ } else {
737
+ // Try single @terminal with possible comma-separated names
738
+ let match = message.match(/^@"([^"]+)"\s+(.+)$/); // Handle quoted names
739
+
740
+ if (!match) {
741
+ match = message.match(/^@(\S+)\s+(.+)$/); // Handle unquoted names (including comma-separated)
742
+ }
743
+
744
+ if (!match) {
745
+ addChatMessage('System', 'Usage: @terminal <command>\nExamples:\n @Tom ls -la\n @term1,term2 pwd\n @term1 @term2 date\n @all whoami');
746
+ return;
747
+ }
748
+
749
+ const terminalNameStr = match[1];
750
+ command = match[2];
751
+
752
+ // Check if it's @all
753
+ if (terminalNameStr.toLowerCase() === 'all') {
754
+ // Send to all terminals
755
+ const allTerminals = Array.from(terminals.entries());
756
+ if (allTerminals.length === 0) {
757
+ addChatMessage('System', 'No terminals available');
758
+ return;
759
+ }
760
+
761
+ const terminalNames = [];
762
+ for (const [id, termData] of allTerminals) {
763
+ ipcRenderer.invoke('terminal:write', id, command + '\r');
764
+ terminalNames.push(termData.name);
765
+ }
766
+
767
+ addChatMessage('System', `✅ Sent '${command}' to all ${allTerminals.length} terminals`);
768
+
769
+ // Auto-return to all terminals after a short delay
770
+ setTimeout(() => {
771
+ for (const name of terminalNames) {
772
+ executeReturnCommand([name], true);
773
+ }
774
+ }, 150);
775
+ return;
776
+ }
777
+
778
+ // Handle comma-separated terminal names
779
+ targetTerminals = terminalNameStr.split(',').map(name => name.trim());
780
+ }
781
+
782
+ // Process all target terminals
783
+ const foundTerminals = [];
784
+ const notFoundTerminals = [];
785
+
786
+ for (const terminalName of targetTerminals) {
787
+ const result = findTerminal(terminalName);
788
+ if (result) {
789
+ foundTerminals.push({ name: terminalName, id: result.id, terminal: result.terminal });
790
+ } else {
791
+ notFoundTerminals.push(terminalName);
792
+ }
793
+ }
794
+
795
+ // Report not found terminals
796
+ if (notFoundTerminals.length > 0) {
797
+ const available = Array.from(terminals.values()).map(t => t.name).join(', ');
798
+ addChatMessage('System', `Terminal(s) not found: ${notFoundTerminals.join(', ')}. Available: ${available}`);
799
+ }
800
+
801
+ // Send command to all found terminals
802
+ if (foundTerminals.length > 0) {
803
+ const sentToNames = [];
804
+ for (const { id, terminal } of foundTerminals) {
805
+ ipcRenderer.invoke('terminal:write', id, command + '\r');
806
+ sentToNames.push(terminal.name);
807
+ }
808
+ addChatMessage('System', `✅ Sent '${command}' to ${foundTerminals.length} terminal(s): ${sentToNames.join(', ')}`);
809
+
810
+ // Auto-return to all terminals after a short delay
811
+ setTimeout(() => {
812
+ for (const name of sentToNames) {
813
+ executeReturnCommand([name], true); // true = silent mode
814
+ }
815
+ }, 150);
816
+ }
817
+ }
818
+
607
819
  // Dropdown Functions
608
- function showCommandDropdown() {
820
+ function showCommandDropdown(cursorPos = null) {
609
821
  const dropdown = document.getElementById('command-dropdown');
610
822
  const input = document.getElementById('chat-input');
611
823
  const inputValue = input.value;
824
+ const pos = cursorPos !== null ? cursorPos : input.selectionStart;
825
+
826
+ // Find the / symbol and extract the partial command name
827
+ let slashPos = -1;
828
+ let searchTerm = '';
829
+
830
+ // Search backwards from cursor to find the / symbol
831
+ for (let i = pos - 1; i >= 0; i--) {
832
+ if (inputValue[i] === '/') {
833
+ slashPos = i;
834
+ // Extract text from / to cursor position
835
+ searchTerm = inputValue.substring(i + 1, pos).toLowerCase();
836
+ break;
837
+ } else if (inputValue[i] === ' ' || inputValue[i] === '@') {
838
+ // Stop if we hit a space or @ before finding /
839
+ break;
840
+ }
841
+ }
612
842
 
613
- // Filter commands based on current input
614
- const searchTerm = inputValue.substring(1).toLowerCase(); // Remove the '/'
843
+ // If no / found or / is not at a valid position, hide dropdown
844
+ if (slashPos === -1) {
845
+ hideCommandDropdown();
846
+ return;
847
+ }
848
+
849
+ // Filter commands based on partial input
615
850
  currentCommands = availableCommands.filter(cmd =>
616
851
  cmd.name.substring(1).toLowerCase().startsWith(searchTerm)
852
+ ).map(cmd => ({
853
+ ...cmd,
854
+ isSlashCommand: true,
855
+ slashPosition: slashPos
856
+ }));
857
+
858
+ if (currentCommands.length === 0) {
859
+ hideCommandDropdown();
860
+ return;
861
+ }
862
+
863
+ populateCommandDropdown();
864
+ dropdown.style.display = 'block';
865
+ selectedCommandIndex = -1;
866
+ }
867
+
868
+ function showTerminalDropdown(cursorPos = null) {
869
+ const dropdown = document.getElementById('command-dropdown');
870
+ const input = document.getElementById('chat-input');
871
+ const inputValue = input.value;
872
+ const pos = cursorPos !== null ? cursorPos : input.selectionStart;
873
+
874
+ // Find the @ symbol and extract the partial terminal name
875
+ let atPos = -1;
876
+ let searchTerm = '';
877
+
878
+ // Search backwards from cursor to find the @ symbol
879
+ for (let i = pos - 1; i >= 0; i--) {
880
+ if (inputValue[i] === '@') {
881
+ atPos = i;
882
+ // Extract text from @ to cursor position
883
+ searchTerm = inputValue.substring(i + 1, pos).toLowerCase();
884
+ break;
885
+ } else if (inputValue[i] === ' ' || inputValue[i] === ',') {
886
+ // Stop if we hit a space or comma before finding @
887
+ break;
888
+ }
889
+ }
890
+
891
+ // If no @ found or @ is not at a valid position, hide dropdown
892
+ if (atPos === -1) {
893
+ hideCommandDropdown();
894
+ return;
895
+ }
896
+
897
+ const availableTerminals = Array.from(terminals.entries()).map(([id, termData]) => ({
898
+ name: '@' + termData.name,
899
+ syntax: '@' + termData.name + ' <command>',
900
+ description: `Send command to ${termData.name}`,
901
+ id: id,
902
+ isTerminal: true,
903
+ terminalName: termData.name,
904
+ atPosition: atPos
905
+ }));
906
+
907
+ // Add @all option
908
+ availableTerminals.unshift({
909
+ name: '@all',
910
+ syntax: '@all <command>',
911
+ description: 'Send command to all terminals',
912
+ isTerminal: true,
913
+ terminalName: 'all',
914
+ atPosition: atPos
915
+ });
916
+
917
+ currentCommands = availableTerminals.filter(term =>
918
+ term.terminalName.toLowerCase().startsWith(searchTerm)
617
919
  );
618
920
 
619
921
  if (currentCommands.length === 0) {
@@ -671,15 +973,53 @@ function selectCommand(index) {
671
973
 
672
974
  const command = currentCommands[index];
673
975
  const input = document.getElementById('chat-input');
976
+ const value = input.value;
674
977
 
675
- // Set the command name and position cursor for arguments
676
- input.value = command.name + ' ';
677
- input.focus();
678
-
679
- // Position cursor at the end
680
- input.setSelectionRange(input.value.length, input.value.length);
978
+ // Check if this is a terminal selection with position info
979
+ if (command.isTerminal && command.atPosition !== undefined) {
980
+ const atPos = command.atPosition;
981
+
982
+ // Find the end of the partial terminal name
983
+ let endPos = input.selectionStart;
984
+
985
+ // Replace the partial terminal name with the selected one
986
+ const beforeAt = value.substring(0, atPos);
987
+ const afterCursor = value.substring(endPos);
988
+
989
+ // Insert the selected terminal name (without the @, as it's already there)
990
+ input.value = beforeAt + '@' + command.terminalName + ' ' + afterCursor;
991
+
992
+ // Position cursor after the inserted terminal name and space
993
+ const newCursorPos = atPos + command.terminalName.length + 2; // +2 for @ and space
994
+ input.setSelectionRange(newCursorPos, newCursorPos);
995
+ } else if (command.isSlashCommand && command.slashPosition !== undefined) {
996
+ // Slash command at specific position
997
+ const slashPos = command.slashPosition;
998
+
999
+ // Find the end of the partial command name
1000
+ let endPos = input.selectionStart;
1001
+
1002
+ // Replace the partial command with the selected one
1003
+ const beforeSlash = value.substring(0, slashPos);
1004
+ const afterCursor = value.substring(endPos);
1005
+
1006
+ // Insert the selected command name
1007
+ input.value = beforeSlash + command.name + ' ' + afterCursor;
1008
+
1009
+ // Position cursor after the inserted command name and space
1010
+ const newCursorPos = slashPos + command.name.length + 1; // +1 for space
1011
+ input.setSelectionRange(newCursorPos, newCursorPos);
1012
+ } else {
1013
+ // Original behavior (shouldn't reach here with new system)
1014
+ input.value = command.name + ' ';
1015
+ input.setSelectionRange(input.value.length, input.value.length);
1016
+ }
681
1017
 
1018
+ input.focus();
682
1019
  hideCommandDropdown();
1020
+
1021
+ // Trigger input event to update highlighting
1022
+ input.dispatchEvent(new Event('input', { bubbles: true }));
683
1023
  }
684
1024
 
685
1025
  function navigateDropdown(direction) {
@@ -705,16 +1045,96 @@ function selectCurrentCommand() {
705
1045
  }
706
1046
  }
707
1047
 
1048
+ // Update overlay with syntax highlighting
1049
+ function updateOverlayHighlighting() {
1050
+ const input = document.getElementById('chat-input');
1051
+ const overlay = document.getElementById('input-overlay');
1052
+ const value = input.value;
1053
+
1054
+ // Parse and highlight text
1055
+ let highlightedText = escapeHtml(value);
1056
+
1057
+ // Highlight @terminal commands
1058
+ highlightedText = highlightedText.replace(
1059
+ /@"([^"]+)"/g,
1060
+ (match, terminalName) => {
1061
+ const result = findTerminal(terminalName);
1062
+ return result ? `@<span class="highlight-terminal">"${terminalName}"</span>` : match;
1063
+ }
1064
+ );
1065
+
1066
+ highlightedText = highlightedText.replace(
1067
+ /@(\S+)/g,
1068
+ (match, terminalName) => {
1069
+ // Handle @all specially
1070
+ if (terminalName.toLowerCase() === 'all') {
1071
+ return `@<span class="highlight-terminal">${terminalName}</span>`;
1072
+ }
1073
+ // Handle comma-separated terminals
1074
+ if (terminalName.includes(',')) {
1075
+ const parts = terminalName.split(',');
1076
+ const highlightedParts = parts.map((part, index) => {
1077
+ const trimmedPart = part.trim();
1078
+ const result = findTerminal(trimmedPart);
1079
+ return (index > 0 ? ',' : '') + (result ? `<span class="highlight-terminal">${trimmedPart}</span>` : trimmedPart);
1080
+ }).join('');
1081
+ return '@' + highlightedParts;
1082
+ }
1083
+ // Single terminal
1084
+ const result = findTerminal(terminalName);
1085
+ return result ? `@<span class="highlight-terminal">${terminalName}</span>` : match;
1086
+ }
1087
+ );
1088
+
1089
+ // Highlight slash commands (anywhere in the text)
1090
+ highlightedText = highlightedText.replace(
1091
+ /(\/\w+)/g,
1092
+ (match, command, offset) => {
1093
+ // Check if this is a valid position for a command (start of string or after space/@ )
1094
+ const isValidPosition = offset === 0 ||
1095
+ highlightedText[offset - 1] === ' ' ||
1096
+ highlightedText[offset - 1] === '@';
1097
+
1098
+ if (isValidPosition) {
1099
+ const validCommand = availableCommands.find(cmd => cmd.name === command);
1100
+ return validCommand ? `<span class="highlight-command">${command}</span>` : match;
1101
+ }
1102
+ return match;
1103
+ }
1104
+ );
1105
+
1106
+ overlay.innerHTML = highlightedText;
1107
+
1108
+ // Sync scroll position
1109
+ overlay.scrollTop = input.scrollTop;
1110
+ overlay.scrollLeft = input.scrollLeft;
1111
+ }
1112
+
1113
+ function escapeHtml(text) {
1114
+ const div = document.createElement('div');
1115
+ div.textContent = text;
1116
+ return div.innerHTML;
1117
+ }
1118
+
708
1119
  // Initialize collapsible default prompt section
709
1120
  function initCollapsible() {
710
1121
  const toggle = document.querySelector('.collapse-toggle');
711
1122
  const content = document.querySelector('.collapsible-content');
1123
+ const header = document.querySelector('.collapsible-header');
712
1124
 
713
- toggle.addEventListener('click', () => {
1125
+ const toggleSection = (e) => {
1126
+ // Prevent double-triggering when clicking the toggle button
1127
+ if (e && e.target === toggle) {
1128
+ e.stopPropagation();
1129
+ }
1130
+
714
1131
  const isExpanded = content.style.display !== 'none';
715
1132
  content.style.display = isExpanded ? 'none' : 'block';
716
1133
  toggle.textContent = isExpanded ? '▶' : '▼';
717
- });
1134
+ };
1135
+
1136
+ // Make the entire header clickable
1137
+ header.addEventListener('click', toggleSection);
718
1138
  }
719
1139
 
720
1140
 
@@ -753,6 +1173,9 @@ document.addEventListener('DOMContentLoaded', () => {
753
1173
  e.preventDefault();
754
1174
  if (selectedCommandIndex >= 0) {
755
1175
  selectCurrentCommand();
1176
+ } else if (currentCommands.length > 0) {
1177
+ // Auto-select first option if none selected
1178
+ selectCommand(0);
756
1179
  }
757
1180
  } else if (e.key === 'ArrowUp' && !isDropdownVisible && commandHistory.length > 0) {
758
1181
  e.preventDefault();
@@ -775,21 +1198,51 @@ document.addEventListener('DOMContentLoaded', () => {
775
1198
  }
776
1199
  });
777
1200
 
778
- // Chat input - Show dropdown on '/' and filter as user types
1201
+ // Chat input - Show dropdown on '/' and '@' and filter as user types
779
1202
  document.getElementById('chat-input').addEventListener('input', (e) => {
780
1203
  const input = e.target;
781
1204
  const value = input.value;
782
1205
  const cursorPos = input.selectionStart;
783
1206
 
784
- // Check if cursor is at end and input starts with '/'
785
- if (cursorPos === value.length && value.startsWith('/') && value.length > 1) {
786
- showCommandDropdown();
787
- } else if (value.startsWith('/') && value.length === 1) {
788
- // Show all commands when just '/' is typed
789
- showCommandDropdown();
790
- } else {
791
- hideCommandDropdown();
1207
+ // Update overlay highlighting
1208
+ updateOverlayHighlighting();
1209
+
1210
+ // Check for both @ and / at cursor position
1211
+ if (cursorPos > 0) {
1212
+ // Look for @ at or before cursor position
1213
+ let foundAt = false;
1214
+ let foundSlash = false;
1215
+
1216
+ for (let i = cursorPos - 1; i >= 0; i--) {
1217
+ if (value[i] === '@') {
1218
+ foundAt = true;
1219
+ break;
1220
+ } else if (value[i] === '/') {
1221
+ foundSlash = true;
1222
+ break;
1223
+ } else if (value[i] === ' ' || value[i] === ',') {
1224
+ // If we hit a space or comma, check if it's immediately followed by @ or /
1225
+ if (i === cursorPos - 1) {
1226
+ if (value[cursorPos - 1] === '@') {
1227
+ foundAt = true;
1228
+ } else if (value[cursorPos - 1] === '/') {
1229
+ foundSlash = true;
1230
+ }
1231
+ }
1232
+ break;
1233
+ }
1234
+ }
1235
+
1236
+ if (foundAt) {
1237
+ showTerminalDropdown(cursorPos);
1238
+ return;
1239
+ } else if (foundSlash) {
1240
+ showCommandDropdown(cursorPos);
1241
+ return;
1242
+ }
792
1243
  }
1244
+
1245
+ hideCommandDropdown();
793
1246
  });
794
1247
 
795
1248
  // Hide dropdown when clicking outside
@@ -802,6 +1255,14 @@ document.addEventListener('DOMContentLoaded', () => {
802
1255
  }
803
1256
  });
804
1257
 
1258
+ // Sync overlay scroll with input
1259
+ document.getElementById('chat-input').addEventListener('scroll', () => {
1260
+ const input = document.getElementById('chat-input');
1261
+ const overlay = document.getElementById('input-overlay');
1262
+ overlay.scrollTop = input.scrollTop;
1263
+ overlay.scrollLeft = input.scrollLeft;
1264
+ });
1265
+
805
1266
  // Initialize collapsible
806
1267
  initCollapsible();
807
1268
 
package/style.css CHANGED
@@ -176,59 +176,6 @@ body {
176
176
  position: relative;
177
177
  }
178
178
 
179
- /* Terminal Selection */
180
- #terminal-selector > label {
181
- display: block;
182
- font-size: 13px;
183
- color: #cccccc;
184
- margin-bottom: 8px;
185
- font-weight: 500;
186
- }
187
-
188
- #terminal-checkboxes {
189
- display: flex;
190
- flex-direction: row;
191
- flex-wrap: wrap;
192
- gap: 8px 15px;
193
- max-height: 80px;
194
- overflow-y: auto;
195
- padding-right: 5px;
196
- }
197
-
198
- .terminal-checkbox-label {
199
- font-size: 13px !important;
200
- padding: 3px 8px;
201
- display: flex;
202
- align-items: center;
203
- gap: 6px;
204
- white-space: nowrap;
205
- background-color: rgba(255, 255, 255, 0.05);
206
- border-radius: 4px;
207
- transition: background-color 0.2s;
208
- }
209
-
210
- .terminal-checkbox-label:hover {
211
- background-color: rgba(255, 255, 255, 0.1);
212
- }
213
-
214
- .terminal-checkbox-label {
215
- display: flex;
216
- align-items: center;
217
- gap: 8px;
218
- font-size: 14px;
219
- color: #cccccc;
220
- cursor: pointer;
221
- }
222
-
223
- .terminal-checkbox-label:hover {
224
- color: #ffffff;
225
- }
226
-
227
- .terminal-selector-checkbox {
228
- width: 16px;
229
- height: 16px;
230
- cursor: pointer;
231
- }
232
179
 
233
180
  /* Default Prompt Section */
234
181
  #default-prompt-section {
@@ -312,15 +259,39 @@ body {
312
259
 
313
260
  /* Removed - Mode selector is now integrated into input area */
314
261
 
262
+ /* Input Overlay for Syntax Highlighting */
263
+ .input-overlay {
264
+ position: absolute;
265
+ top: 0;
266
+ left: 0;
267
+ right: 0;
268
+ min-height: 80px;
269
+ max-height: 200px;
270
+ padding: 10px 12px;
271
+ border-radius: 4px;
272
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
273
+ font-size: 14px;
274
+ line-height: 1.5;
275
+ color: transparent;
276
+ background: transparent;
277
+ pointer-events: none;
278
+ z-index: 1;
279
+ white-space: pre-wrap;
280
+ word-wrap: break-word;
281
+ overflow: hidden;
282
+ }
283
+
315
284
  /* Chat Input - Full Width */
316
285
  #chat-input {
286
+ position: relative;
287
+ z-index: 2;
317
288
  width: 100%;
318
289
  min-height: 80px;
319
290
  max-height: 200px;
320
291
  background-color: #3c3c3c;
321
292
  border: 1px solid #555;
322
293
  color: white;
323
- padding: 10px 12px 35px 12px;
294
+ padding: 10px 12px;
324
295
  border-radius: 4px;
325
296
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
326
297
  font-size: 14px;
@@ -334,26 +305,38 @@ body {
334
305
  box-shadow: 0 0 0 2px rgba(14, 99, 156, 0.2);
335
306
  }
336
307
 
308
+ /* Syntax highlighting styles */
309
+ .highlight-terminal {
310
+ color: #4CAF50;
311
+ }
312
+
313
+ .highlight-command {
314
+ color: #FF9800;
315
+ }
316
+
317
+ /* Plan Mode Row */
318
+ .plan-mode-row {
319
+ margin-bottom: 8px;
320
+ }
321
+
337
322
  /* Plan Mode Option */
338
323
  .plan-mode-option {
339
- position: absolute;
340
- bottom: 10px;
341
- left: 12px;
342
324
  display: flex;
343
325
  align-items: center;
344
- gap: 6px;
345
- font-size: 12px;
346
- color: #999;
326
+ gap: 8px;
327
+ font-size: 14px;
328
+ color: #cccccc;
347
329
  cursor: pointer;
330
+ padding: 4px 0;
348
331
  }
349
332
 
350
333
  .plan-mode-option:hover {
351
- color: #cccccc;
334
+ color: #ffffff;
352
335
  }
353
336
 
354
337
  .plan-mode-option input[type="checkbox"] {
355
- width: 14px;
356
- height: 14px;
338
+ width: 16px;
339
+ height: 16px;
357
340
  cursor: pointer;
358
341
  }
359
342