| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- <!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>
|