/*
Code Studio — integrated terminal
Each session is an independent WebSocket to the Terminal web app's
REPL backend (Terminal/backend/session.agi), so a session keeps one
persistent Otto VM: variables defined in one command survive into the
next, requirelib() works, and typing "bash()" drops into the virtual
file system shell — exactly like the standalone Terminal app.
Protocol (see Terminal/backend/session.agi)
client → server : {type:"exec", code} {type:"ping"} {type:"complete", line}
server → client : ready | result | shellmode | clear | pong | complete
*/
var csTerminals = []; //All live sessions
var csActiveTerminal = null; //id of the focused session
var csTerminalCounter = 0;
function terminalEndpoint(){
var scheme = (location.protocol === "https:") ? "wss://" : "ws://";
return scheme + location.host + "/system/ajgi/interface?script=Terminal/backend/session.agi";
}
function getTerminal(id){
for (var i = 0; i < csTerminals.length; i++){
if (csTerminals[i].id == id) return csTerminals[i];
}
return null;
}
function getActiveTerminal(){ return getTerminal(csActiveTerminal); }
/* ═══════════════════════════════════════════════════════════════════
Session lifecycle
═══════════════════════════════════════════════════════════════════ */
function newTerminalSession(){
csTerminalCounter++;
var session = {
id: "term" + csTerminalCounter,
index: csTerminalCounter,
name: "agi",
ready: false,
everReady: false, //true once the backend has sent its ready frame
shellMode: false,
prompt: ">",
history: [],
historyIndex: -1,
lastTabLine: null,
suppressResults: 0, //replies to editor-issued commands still to swallow
ws: null
};
$("#terminalStack").append(
'
' +
'' +
'
' +
'>' +
'' +
'
' +
'
' +
'' +
'Connecting…' +
'' +
'' +
'' +
'
' +
'
');
csTerminals.push(session);
connectTerminal(session);
renderTerminalSessionList();
selectTerminal(session.id);
togglePanel(true);
showPanelView("terminal");
return session;
}
function connectTerminal(session){
setTerminalConnection(session, "connecting", "Connecting…");
try {
session.ws = new WebSocket(terminalEndpoint());
} catch(e){
setTerminalConnection(session, "error", "Cannot open a session");
return;
}
session.ws.onmessage = function(event){
var message;
try { message = JSON.parse(event.data); } catch(e){ return; }
handleTerminalMessage(session, message);
};
session.ws.onclose = function(){
var neverStarted = !session.everReady;
session.ready = false;
setTerminalConnection(session, "error", "Disconnected");
if (neverStarted){
//The gateway authorises an AGI script by the module it belongs to,
//so the session backend needs the Terminal module permission.
termLine(session, "err", "✕ Could not start a session.");
termLine(session, "sys", "# The terminal runs the Terminal module's AGI session backend, so your");
termLine(session, "sys", "# account needs access to the Terminal module. Ask an administrator to");
termLine(session, "sys", "# grant it, then press the refresh button below.");
} else {
termLine(session, "sys", "# Session closed. Use the refresh button to start a new one.");
}
};
session.ws.onerror = function(){
setTerminalConnection(session, "error", "Connection error");
};
}
function reconnectTerminal(id){
var session = getTerminal(id);
if (!session) return;
if (session.ws){
try { session.ws.close(); } catch(e){ /* already closed */ }
}
connectTerminal(session);
}
function killActiveTerminal(){
if (!csActiveTerminal) return;
killTerminal(csActiveTerminal);
}
function killTerminal(id){
var session = getTerminal(id);
if (!session) return;
if (session.ws){
try { session.ws.close(); } catch(e){ /* already closed */ }
}
$('.terminstance[data-term="' + id + '"]').remove();
csTerminals = csTerminals.filter(function(entry){ return entry.id != id; });
if (csTerminals.length > 0){
selectTerminal(csTerminals[csTerminals.length - 1].id);
} else {
csActiveTerminal = null;
}
renderTerminalSessionList();
}
function selectTerminal(id){
csActiveTerminal = id;
$(".terminstance").removeClass("active");
$('.terminstance[data-term="' + id + '"]').addClass("active");
$("#terminalSessions .row").removeClass("selected");
$('#terminalSessions .row[data-term="' + id + '"]').addClass("selected");
focusTerminalInput(id);
}
function renderTerminalSessionList(){
var html = "";
csTerminals.forEach(function(session){
html += '