first commit
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
// pigo remote-control SPA.
|
||||
//
|
||||
// Connects to /ws, renders streamed session output, submits prompts, and
|
||||
// answers confirmation requests. Vanilla JS, no build step — the server embeds
|
||||
// this file and serves it as a static asset.
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var out = document.getElementById("output");
|
||||
var form = document.getElementById("composer");
|
||||
var prompt = document.getElementById("prompt");
|
||||
var sendBtn = document.getElementById("send");
|
||||
var dot = document.getElementById("dot");
|
||||
var statusText = document.getElementById("statusText");
|
||||
|
||||
var confirmEl = document.getElementById("confirm");
|
||||
var confirmTool = document.getElementById("confirmTool");
|
||||
var confirmSummary = document.getElementById("confirmSummary");
|
||||
var confirmAlways = document.getElementById("confirmAlways");
|
||||
var btnApprove = document.getElementById("btnApprove");
|
||||
var btnReject = document.getElementById("btnReject");
|
||||
|
||||
var ws = null;
|
||||
var backoff = 500; // ms, exponential up to 10s
|
||||
var pendingConfirmId = null;
|
||||
|
||||
// Strip ANSI escape sequences so terminal color codes don't clutter mobile.
|
||||
var ansi = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
||||
function clean(s) { return s.replace(ansi, ""); }
|
||||
|
||||
function atBottom() {
|
||||
return out.scrollHeight - out.scrollTop - out.clientHeight < 40;
|
||||
}
|
||||
function appendOutput(text) {
|
||||
var stick = atBottom();
|
||||
out.appendChild(document.createTextNode(clean(text)));
|
||||
// Cap the scrollback so long sessions don't exhaust memory.
|
||||
while (out.childNodes.length > 4000) {
|
||||
out.removeChild(out.firstChild);
|
||||
}
|
||||
if (stick) out.scrollTop = out.scrollHeight;
|
||||
}
|
||||
|
||||
function setStatus(state, label) {
|
||||
dot.className = state === "on" ? "on" : state === "off" ? "off" : "";
|
||||
statusText.textContent = label;
|
||||
var live = state === "on";
|
||||
prompt.disabled = !live;
|
||||
sendBtn.disabled = !live;
|
||||
}
|
||||
|
||||
function send(obj) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify(obj));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function showConfirm(frame) {
|
||||
pendingConfirmId = frame.confirmId;
|
||||
confirmTool.textContent = frame.tool || "tool";
|
||||
confirmSummary.textContent = clean(frame.summary || "");
|
||||
confirmAlways.checked = false;
|
||||
confirmEl.classList.add("show");
|
||||
}
|
||||
function hideConfirm() {
|
||||
confirmEl.classList.remove("show");
|
||||
pendingConfirmId = null;
|
||||
}
|
||||
function decide(approve) {
|
||||
if (pendingConfirmId == null) return;
|
||||
send({ type: "decide", confirmId: pendingConfirmId, approve: approve, always: confirmAlways.checked });
|
||||
hideConfirm();
|
||||
}
|
||||
btnApprove.addEventListener("click", function () { decide(true); });
|
||||
btnReject.addEventListener("click", function () { decide(false); });
|
||||
|
||||
function handleFrame(frame) {
|
||||
switch (frame.type) {
|
||||
case "output":
|
||||
appendOutput(frame.text || "");
|
||||
break;
|
||||
case "confirm":
|
||||
showConfirm(frame);
|
||||
break;
|
||||
case "status":
|
||||
if (frame.state === "connected") {
|
||||
setStatus("on", "connected");
|
||||
} else if (frame.state === "ended") {
|
||||
setStatus("off", "session ended");
|
||||
} else if (frame.state === "disconnected") {
|
||||
setStatus("off", frame.reason || "disconnected");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
var proto = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
ws = new WebSocket(proto + "//" + location.host + "/ws");
|
||||
|
||||
ws.onopen = function () {
|
||||
backoff = 500;
|
||||
setStatus("on", "connected");
|
||||
};
|
||||
ws.onmessage = function (ev) {
|
||||
var frame;
|
||||
try { frame = JSON.parse(ev.data); } catch (e) { return; }
|
||||
handleFrame(frame);
|
||||
};
|
||||
ws.onclose = function () {
|
||||
setStatus("off", "reconnecting…");
|
||||
hideConfirm();
|
||||
scheduleReconnect();
|
||||
};
|
||||
ws.onerror = function () {
|
||||
try { ws.close(); } catch (e) {}
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleReconnect() {
|
||||
setTimeout(function () {
|
||||
backoff = Math.min(backoff * 2, 10000);
|
||||
connect();
|
||||
}, backoff);
|
||||
}
|
||||
|
||||
// Auto-grow the textarea and submit on Enter (Shift+Enter = newline).
|
||||
prompt.addEventListener("input", function () {
|
||||
prompt.style.height = "auto";
|
||||
prompt.style.height = Math.min(prompt.scrollHeight, window.innerHeight * 0.4) + "px";
|
||||
});
|
||||
prompt.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
form.requestSubmit();
|
||||
}
|
||||
});
|
||||
|
||||
form.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
var text = prompt.value;
|
||||
if (!text.trim()) return;
|
||||
if (send({ type: "input", text: text })) {
|
||||
prompt.value = "";
|
||||
prompt.style.height = "auto";
|
||||
}
|
||||
});
|
||||
|
||||
setStatus("", "connecting…");
|
||||
connect();
|
||||
})();
|
||||
Reference in New Issue
Block a user