|
|
@@ -0,0 +1,513 @@
|
|
|
+<!DOCTYPE html>
|
|
|
+<html>
|
|
|
+ <head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1">
|
|
|
+ <title>Terminal</title>
|
|
|
+ <script src="../script/jquery.min.js"></script>
|
|
|
+ <script src="../script/ao_module.js"></script>
|
|
|
+ <style>
|
|
|
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
+
|
|
|
+ :root {
|
|
|
+ --bg: #1e1e1e;
|
|
|
+ --bg2: #252526;
|
|
|
+ --bg3: #2d2d2d;
|
|
|
+ --border: #3c3c3c;
|
|
|
+ --fg: #d4d4d4;
|
|
|
+ --fg-dim: #858585;
|
|
|
+ --green: #4ec94e;
|
|
|
+ --red: #f44747;
|
|
|
+ --yellow: #dcdcaa;
|
|
|
+ --blue: #569cd6;
|
|
|
+ --orange: #ce9178;
|
|
|
+ --prompt: #4ec94e;
|
|
|
+ }
|
|
|
+
|
|
|
+ html, body {
|
|
|
+ height: 100%;
|
|
|
+ background: var(--bg);
|
|
|
+ color: var(--fg);
|
|
|
+ font-family: 'Cascadia Code', 'Consolas', 'Courier New', monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ── Shell layout ──────────────────────────────────────── */
|
|
|
+ .shell {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100vh;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ── Toolbar ───────────────────────────────────────────── */
|
|
|
+ .toolbar {
|
|
|
+ flex-shrink: 0;
|
|
|
+ background: var(--bg3);
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 4px 10px;
|
|
|
+ height: 34px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .toolbar-title {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--fg-dim);
|
|
|
+ margin-right: 6px;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .tb-btn {
|
|
|
+ padding: 2px 10px;
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ color: var(--fg-dim);
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 11px;
|
|
|
+ transition: background 0.1s, color 0.1s;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+ .tb-btn:hover { background: var(--bg2); color: var(--fg); }
|
|
|
+
|
|
|
+ .conn-badge {
|
|
|
+ margin-left: auto;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--fg-dim);
|
|
|
+ }
|
|
|
+ .conn-dot {
|
|
|
+ width: 7px; height: 7px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: var(--fg-dim);
|
|
|
+ flex-shrink: 0;
|
|
|
+ transition: background 0.2s;
|
|
|
+ }
|
|
|
+ .conn-dot.connecting { background: var(--yellow); animation: blink 1s ease-in-out infinite; }
|
|
|
+ .conn-dot.connected { background: var(--green); }
|
|
|
+ .conn-dot.error { background: var(--red); }
|
|
|
+ @keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
|
|
|
+
|
|
|
+ /* ── Output area ───────────────────────────────────────── */
|
|
|
+ .term-output {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px 14px 6px;
|
|
|
+ cursor: text;
|
|
|
+ /* custom scrollbar */
|
|
|
+ scrollbar-width: thin;
|
|
|
+ scrollbar-color: var(--border) transparent;
|
|
|
+ }
|
|
|
+ .term-output::-webkit-scrollbar { width: 6px; }
|
|
|
+ .term-output::-webkit-scrollbar-track { background: transparent; }
|
|
|
+ .term-output::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
|
+
|
|
|
+ /* ── Lines ─────────────────────────────────────────────── */
|
|
|
+ .tl {
|
|
|
+ line-height: 1.65;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-all;
|
|
|
+ min-height: 1.65em;
|
|
|
+ }
|
|
|
+ /* type variants */
|
|
|
+ .tl-in { color: var(--fg); } /* user input echo */
|
|
|
+ .tl-ok { color: var(--green); } /* successful result */
|
|
|
+ .tl-err { color: var(--red); } /* error result */
|
|
|
+ .tl-log { color: var(--yellow); } /* console.log */
|
|
|
+ .tl-sys { color: var(--blue); } /* system / session messages */
|
|
|
+ .tl-file{ color: var(--orange); } /* file preview lines */
|
|
|
+ .tl-dim { color: var(--fg-dim); } /* decorative separators */
|
|
|
+
|
|
|
+ /* Blinking cursor on the last line when idle */
|
|
|
+ .tl-cursor::after {
|
|
|
+ content: '▋';
|
|
|
+ animation: blink 1s step-end infinite;
|
|
|
+ color: var(--prompt);
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ── Pending file banner ───────────────────────────────── */
|
|
|
+ .pending-banner {
|
|
|
+ display: none;
|
|
|
+ background: #2a2d2e;
|
|
|
+ border-top: 1px solid var(--border);
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+ padding: 5px 14px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--yellow);
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* ── Input row ─────────────────────────────────────────── */
|
|
|
+ .term-input-row {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 7px 14px;
|
|
|
+ background: var(--bg2);
|
|
|
+ border-top: 1px solid var(--border);
|
|
|
+ gap: 8px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .term-prompt {
|
|
|
+ color: var(--prompt);
|
|
|
+ flex-shrink: 0;
|
|
|
+ user-select: none;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .term-input {
|
|
|
+ flex: 1;
|
|
|
+ background: transparent;
|
|
|
+ border: none;
|
|
|
+ color: var(--fg);
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 13px;
|
|
|
+ outline: none;
|
|
|
+ caret-color: var(--prompt);
|
|
|
+ }
|
|
|
+ .term-input::placeholder { color: var(--fg-dim); opacity: 0.5; }
|
|
|
+
|
|
|
+ .run-btn {
|
|
|
+ padding: 3px 12px;
|
|
|
+ background: transparent;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+ color: var(--fg-dim);
|
|
|
+ cursor: pointer;
|
|
|
+ border-radius: 3px;
|
|
|
+ font-family: inherit;
|
|
|
+ font-size: 11px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ .run-btn:hover { background: var(--bg3); color: var(--green); border-color: var(--green); }
|
|
|
+ </style>
|
|
|
+ </head>
|
|
|
+ <body>
|
|
|
+ <div class="shell">
|
|
|
+
|
|
|
+ <!-- Toolbar -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <span class="toolbar-title">▶ AGI Terminal</span>
|
|
|
+ <button class="tb-btn" onclick="clearOutput()">Clear</button>
|
|
|
+ <button class="tb-btn" onclick="reconnect()">Reconnect</button>
|
|
|
+ <button class="tb-btn" onclick="showHelp()">Help</button>
|
|
|
+ <div class="conn-badge">
|
|
|
+ <span class="conn-dot connecting" id="connDot"></span>
|
|
|
+ <span id="connLabel">Connecting…</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Output -->
|
|
|
+ <div class="term-output" id="termOutput" onclick="focusInput()"></div>
|
|
|
+
|
|
|
+ <!-- Pending-file banner -->
|
|
|
+ <div class="pending-banner" id="pendingBanner">
|
|
|
+ ► File loaded — press <kbd style="background:#3c3c3c;padding:0 4px;border-radius:2px">Enter</kbd> to run, or type a new command to cancel
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Input row -->
|
|
|
+ <div class="term-input-row">
|
|
|
+ <span class="term-prompt">></span>
|
|
|
+ <input class="term-input" id="termInput" type="text"
|
|
|
+ autocomplete="off" autocorrect="off" spellcheck="false"
|
|
|
+ placeholder="JavaScript / AGI…"
|
|
|
+ onkeydown="handleKey(event)">
|
|
|
+ <button class="run-btn" onclick="submitInput()">▶ Run</button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script>
|
|
|
+ // ── State ─────────────────────────────────────────────────────────
|
|
|
+ var ws = null;
|
|
|
+ var cmdHistory = [];
|
|
|
+ var histIdx = -1;
|
|
|
+ var pendingCode = null; // code loaded from a file, awaiting confirm
|
|
|
+ var sessionReady = false;
|
|
|
+ var pendingFiles = [];
|
|
|
+
|
|
|
+ // ── WebSocket helpers ─────────────────────────────────────────────
|
|
|
+ function wsEndpoint() {
|
|
|
+ var proto = location.protocol === "https:" ? "wss://" : "ws://";
|
|
|
+ return proto + location.hostname + ":" + location.port;
|
|
|
+ }
|
|
|
+
|
|
|
+ function connect() {
|
|
|
+ setConn("connecting", "Connecting…");
|
|
|
+ ws = new WebSocket(wsEndpoint() + "/system/ajgi/interface?script=Terminal/backend/session.agi");
|
|
|
+
|
|
|
+ ws.onopen = function() {
|
|
|
+ // Wait for "ready" message before marking connected
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onmessage = function(e) {
|
|
|
+ var msg;
|
|
|
+ try { msg = JSON.parse(e.data); } catch(_) { return; }
|
|
|
+ handleServerMsg(msg);
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onclose = function() {
|
|
|
+ sessionReady = false;
|
|
|
+ setConn("error", "Disconnected");
|
|
|
+ line("sys", "# Session closed. Click Reconnect to start a new one.");
|
|
|
+ ws = null;
|
|
|
+ };
|
|
|
+
|
|
|
+ ws.onerror = function() {
|
|
|
+ setConn("error", "Connection error");
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function send(obj) {
|
|
|
+ if (ws && ws.readyState === WebSocket.OPEN) {
|
|
|
+ ws.send(JSON.stringify(obj));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Keep session alive
|
|
|
+ setInterval(function() {
|
|
|
+ if (ws && ws.readyState === WebSocket.OPEN) send({ type: "ping" });
|
|
|
+ }, 30000);
|
|
|
+
|
|
|
+ // ── Server message handler ────────────────────────────────────────
|
|
|
+ function handleServerMsg(msg) {
|
|
|
+ if (msg.type === "ready") {
|
|
|
+ sessionReady = true;
|
|
|
+ setConn("connected", "Connected — " + msg.user);
|
|
|
+ line("sys", "# AGI Terminal — ArozOS " + (msg.build || "") + " | user: " + msg.user);
|
|
|
+ line("sys", "# All AGI globals and requirelib() are available.");
|
|
|
+ line("sys", "# Type .help for commands. Variables persist for this session.");
|
|
|
+ line("dim", "");
|
|
|
+ // Now it is safe to load any files passed at launch
|
|
|
+ if (pendingFiles.length > 0) {
|
|
|
+ pendingFiles.forEach(function(vpath) { loadFile(vpath); });
|
|
|
+ pendingFiles = [];
|
|
|
+ }
|
|
|
+ focusInput();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (msg.type === "result") {
|
|
|
+ // console.log lines
|
|
|
+ if (msg.logs && msg.logs.length > 0) {
|
|
|
+ msg.logs.forEach(function(l) { line("log", "· " + l); });
|
|
|
+ }
|
|
|
+ // result value / error
|
|
|
+ if (msg.error) {
|
|
|
+ line("err", "✕ " + msg.output);
|
|
|
+ } else if (msg.output !== "" && msg.output !== undefined) {
|
|
|
+ // Pretty-print multi-line results (e.g. JSON.stringify'd arrays)
|
|
|
+ var lines = String(msg.output).split("\n");
|
|
|
+ lines.forEach(function(l, i) {
|
|
|
+ line("ok", (i === 0 ? "← " : " ") + l);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ line("dim", "← undefined");
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // pong: silently ignored
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Execution ────────────────────────────────────────────────────
|
|
|
+ function execCode(code) {
|
|
|
+ if (!sessionReady) {
|
|
|
+ line("err", "✕ Session not ready. Please wait or click Reconnect.");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ send({ type: "exec", code: code });
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Input handling ────────────────────────────────────────────────
|
|
|
+ function handleKey(e) {
|
|
|
+ if (e.key === "Enter") {
|
|
|
+ submitInput();
|
|
|
+ } else if (e.key === "ArrowUp") {
|
|
|
+ e.preventDefault();
|
|
|
+ if (histIdx < cmdHistory.length - 1) {
|
|
|
+ histIdx++;
|
|
|
+ inp().value = cmdHistory[histIdx];
|
|
|
+ caretToEnd();
|
|
|
+ }
|
|
|
+ } else if (e.key === "ArrowDown") {
|
|
|
+ e.preventDefault();
|
|
|
+ if (histIdx > 0) {
|
|
|
+ histIdx--;
|
|
|
+ inp().value = cmdHistory[histIdx];
|
|
|
+ } else {
|
|
|
+ histIdx = -1;
|
|
|
+ inp().value = "";
|
|
|
+ }
|
|
|
+ } else if (e.key === "Escape") {
|
|
|
+ cancelPending();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function submitInput() {
|
|
|
+ var raw = inp().value;
|
|
|
+ inp().value = "";
|
|
|
+ histIdx = -1;
|
|
|
+
|
|
|
+ // Empty Enter with pending file → run it
|
|
|
+ if (raw.trim() === "" && pendingCode !== null) {
|
|
|
+ var code = pendingCode;
|
|
|
+ cancelPending();
|
|
|
+ line("sys", "# Running loaded file…");
|
|
|
+ line("in", "> (file)");
|
|
|
+ execCode(code);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (raw.trim() === "") return;
|
|
|
+
|
|
|
+ // Record history (deduplicate consecutive)
|
|
|
+ if (cmdHistory.length === 0 || cmdHistory[0] !== raw) {
|
|
|
+ cmdHistory.unshift(raw);
|
|
|
+ if (cmdHistory.length > 500) cmdHistory.pop();
|
|
|
+ }
|
|
|
+
|
|
|
+ // Cancel any pending file if the user typed something new
|
|
|
+ if (pendingCode !== null) cancelPending();
|
|
|
+
|
|
|
+ line("in", "> " + raw);
|
|
|
+
|
|
|
+ // Built-in dot-commands (handled client-side)
|
|
|
+ var cmd = raw.trim();
|
|
|
+ if (cmd === ".help") { showHelp(); return; }
|
|
|
+ else if (cmd === ".clear") { clearOutput(); return; }
|
|
|
+ else if (cmd === ".reset") { reconnect(); return; }
|
|
|
+ else if (cmd === ".history") { showHistory(); return; }
|
|
|
+
|
|
|
+ execCode(raw);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── File loading ──────────────────────────────────────────────────
|
|
|
+ function loadFile(vpath) {
|
|
|
+ ao_module_agirun("Terminal/backend/readfile.agi", { path: vpath },
|
|
|
+ function(content) {
|
|
|
+ if (typeof content === "string" && content.indexOf("__ERROR__") === 0) {
|
|
|
+ line("err", "✕ " + content.replace("__ERROR__", ""));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var codeLines = String(content).split("\n");
|
|
|
+ var preview = Math.min(codeLines.length, 20);
|
|
|
+ var filename = vpath.split("/").pop();
|
|
|
+
|
|
|
+ line("dim", "");
|
|
|
+ line("file", "# ■ Loaded: " + filename + " (" + codeLines.length + " line" + (codeLines.length !== 1 ? "s" : "") + ")");
|
|
|
+ line("dim", "# " + "─".repeat(50));
|
|
|
+ for (var i = 0; i < preview; i++) {
|
|
|
+ var num = String(i + 1);
|
|
|
+ while (num.length < 3) num = " " + num;
|
|
|
+ line("file", num + " " + codeLines[i]);
|
|
|
+ }
|
|
|
+ if (codeLines.length > preview) {
|
|
|
+ line("dim", " … and " + (codeLines.length - preview) + " more line" + (codeLines.length - preview !== 1 ? "s" : ""));
|
|
|
+ }
|
|
|
+ line("dim", "# " + "─".repeat(50));
|
|
|
+
|
|
|
+ pendingCode = content;
|
|
|
+ document.getElementById("pendingBanner").style.display = "block";
|
|
|
+ focusInput();
|
|
|
+ },
|
|
|
+ function() {
|
|
|
+ line("err", "✕ Failed to read file: " + vpath);
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ function cancelPending() {
|
|
|
+ pendingCode = null;
|
|
|
+ document.getElementById("pendingBanner").style.display = "none";
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Built-in commands ─────────────────────────────────────────────
|
|
|
+ function showHelp() {
|
|
|
+ line("sys", "# ── Terminal Commands ──────────────────────────────");
|
|
|
+ line("sys", "# .help show this help");
|
|
|
+ line("sys", "# .clear clear all output");
|
|
|
+ line("sys", "# .reset reconnect and start a fresh session");
|
|
|
+ line("sys", "# .history print command history");
|
|
|
+ line("sys", "# ");
|
|
|
+ line("sys", "# ── Key Bindings ───────────────────────────────");
|
|
|
+ line("sys", "# Enter execute input (or run loaded file)");
|
|
|
+ line("sys", "# ↑ / ↓ navigate command history");
|
|
|
+ line("sys", "# Escape cancel loaded file");
|
|
|
+ line("sys", "# ");
|
|
|
+ line("sys", "# ── Tips ─────────────────────────────────────");
|
|
|
+ line("sys", "# Variables persist across commands within a session.");
|
|
|
+ line("sys", "# requirelib(\"filelib\") works exactly as in .agi scripts.");
|
|
|
+ line("sys", "# sendResp() / echo() output is captured and returned.");
|
|
|
+ line("sys", "# Open .agi files from the file manager to load them here.");
|
|
|
+ }
|
|
|
+
|
|
|
+ function showHistory() {
|
|
|
+ if (cmdHistory.length === 0) {
|
|
|
+ line("sys", "# (no history yet)");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ cmdHistory.slice().reverse().forEach(function(cmd, i) {
|
|
|
+ line("sys", "# " + String(i + 1) + " " + cmd);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ function clearOutput() {
|
|
|
+ document.getElementById("termOutput").innerHTML = "";
|
|
|
+ }
|
|
|
+
|
|
|
+ function reconnect() {
|
|
|
+ if (ws) { ws.close(); ws = null; }
|
|
|
+ sessionReady = false;
|
|
|
+ cancelPending();
|
|
|
+ clearOutput();
|
|
|
+ setTimeout(connect, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── UI helpers ────────────────────────────────────────────────────
|
|
|
+ function line(type, text) {
|
|
|
+ var out = document.getElementById("termOutput");
|
|
|
+ var div = document.createElement("div");
|
|
|
+ var map = { in:"tl-in", ok:"tl-ok", err:"tl-err",
|
|
|
+ log:"tl-log", sys:"tl-sys", file:"tl-file", dim:"tl-dim" };
|
|
|
+ div.className = "tl " + (map[type] || "tl-dim");
|
|
|
+ div.textContent = text;
|
|
|
+ out.appendChild(div);
|
|
|
+ out.scrollTop = out.scrollHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ function inp() { return document.getElementById("termInput"); }
|
|
|
+
|
|
|
+ function focusInput() { inp().focus(); }
|
|
|
+
|
|
|
+ function caretToEnd() {
|
|
|
+ var el = inp();
|
|
|
+ var v = el.value.length;
|
|
|
+ el.setSelectionRange(v, v);
|
|
|
+ }
|
|
|
+
|
|
|
+ function setConn(state, label) {
|
|
|
+ var dot = document.getElementById("connDot");
|
|
|
+ dot.className = "conn-dot " + state;
|
|
|
+ document.getElementById("connLabel").textContent = label;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── Boot ──────────────────────────────────────────────────────────
|
|
|
+ // Collect any files passed from the file manager (URL hash)
|
|
|
+ (function() {
|
|
|
+ var inputFiles = ao_module_loadInputFiles();
|
|
|
+ inputFiles.forEach(function(vpath) {
|
|
|
+ vpath = vpath.trim();
|
|
|
+ if (vpath !== "" && (vpath.match(/\.agi$/) || vpath.match(/\.js$/))) {
|
|
|
+ pendingFiles.push(vpath);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ })();
|
|
|
+
|
|
|
+ connect();
|
|
|
+ </script>
|
|
|
+ </body>
|
|
|
+</html>
|