index.html 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Terminal</title>
  7. <script src="../script/jquery.min.js"></script>
  8. <script src="../script/ao_module.js"></script>
  9. <style>
  10. *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  11. :root {
  12. --bg: #1e1e1e;
  13. --bg2: #252526;
  14. --bg3: #2d2d2d;
  15. --border: #3c3c3c;
  16. --fg: #d4d4d4;
  17. --fg-dim: #858585;
  18. --green: #4ec94e;
  19. --red: #f44747;
  20. --yellow: #dcdcaa;
  21. --blue: #569cd6;
  22. --orange: #ce9178;
  23. --prompt: #4ec94e;
  24. }
  25. html, body {
  26. height: 100%;
  27. background: var(--bg);
  28. color: var(--fg);
  29. font-family: 'Cascadia Code', 'Consolas', 'Courier New', monospace;
  30. font-size: 13px;
  31. overflow: hidden;
  32. }
  33. /* ── Shell layout ──────────────────────────────────────── */
  34. .shell {
  35. display: flex;
  36. flex-direction: column;
  37. height: 100vh;
  38. }
  39. /* ── Toolbar ───────────────────────────────────────────── */
  40. .toolbar {
  41. flex-shrink: 0;
  42. background: var(--bg3);
  43. border-bottom: 1px solid var(--border);
  44. display: flex;
  45. align-items: center;
  46. gap: 4px;
  47. padding: 4px 10px;
  48. height: 34px;
  49. }
  50. .toolbar-title {
  51. font-size: 12px;
  52. color: var(--fg-dim);
  53. margin-right: 6px;
  54. white-space: nowrap;
  55. }
  56. .tb-btn {
  57. padding: 2px 10px;
  58. background: transparent;
  59. border: 1px solid var(--border);
  60. color: var(--fg-dim);
  61. cursor: pointer;
  62. border-radius: 3px;
  63. font-family: inherit;
  64. font-size: 11px;
  65. transition: background 0.1s, color 0.1s;
  66. white-space: nowrap;
  67. }
  68. .tb-btn:hover { background: var(--bg2); color: var(--fg); }
  69. .conn-badge {
  70. margin-left: auto;
  71. display: flex;
  72. align-items: center;
  73. gap: 5px;
  74. font-size: 11px;
  75. color: var(--fg-dim);
  76. }
  77. .conn-dot {
  78. width: 7px; height: 7px;
  79. border-radius: 50%;
  80. background: var(--fg-dim);
  81. flex-shrink: 0;
  82. transition: background 0.2s;
  83. }
  84. .conn-dot.connecting { background: var(--yellow); animation: blink 1s ease-in-out infinite; }
  85. .conn-dot.connected { background: var(--green); }
  86. .conn-dot.error { background: var(--red); }
  87. @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
  88. /* ── Output area ───────────────────────────────────────── */
  89. .term-output {
  90. flex: 1;
  91. overflow-y: auto;
  92. padding: 10px 14px 6px;
  93. cursor: text;
  94. /* custom scrollbar */
  95. scrollbar-width: thin;
  96. scrollbar-color: var(--border) transparent;
  97. }
  98. .term-output::-webkit-scrollbar { width: 6px; }
  99. .term-output::-webkit-scrollbar-track { background: transparent; }
  100. .term-output::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
  101. /* ── Lines ─────────────────────────────────────────────── */
  102. .tl {
  103. line-height: 1.65;
  104. white-space: pre-wrap;
  105. word-break: break-all;
  106. min-height: 1.65em;
  107. }
  108. /* type variants */
  109. .tl-in { color: var(--fg); } /* user input echo */
  110. .tl-ok { color: var(--green); } /* successful result */
  111. .tl-err { color: var(--red); } /* error result */
  112. .tl-log { color: var(--yellow); } /* console.log */
  113. .tl-sys { color: var(--blue); } /* system / session messages */
  114. .tl-file{ color: var(--orange); } /* file preview lines */
  115. .tl-dim { color: var(--fg-dim); } /* decorative separators */
  116. /* Blinking cursor on the last line when idle */
  117. .tl-cursor::after {
  118. content: '▋';
  119. animation: blink 1s step-end infinite;
  120. color: var(--prompt);
  121. }
  122. /* ── Pending file banner ───────────────────────────────── */
  123. .pending-banner {
  124. display: none;
  125. background: #2a2d2e;
  126. border-top: 1px solid var(--border);
  127. border-bottom: 1px solid var(--border);
  128. padding: 5px 14px;
  129. font-size: 11px;
  130. color: var(--yellow);
  131. flex-shrink: 0;
  132. }
  133. /* ── Input row ─────────────────────────────────────────── */
  134. .term-input-row {
  135. flex-shrink: 0;
  136. display: flex;
  137. align-items: center;
  138. padding: 7px 14px;
  139. background: var(--bg2);
  140. border-top: 1px solid var(--border);
  141. gap: 8px;
  142. }
  143. .term-prompt {
  144. color: var(--prompt);
  145. flex-shrink: 0;
  146. user-select: none;
  147. font-size: 13px;
  148. }
  149. .term-input {
  150. flex: 1;
  151. background: transparent;
  152. border: none;
  153. color: var(--fg);
  154. font-family: inherit;
  155. font-size: 13px;
  156. outline: none;
  157. caret-color: var(--prompt);
  158. }
  159. .term-input::placeholder { color: var(--fg-dim); opacity: 0.5; }
  160. .run-btn {
  161. padding: 3px 12px;
  162. background: transparent;
  163. border: 1px solid var(--border);
  164. color: var(--fg-dim);
  165. cursor: pointer;
  166. border-radius: 3px;
  167. font-family: inherit;
  168. font-size: 11px;
  169. flex-shrink: 0;
  170. }
  171. .run-btn:hover { background: var(--bg3); color: var(--green); border-color: var(--green); }
  172. </style>
  173. </head>
  174. <body>
  175. <div class="shell">
  176. <!-- Toolbar -->
  177. <div class="toolbar">
  178. <span class="toolbar-title">&#9654; AGI Terminal</span>
  179. <button class="tb-btn" onclick="clearOutput()">Clear</button>
  180. <button class="tb-btn" onclick="reconnect()">Reconnect</button>
  181. <button class="tb-btn" onclick="showHelp()">Help</button>
  182. <div class="conn-badge">
  183. <span class="conn-dot connecting" id="connDot"></span>
  184. <span id="connLabel">Connecting&hellip;</span>
  185. </div>
  186. </div>
  187. <!-- Output -->
  188. <div class="term-output" id="termOutput" onclick="focusInput()"></div>
  189. <!-- Pending-file banner -->
  190. <div class="pending-banner" id="pendingBanner">
  191. &#9658; File loaded &mdash; press <kbd style="background:#3c3c3c;padding:0 4px;border-radius:2px">Enter</kbd> to run, or type a new command to cancel
  192. </div>
  193. <!-- Input row -->
  194. <div class="term-input-row">
  195. <span class="term-prompt">&gt;</span>
  196. <input class="term-input" id="termInput" type="text"
  197. autocomplete="off" autocorrect="off" spellcheck="false"
  198. placeholder="JavaScript / AGI&hellip;"
  199. onkeydown="handleKey(event)">
  200. <button class="run-btn" onclick="submitInput()">&#9654; Run</button>
  201. </div>
  202. </div>
  203. <script>
  204. // ── State ─────────────────────────────────────────────────────────
  205. var ws = null;
  206. var cmdHistory = [];
  207. var histIdx = -1;
  208. var pendingCode = null; // code loaded from a file, awaiting confirm
  209. var sessionReady = false;
  210. var pendingFiles = [];
  211. // ── WebSocket helpers ─────────────────────────────────────────────
  212. function wsEndpoint() {
  213. var proto = location.protocol === "https:" ? "wss://" : "ws://";
  214. return proto + location.hostname + ":" + location.port;
  215. }
  216. function connect() {
  217. setConn("connecting", "Connecting…");
  218. ws = new WebSocket(wsEndpoint() + "/system/ajgi/interface?script=Terminal/backend/session.agi");
  219. ws.onopen = function() {
  220. // Wait for "ready" message before marking connected
  221. };
  222. ws.onmessage = function(e) {
  223. var msg;
  224. try { msg = JSON.parse(e.data); } catch(_) { return; }
  225. handleServerMsg(msg);
  226. };
  227. ws.onclose = function() {
  228. sessionReady = false;
  229. setConn("error", "Disconnected");
  230. line("sys", "# Session closed. Click Reconnect to start a new one.");
  231. ws = null;
  232. };
  233. ws.onerror = function() {
  234. setConn("error", "Connection error");
  235. };
  236. }
  237. function send(obj) {
  238. if (ws && ws.readyState === WebSocket.OPEN) {
  239. ws.send(JSON.stringify(obj));
  240. }
  241. }
  242. // Keep session alive
  243. setInterval(function() {
  244. if (ws && ws.readyState === WebSocket.OPEN) send({ type: "ping" });
  245. }, 30000);
  246. // ── Server message handler ────────────────────────────────────────
  247. function handleServerMsg(msg) {
  248. if (msg.type === "ready") {
  249. sessionReady = true;
  250. setConn("connected", "Connected — " + msg.user);
  251. line("sys", "# AGI Terminal — ArozOS " + (msg.build || "") + " | user: " + msg.user);
  252. line("sys", "# All AGI globals and requirelib() are available.");
  253. line("sys", "# Type .help for commands. Variables persist for this session.");
  254. line("dim", "");
  255. // Now it is safe to load any files passed at launch
  256. if (pendingFiles.length > 0) {
  257. pendingFiles.forEach(function(vpath) { loadFile(vpath); });
  258. pendingFiles = [];
  259. }
  260. focusInput();
  261. return;
  262. }
  263. if (msg.type === "result") {
  264. // console.log lines
  265. if (msg.logs && msg.logs.length > 0) {
  266. msg.logs.forEach(function(l) { line("log", "· " + l); });
  267. }
  268. // result value / error
  269. if (msg.error) {
  270. line("err", "✕ " + msg.output);
  271. } else if (msg.output !== "" && msg.output !== undefined) {
  272. // Pretty-print multi-line results (e.g. JSON.stringify'd arrays)
  273. var lines = String(msg.output).split("\n");
  274. lines.forEach(function(l, i) {
  275. line("ok", (i === 0 ? "← " : " ") + l);
  276. });
  277. } else {
  278. line("dim", "← undefined");
  279. }
  280. return;
  281. }
  282. // pong: silently ignored
  283. }
  284. // ── Execution ────────────────────────────────────────────────────
  285. function execCode(code) {
  286. if (!sessionReady) {
  287. line("err", "✕ Session not ready. Please wait or click Reconnect.");
  288. return;
  289. }
  290. send({ type: "exec", code: code });
  291. }
  292. // ── Input handling ────────────────────────────────────────────────
  293. function handleKey(e) {
  294. if (e.key === "Enter") {
  295. submitInput();
  296. } else if (e.key === "ArrowUp") {
  297. e.preventDefault();
  298. if (histIdx < cmdHistory.length - 1) {
  299. histIdx++;
  300. inp().value = cmdHistory[histIdx];
  301. caretToEnd();
  302. }
  303. } else if (e.key === "ArrowDown") {
  304. e.preventDefault();
  305. if (histIdx > 0) {
  306. histIdx--;
  307. inp().value = cmdHistory[histIdx];
  308. } else {
  309. histIdx = -1;
  310. inp().value = "";
  311. }
  312. } else if (e.key === "Escape") {
  313. cancelPending();
  314. }
  315. }
  316. function submitInput() {
  317. var raw = inp().value;
  318. inp().value = "";
  319. histIdx = -1;
  320. // Empty Enter with pending file → run it
  321. if (raw.trim() === "" && pendingCode !== null) {
  322. var code = pendingCode;
  323. cancelPending();
  324. line("sys", "# Running loaded file…");
  325. line("in", "> (file)");
  326. execCode(code);
  327. return;
  328. }
  329. if (raw.trim() === "") return;
  330. // Record history (deduplicate consecutive)
  331. if (cmdHistory.length === 0 || cmdHistory[0] !== raw) {
  332. cmdHistory.unshift(raw);
  333. if (cmdHistory.length > 500) cmdHistory.pop();
  334. }
  335. // Cancel any pending file if the user typed something new
  336. if (pendingCode !== null) cancelPending();
  337. line("in", "> " + raw);
  338. // Built-in dot-commands (handled client-side)
  339. var cmd = raw.trim();
  340. if (cmd === ".help") { showHelp(); return; }
  341. else if (cmd === ".clear") { clearOutput(); return; }
  342. else if (cmd === ".reset") { reconnect(); return; }
  343. else if (cmd === ".history") { showHistory(); return; }
  344. execCode(raw);
  345. }
  346. // ── File loading ──────────────────────────────────────────────────
  347. function loadFile(vpath) {
  348. ao_module_agirun("Terminal/backend/readfile.agi", { path: vpath },
  349. function(content) {
  350. if (typeof content === "string" && content.indexOf("__ERROR__") === 0) {
  351. line("err", "✕ " + content.replace("__ERROR__", ""));
  352. return;
  353. }
  354. var codeLines = String(content).split("\n");
  355. var preview = Math.min(codeLines.length, 20);
  356. var filename = vpath.split("/").pop();
  357. line("dim", "");
  358. line("file", "# ■ Loaded: " + filename + " (" + codeLines.length + " line" + (codeLines.length !== 1 ? "s" : "") + ")");
  359. line("dim", "# " + "─".repeat(50));
  360. for (var i = 0; i < preview; i++) {
  361. var num = String(i + 1);
  362. while (num.length < 3) num = " " + num;
  363. line("file", num + " " + codeLines[i]);
  364. }
  365. if (codeLines.length > preview) {
  366. line("dim", " … and " + (codeLines.length - preview) + " more line" + (codeLines.length - preview !== 1 ? "s" : ""));
  367. }
  368. line("dim", "# " + "─".repeat(50));
  369. pendingCode = content;
  370. document.getElementById("pendingBanner").style.display = "block";
  371. focusInput();
  372. },
  373. function() {
  374. line("err", "✕ Failed to read file: " + vpath);
  375. }
  376. );
  377. }
  378. function cancelPending() {
  379. pendingCode = null;
  380. document.getElementById("pendingBanner").style.display = "none";
  381. }
  382. // ── Built-in commands ─────────────────────────────────────────────
  383. function showHelp() {
  384. line("sys", "# ── Terminal Commands ──────────────────────────────");
  385. line("sys", "# .help show this help");
  386. line("sys", "# .clear clear all output");
  387. line("sys", "# .reset reconnect and start a fresh session");
  388. line("sys", "# .history print command history");
  389. line("sys", "# ");
  390. line("sys", "# ── Key Bindings ───────────────────────────────");
  391. line("sys", "# Enter execute input (or run loaded file)");
  392. line("sys", "# ↑ / ↓ navigate command history");
  393. line("sys", "# Escape cancel loaded file");
  394. line("sys", "# ");
  395. line("sys", "# ── Tips ─────────────────────────────────────");
  396. line("sys", "# Variables persist across commands within a session.");
  397. line("sys", "# requirelib(\"filelib\") works exactly as in .agi scripts.");
  398. line("sys", "# sendResp() / echo() output is captured and returned.");
  399. line("sys", "# Open .agi files from the file manager to load them here.");
  400. }
  401. function showHistory() {
  402. if (cmdHistory.length === 0) {
  403. line("sys", "# (no history yet)");
  404. return;
  405. }
  406. cmdHistory.slice().reverse().forEach(function(cmd, i) {
  407. line("sys", "# " + String(i + 1) + " " + cmd);
  408. });
  409. }
  410. function clearOutput() {
  411. document.getElementById("termOutput").innerHTML = "";
  412. }
  413. function reconnect() {
  414. if (ws) { ws.close(); ws = null; }
  415. sessionReady = false;
  416. cancelPending();
  417. clearOutput();
  418. setTimeout(connect, 100);
  419. }
  420. // ── UI helpers ────────────────────────────────────────────────────
  421. function line(type, text) {
  422. var out = document.getElementById("termOutput");
  423. var div = document.createElement("div");
  424. var map = { in:"tl-in", ok:"tl-ok", err:"tl-err",
  425. log:"tl-log", sys:"tl-sys", file:"tl-file", dim:"tl-dim" };
  426. div.className = "tl " + (map[type] || "tl-dim");
  427. div.textContent = text;
  428. out.appendChild(div);
  429. out.scrollTop = out.scrollHeight;
  430. }
  431. function inp() { return document.getElementById("termInput"); }
  432. function focusInput() { inp().focus(); }
  433. function caretToEnd() {
  434. var el = inp();
  435. var v = el.value.length;
  436. el.setSelectionRange(v, v);
  437. }
  438. function setConn(state, label) {
  439. var dot = document.getElementById("connDot");
  440. dot.className = "conn-dot " + state;
  441. document.getElementById("connLabel").textContent = label;
  442. }
  443. // ── Boot ──────────────────────────────────────────────────────────
  444. // Collect any files passed from the file manager (URL hash)
  445. (function() {
  446. var inputFiles = ao_module_loadInputFiles();
  447. inputFiles.forEach(function(vpath) {
  448. vpath = vpath.trim();
  449. if (vpath !== "" && (vpath.match(/\.agi$/) || vpath.match(/\.js$/))) {
  450. pendingFiles.push(vpath);
  451. }
  452. });
  453. })();
  454. connect();
  455. </script>
  456. </body>
  457. </html>