@darksol/terminal 0.4.7 → 0.4.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksol/terminal",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "DARKSOL Terminal — unified CLI for all DARKSOL services. Market intel, trading, oracle, casino, and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -69,6 +69,96 @@ const USDC_ADDRESSES = {
69
69
 
70
70
  const ERC20_ABI = ['function balanceOf(address) view returns (uint256)'];
71
71
 
72
+ /**
73
+ * Handle interactive menu selections from the client
74
+ */
75
+ export async function handleMenuSelect(id, value, item, ws) {
76
+ switch (id) {
77
+ case 'wallet_select':
78
+ return await showWalletDetail(value, ws);
79
+
80
+ case 'wallet_action':
81
+ switch (value) {
82
+ case 'receive': {
83
+ const name = getConfig('activeWallet');
84
+ if (!name) return {};
85
+ const { loadWallet } = await import('../wallet/keystore.js');
86
+ const w = loadWallet(name);
87
+ ws.sendLine('');
88
+ ws.sendLine(`${ANSI.gold} ◆ RECEIVE — ${name}${ANSI.reset}`);
89
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
90
+ ws.sendLine('');
91
+ ws.sendLine(` ${ANSI.white}Your address:${ANSI.reset}`);
92
+ ws.sendLine('');
93
+ const addr = w.address;
94
+ ws.sendLine(` ${ANSI.dim}┌${'─'.repeat(addr.length + 4)}┐${ANSI.reset}`);
95
+ ws.sendLine(` ${ANSI.dim}│ ${ANSI.gold}${addr}${ANSI.dim} │${ANSI.reset}`);
96
+ ws.sendLine(` ${ANSI.dim}└${'─'.repeat(addr.length + 4)}┘${ANSI.reset}`);
97
+ ws.sendLine('');
98
+ ws.sendLine(` ${ANSI.dim}Works on ALL EVM chains: Base • Ethereum • Arbitrum • Optimism • Polygon${ANSI.reset}`);
99
+ ws.sendLine(` ${ANSI.red}Make sure the sender is on the same chain!${ANSI.reset}`);
100
+ ws.sendLine('');
101
+ return {};
102
+ }
103
+ case 'send':
104
+ ws.sendLine('');
105
+ ws.sendLine(` ${ANSI.gold}◆ SEND${ANSI.reset}`);
106
+ ws.sendLine(` ${ANSI.dim}Sending requires wallet password — use the CLI:${ANSI.reset}`);
107
+ ws.sendLine(` ${ANSI.gold}darksol send --to 0x... --amount 0.1 --token ETH${ANSI.reset}`);
108
+ ws.sendLine(` ${ANSI.gold}darksol send${ANSI.reset} ${ANSI.dim}(interactive mode)${ANSI.reset}`);
109
+ ws.sendLine('');
110
+ return {};
111
+ case 'portfolio':
112
+ return await handleCommand('portfolio', ws);
113
+ case 'history':
114
+ return await handleCommand('history', ws);
115
+ case 'switch': {
116
+ const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
117
+ const current = getConfig('chain') || 'base';
118
+ ws.sendMenu('chain_select', '◆ Select Chain', chains.map(c => ({
119
+ value: c,
120
+ label: c === current ? `★ ${c}` : c,
121
+ desc: c === current ? 'current' : '',
122
+ })));
123
+ return {};
124
+ }
125
+ case 'back':
126
+ ws.sendLine('');
127
+ return {};
128
+ }
129
+ break;
130
+
131
+ case 'chain_select':
132
+ setConfig('chain', value);
133
+ ws.sendLine('');
134
+ ws.sendLine(` ${ANSI.green}✓ Chain set to ${value}${ANSI.reset}`);
135
+ ws.sendLine('');
136
+ return {};
137
+
138
+ case 'config_action':
139
+ if (value === 'chain') {
140
+ const chains = ['base', 'ethereum', 'arbitrum', 'optimism', 'polygon'];
141
+ const current = getConfig('chain') || 'base';
142
+ ws.sendMenu('chain_select', '◆ Select Chain', chains.map(c => ({
143
+ value: c,
144
+ label: c === current ? `★ ${c}` : c,
145
+ desc: c === current ? 'current' : '',
146
+ })));
147
+ return {};
148
+ }
149
+ if (value === 'keys') {
150
+ return await handleCommand('keys', ws);
151
+ }
152
+ ws.sendLine('');
153
+ return {};
154
+
155
+ case 'main_menu':
156
+ return await handleCommand(value, ws);
157
+ }
158
+
159
+ return {};
160
+ }
161
+
72
162
  /**
73
163
  * AI status check — shown on connection
74
164
  */
@@ -402,47 +492,121 @@ async function cmdMarket(args, ws) {
402
492
  // WALLET
403
493
  // ══════════════════════════════════════════════════
404
494
  async function cmdWallet(args, ws) {
405
- const sub = args[0] || 'list';
495
+ const sub = args[0];
406
496
 
407
- if (sub === 'list') {
408
- const { listWallets } = await import('../wallet/keystore.js');
409
- const wallets = listWallets();
410
- const active = getConfig('activeWallet');
497
+ // If a specific subcommand, handle directly
498
+ if (sub === 'balance') {
499
+ return await showWalletDetail(args[1] || getConfig('activeWallet'), ws);
500
+ }
411
501
 
412
- ws.sendLine(`${ANSI.gold} WALLETS${ANSI.reset}`);
413
- ws.sendLine(`${ANSI.dim} ${''.repeat(50)}${ANSI.reset}`);
502
+ if (sub === 'use' && args[1]) {
503
+ setConfig('activeWallet', args[1]);
504
+ ws.sendLine(` ${ANSI.green}✓ Active wallet set to "${args[1]}"${ANSI.reset}`);
505
+ ws.sendLine('');
506
+ return {};
507
+ }
414
508
 
415
- if (wallets.length === 0) {
416
- ws.sendLine(` ${ANSI.dim}No wallets. Create one in the CLI: darksol wallet create${ANSI.reset}`);
417
- } else {
418
- for (const w of wallets) {
419
- const indicator = w === active ? `${ANSI.gold}► ${ANSI.reset}` : ' ';
420
- ws.sendLine(` ${indicator}${ANSI.white}${w}${ANSI.reset}`);
421
- }
422
- }
509
+ // Default: interactive wallet picker
510
+ const { listWallets } = await import('../wallet/keystore.js');
511
+ const wallets = listWallets();
512
+ const active = getConfig('activeWallet');
423
513
 
514
+ if (wallets.length === 0) {
515
+ ws.sendLine(`${ANSI.gold} ◆ WALLETS${ANSI.reset}`);
516
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
517
+ ws.sendLine(` ${ANSI.dim}No wallets found.${ANSI.reset}`);
518
+ ws.sendLine(` ${ANSI.dim}Create one: ${ANSI.gold}darksol wallet create <name>${ANSI.reset}`);
424
519
  ws.sendLine('');
425
520
  return {};
426
521
  }
427
522
 
428
- if (sub === 'balance') {
429
- const name = args[1] || getConfig('activeWallet');
430
- if (!name) return { output: ` ${ANSI.red}No active wallet${ANSI.reset}\r\n` };
523
+ if (wallets.length === 1) {
524
+ // Only one wallet go straight to it
525
+ return await showWalletDetail(wallets[0].name, ws);
526
+ }
431
527
 
432
- const { loadWallet } = await import('../wallet/keystore.js');
433
- const w = loadWallet(name);
434
- const chain = getConfig('chain') || 'base';
435
- const provider = new ethers.JsonRpcProvider(RPCS[chain]);
436
- const bal = parseFloat(ethers.formatEther(await provider.getBalance(w.address)));
528
+ // Multiple wallets show interactive menu
529
+ const menuItems = wallets.map(w => ({
530
+ value: w.name,
531
+ label: `${w.name === active ? '★ ' : ''}${w.name}`,
532
+ desc: `${w.address.slice(0, 6)}...${w.address.slice(-4)}`,
533
+ }));
534
+
535
+ ws.sendMenu('wallet_select', '◆ Select Wallet', menuItems);
536
+ return {};
537
+ }
538
+
539
+ async function showWalletDetail(name, ws) {
540
+ if (!name) {
541
+ ws.sendLine(` ${ANSI.red}No wallet selected${ANSI.reset}`);
542
+ ws.sendLine('');
543
+ return {};
544
+ }
437
545
 
438
- ws.sendLine(`${ANSI.gold} BALANCE ${name}${ANSI.reset}`);
439
- ws.sendLine(`${ANSI.dim} ${w.address}${ANSI.reset}`);
440
- ws.sendLine(` ${ANSI.white}${bal.toFixed(6)} ETH${ANSI.reset} on ${chain}`);
546
+ const { loadWallet } = await import('../wallet/keystore.js');
547
+ let walletData;
548
+ try {
549
+ walletData = loadWallet(name);
550
+ } catch {
551
+ ws.sendLine(` ${ANSI.red}Wallet "${name}" not found${ANSI.reset}`);
441
552
  ws.sendLine('');
442
553
  return {};
443
554
  }
444
555
 
445
- return { output: ` ${ANSI.dim}Wallet commands: list, balance${ANSI.reset}\r\n` };
556
+ const chain = getConfig('chain') || 'base';
557
+ const provider = new ethers.JsonRpcProvider(RPCS[chain]);
558
+
559
+ ws.sendLine(`${ANSI.gold} ◆ WALLET — ${name}${ANSI.reset}`);
560
+ ws.sendLine(`${ANSI.dim} ${'─'.repeat(50)}${ANSI.reset}`);
561
+ ws.sendLine(` ${ANSI.darkGold}Address${ANSI.reset} ${ANSI.white}${walletData.address}${ANSI.reset}`);
562
+ ws.sendLine(` ${ANSI.darkGold}Chain${ANSI.reset} ${ANSI.white}${chain}${ANSI.reset}`);
563
+ ws.sendLine(` ${ANSI.darkGold}Created${ANSI.reset} ${ANSI.dim}${walletData.createdAt ? new Date(walletData.createdAt).toLocaleDateString() : 'unknown'}${ANSI.reset}`);
564
+ ws.sendLine(` ${ANSI.darkGold}Encryption${ANSI.reset} ${ANSI.dim}AES-256-GCM + scrypt${ANSI.reset}`);
565
+
566
+ // Fetch balance
567
+ ws.sendLine('');
568
+ ws.sendLine(` ${ANSI.dim}Fetching balance...${ANSI.reset}`);
569
+
570
+ try {
571
+ const balance = await provider.getBalance(walletData.address);
572
+ const ethBal = parseFloat(ethers.formatEther(balance));
573
+
574
+ // Also check USDC
575
+ const usdcAddr = USDC_ADDRESSES[chain];
576
+ let usdcBal = 0;
577
+ if (usdcAddr) {
578
+ try {
579
+ const usdc = new ethers.Contract(usdcAddr, ['function balanceOf(address) view returns (uint256)'], provider);
580
+ const raw = await usdc.balanceOf(walletData.address);
581
+ usdcBal = parseFloat(ethers.formatUnits(raw, 6));
582
+ } catch {}
583
+ }
584
+
585
+ // Get ETH price for USD value
586
+ const ethPrice = await getEthPrice();
587
+ const usdValue = (ethBal * ethPrice) + usdcBal;
588
+
589
+ // Overwrite "Fetching..." line
590
+ ws.sendLine(`\x1b[1A\x1b[2K ${ANSI.darkGold}ETH${ANSI.reset} ${ANSI.white}${ethBal.toFixed(6)}${ANSI.reset} ${ANSI.dim}($${(ethBal * ethPrice).toFixed(2)})${ANSI.reset}`);
591
+ ws.sendLine(` ${ANSI.darkGold}USDC${ANSI.reset} ${ANSI.white}$${usdcBal.toFixed(2)}${ANSI.reset}`);
592
+ ws.sendLine(` ${ANSI.darkGold}Total${ANSI.reset} ${ANSI.green}$${usdValue.toFixed(2)}${ANSI.reset}`);
593
+ } catch (err) {
594
+ ws.sendLine(` ${ANSI.red}Balance fetch failed: ${err.message}${ANSI.reset}`);
595
+ }
596
+
597
+ ws.sendLine('');
598
+
599
+ // Show action menu
600
+ ws.sendMenu('wallet_action', '◆ What would you like to do?', [
601
+ { value: 'receive', label: '📥 Receive', desc: 'Show address to receive funds' },
602
+ { value: 'send', label: '📤 Send', desc: 'Send ETH or tokens (CLI required)' },
603
+ { value: 'portfolio', label: '📊 Portfolio', desc: 'Multi-chain balance view' },
604
+ { value: 'history', label: '📜 History', desc: 'Transaction history' },
605
+ { value: 'switch', label: '🔄 Switch chain', desc: `Currently: ${chain}` },
606
+ { value: 'back', label: '← Back', desc: '' },
607
+ ]);
608
+
609
+ return {};
446
610
  }
447
611
 
448
612
  // ══════════════════════════════════════════════════
@@ -581,8 +745,16 @@ async function cmdConfig(ws) {
581
745
  ws.sendLine(` ${ANSI.darkGold}Wallet${ANSI.reset} ${ANSI.white}${wallet}${ANSI.reset}`);
582
746
  ws.sendLine(` ${ANSI.darkGold}Slippage${ANSI.reset} ${ANSI.white}${slippage}%${ANSI.reset}`);
583
747
  ws.sendLine(` ${ANSI.darkGold}Mail${ANSI.reset} ${ANSI.white}${email}${ANSI.reset}`);
584
- ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${hasKey('openai') || hasKey('anthropic') || hasKey('openrouter') || hasKey('ollama') ? `${ANSI.green}● Ready${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
748
+ ws.sendLine(` ${ANSI.darkGold}AI${ANSI.reset} ${hasAnyLLM() ? `${ANSI.green}● Ready${ANSI.reset}` : `${ANSI.dim}○ Not configured${ANSI.reset}`}`);
585
749
  ws.sendLine('');
750
+
751
+ // Offer interactive config
752
+ ws.sendMenu('config_action', '◆ Configure', [
753
+ { value: 'chain', label: '🔗 Change chain', desc: `Currently: ${chain}` },
754
+ { value: 'keys', label: '🔑 LLM / API keys', desc: '' },
755
+ { value: 'back', label: '← Back', desc: '' },
756
+ ]);
757
+
586
758
  return {};
587
759
  }
588
760
 
package/src/web/server.js CHANGED
@@ -100,6 +100,27 @@ export async function startWebShell(opts = {}) {
100
100
  try {
101
101
  const msg = JSON.parse(raw.toString());
102
102
 
103
+ if (msg.type === 'menu_select') {
104
+ // User selected something from an interactive menu
105
+ try {
106
+ const { handleMenuSelect } = await import('./commands.js');
107
+ const result = await handleMenuSelect(msg.id, msg.value, msg.item, {
108
+ send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
109
+ sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
110
+ sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
111
+ });
112
+ if (result?.output) {
113
+ ws.send(JSON.stringify({ type: 'output', data: result.output }));
114
+ }
115
+ } catch (err) {
116
+ ws.send(JSON.stringify({
117
+ type: 'output',
118
+ data: `\r\n \x1b[31m✗ Error: ${err.message}\x1b[0m\r\n`,
119
+ }));
120
+ }
121
+ return;
122
+ }
123
+
103
124
  if (msg.type === 'command') {
104
125
  const cmd = msg.data.trim();
105
126
 
@@ -133,6 +154,7 @@ export async function startWebShell(opts = {}) {
133
154
  const result = await handleCommand(cmd, {
134
155
  send: (text) => ws.send(JSON.stringify({ type: 'output', data: text })),
135
156
  sendLine: (text) => ws.send(JSON.stringify({ type: 'output', data: text + '\r\n' })),
157
+ sendMenu: (id, title, items) => ws.send(JSON.stringify({ type: 'menu', id, title, items })),
136
158
  });
137
159
 
138
160
  if (result?.output) {
@@ -4,11 +4,20 @@
4
4
 
5
5
  const PROMPT = '\x1b[38;2;255;215;0m❯\x1b[0m ';
6
6
  const PROMPT_LENGTH = 2; // visible chars: ❯ + space
7
+ const A = {
8
+ gold: '\x1b[38;2;255;215;0m',
9
+ dim: '\x1b[38;2;102;102;102m',
10
+ green: '\x1b[38;2;0;255;136m',
11
+ red: '\x1b[38;2;233;69;96m',
12
+ white: '\x1b[1;37m',
13
+ blue: '\x1b[38;2;68;136;255m',
14
+ r: '\x1b[0m',
15
+ };
7
16
 
8
17
  const COMMANDS = [
9
- 'price', 'watch', 'gas', 'portfolio', 'history', 'market',
10
- 'wallet', 'mail', 'oracle', 'casino', 'facilitator', 'config',
11
- 'help', 'clear', 'banner', 'exit',
18
+ 'ai', 'price', 'watch', 'gas', 'portfolio', 'history', 'market',
19
+ 'wallet', 'send', 'receive', 'mail', 'keys', 'oracle', 'casino',
20
+ 'facilitator', 'config', 'logs', 'help', 'clear', 'banner', 'exit',
12
21
  ];
13
22
 
14
23
  let term;
@@ -18,8 +27,14 @@ let commandHistory = [];
18
27
  let historyIndex = -1;
19
28
  let connected = false;
20
29
 
30
+ // ── MENU STATE ────────────────────────────────
31
+ let menuActive = false;
32
+ let menuItems = [];
33
+ let menuIndex = 0;
34
+ let menuId = '';
35
+ let menuTitle = '';
36
+
21
37
  async function init() {
22
- // Load xterm.js
23
38
  term = new Terminal({
24
39
  theme: {
25
40
  background: '#0a0a1a',
@@ -64,16 +79,42 @@ async function init() {
64
79
 
65
80
  window.addEventListener('resize', () => fitAddon.fit());
66
81
 
67
- // Connect WebSocket
68
82
  connectWS();
69
83
 
70
- // Input handling
71
84
  term.onKey(({ key, domEvent }) => {
72
85
  const code = domEvent.keyCode;
73
86
  const ctrl = domEvent.ctrlKey;
74
87
 
88
+ // ── MENU MODE ──
89
+ if (menuActive) {
90
+ if (code === 38) { // Up
91
+ if (menuIndex > 0) { menuIndex--; renderMenu(); }
92
+ return;
93
+ }
94
+ if (code === 40) { // Down
95
+ if (menuIndex < menuItems.length - 1) { menuIndex++; renderMenu(); }
96
+ return;
97
+ }
98
+ if (code === 13) { // Enter — select
99
+ const selected = menuItems[menuIndex];
100
+ menuActive = false;
101
+ // Clear menu display
102
+ term.write('\x1b[?25h'); // show cursor
103
+ sendMenuSelection(menuId, selected);
104
+ return;
105
+ }
106
+ if (code === 27 || (ctrl && code === 67)) { // Esc or Ctrl+C — cancel
107
+ menuActive = false;
108
+ term.write('\r\n');
109
+ term.write(` ${A.dim}Cancelled${A.r}\r\n\r\n`);
110
+ writePrompt();
111
+ return;
112
+ }
113
+ return; // Ignore other keys in menu mode
114
+ }
115
+
116
+ // ── NORMAL MODE ──
75
117
  if (ctrl && code === 67) {
76
- // Ctrl+C — cancel
77
118
  currentLine = '';
78
119
  term.write('^C\r\n');
79
120
  writePrompt();
@@ -81,14 +122,12 @@ async function init() {
81
122
  }
82
123
 
83
124
  if (ctrl && code === 76) {
84
- // Ctrl+L — clear
85
125
  term.clear();
86
126
  writePrompt();
87
127
  return;
88
128
  }
89
129
 
90
130
  if (code === 13) {
91
- // Enter
92
131
  term.write('\r\n');
93
132
  const cmd = currentLine.trim();
94
133
 
@@ -106,7 +145,6 @@ async function init() {
106
145
  }
107
146
 
108
147
  if (code === 8) {
109
- // Backspace
110
148
  if (currentLine.length > 0) {
111
149
  currentLine = currentLine.slice(0, -1);
112
150
  term.write('\b \b');
@@ -115,7 +153,6 @@ async function init() {
115
153
  }
116
154
 
117
155
  if (code === 38) {
118
- // Arrow up — history
119
156
  if (historyIndex < commandHistory.length - 1) {
120
157
  historyIndex++;
121
158
  replaceInput(commandHistory[historyIndex]);
@@ -124,7 +161,6 @@ async function init() {
124
161
  }
125
162
 
126
163
  if (code === 40) {
127
- // Arrow down — history
128
164
  if (historyIndex > 0) {
129
165
  historyIndex--;
130
166
  replaceInput(commandHistory[historyIndex]);
@@ -136,13 +172,11 @@ async function init() {
136
172
  }
137
173
 
138
174
  if (code === 9) {
139
- // Tab — autocomplete
140
175
  domEvent.preventDefault();
141
176
  autocomplete();
142
177
  return;
143
178
  }
144
179
 
145
- // Printable characters
146
180
  if (key.length === 1 && !ctrl) {
147
181
  currentLine += key;
148
182
  term.write(key);
@@ -151,7 +185,7 @@ async function init() {
151
185
 
152
186
  // Paste support
153
187
  term.onData((data) => {
154
- // Only handle paste (multi-char input)
188
+ if (menuActive) return;
155
189
  if (data.length > 1 && !data.startsWith('\x1b')) {
156
190
  const clean = data.replace(/[\r\n]/g, '');
157
191
  currentLine += clean;
@@ -162,6 +196,67 @@ async function init() {
162
196
  term.focus();
163
197
  }
164
198
 
199
+ // ── MENU RENDERING ────────────────────────────
200
+ function renderMenu() {
201
+ // Move cursor up to redraw (clear previous menu)
202
+ const totalLines = menuItems.length + 2; // title + items + hint
203
+ term.write(`\x1b[${totalLines}A\x1b[J`);
204
+
205
+ term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
206
+
207
+ for (let i = 0; i < menuItems.length; i++) {
208
+ const item = menuItems[i];
209
+ const label = item.label || item.value || String(item);
210
+ const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
211
+
212
+ if (i === menuIndex) {
213
+ term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
214
+ } else {
215
+ term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
216
+ }
217
+ }
218
+
219
+ term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
220
+ }
221
+
222
+ function showMenu(id, title, items) {
223
+ menuActive = true;
224
+ menuId = id;
225
+ menuTitle = title;
226
+ menuItems = items;
227
+ menuIndex = 0;
228
+
229
+ term.write('\x1b[?25l'); // hide cursor during menu
230
+ term.write('\r\n');
231
+
232
+ // Initial render
233
+ term.write(` ${A.gold}${menuTitle}${A.r}\r\n`);
234
+ for (let i = 0; i < menuItems.length; i++) {
235
+ const item = menuItems[i];
236
+ const label = item.label || item.value || String(item);
237
+ const desc = item.desc ? ` ${A.dim}${item.desc}${A.r}` : '';
238
+
239
+ if (i === menuIndex) {
240
+ term.write(` ${A.gold}► ${A.white}${label}${A.r}${desc}\r\n`);
241
+ } else {
242
+ term.write(` ${A.dim}${label}${A.r}${desc}\r\n`);
243
+ }
244
+ }
245
+ term.write(` ${A.dim}↑/↓ navigate • Enter select • Esc cancel${A.r}\r\n`);
246
+ }
247
+
248
+ function sendMenuSelection(id, item) {
249
+ if (ws && ws.readyState === WebSocket.OPEN) {
250
+ ws.send(JSON.stringify({
251
+ type: 'menu_select',
252
+ id,
253
+ value: item.value || item.label || String(item),
254
+ item,
255
+ }));
256
+ }
257
+ }
258
+
259
+ // ── WEBSOCKET ─────────────────────────────────
165
260
  function connectWS() {
166
261
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
167
262
  ws = new WebSocket(`${protocol}//${window.location.host}`);
@@ -177,13 +272,15 @@ function connectWS() {
177
272
 
178
273
  if (msg.type === 'output') {
179
274
  term.write(msg.data);
180
- writePrompt();
275
+ if (!menuActive) writePrompt();
181
276
  } else if (msg.type === 'clear') {
182
277
  term.clear();
183
278
  writePrompt();
279
+ } else if (msg.type === 'menu') {
280
+ // Server is requesting user to pick from a menu
281
+ showMenu(msg.id, msg.title, msg.items);
184
282
  }
185
283
  } catch {
186
- // Raw text fallback
187
284
  term.write(event.data);
188
285
  }
189
286
  };
@@ -191,12 +288,8 @@ function connectWS() {
191
288
  ws.onclose = () => {
192
289
  connected = false;
193
290
  updateStatus(false);
194
- term.write('\r\n\x1b[38;2;233;69;96m ⚡ Connection lost. Reconnecting...\x1b[0m\r\n');
195
-
196
- // Reconnect after 3s
197
- setTimeout(() => {
198
- connectWS();
199
- }, 3000);
291
+ term.write(`\r\n${A.red} ⚡ Connection lost. Reconnecting...${A.r}\r\n`);
292
+ setTimeout(() => connectWS(), 3000);
200
293
  };
201
294
 
202
295
  ws.onerror = () => {
@@ -209,7 +302,7 @@ function sendCommand(cmd) {
209
302
  if (ws && ws.readyState === WebSocket.OPEN) {
210
303
  ws.send(JSON.stringify({ type: 'command', data: cmd }));
211
304
  } else {
212
- term.write('\x1b[38;2;233;69;96m ✗ Not connected\x1b[0m\r\n');
305
+ term.write(`${A.red} ✗ Not connected${A.r}\r\n`);
213
306
  writePrompt();
214
307
  }
215
308
  }
@@ -219,28 +312,20 @@ function writePrompt() {
219
312
  }
220
313
 
221
314
  function replaceInput(text) {
222
- // Clear current input
223
315
  const clearLen = currentLine.length;
224
- for (let i = 0; i < clearLen; i++) {
225
- term.write('\b \b');
226
- }
316
+ for (let i = 0; i < clearLen; i++) term.write('\b \b');
227
317
  currentLine = text;
228
318
  term.write(text);
229
319
  }
230
320
 
231
321
  function autocomplete() {
232
322
  if (!currentLine) return;
233
-
234
323
  const matches = COMMANDS.filter((c) => c.startsWith(currentLine.toLowerCase()));
235
-
236
324
  if (matches.length === 1) {
237
325
  replaceInput(matches[0]);
238
326
  } else if (matches.length > 1) {
239
- // Show options
240
327
  term.write('\r\n');
241
- term.write(
242
- matches.map((m) => ` \x1b[38;2;102;102;102m${m}\x1b[0m`).join(' ')
243
- );
328
+ term.write(matches.map((m) => ` ${A.dim}${m}${A.r}`).join(' '));
244
329
  term.write('\r\n');
245
330
  writePrompt();
246
331
  term.write(currentLine);
@@ -250,13 +335,8 @@ function autocomplete() {
250
335
  function updateStatus(isConnected) {
251
336
  const dot = document.querySelector('.status-dot');
252
337
  const text = document.querySelector('.status-text');
253
- if (dot) {
254
- dot.className = isConnected ? 'status-dot' : 'status-dot disconnected';
255
- }
256
- if (text) {
257
- text.textContent = isConnected ? 'Connected' : 'Disconnected';
258
- }
338
+ if (dot) dot.className = isConnected ? 'status-dot' : 'status-dot disconnected';
339
+ if (text) text.textContent = isConnected ? 'Connected' : 'Disconnected';
259
340
  }
260
341
 
261
- // Boot
262
342
  document.addEventListener('DOMContentLoaded', init);