airloom 0.1.13 → 0.1.16

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/dist/index.js CHANGED
@@ -600,16 +600,15 @@ var AblyAdapter = class {
600
600
 
601
601
  // src/index.ts
602
602
  import { sha256 as sha2562 } from "@noble/hashes/sha256";
603
- import { networkInterfaces } from "os";
604
- import { fileURLToPath } from "url";
605
- import { dirname, resolve as resolve3 } from "path";
606
- import { existsSync as existsSync4 } from "fs";
607
- import QRCode from "qrcode";
603
+ import { networkInterfaces } from "node:os";
604
+ import { fileURLToPath } from "node:url";
605
+ import { dirname, resolve as resolve3 } from "node:path";
606
+ import { existsSync as existsSync4 } from "node:fs";
608
607
 
609
608
  // src/server.ts
610
609
  import express from "express";
611
- import { createServer } from "http";
612
- import { existsSync as existsSync3 } from "fs";
610
+ import { createServer } from "node:http";
611
+ import { existsSync as existsSync3 } from "node:fs";
613
612
  import { WebSocketServer, WebSocket } from "ws";
614
613
 
615
614
  // src/adapters/anthropic.ts
@@ -742,9 +741,9 @@ var OpenAIAdapter = class {
742
741
  };
743
742
 
744
743
  // src/adapters/cli.ts
745
- import { spawn } from "child_process";
746
- import { existsSync } from "fs";
747
- import { delimiter, isAbsolute, join, resolve } from "path";
744
+ import { spawn } from "node:child_process";
745
+ import { existsSync } from "node:fs";
746
+ import { delimiter, isAbsolute, join, resolve } from "node:path";
748
747
  var CLI_PRESETS = [
749
748
  {
750
749
  id: "devin",
@@ -998,9 +997,9 @@ var CLIAdapter = class {
998
997
  };
999
998
 
1000
999
  // src/config.ts
1001
- import { readFileSync, writeFileSync, mkdirSync } from "fs";
1002
- import { homedir } from "os";
1003
- import { join as join2 } from "path";
1000
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
1001
+ import { homedir } from "node:os";
1002
+ import { join as join2 } from "node:path";
1004
1003
  var CONFIG_DIR = join2(homedir(), ".config", "airloom");
1005
1004
  var CONFIG_PATH = join2(CONFIG_DIR, "config.json");
1006
1005
  function loadConfig() {
@@ -1026,8 +1025,8 @@ function getConfigPath() {
1026
1025
  }
1027
1026
 
1028
1027
  // src/terminal.ts
1029
- import { basename, delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "path";
1030
- import { existsSync as existsSync2 } from "fs";
1028
+ import { basename, delimiter as delimiter2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
1029
+ import { existsSync as existsSync2 } from "node:fs";
1031
1030
  import { spawn as spawn2 } from "node-pty";
1032
1031
  function resolveExecutable2(command, envPath = process.env.PATH ?? "") {
1033
1032
  if (!command) return null;
@@ -1113,9 +1112,10 @@ function getTerminalLaunchDisplay(explicitCommand) {
1113
1112
  return [command.file, ...command.args].join(" ");
1114
1113
  }
1115
1114
  var TerminalSession = class {
1116
- constructor(channel, getLaunchCommand) {
1115
+ constructor(channel, getLaunchCommand, broadcastFn) {
1117
1116
  this.channel = channel;
1118
1117
  this.getLaunchCommand = getLaunchCommand;
1118
+ this.broadcastFn = broadcastFn;
1119
1119
  }
1120
1120
  pty = null;
1121
1121
  stream = null;
@@ -1156,13 +1156,23 @@ var TerminalSession = class {
1156
1156
  this.rows = Math.max(5, Math.floor(message.rows || this.rows));
1157
1157
  if (this.pty) {
1158
1158
  this.pty.resize(this.cols, this.rows);
1159
+ if (!this.stream || this.stream.ended) {
1160
+ const meta2 = { kind: "terminal", cols: this.cols, rows: this.rows };
1161
+ this.stream = this.channel.createStream(meta2);
1162
+ this.batcher = new AdaptiveOutputBatcher((data) => {
1163
+ this.stream?.write(data);
1164
+ });
1165
+ this.pty.write("\f");
1166
+ }
1159
1167
  return;
1160
1168
  }
1161
1169
  const command = getDefaultTerminalCommand(this.getLaunchCommand?.());
1162
1170
  const file = resolveExecutable2(command.file) ?? command.file;
1163
1171
  const meta = { kind: "terminal", cols: this.cols, rows: this.rows };
1164
1172
  this.stream = this.channel.createStream(meta);
1165
- this.batcher = new AdaptiveOutputBatcher((data) => this.stream?.write(data));
1173
+ this.batcher = new AdaptiveOutputBatcher((data) => {
1174
+ this.stream?.write(data);
1175
+ });
1166
1176
  const env = { ...process.env, TERM: "xterm-256color" };
1167
1177
  this.pty = spawn2(file, command.args, {
1168
1178
  name: "xterm-256color",
@@ -1171,7 +1181,11 @@ var TerminalSession = class {
1171
1181
  cwd: process.cwd(),
1172
1182
  env
1173
1183
  });
1174
- this.pty.onData((data) => this.batcher?.write(data));
1184
+ this.pty.onData((data) => {
1185
+ process.stdout.write(data);
1186
+ this.batcher?.write(data);
1187
+ this.broadcastFn?.({ type: "terminal_output", data });
1188
+ });
1175
1189
  this.pty.onExit(({ exitCode, signal }) => {
1176
1190
  this.batcher?.flush();
1177
1191
  this.stream?.end();
@@ -1186,6 +1200,11 @@ var TerminalSession = class {
1186
1200
  this.batcher?.noteInput();
1187
1201
  this.pty.write(message.data);
1188
1202
  }
1203
+ writeRawInput(data) {
1204
+ if (!this.pty) return;
1205
+ this.batcher?.noteInput();
1206
+ this.pty.write(data);
1207
+ }
1189
1208
  resize(message) {
1190
1209
  this.cols = Math.max(20, Math.floor(message.cols || this.cols));
1191
1210
  this.rows = Math.max(5, Math.floor(message.rows || this.rows));
@@ -1310,16 +1329,30 @@ function createHostServer(opts) {
1310
1329
  }
1311
1330
  res.json({ ok: true });
1312
1331
  });
1332
+ app.get("/api/pair", (req, res) => {
1333
+ const { session } = req.query;
1334
+ if (!session || session !== opts.state.sessionToken) {
1335
+ res.status(401).json({ error: "Invalid session" });
1336
+ return;
1337
+ }
1338
+ res.json({
1339
+ token: opts.state.ablyToken,
1340
+ transport: opts.state.transport ?? "ws",
1341
+ relay: opts.state.relayUrl
1342
+ });
1343
+ });
1313
1344
  wss.on("connection", (ws) => {
1314
1345
  uiClients.add(ws);
1315
1346
  ws.on("close", () => uiClients.delete(ws));
1316
1347
  ws.on("message", (data) => {
1317
1348
  try {
1318
1349
  const message = JSON.parse(data.toString());
1319
- if (message.type && message.type.startsWith("terminal_")) {
1320
- if (opts.state.channel) {
1321
- opts.state.channel.send(message);
1322
- }
1350
+ if (message.type === "terminal_input" && typeof message.data === "string") {
1351
+ const filtered = message.data.replace(/\x1b\]\d+;[^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
1352
+ if (!filtered) return;
1353
+ opts.state.terminal?.writeRawInput(filtered);
1354
+ } else if (message.type === "terminal_resize") {
1355
+ opts.state.terminal?.handleMessage(message);
1323
1356
  }
1324
1357
  } catch (err) {
1325
1358
  console.error("[host] Invalid WebSocket message:", err);
@@ -1389,6 +1422,7 @@ var HOST_HTML = `<!DOCTYPE html>
1389
1422
  <meta charset="UTF-8">
1390
1423
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1391
1424
  <title>Airloom - Host</title>
1425
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.css">
1392
1426
  <style>
1393
1427
  *{margin:0;padding:0;box-sizing:border-box}
1394
1428
  body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0a;color:#e0e0e0;min-height:100vh}
@@ -1411,39 +1445,14 @@ var HOST_HTML = `<!DOCTYPE html>
1411
1445
  button{background:#7c8aff;color:#fff;border:none;cursor:pointer;font-weight:600}
1412
1446
  button:hover{background:#6b79ee}
1413
1447
  .messages{max-height:400px;overflow-y:auto;display:flex;flex-direction:column;gap:8px;margin-bottom:12px}
1414
- .msg{padding:10px 14px;border-radius:10px;max-width:85%;word-break:break-word;font-size:.9rem;line-height:1.5}
1415
- .msg.user{background:#2a3a6a;align-self:flex-end;white-space:pre-wrap}
1448
+ .msg{padding:10px 14px;border-radius:10px;max-width:85%;word-break:break-word;font-size:.9rem;line-height:1.5;white-space:pre-wrap}
1449
+ .msg.user{background:#2a3a6a;align-self:flex-end}
1416
1450
  .msg.assistant{background:#1e1e1e;border:1px solid #2a2a2a;align-self:flex-start}
1417
- .msg.assistant p{margin:0 0 .5em}.msg.assistant p:last-child{margin-bottom:0}
1418
- .msg.assistant h1,.msg.assistant h2,.msg.assistant h3,.msg.assistant h4,.msg.assistant h5,.msg.assistant h6{font-size:.95rem;font-weight:600;margin:.6em 0 .3em;color:#e0e0e0}
1419
- .msg.assistant h1{font-size:1.05rem}.msg.assistant h2{font-size:1rem}
1420
- .msg.assistant ul,.msg.assistant ol{margin:.3em 0;padding-left:1.4em}.msg.assistant li{margin:.15em 0}
1421
- .msg.assistant a{color:#7c8aff;text-decoration:underline}
1422
- .msg.assistant code{font-family:'SF Mono',Menlo,Consolas,monospace;font-size:.82rem;background:rgba(255,255,255,.07);padding:1px 5px;border-radius:4px}
1423
- .msg.assistant pre{margin:.5em 0;padding:10px 12px;border-radius:8px;background:#111;overflow-x:auto}
1424
- .msg.assistant pre code{background:none;padding:0;font-size:.8rem;line-height:1.5;white-space:pre;display:block}
1425
- .msg.assistant blockquote{margin:.4em 0;padding:4px 12px;border-left:3px solid #2a2a2a;color:#888}
1426
- .msg.assistant table{border-collapse:collapse;margin:.4em 0;font-size:.82rem}
1427
- .msg.assistant th,.msg.assistant td{border:1px solid #2a2a2a;padding:4px 8px}
1428
- .msg.assistant th{background:rgba(255,255,255,.05)}
1429
- .msg.assistant hr{border:none;border-top:1px solid #2a2a2a;margin:.5em 0}
1430
- .msg.typing{display:flex;align-items:center;gap:5px;padding:14px 18px}
1431
- .msg.typing .dot{width:7px;height:7px;border-radius:50%;background:#888;animation:dot-pulse 1.4s ease-in-out infinite}
1432
- .msg.typing .dot:nth-child(2){animation-delay:.2s}
1433
- .msg.typing .dot:nth-child(3){animation-delay:.4s}
1434
- @keyframes dot-pulse{0%,80%,100%{opacity:.25;transform:scale(.8)}40%{opacity:1;transform:scale(1)}}
1435
1451
  .input-area{display:flex;gap:8px}
1436
1452
  .input-area textarea{flex:1;resize:none;min-height:44px;max-height:120px;padding:10px 14px;border-radius:8px;border:1px solid #333;background:#111;color:#e0e0e0;font-family:inherit;font-size:.9rem}
1437
-
1438
- /* Terminal mode styles */
1439
- .terminal-mode .chat-only { display: none !important; }
1440
- .terminal-mode .terminal-only { display: block; }
1441
- .chat-mode .terminal-only { display: none !important; }
1442
- .chat-mode .chat-only { display: block; }
1443
-
1444
- .terminal-container{background:#05070c;border:1px solid #2a2a2a;border-radius:8px;height:400px;overflow:hidden;margin-bottom:12px}
1453
+ .terminal-container{background:#05070c;border:1px solid #2a2a2a;border-radius:8px;height:420px;overflow:hidden;margin-bottom:12px}
1445
1454
  #terminal{width:100%;height:100%;padding:8px}
1446
- .terminal-toolbar{display:flex;gap:8px;margin-bottom:12px}
1455
+ .toolbar{display:flex;gap:8px;margin-bottom:12px}
1447
1456
  .tool-btn{padding:6px 12px;font-size:.85rem;background:#333;border:none;border-radius:6px;color:#e0e0e0;cursor:pointer}
1448
1457
  .tool-btn:hover{background:#444}
1449
1458
  </style>
@@ -1458,360 +1467,251 @@ var HOST_HTML = `<!DOCTYPE html>
1458
1467
  <div class="status"><div class="dot wait" id="dot"></div><span id="statusText">Initializing...</span></div>
1459
1468
  <p style="color:#888;font-size:.9rem;margin-top:8px" id="launchText">Launch: current shell</p>
1460
1469
  </div>
1461
-
1462
- <!-- Chat mode UI -->
1463
- <div id="chatMode" class="chat-mode">
1464
- <div class="card" id="configCard">
1465
- <h2>AI Configuration</h2>
1466
- <div class="config-form">
1467
- <select id="adapterType">
1468
- <option value="anthropic">Anthropic Claude</option>
1469
- <option value="openai">OpenAI GPT</option>
1470
- <option value="cli">CLI Adapter</option>
1471
- </select>
1472
- <input type="text" id="model" placeholder="Model (optional)" />
1473
- <input type="text" id="apiKey" placeholder="API Key (if not set in environment)" />
1474
- <button onclick="configureAdapter()">Configure Adapter</button>
1475
- </div>
1476
- </div>
1477
-
1478
- <div class="card chat-only" id="chatCard" style="display:none">
1479
- <h2>Chat Messages</h2>
1480
- <div class="messages" id="messages"></div>
1481
- <div class="input-area chat-only">
1482
- <textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
1483
- <button onclick="sendMessage()">Send</button>
1484
- </div>
1485
- </div>
1470
+
1471
+ <div class="card pairing" id="pairingCard" style="display:none">
1472
+ <h2>Connect Your Phone</h2>
1473
+ <img id="qrCode" alt="QR Code"/>
1474
+ <div class="pairing-code" id="pairingCode"></div>
1475
+ <p style="color:#888;font-size:.85rem">Scan QR or enter code in viewer</p>
1486
1476
  </div>
1487
-
1488
- <!-- Terminal mode UI -->
1489
- <div id="terminalMode" class="terminal-mode" style="display:none">
1477
+
1478
+ <!-- Terminal mode: shell, Devin, Codex etc. (default when no AI adapter) -->
1479
+ <div id="terminalSection" style="display:none">
1490
1480
  <div class="card">
1491
- <h2>Terminal Configuration</h2>
1481
+ <h2>Launch Configuration</h2>
1492
1482
  <div class="config-form">
1493
1483
  <select id="cliPreset"></select>
1494
1484
  <input type="text" id="command" placeholder="Custom launch command" style="display:none"/>
1495
1485
  <p style="color:#666;font-size:.8rem;margin-top:4px" id="presetDesc"></p>
1496
- <button onclick="configureTerminal()">Apply Launch Target</button>
1486
+ <button id="applyLaunchBtn">Apply Launch Target</button>
1497
1487
  </div>
1498
1488
  </div>
1499
-
1500
- <div class="card terminal-only">
1501
- <h2>Terminal Session</h2>
1502
- <div class="terminal-toolbar terminal-only">
1503
- <button class="tool-btn" onclick="focusTerminal()">Focus</button>
1504
- <button class="tool-btn" onclick="sendCtrlC()">Ctrl+C</button>
1505
- <button class="tool-btn" onclick="sendEsc()">Esc</button>
1506
- <button class="tool-btn" onclick="sendTab()">Tab</button>
1489
+ <div class="card">
1490
+ <h2>Terminal</h2>
1491
+ <div class="toolbar">
1492
+ <button class="tool-btn" id="focusTermBtn">Focus</button>
1493
+ <button class="tool-btn" id="ctrlCBtn">Ctrl+C</button>
1494
+ <button class="tool-btn" id="escBtn">Esc</button>
1495
+ <button class="tool-btn" id="tabBtn">Tab</button>
1507
1496
  </div>
1508
- <div class="terminal-container terminal-only" id="terminalContainer">
1497
+ <div class="terminal-container" id="terminalContainer">
1509
1498
  <div id="terminal"></div>
1510
1499
  </div>
1511
1500
  </div>
1512
1501
  </div>
1513
-
1514
- <div class="card pairing" id="pairingCard" style="display:none">
1515
- <h2>Connect Your Phone</h2>
1516
- <img id="qrCode" alt="QR Code"/>
1517
- <div class="pairing-code" id="pairingCode"></div>
1518
- <p style="color:#888;font-size:.85rem">Scan QR or enter code in viewer</p>
1502
+
1503
+ <!-- Chat mode: direct LLM connections (Anthropic, OpenAI) -->
1504
+ <div id="chatSection" style="display:none">
1505
+ <div class="card">
1506
+ <h2>Chat</h2>
1507
+ <div class="messages" id="messages"></div>
1508
+ <div class="input-area">
1509
+ <textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
1510
+ <button id="sendBtn">Send</button>
1511
+ </div>
1512
+ </div>
1519
1513
  </div>
1514
+
1520
1515
  </div>
1521
- <script>
1522
- // Import xterm dynamically to avoid bundling issues
1523
- const loadTerminal = async () => {
1524
- if (window.Terminal) return;
1525
- const [{ Terminal }, { FitAddon }] = await Promise.all([
1526
- import('@xterm/xterm'),
1527
- import('@xterm/addon-fit')
1528
- ]);
1529
- window.Terminal = Terminal;
1530
- window.FitAddon = FitAddon;
1531
- };
1516
+ <script type="module">
1517
+ import { Terminal } from 'https://esm.sh/@xterm/xterm@6';
1518
+ import { FitAddon } from 'https://esm.sh/@xterm/addon-fit@0.11';
1532
1519
 
1533
- const ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
1534
- let cliPresets=[];
1535
- let currentMode = null; // 'chat' or 'terminal'
1520
+ const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
1536
1521
  let term = null;
1537
1522
  let fitAddon = null;
1523
+ let termInitialized = false;
1524
+ let cliPresets = [];
1538
1525
 
1539
- // Mode detection and UI switching
1540
- function switchToMode(mode) {
1541
- if (currentMode === mode) return;
1542
- currentMode = mode;
1543
-
1544
- const chatMode = document.getElementById('chatMode');
1545
- const terminalMode = document.getElementById('terminalMode');
1546
-
1547
- if (mode === 'terminal') {
1548
- chatMode.style.display = 'none';
1549
- terminalMode.style.display = 'block';
1550
- document.body.classList.add('terminal-mode');
1551
- document.body.classList.remove('chat-mode');
1552
- initTerminal();
1553
- } else {
1554
- chatMode.style.display = 'block';
1555
- terminalMode.style.display = 'none';
1556
- document.body.classList.add('chat-mode');
1557
- document.body.classList.remove('terminal-mode');
1558
- }
1559
- }
1560
-
1561
- // Terminal functionality
1562
- async function initTerminal() {
1563
- if (term) return;
1564
- await loadTerminal();
1565
-
1566
- term = new window.Terminal({
1526
+ function initTerminal() {
1527
+ if (termInitialized) return;
1528
+ termInitialized = true;
1529
+ term = new Terminal({
1567
1530
  cursorBlink: true,
1568
- fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
1531
+ fontFamily: 'ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace',
1569
1532
  fontSize: 14,
1570
1533
  lineHeight: 1.25,
1571
- allowTransparency: true,
1572
1534
  scrollback: 5000,
1573
1535
  theme: {
1574
- background: '#05070c',
1575
- foreground: '#e6edf3',
1576
- cursor: '#7c8aff',
1577
- cursorAccent: '#05070c',
1536
+ background: '#05070c', foreground: '#e6edf3', cursor: '#7c8aff', cursorAccent: '#05070c',
1578
1537
  selectionBackground: 'rgba(124,138,255,0.28)',
1579
- black: '#0a0d14',
1580
- red: '#ff7b72',
1581
- green: '#3fb950',
1582
- yellow: '#d29922',
1583
- blue: '#7c8aff',
1584
- magenta: '#bc8cff',
1585
- cyan: '#39c5cf',
1586
- white: '#c9d1d9',
1587
- brightBlack: '#6e7681',
1588
- brightRed: '#ffa198',
1589
- brightGreen: '#56d364',
1590
- brightYellow: '#e3b341',
1591
- brightBlue: '#a5b4ff',
1592
- brightMagenta: '#d2a8ff',
1593
- brightCyan: '#56d4dd',
1594
- brightWhite: '#f0f6fc',
1538
+ black: '#0a0d14', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
1539
+ blue: '#7c8aff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#c9d1d9',
1540
+ brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
1541
+ brightBlue: '#a5b4ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
1595
1542
  },
1596
1543
  });
1597
-
1598
- fitAddon = new window.FitAddon();
1544
+ fitAddon = new FitAddon();
1599
1545
  term.loadAddon(fitAddon);
1600
1546
  term.open(document.getElementById('terminal'));
1601
-
1602
1547
  term.onData((data) => {
1603
- if (ws.readyState === WebSocket.OPEN) {
1604
- ws.send(JSON.stringify({ type: 'terminal_input', data }));
1605
- }
1548
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data }));
1606
1549
  });
1607
-
1608
- // Handle terminal resize
1609
- const resizeObserver = new ResizeObserver(() => {
1610
- if (fitAddon && term) {
1611
- fitAddon.fit();
1612
- if (ws.readyState === WebSocket.OPEN) {
1613
- ws.send(JSON.stringify({
1614
- type: 'terminal_resize',
1615
- cols: term.cols,
1616
- rows: term.rows
1617
- }));
1618
- }
1550
+ new ResizeObserver(() => {
1551
+ fitAddon.fit();
1552
+ if (ws.readyState === WebSocket.OPEN && term) {
1553
+ ws.send(JSON.stringify({ type: 'terminal_resize', cols: term.cols, rows: term.rows }));
1619
1554
  }
1620
- });
1621
- resizeObserver.observe(document.getElementById('terminalContainer'));
1622
-
1623
- // Auto-focus terminal
1624
- term.focus();
1625
- }
1626
-
1627
- function focusTerminal() {
1628
- if (term) term.focus();
1629
- }
1630
-
1631
- function sendCtrlC() {
1632
- if (ws.readyState === WebSocket.OPEN) {
1633
- ws.send(JSON.stringify({ type: 'terminal_input', data: '' }));
1634
- }
1635
- if (term) term.focus();
1636
- }
1637
-
1638
- function sendEsc() {
1639
- if (ws.readyState === WebSocket.OPEN) {
1640
- ws.send(JSON.stringify({ type: 'terminal_input', data: '\x1B' }));
1641
- }
1642
- if (term) term.focus();
1555
+ }).observe(document.getElementById('terminalContainer'));
1556
+ if (ws.readyState === WebSocket.OPEN) sendTerminalOpen();
1643
1557
  }
1644
1558
 
1645
- function sendTab() {
1646
- if (ws.readyState === WebSocket.OPEN) {
1647
- ws.send(JSON.stringify({ type: 'terminal_input', data: ' ' }));
1648
- }
1649
- if (term) term.focus();
1559
+ function sendTerminalOpen() {
1560
+ if (!term || !fitAddon) return;
1561
+ fitAddon.fit();
1562
+ ws.send(JSON.stringify({ type: 'terminal_open', cols: term.cols, rows: term.rows }));
1563
+ term.focus();
1650
1564
  }
1651
1565
 
1652
- // Load CLI presets for terminal mode
1653
- fetch('/api/cli-presets').then(r=>r.json()).then(presets=>{
1654
- cliPresets=presets;
1655
- const sel=document.getElementById('cliPreset');
1656
- const shellOpt=document.createElement('option');
1657
- shellOpt.value='shell';
1658
- shellOpt.textContent='Shell (default)';
1659
- sel.appendChild(shellOpt);
1660
- presets.forEach(p=>{const o=document.createElement('option');o.value=p.id;o.textContent=p.name;sel.appendChild(o)});
1661
- sel.addEventListener('change',()=>{
1662
- const id=sel.value;
1663
- const p=cliPresets.find(x=>x.id===id);
1664
- const cmd=document.getElementById('command');
1665
- if(id==='shell'){cmd.style.display='none';cmd.value='';document.getElementById('presetDesc').textContent='Start an interactive login shell. Run commands directly in the phone terminal.';return;}
1666
- if(id==='custom'){cmd.style.display='';document.getElementById('presetDesc').textContent='Enter the exact launch command.';return;}
1667
- cmd.style.display='none';
1668
- document.getElementById('presetDesc').textContent=p?p.description:'';
1669
- });
1670
- sel.value='shell';
1671
- sel.dispatchEvent(new Event('change'));
1672
- return fetch('/api/config').then(r=>r.json());
1673
- }).then(cfg=>{
1674
- if(!cfg) return;
1675
- const {saved}=cfg;
1676
- if(saved && (saved.type==='terminal' || saved.type==='cli')){
1677
- if(saved.preset) document.getElementById('cliPreset').value=saved.preset;
1678
- if(saved.command) document.getElementById('command').value=saved.command;
1679
- document.getElementById('cliPreset').dispatchEvent(new Event('change'));
1680
- }
1681
- }).catch(()=>{});
1566
+ ws.onopen = () => { if (termInitialized) sendTerminalOpen(); };
1682
1567
 
1683
- // WebSocket message handling
1684
- ws.onmessage=e=>{
1685
- const d=JSON.parse(e.data);
1686
- if(d.type==='peer_connected'){
1687
- document.getElementById('dot').className='dot on';
1688
- document.getElementById('statusText').textContent='Phone connected';
1689
- }
1690
- else if(d.type==='peer_disconnected'){
1691
- document.getElementById('dot').className='dot wait';
1692
- document.getElementById('statusText').textContent='Phone disconnected';
1693
- }
1694
- else if(d.type==='terminal_configured'){
1695
- document.getElementById('launchText').textContent='Launch: '+d.terminalLaunch;
1696
- document.getElementById('statusText').textContent='Launch target updated';
1697
- }
1698
- else if(d.type==='configured'){
1699
- document.getElementById('statusText').textContent='AI adapter configured';
1700
- }
1701
- else if(d.type==='stream_chunk' && term){
1568
+ ws.onmessage = (e) => {
1569
+ const d = JSON.parse(e.data);
1570
+ if (d.type === 'peer_connected') {
1571
+ document.getElementById('dot').className = 'dot on';
1572
+ document.getElementById('statusText').textContent = 'Phone connected';
1573
+ } else if (d.type === 'peer_disconnected') {
1574
+ document.getElementById('dot').className = 'dot wait';
1575
+ document.getElementById('statusText').textContent = 'Phone disconnected';
1576
+ } else if (d.type === 'terminal_configured') {
1577
+ document.getElementById('launchText').textContent = 'Launch: ' + d.terminalLaunch;
1578
+ } else if (d.type === 'terminal_output' && term) {
1702
1579
  term.write(d.data);
1703
- }
1704
- else if(d.type==='stream_end' && term){
1705
- // Terminal stream ended
1706
- }
1707
- else if(d.type==='terminal_exit' && term){
1708
- const detail = typeof d.exitCode === 'number' ? \`exit \${d.exitCode}\` : 'terminated';
1580
+ } else if (d.type === 'terminal_exit' && term) {
1581
+ const code = typeof d.exitCode === 'number' ? \`exit \${d.exitCode}\` : 'terminated';
1709
1582
  term.writeln(\`\r
1710
- [terminal \${detail}]\`);
1711
- }
1712
- else if(d.type==='message'){
1713
- // Chat message - add to chat UI
1714
- const messagesDiv = document.getElementById('messages');
1715
- if (messagesDiv) {
1716
- const msgDiv = document.createElement('div');
1717
- msgDiv.className = \`msg \${d.role}\`;
1718
- msgDiv.textContent = d.content;
1719
- messagesDiv.appendChild(msgDiv);
1720
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
1583
+ [terminal \${code}]\`);
1584
+ } else if (d.type === 'stream_chunk') {
1585
+ const msgs = document.getElementById('messages');
1586
+ if (msgs) {
1587
+ let last = msgs.lastElementChild;
1588
+ if (!last || last.dataset.streaming !== 'true') {
1589
+ last = document.createElement('div');
1590
+ last.className = 'msg assistant';
1591
+ last.dataset.streaming = 'true';
1592
+ msgs.appendChild(last);
1593
+ }
1594
+ last.textContent += d.data;
1595
+ msgs.scrollTop = msgs.scrollHeight;
1596
+ }
1597
+ } else if (d.type === 'stream_end') {
1598
+ const msgs = document.getElementById('messages');
1599
+ if (msgs?.lastElementChild) delete msgs.lastElementChild.dataset.streaming;
1600
+ } else if (d.type === 'message') {
1601
+ const msgs = document.getElementById('messages');
1602
+ if (msgs) {
1603
+ const el = document.createElement('div');
1604
+ el.className = \`msg \${d.role}\`;
1605
+ el.textContent = d.content;
1606
+ msgs.appendChild(el);
1607
+ msgs.scrollTop = msgs.scrollHeight;
1721
1608
  }
1722
1609
  }
1723
1610
  };
1724
1611
 
1725
- // Load initial status and determine mode
1726
- fetch('/api/status').then(r=>r.json()).then(d=>{
1727
- if(d.terminalLaunch) document.getElementById('launchText').textContent='Launch: '+d.terminalLaunch;
1728
- if(d.pairingCode){
1729
- document.getElementById('pairingCard').style.display='';
1730
- document.getElementById('pairingCode').textContent=d.pairingCode;
1731
- if(d.pairingQR) document.getElementById('qrCode').src=d.pairingQR;
1732
- }
1733
- if(d.connected){
1734
- document.getElementById('dot').className='dot on';
1735
- document.getElementById('statusText').textContent='Phone connected';
1736
- } else{
1737
- document.getElementById('dot').className='dot wait';
1738
- document.getElementById('statusText').textContent='Waiting for phone...';
1739
- }
1740
-
1741
- // Determine mode: terminal mode if no adapter but has terminal launch
1742
- if (!d.adapter && d.terminalLaunch) {
1743
- switchToMode('terminal');
1612
+ fetch('/api/status').then(r => r.json()).then(d => {
1613
+ if (d.terminalLaunch) document.getElementById('launchText').textContent = 'Launch: ' + d.terminalLaunch;
1614
+ if (d.pairingCode) {
1615
+ document.getElementById('pairingCard').style.display = '';
1616
+ document.getElementById('pairingCode').textContent = d.pairingCode;
1617
+ if (d.pairingQR) document.getElementById('qrCode').src = d.pairingQR;
1618
+ }
1619
+ document.getElementById('dot').className = d.connected ? 'dot on' : 'dot wait';
1620
+ document.getElementById('statusText').textContent = d.connected ? 'Phone connected' : 'Waiting for phone...';
1621
+ if (d.adapter) {
1622
+ document.getElementById('chatSection').style.display = '';
1623
+ (d.messages || []).forEach(m => {
1624
+ const msgs = document.getElementById('messages');
1625
+ const el = document.createElement('div');
1626
+ el.className = \`msg \${m.role}\`;
1627
+ el.textContent = m.content;
1628
+ msgs.appendChild(el);
1629
+ });
1744
1630
  } else {
1745
- switchToMode('chat');
1746
- if (d.adapter) {
1747
- document.getElementById('chatCard').style.display = 'block';
1748
- }
1631
+ document.getElementById('terminalSection').style.display = '';
1632
+ initTerminal();
1749
1633
  }
1750
1634
  });
1751
1635
 
1752
- // Configuration functions
1753
- async function configureAdapter() {
1754
- const type = document.getElementById('adapterType').value;
1755
- const model = document.getElementById('model').value;
1756
- const apiKey = document.getElementById('apiKey').value;
1757
-
1758
- const response = await fetch('/api/configure', {
1759
- method: 'POST',
1760
- headers: { 'Content-Type': 'application/json' },
1761
- body: JSON.stringify({ type, model, apiKey })
1636
+ fetch('/api/cli-presets').then(r => r.json()).then(presets => {
1637
+ cliPresets = presets;
1638
+ const sel = document.getElementById('cliPreset');
1639
+ const shellOpt = document.createElement('option');
1640
+ shellOpt.value = 'shell'; shellOpt.textContent = 'Shell (default)';
1641
+ sel.appendChild(shellOpt);
1642
+ presets.forEach(p => {
1643
+ const o = document.createElement('option');
1644
+ o.value = p.id; o.textContent = p.name;
1645
+ sel.appendChild(o);
1762
1646
  });
1763
- const result = await response.json();
1764
- if (result.error) {
1765
- alert(result.error);
1766
- } else {
1767
- document.getElementById('chatCard').style.display = 'block';
1647
+ sel.addEventListener('change', () => {
1648
+ const id = sel.value;
1649
+ const p = cliPresets.find(x => x.id === id);
1650
+ const cmd = document.getElementById('command');
1651
+ const desc = document.getElementById('presetDesc');
1652
+ if (id === 'shell') { cmd.style.display = 'none'; cmd.value = ''; desc.textContent = 'Interactive login shell.'; return; }
1653
+ if (id === 'custom') { cmd.style.display = ''; desc.textContent = 'Enter the exact launch command.'; return; }
1654
+ cmd.style.display = 'none';
1655
+ desc.textContent = p ? p.description : '';
1656
+ });
1657
+ sel.value = 'shell'; sel.dispatchEvent(new Event('change'));
1658
+ return fetch('/api/config').then(r => r.json());
1659
+ }).then(cfg => {
1660
+ if (!cfg?.saved) return;
1661
+ const { saved } = cfg;
1662
+ if (saved.type === 'terminal' || saved.type === 'cli') {
1663
+ if (saved.preset) document.getElementById('cliPreset').value = saved.preset;
1664
+ if (saved.command) document.getElementById('command').value = saved.command;
1665
+ document.getElementById('cliPreset').dispatchEvent(new Event('change'));
1768
1666
  }
1769
- }
1667
+ }).catch(() => {});
1770
1668
 
1771
- async function configureTerminal() {
1669
+ document.getElementById('applyLaunchBtn').addEventListener('click', async () => {
1772
1670
  const preset = document.getElementById('cliPreset').value;
1773
1671
  const command = document.getElementById('command').value;
1774
-
1775
- const response = await fetch('/api/configure', {
1776
- method: 'POST',
1777
- headers: { 'Content-Type': 'application/json' },
1778
- body: JSON.stringify({ type: 'cli', preset, command })
1779
- });
1780
- const result = await response.json();
1781
- if (result.error) {
1782
- alert(result.error);
1783
- }
1784
- }
1672
+ const r = await fetch('/api/configure', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'cli', preset, command }) });
1673
+ const d = await r.json();
1674
+ if (d.error) alert(d.error);
1675
+ });
1676
+
1677
+ document.getElementById('focusTermBtn').addEventListener('click', () => term?.focus());
1678
+ document.getElementById('ctrlCBtn').addEventListener('click', () => {
1679
+ term?.focus();
1680
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: '' }));
1681
+ });
1682
+ document.getElementById('escBtn').addEventListener('click', () => {
1683
+ term?.focus();
1684
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: '\x1B' }));
1685
+ });
1686
+ document.getElementById('tabBtn').addEventListener('click', () => {
1687
+ term?.focus();
1688
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: ' ' }));
1689
+ });
1690
+
1691
+ document.getElementById('sendBtn').addEventListener('click', sendMessage);
1692
+ document.getElementById('messageInput').addEventListener('keydown', (e) => {
1693
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
1694
+ });
1785
1695
 
1786
- // Chat functionality
1787
1696
  function sendMessage() {
1788
1697
  const input = document.getElementById('messageInput');
1789
1698
  const content = input.value.trim();
1790
1699
  if (!content) return;
1791
-
1792
- fetch('/api/send', {
1793
- method: 'POST',
1794
- headers: { 'Content-Type': 'application/json' },
1795
- body: JSON.stringify({ content })
1796
- }).then(() => {
1797
- input.value = '';
1798
- }).catch(err => {
1799
- console.error('Failed to send message:', err);
1800
- });
1700
+ fetch('/api/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) })
1701
+ .then(() => { input.value = ''; })
1702
+ .catch(err => console.error('Failed to send:', err));
1801
1703
  }
1802
-
1803
- // Handle Enter key in chat input
1804
- document.getElementById('messageInput')?.addEventListener('keydown', (e) => {
1805
- if (e.key === 'Enter' && !e.shiftKey) {
1806
- e.preventDefault();
1807
- sendMessage();
1808
- }
1809
- });
1810
1704
  </script>
1811
1705
  </body>
1812
1706
  </html>`;
1813
1707
 
1814
1708
  // src/index.ts
1709
+ var QRCode = null;
1710
+ async function getQRCode() {
1711
+ if (!QRCode) QRCode = await import("qrcode");
1712
+ return QRCode;
1713
+ }
1714
+ console.log("[host] Module loaded");
1815
1715
  function parseArgs(argv) {
1816
1716
  const args = {};
1817
1717
  const rest = argv.slice(2);
@@ -1873,6 +1773,7 @@ if (cliArgs.help) {
1873
1773
  var DEFAULT_ABLY_KEY = "SfHSAQ.IRTOQQ:FBbi9a7ZV6jIu0Gdo_UeYhIN4rzpMrud5-LldURNh9s";
1874
1774
  var DEFAULT_VIEWER_URL = "https://bobstrogg.github.io/Airloom/";
1875
1775
  var VIEWER_URL = process.env.VIEWER_URL ?? DEFAULT_VIEWER_URL;
1776
+ var IS_DEV = !process.env.VIEWER_URL && !new URL(import.meta.url).pathname.includes("node_modules");
1876
1777
  var RELAY_URL = process.env.RELAY_URL;
1877
1778
  var ABLY_API_KEY = process.env.ABLY_API_KEY ?? (RELAY_URL ? void 0 : DEFAULT_ABLY_KEY);
1878
1779
  var ABLY_TOKEN_TTL = parseInt(process.env.ABLY_TOKEN_TTL ?? String(24 * 60 * 60 * 1e3), 10);
@@ -1968,7 +1869,10 @@ async function main() {
1968
1869
  connected: false,
1969
1870
  terminalLaunch,
1970
1871
  terminalLaunchCommand: launchCommand,
1971
- messages: []
1872
+ messages: [],
1873
+ sessionToken: session.sessionToken,
1874
+ ablyToken: useAbly ? pairingData.token : void 0,
1875
+ transport: useAbly ? "ably" : "ws"
1972
1876
  };
1973
1877
  console.log(`[host] Terminal launch: ${terminalLaunch}`);
1974
1878
  if (cliArgs.cli || cliArgs.preset) {
@@ -2034,31 +1938,37 @@ async function main() {
2034
1938
  const { server, broadcast, port } = await createHostServer({ port: HOST_PORT, state, viewerDir });
2035
1939
  const pairingBase64 = Buffer.from(pairingJSON).toString("base64url");
2036
1940
  const viewerBase = VIEWER_URL.replace(/\/+$/, "");
2037
- const qrContent = `${viewerBase}/#${pairingBase64}`;
1941
+ const pagesUrl = `${viewerBase}/#${pairingBase64}`;
2038
1942
  const lanIP = getLanIP();
2039
1943
  const lanHost = lanIP ?? "localhost";
2040
1944
  const lanBaseUrl = `http://${lanHost}:${port}`;
2041
1945
  const lanViewerUrl = viewerDir ? `${lanBaseUrl}/viewer/#${pairingBase64}` : null;
2042
- const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 300, margin: 2 });
2043
- const qrTerminal = await QRCode.toString(qrContent, { type: "terminal", small: true });
1946
+ const qrTarget = IS_DEV && lanViewerUrl ? lanViewerUrl : pagesUrl;
1947
+ const qrcode = await getQRCode();
1948
+ const qrDataUrl = await qrcode.toDataURL(qrTarget, { width: 300, margin: 2 });
1949
+ const qrTerminal = await qrcode.toString(qrTarget, { type: "terminal", small: true });
2044
1950
  state.pairingQR = qrDataUrl;
2045
1951
  console.log("\nPairing QR Code:");
2046
1952
  console.log(qrTerminal);
2047
1953
  console.log(`Pairing Code: ${displayCode}`);
2048
- console.log(`Viewer URL: ${qrContent}`);
2049
- if (lanViewerUrl) {
2050
- console.log(`LAN Viewer: ${lanViewerUrl}`);
1954
+ if (IS_DEV && lanViewerUrl) {
1955
+ console.log(`Viewer URL (LAN/dev): ${lanViewerUrl}`);
1956
+ console.log(`Pages URL: ${pagesUrl}`);
1957
+ } else {
1958
+ console.log(`Viewer URL: ${pagesUrl}`);
1959
+ if (lanViewerUrl) console.log(`LAN Viewer: ${lanViewerUrl}`);
2051
1960
  }
2052
1961
  if (!useAbly) console.log(`Relay: ${RELAY_URL}`);
2053
1962
  const localUrl = `http://localhost:${port}`;
2054
1963
  console.log(`[host] Web UI at ${localUrl}
2055
1964
  `);
2056
- import("child_process").then(({ exec }) => {
1965
+ import("node:child_process").then(({ exec }) => {
2057
1966
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2058
1967
  exec(`${cmd} ${localUrl}`);
2059
1968
  }).catch(() => {
2060
1969
  });
2061
- const terminal = new TerminalSession(channel, () => state.terminalLaunchCommand);
1970
+ const terminal = new TerminalSession(channel, () => state.terminalLaunchCommand, broadcast);
1971
+ state.terminal = terminal;
2062
1972
  channel.on("ready", () => {
2063
1973
  console.log("[host] Phone connected! Channel ready.");
2064
1974
  state.connected = true;