airloom 0.1.13 → 0.1.17
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 +310 -384
- package/dist/viewer/assets/{browser-BRKgrEB1.js → browser-B9RMX6OC.js} +1 -1
- package/dist/viewer/assets/{index-D9J9LZLl.css → index-C27Ct95O.css} +1 -1
- package/dist/viewer/assets/{index-CCYyPnVc.js → index-CoclVuPf.js} +17 -17
- package/dist/viewer/assets/{index-DKFwSRim.js → index-D3PxNxWB.js} +1 -1
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/sw.js +30 -86
- package/package.json +9 -7
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;
|
|
@@ -1112,20 +1111,57 @@ function getTerminalLaunchDisplay(explicitCommand) {
|
|
|
1112
1111
|
const command = getDefaultTerminalCommand(explicitCommand);
|
|
1113
1112
|
return [command.file, ...command.args].join(" ");
|
|
1114
1113
|
}
|
|
1114
|
+
var MAX_BUFFER_BYTES = 512 * 1024;
|
|
1115
1115
|
var TerminalSession = class {
|
|
1116
|
-
constructor(channel, getLaunchCommand) {
|
|
1116
|
+
constructor(channel, getLaunchCommand, broadcastFn) {
|
|
1117
1117
|
this.channel = channel;
|
|
1118
1118
|
this.getLaunchCommand = getLaunchCommand;
|
|
1119
|
+
this.broadcastFn = broadcastFn;
|
|
1120
|
+
this.start();
|
|
1119
1121
|
}
|
|
1120
1122
|
pty = null;
|
|
1121
1123
|
stream = null;
|
|
1122
1124
|
batcher = null;
|
|
1123
1125
|
cols = 120;
|
|
1124
1126
|
rows = 36;
|
|
1127
|
+
outputBuffer = "";
|
|
1128
|
+
start() {
|
|
1129
|
+
const command = getDefaultTerminalCommand(this.getLaunchCommand?.());
|
|
1130
|
+
const file = resolveExecutable2(command.file) ?? command.file;
|
|
1131
|
+
console.log(`[host] PTY spawn: ${file} ${command.args.join(" ")} (${this.cols}x${this.rows})`);
|
|
1132
|
+
const env = { ...process.env, TERM: "xterm-256color" };
|
|
1133
|
+
try {
|
|
1134
|
+
this.pty = spawn2(file, command.args, {
|
|
1135
|
+
name: "xterm-256color",
|
|
1136
|
+
cols: this.cols,
|
|
1137
|
+
rows: this.rows,
|
|
1138
|
+
cwd: process.cwd(),
|
|
1139
|
+
env
|
|
1140
|
+
});
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
console.error("[host] PTY spawn failed:", err.message);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
this.pty.onData((data) => {
|
|
1146
|
+
process.stdout.write(data);
|
|
1147
|
+
this.outputBuffer += data;
|
|
1148
|
+
if (this.outputBuffer.length > MAX_BUFFER_BYTES) {
|
|
1149
|
+
this.outputBuffer = this.outputBuffer.slice(this.outputBuffer.length - MAX_BUFFER_BYTES);
|
|
1150
|
+
}
|
|
1151
|
+
this.batcher?.write(data);
|
|
1152
|
+
this.broadcastFn?.({ type: "terminal_output", data });
|
|
1153
|
+
});
|
|
1154
|
+
this.pty.onExit(({ exitCode, signal }) => {
|
|
1155
|
+
this.batcher?.flush();
|
|
1156
|
+
this.detachStream();
|
|
1157
|
+
this.channel.send({ type: "terminal_exit", exitCode, signal });
|
|
1158
|
+
this.pty = null;
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1125
1161
|
handleMessage(message) {
|
|
1126
1162
|
switch (message.type) {
|
|
1127
1163
|
case "terminal_open":
|
|
1128
|
-
this.
|
|
1164
|
+
this.attach(message);
|
|
1129
1165
|
break;
|
|
1130
1166
|
case "terminal_input":
|
|
1131
1167
|
this.writeInput(message);
|
|
@@ -1134,58 +1170,56 @@ var TerminalSession = class {
|
|
|
1134
1170
|
this.resize(message);
|
|
1135
1171
|
break;
|
|
1136
1172
|
case "terminal_close":
|
|
1137
|
-
this.
|
|
1173
|
+
this.detachStream();
|
|
1138
1174
|
break;
|
|
1139
1175
|
case "terminal_exit":
|
|
1140
1176
|
break;
|
|
1141
1177
|
}
|
|
1142
1178
|
}
|
|
1143
|
-
|
|
1179
|
+
attach(message) {
|
|
1180
|
+
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1181
|
+
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
1182
|
+
this.pty?.resize(this.cols, this.rows);
|
|
1183
|
+
this.detachStream();
|
|
1184
|
+
const meta = { kind: "terminal", cols: this.cols, rows: this.rows };
|
|
1185
|
+
this.stream = this.channel.createStream(meta);
|
|
1186
|
+
this.batcher = new AdaptiveOutputBatcher((data) => {
|
|
1187
|
+
this.stream?.write(data);
|
|
1188
|
+
});
|
|
1189
|
+
if (this.outputBuffer) {
|
|
1190
|
+
this.stream.write(this.outputBuffer);
|
|
1191
|
+
}
|
|
1192
|
+
if (!this.pty) {
|
|
1193
|
+
this.start();
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/** End the current stream without killing the PTY (called on peer disconnect). */
|
|
1197
|
+
detachStream() {
|
|
1144
1198
|
this.batcher?.destroy();
|
|
1145
1199
|
this.batcher = null;
|
|
1146
1200
|
if (this.stream && !this.stream.ended) this.stream.end();
|
|
1147
1201
|
this.stream = null;
|
|
1202
|
+
}
|
|
1203
|
+
/** Kill the PTY — called only on host shutdown. */
|
|
1204
|
+
destroy() {
|
|
1205
|
+
this.detachStream();
|
|
1148
1206
|
try {
|
|
1149
1207
|
this.pty?.kill();
|
|
1150
1208
|
} catch {
|
|
1151
1209
|
}
|
|
1152
1210
|
this.pty = null;
|
|
1153
|
-
|
|
1154
|
-
open(message) {
|
|
1155
|
-
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1156
|
-
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
1157
|
-
if (this.pty) {
|
|
1158
|
-
this.pty.resize(this.cols, this.rows);
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
const command = getDefaultTerminalCommand(this.getLaunchCommand?.());
|
|
1162
|
-
const file = resolveExecutable2(command.file) ?? command.file;
|
|
1163
|
-
const meta = { kind: "terminal", cols: this.cols, rows: this.rows };
|
|
1164
|
-
this.stream = this.channel.createStream(meta);
|
|
1165
|
-
this.batcher = new AdaptiveOutputBatcher((data) => this.stream?.write(data));
|
|
1166
|
-
const env = { ...process.env, TERM: "xterm-256color" };
|
|
1167
|
-
this.pty = spawn2(file, command.args, {
|
|
1168
|
-
name: "xterm-256color",
|
|
1169
|
-
cols: this.cols,
|
|
1170
|
-
rows: this.rows,
|
|
1171
|
-
cwd: process.cwd(),
|
|
1172
|
-
env
|
|
1173
|
-
});
|
|
1174
|
-
this.pty.onData((data) => this.batcher?.write(data));
|
|
1175
|
-
this.pty.onExit(({ exitCode, signal }) => {
|
|
1176
|
-
this.batcher?.flush();
|
|
1177
|
-
this.stream?.end();
|
|
1178
|
-
this.channel.send({ type: "terminal_exit", exitCode, signal });
|
|
1179
|
-
this.stream = null;
|
|
1180
|
-
this.batcher = null;
|
|
1181
|
-
this.pty = null;
|
|
1182
|
-
});
|
|
1211
|
+
this.outputBuffer = "";
|
|
1183
1212
|
}
|
|
1184
1213
|
writeInput(message) {
|
|
1185
1214
|
if (!this.pty) return;
|
|
1186
1215
|
this.batcher?.noteInput();
|
|
1187
1216
|
this.pty.write(message.data);
|
|
1188
1217
|
}
|
|
1218
|
+
writeRawInput(data) {
|
|
1219
|
+
if (!this.pty) return;
|
|
1220
|
+
this.batcher?.noteInput();
|
|
1221
|
+
this.pty.write(data);
|
|
1222
|
+
}
|
|
1189
1223
|
resize(message) {
|
|
1190
1224
|
this.cols = Math.max(20, Math.floor(message.cols || this.cols));
|
|
1191
1225
|
this.rows = Math.max(5, Math.floor(message.rows || this.rows));
|
|
@@ -1310,16 +1344,30 @@ function createHostServer(opts) {
|
|
|
1310
1344
|
}
|
|
1311
1345
|
res.json({ ok: true });
|
|
1312
1346
|
});
|
|
1347
|
+
app.get("/api/pair", (req, res) => {
|
|
1348
|
+
const { session } = req.query;
|
|
1349
|
+
if (!session || session !== opts.state.sessionToken) {
|
|
1350
|
+
res.status(401).json({ error: "Invalid session" });
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
res.json({
|
|
1354
|
+
token: opts.state.ablyToken,
|
|
1355
|
+
transport: opts.state.transport ?? "ws",
|
|
1356
|
+
relay: opts.state.relayUrl
|
|
1357
|
+
});
|
|
1358
|
+
});
|
|
1313
1359
|
wss.on("connection", (ws) => {
|
|
1314
1360
|
uiClients.add(ws);
|
|
1315
1361
|
ws.on("close", () => uiClients.delete(ws));
|
|
1316
1362
|
ws.on("message", (data) => {
|
|
1317
1363
|
try {
|
|
1318
1364
|
const message = JSON.parse(data.toString());
|
|
1319
|
-
if (message.type && message.
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1365
|
+
if (message.type === "terminal_input" && typeof message.data === "string") {
|
|
1366
|
+
const filtered = message.data.replace(/\x1b\]\d+;[^\x07\x1b]*(?:\x07|\x1b\\)/g, "");
|
|
1367
|
+
if (!filtered) return;
|
|
1368
|
+
opts.state.terminal?.writeRawInput(filtered);
|
|
1369
|
+
} else if (message.type === "terminal_resize") {
|
|
1370
|
+
opts.state.terminal?.handleMessage(message);
|
|
1323
1371
|
}
|
|
1324
1372
|
} catch (err) {
|
|
1325
1373
|
console.error("[host] Invalid WebSocket message:", err);
|
|
@@ -1389,6 +1437,7 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1389
1437
|
<meta charset="UTF-8">
|
|
1390
1438
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1391
1439
|
<title>Airloom - Host</title>
|
|
1440
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@6.0.0/css/xterm.css">
|
|
1392
1441
|
<style>
|
|
1393
1442
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
1394
1443
|
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0a;color:#e0e0e0;min-height:100vh}
|
|
@@ -1411,39 +1460,14 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1411
1460
|
button{background:#7c8aff;color:#fff;border:none;cursor:pointer;font-weight:600}
|
|
1412
1461
|
button:hover{background:#6b79ee}
|
|
1413
1462
|
.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
|
|
1463
|
+
.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}
|
|
1464
|
+
.msg.user{background:#2a3a6a;align-self:flex-end}
|
|
1416
1465
|
.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
1466
|
.input-area{display:flex;gap:8px}
|
|
1436
1467
|
.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}
|
|
1468
|
+
.terminal-container{background:#05070c;border:1px solid #2a2a2a;border-radius:8px;height:420px;overflow:hidden;margin-bottom:12px}
|
|
1445
1469
|
#terminal{width:100%;height:100%;padding:8px}
|
|
1446
|
-
.
|
|
1470
|
+
.toolbar{display:flex;gap:8px;margin-bottom:12px}
|
|
1447
1471
|
.tool-btn{padding:6px 12px;font-size:.85rem;background:#333;border:none;border-radius:6px;color:#e0e0e0;cursor:pointer}
|
|
1448
1472
|
.tool-btn:hover{background:#444}
|
|
1449
1473
|
</style>
|
|
@@ -1458,360 +1482,251 @@ var HOST_HTML = `<!DOCTYPE html>
|
|
|
1458
1482
|
<div class="status"><div class="dot wait" id="dot"></div><span id="statusText">Initializing...</span></div>
|
|
1459
1483
|
<p style="color:#888;font-size:.9rem;margin-top:8px" id="launchText">Launch: current shell</p>
|
|
1460
1484
|
</div>
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
<
|
|
1465
|
-
|
|
1466
|
-
|
|
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>
|
|
1485
|
+
|
|
1486
|
+
<div class="card pairing" id="pairingCard" style="display:none">
|
|
1487
|
+
<h2>Connect Your Phone</h2>
|
|
1488
|
+
<img id="qrCode" alt="QR Code"/>
|
|
1489
|
+
<div class="pairing-code" id="pairingCode"></div>
|
|
1490
|
+
<p style="color:#888;font-size:.85rem">Scan QR or enter code in viewer</p>
|
|
1486
1491
|
</div>
|
|
1487
|
-
|
|
1488
|
-
<!-- Terminal mode
|
|
1489
|
-
<div id="
|
|
1492
|
+
|
|
1493
|
+
<!-- Terminal mode: shell, Devin, Codex etc. (default when no AI adapter) -->
|
|
1494
|
+
<div id="terminalSection" style="display:none">
|
|
1490
1495
|
<div class="card">
|
|
1491
|
-
<h2>
|
|
1496
|
+
<h2>Launch Configuration</h2>
|
|
1492
1497
|
<div class="config-form">
|
|
1493
1498
|
<select id="cliPreset"></select>
|
|
1494
1499
|
<input type="text" id="command" placeholder="Custom launch command" style="display:none"/>
|
|
1495
1500
|
<p style="color:#666;font-size:.8rem;margin-top:4px" id="presetDesc"></p>
|
|
1496
|
-
<button
|
|
1501
|
+
<button id="applyLaunchBtn">Apply Launch Target</button>
|
|
1497
1502
|
</div>
|
|
1498
1503
|
</div>
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
<
|
|
1502
|
-
|
|
1503
|
-
<button class="tool-btn"
|
|
1504
|
-
<button class="tool-btn"
|
|
1505
|
-
<button class="tool-btn"
|
|
1506
|
-
<button class="tool-btn" onclick="sendTab()">Tab</button>
|
|
1504
|
+
<div class="card">
|
|
1505
|
+
<h2>Terminal</h2>
|
|
1506
|
+
<div class="toolbar">
|
|
1507
|
+
<button class="tool-btn" id="focusTermBtn">Focus</button>
|
|
1508
|
+
<button class="tool-btn" id="ctrlCBtn">Ctrl+C</button>
|
|
1509
|
+
<button class="tool-btn" id="escBtn">Esc</button>
|
|
1510
|
+
<button class="tool-btn" id="tabBtn">Tab</button>
|
|
1507
1511
|
</div>
|
|
1508
|
-
<div class="terminal-container
|
|
1512
|
+
<div class="terminal-container" id="terminalContainer">
|
|
1509
1513
|
<div id="terminal"></div>
|
|
1510
1514
|
</div>
|
|
1511
1515
|
</div>
|
|
1512
1516
|
</div>
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
<
|
|
1517
|
-
|
|
1518
|
-
|
|
1517
|
+
|
|
1518
|
+
<!-- Chat mode: direct LLM connections (Anthropic, OpenAI) -->
|
|
1519
|
+
<div id="chatSection" style="display:none">
|
|
1520
|
+
<div class="card">
|
|
1521
|
+
<h2>Chat</h2>
|
|
1522
|
+
<div class="messages" id="messages"></div>
|
|
1523
|
+
<div class="input-area">
|
|
1524
|
+
<textarea id="messageInput" placeholder="Type your message..." rows="1"></textarea>
|
|
1525
|
+
<button id="sendBtn">Send</button>
|
|
1526
|
+
</div>
|
|
1527
|
+
</div>
|
|
1519
1528
|
</div>
|
|
1529
|
+
|
|
1520
1530
|
</div>
|
|
1521
|
-
<script>
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
-
};
|
|
1531
|
+
<script type="module">
|
|
1532
|
+
import { Terminal } from 'https://esm.sh/@xterm/xterm@6';
|
|
1533
|
+
import { FitAddon } from 'https://esm.sh/@xterm/addon-fit@0.11';
|
|
1532
1534
|
|
|
1533
|
-
const ws=new WebSocket((location.protocol==='https:'?'wss://':'ws://')+location.host+'/ws');
|
|
1534
|
-
let cliPresets=[];
|
|
1535
|
-
let currentMode = null; // 'chat' or 'terminal'
|
|
1535
|
+
const ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws');
|
|
1536
1536
|
let term = null;
|
|
1537
1537
|
let fitAddon = null;
|
|
1538
|
+
let termInitialized = false;
|
|
1539
|
+
let cliPresets = [];
|
|
1538
1540
|
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
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({
|
|
1541
|
+
function initTerminal() {
|
|
1542
|
+
if (termInitialized) return;
|
|
1543
|
+
termInitialized = true;
|
|
1544
|
+
term = new Terminal({
|
|
1567
1545
|
cursorBlink: true,
|
|
1568
|
-
fontFamily: 'ui-monospace,
|
|
1546
|
+
fontFamily: 'ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace',
|
|
1569
1547
|
fontSize: 14,
|
|
1570
1548
|
lineHeight: 1.25,
|
|
1571
|
-
allowTransparency: true,
|
|
1572
1549
|
scrollback: 5000,
|
|
1573
1550
|
theme: {
|
|
1574
|
-
background: '#05070c',
|
|
1575
|
-
foreground: '#e6edf3',
|
|
1576
|
-
cursor: '#7c8aff',
|
|
1577
|
-
cursorAccent: '#05070c',
|
|
1551
|
+
background: '#05070c', foreground: '#e6edf3', cursor: '#7c8aff', cursorAccent: '#05070c',
|
|
1578
1552
|
selectionBackground: 'rgba(124,138,255,0.28)',
|
|
1579
|
-
black: '#0a0d14',
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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',
|
|
1553
|
+
black: '#0a0d14', red: '#ff7b72', green: '#3fb950', yellow: '#d29922',
|
|
1554
|
+
blue: '#7c8aff', magenta: '#bc8cff', cyan: '#39c5cf', white: '#c9d1d9',
|
|
1555
|
+
brightBlack: '#6e7681', brightRed: '#ffa198', brightGreen: '#56d364', brightYellow: '#e3b341',
|
|
1556
|
+
brightBlue: '#a5b4ff', brightMagenta: '#d2a8ff', brightCyan: '#56d4dd', brightWhite: '#f0f6fc',
|
|
1595
1557
|
},
|
|
1596
1558
|
});
|
|
1597
|
-
|
|
1598
|
-
fitAddon = new window.FitAddon();
|
|
1559
|
+
fitAddon = new FitAddon();
|
|
1599
1560
|
term.loadAddon(fitAddon);
|
|
1600
1561
|
term.open(document.getElementById('terminal'));
|
|
1601
|
-
|
|
1602
1562
|
term.onData((data) => {
|
|
1603
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1604
|
-
ws.send(JSON.stringify({ type: 'terminal_input', data }));
|
|
1605
|
-
}
|
|
1563
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data }));
|
|
1606
1564
|
});
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
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
|
-
}
|
|
1565
|
+
new ResizeObserver(() => {
|
|
1566
|
+
fitAddon.fit();
|
|
1567
|
+
if (ws.readyState === WebSocket.OPEN && term) {
|
|
1568
|
+
ws.send(JSON.stringify({ type: 'terminal_resize', cols: term.cols, rows: term.rows }));
|
|
1619
1569
|
}
|
|
1620
|
-
});
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
// Auto-focus terminal
|
|
1624
|
-
term.focus();
|
|
1570
|
+
}).observe(document.getElementById('terminalContainer'));
|
|
1571
|
+
if (ws.readyState === WebSocket.OPEN) sendTerminalOpen();
|
|
1625
1572
|
}
|
|
1626
1573
|
|
|
1627
|
-
function
|
|
1628
|
-
if (term)
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
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();
|
|
1574
|
+
function sendTerminalOpen() {
|
|
1575
|
+
if (!term || !fitAddon) return;
|
|
1576
|
+
fitAddon.fit();
|
|
1577
|
+
ws.send(JSON.stringify({ type: 'terminal_open', cols: term.cols, rows: term.rows }));
|
|
1578
|
+
term.focus();
|
|
1643
1579
|
}
|
|
1644
1580
|
|
|
1645
|
-
|
|
1646
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
1647
|
-
ws.send(JSON.stringify({ type: 'terminal_input', data: ' ' }));
|
|
1648
|
-
}
|
|
1649
|
-
if (term) term.focus();
|
|
1650
|
-
}
|
|
1581
|
+
ws.onopen = () => { if (termInitialized) sendTerminalOpen(); };
|
|
1651
1582
|
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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(()=>{});
|
|
1682
|
-
|
|
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){
|
|
1583
|
+
ws.onmessage = (e) => {
|
|
1584
|
+
const d = JSON.parse(e.data);
|
|
1585
|
+
if (d.type === 'peer_connected') {
|
|
1586
|
+
document.getElementById('dot').className = 'dot on';
|
|
1587
|
+
document.getElementById('statusText').textContent = 'Phone connected';
|
|
1588
|
+
} else if (d.type === 'peer_disconnected') {
|
|
1589
|
+
document.getElementById('dot').className = 'dot wait';
|
|
1590
|
+
document.getElementById('statusText').textContent = 'Phone disconnected';
|
|
1591
|
+
} else if (d.type === 'terminal_configured') {
|
|
1592
|
+
document.getElementById('launchText').textContent = 'Launch: ' + d.terminalLaunch;
|
|
1593
|
+
} else if (d.type === 'terminal_output' && term) {
|
|
1702
1594
|
term.write(d.data);
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
// Terminal stream ended
|
|
1706
|
-
}
|
|
1707
|
-
else if(d.type==='terminal_exit' && term){
|
|
1708
|
-
const detail = typeof d.exitCode === 'number' ? \`exit \${d.exitCode}\` : 'terminated';
|
|
1595
|
+
} else if (d.type === 'terminal_exit' && term) {
|
|
1596
|
+
const code = typeof d.exitCode === 'number' ? \`exit \${d.exitCode}\` : 'terminated';
|
|
1709
1597
|
term.writeln(\`\r
|
|
1710
|
-
[terminal \${
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1598
|
+
[terminal \${code}]\`);
|
|
1599
|
+
} else if (d.type === 'stream_chunk') {
|
|
1600
|
+
const msgs = document.getElementById('messages');
|
|
1601
|
+
if (msgs) {
|
|
1602
|
+
let last = msgs.lastElementChild;
|
|
1603
|
+
if (!last || last.dataset.streaming !== 'true') {
|
|
1604
|
+
last = document.createElement('div');
|
|
1605
|
+
last.className = 'msg assistant';
|
|
1606
|
+
last.dataset.streaming = 'true';
|
|
1607
|
+
msgs.appendChild(last);
|
|
1608
|
+
}
|
|
1609
|
+
last.textContent += d.data;
|
|
1610
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
1611
|
+
}
|
|
1612
|
+
} else if (d.type === 'stream_end') {
|
|
1613
|
+
const msgs = document.getElementById('messages');
|
|
1614
|
+
if (msgs?.lastElementChild) delete msgs.lastElementChild.dataset.streaming;
|
|
1615
|
+
} else if (d.type === 'message') {
|
|
1616
|
+
const msgs = document.getElementById('messages');
|
|
1617
|
+
if (msgs) {
|
|
1618
|
+
const el = document.createElement('div');
|
|
1619
|
+
el.className = \`msg \${d.role}\`;
|
|
1620
|
+
el.textContent = d.content;
|
|
1621
|
+
msgs.appendChild(el);
|
|
1622
|
+
msgs.scrollTop = msgs.scrollHeight;
|
|
1721
1623
|
}
|
|
1722
1624
|
}
|
|
1723
1625
|
};
|
|
1724
1626
|
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
if(d.
|
|
1728
|
-
|
|
1729
|
-
document.getElementById('
|
|
1730
|
-
document.getElementById('
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
document.getElementById('
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
switchToMode('terminal');
|
|
1627
|
+
fetch('/api/status').then(r => r.json()).then(d => {
|
|
1628
|
+
if (d.terminalLaunch) document.getElementById('launchText').textContent = 'Launch: ' + d.terminalLaunch;
|
|
1629
|
+
if (d.pairingCode) {
|
|
1630
|
+
document.getElementById('pairingCard').style.display = '';
|
|
1631
|
+
document.getElementById('pairingCode').textContent = d.pairingCode;
|
|
1632
|
+
if (d.pairingQR) document.getElementById('qrCode').src = d.pairingQR;
|
|
1633
|
+
}
|
|
1634
|
+
document.getElementById('dot').className = d.connected ? 'dot on' : 'dot wait';
|
|
1635
|
+
document.getElementById('statusText').textContent = d.connected ? 'Phone connected' : 'Waiting for phone...';
|
|
1636
|
+
if (d.adapter) {
|
|
1637
|
+
document.getElementById('chatSection').style.display = '';
|
|
1638
|
+
(d.messages || []).forEach(m => {
|
|
1639
|
+
const msgs = document.getElementById('messages');
|
|
1640
|
+
const el = document.createElement('div');
|
|
1641
|
+
el.className = \`msg \${m.role}\`;
|
|
1642
|
+
el.textContent = m.content;
|
|
1643
|
+
msgs.appendChild(el);
|
|
1644
|
+
});
|
|
1744
1645
|
} else {
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
document.getElementById('chatCard').style.display = 'block';
|
|
1748
|
-
}
|
|
1646
|
+
document.getElementById('terminalSection').style.display = '';
|
|
1647
|
+
initTerminal();
|
|
1749
1648
|
}
|
|
1750
1649
|
});
|
|
1751
1650
|
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1651
|
+
fetch('/api/cli-presets').then(r => r.json()).then(presets => {
|
|
1652
|
+
cliPresets = presets;
|
|
1653
|
+
const sel = document.getElementById('cliPreset');
|
|
1654
|
+
const shellOpt = document.createElement('option');
|
|
1655
|
+
shellOpt.value = 'shell'; shellOpt.textContent = 'Shell (default)';
|
|
1656
|
+
sel.appendChild(shellOpt);
|
|
1657
|
+
presets.forEach(p => {
|
|
1658
|
+
const o = document.createElement('option');
|
|
1659
|
+
o.value = p.id; o.textContent = p.name;
|
|
1660
|
+
sel.appendChild(o);
|
|
1762
1661
|
});
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
document.getElementById('
|
|
1662
|
+
sel.addEventListener('change', () => {
|
|
1663
|
+
const id = sel.value;
|
|
1664
|
+
const p = cliPresets.find(x => x.id === id);
|
|
1665
|
+
const cmd = document.getElementById('command');
|
|
1666
|
+
const desc = document.getElementById('presetDesc');
|
|
1667
|
+
if (id === 'shell') { cmd.style.display = 'none'; cmd.value = ''; desc.textContent = 'Interactive login shell.'; return; }
|
|
1668
|
+
if (id === 'custom') { cmd.style.display = ''; desc.textContent = 'Enter the exact launch command.'; return; }
|
|
1669
|
+
cmd.style.display = 'none';
|
|
1670
|
+
desc.textContent = p ? p.description : '';
|
|
1671
|
+
});
|
|
1672
|
+
sel.value = 'shell'; sel.dispatchEvent(new Event('change'));
|
|
1673
|
+
return fetch('/api/config').then(r => r.json());
|
|
1674
|
+
}).then(cfg => {
|
|
1675
|
+
if (!cfg?.saved) return;
|
|
1676
|
+
const { saved } = cfg;
|
|
1677
|
+
if (saved.type === 'terminal' || saved.type === 'cli') {
|
|
1678
|
+
if (saved.preset) document.getElementById('cliPreset').value = saved.preset;
|
|
1679
|
+
if (saved.command) document.getElementById('command').value = saved.command;
|
|
1680
|
+
document.getElementById('cliPreset').dispatchEvent(new Event('change'));
|
|
1768
1681
|
}
|
|
1769
|
-
}
|
|
1682
|
+
}).catch(() => {});
|
|
1770
1683
|
|
|
1771
|
-
async
|
|
1684
|
+
document.getElementById('applyLaunchBtn').addEventListener('click', async () => {
|
|
1772
1685
|
const preset = document.getElementById('cliPreset').value;
|
|
1773
1686
|
const command = document.getElementById('command').value;
|
|
1774
|
-
|
|
1775
|
-
const
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1687
|
+
const r = await fetch('/api/configure', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'cli', preset, command }) });
|
|
1688
|
+
const d = await r.json();
|
|
1689
|
+
if (d.error) alert(d.error);
|
|
1690
|
+
});
|
|
1691
|
+
|
|
1692
|
+
document.getElementById('focusTermBtn').addEventListener('click', () => term?.focus());
|
|
1693
|
+
document.getElementById('ctrlCBtn').addEventListener('click', () => {
|
|
1694
|
+
term?.focus();
|
|
1695
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: '' }));
|
|
1696
|
+
});
|
|
1697
|
+
document.getElementById('escBtn').addEventListener('click', () => {
|
|
1698
|
+
term?.focus();
|
|
1699
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: '\x1B' }));
|
|
1700
|
+
});
|
|
1701
|
+
document.getElementById('tabBtn').addEventListener('click', () => {
|
|
1702
|
+
term?.focus();
|
|
1703
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'terminal_input', data: ' ' }));
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
document.getElementById('sendBtn').addEventListener('click', sendMessage);
|
|
1707
|
+
document.getElementById('messageInput').addEventListener('keydown', (e) => {
|
|
1708
|
+
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
|
|
1709
|
+
});
|
|
1785
1710
|
|
|
1786
|
-
// Chat functionality
|
|
1787
1711
|
function sendMessage() {
|
|
1788
1712
|
const input = document.getElementById('messageInput');
|
|
1789
1713
|
const content = input.value.trim();
|
|
1790
1714
|
if (!content) return;
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
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
|
-
});
|
|
1715
|
+
fetch('/api/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) })
|
|
1716
|
+
.then(() => { input.value = ''; })
|
|
1717
|
+
.catch(err => console.error('Failed to send:', err));
|
|
1801
1718
|
}
|
|
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
1719
|
</script>
|
|
1811
1720
|
</body>
|
|
1812
1721
|
</html>`;
|
|
1813
1722
|
|
|
1814
1723
|
// src/index.ts
|
|
1724
|
+
var QRCode = null;
|
|
1725
|
+
async function getQRCode() {
|
|
1726
|
+
if (!QRCode) QRCode = await import("qrcode");
|
|
1727
|
+
return QRCode;
|
|
1728
|
+
}
|
|
1729
|
+
console.log("[host] Module loaded");
|
|
1815
1730
|
function parseArgs(argv) {
|
|
1816
1731
|
const args = {};
|
|
1817
1732
|
const rest = argv.slice(2);
|
|
@@ -1873,6 +1788,7 @@ if (cliArgs.help) {
|
|
|
1873
1788
|
var DEFAULT_ABLY_KEY = "SfHSAQ.IRTOQQ:FBbi9a7ZV6jIu0Gdo_UeYhIN4rzpMrud5-LldURNh9s";
|
|
1874
1789
|
var DEFAULT_VIEWER_URL = "https://bobstrogg.github.io/Airloom/";
|
|
1875
1790
|
var VIEWER_URL = process.env.VIEWER_URL ?? DEFAULT_VIEWER_URL;
|
|
1791
|
+
var IS_DEV = !process.env.VIEWER_URL && !new URL(import.meta.url).pathname.includes("node_modules");
|
|
1876
1792
|
var RELAY_URL = process.env.RELAY_URL;
|
|
1877
1793
|
var ABLY_API_KEY = process.env.ABLY_API_KEY ?? (RELAY_URL ? void 0 : DEFAULT_ABLY_KEY);
|
|
1878
1794
|
var ABLY_TOKEN_TTL = parseInt(process.env.ABLY_TOKEN_TTL ?? String(24 * 60 * 60 * 1e3), 10);
|
|
@@ -1968,7 +1884,10 @@ async function main() {
|
|
|
1968
1884
|
connected: false,
|
|
1969
1885
|
terminalLaunch,
|
|
1970
1886
|
terminalLaunchCommand: launchCommand,
|
|
1971
|
-
messages: []
|
|
1887
|
+
messages: [],
|
|
1888
|
+
sessionToken: session.sessionToken,
|
|
1889
|
+
ablyToken: useAbly ? pairingData.token : void 0,
|
|
1890
|
+
transport: useAbly ? "ably" : "ws"
|
|
1972
1891
|
};
|
|
1973
1892
|
console.log(`[host] Terminal launch: ${terminalLaunch}`);
|
|
1974
1893
|
if (cliArgs.cli || cliArgs.preset) {
|
|
@@ -2034,31 +1953,37 @@ async function main() {
|
|
|
2034
1953
|
const { server, broadcast, port } = await createHostServer({ port: HOST_PORT, state, viewerDir });
|
|
2035
1954
|
const pairingBase64 = Buffer.from(pairingJSON).toString("base64url");
|
|
2036
1955
|
const viewerBase = VIEWER_URL.replace(/\/+$/, "");
|
|
2037
|
-
const
|
|
1956
|
+
const pagesUrl = `${viewerBase}/#${pairingBase64}`;
|
|
2038
1957
|
const lanIP = getLanIP();
|
|
2039
1958
|
const lanHost = lanIP ?? "localhost";
|
|
2040
1959
|
const lanBaseUrl = `http://${lanHost}:${port}`;
|
|
2041
1960
|
const lanViewerUrl = viewerDir ? `${lanBaseUrl}/viewer/#${pairingBase64}` : null;
|
|
2042
|
-
const
|
|
2043
|
-
const
|
|
1961
|
+
const qrTarget = IS_DEV && lanViewerUrl ? lanViewerUrl : pagesUrl;
|
|
1962
|
+
const qrcode = await getQRCode();
|
|
1963
|
+
const qrDataUrl = await qrcode.toDataURL(qrTarget, { width: 300, margin: 2 });
|
|
1964
|
+
const qrTerminal = await qrcode.toString(qrTarget, { type: "terminal", small: true });
|
|
2044
1965
|
state.pairingQR = qrDataUrl;
|
|
2045
1966
|
console.log("\nPairing QR Code:");
|
|
2046
1967
|
console.log(qrTerminal);
|
|
2047
1968
|
console.log(`Pairing Code: ${displayCode}`);
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
console.log(`
|
|
1969
|
+
if (IS_DEV && lanViewerUrl) {
|
|
1970
|
+
console.log(`Viewer URL (LAN/dev): ${lanViewerUrl}`);
|
|
1971
|
+
console.log(`Pages URL: ${pagesUrl}`);
|
|
1972
|
+
} else {
|
|
1973
|
+
console.log(`Viewer URL: ${pagesUrl}`);
|
|
1974
|
+
if (lanViewerUrl) console.log(`LAN Viewer: ${lanViewerUrl}`);
|
|
2051
1975
|
}
|
|
2052
1976
|
if (!useAbly) console.log(`Relay: ${RELAY_URL}`);
|
|
2053
1977
|
const localUrl = `http://localhost:${port}`;
|
|
2054
1978
|
console.log(`[host] Web UI at ${localUrl}
|
|
2055
1979
|
`);
|
|
2056
|
-
import("child_process").then(({ exec }) => {
|
|
1980
|
+
import("node:child_process").then(({ exec }) => {
|
|
2057
1981
|
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2058
1982
|
exec(`${cmd} ${localUrl}`);
|
|
2059
1983
|
}).catch(() => {
|
|
2060
1984
|
});
|
|
2061
|
-
const terminal = new TerminalSession(channel, () => state.terminalLaunchCommand);
|
|
1985
|
+
const terminal = new TerminalSession(channel, () => state.terminalLaunchCommand, broadcast);
|
|
1986
|
+
state.terminal = terminal;
|
|
2062
1987
|
channel.on("ready", () => {
|
|
2063
1988
|
console.log("[host] Phone connected! Channel ready.");
|
|
2064
1989
|
state.connected = true;
|
|
@@ -2067,11 +1992,12 @@ async function main() {
|
|
|
2067
1992
|
channel.on("peer_left", () => {
|
|
2068
1993
|
console.log("[host] Phone disconnected.");
|
|
2069
1994
|
state.connected = false;
|
|
2070
|
-
terminal.
|
|
1995
|
+
terminal.detachStream();
|
|
2071
1996
|
broadcast({ type: "peer_disconnected" });
|
|
2072
1997
|
});
|
|
2073
1998
|
channel.on("message", (data) => {
|
|
2074
1999
|
if (isTerminalMessage(data)) {
|
|
2000
|
+
console.log("[host] Terminal message from phone:", data.type);
|
|
2075
2001
|
terminal.handleMessage(data);
|
|
2076
2002
|
return;
|
|
2077
2003
|
}
|
|
@@ -2093,7 +2019,7 @@ async function main() {
|
|
|
2093
2019
|
if (shuttingDown) return;
|
|
2094
2020
|
shuttingDown = true;
|
|
2095
2021
|
console.log("\n[host] Shutting down...");
|
|
2096
|
-
terminal.
|
|
2022
|
+
terminal.destroy();
|
|
2097
2023
|
state.adapter?.destroy?.();
|
|
2098
2024
|
try {
|
|
2099
2025
|
channel.close();
|