diff --git a/frontend/index.html b/frontend/index.html
index bdbe54b..9772dfc 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -61,6 +61,7 @@
class="sidebar-title sidebar-mode-toggle"
id="mode-leviathan"
type="button"
+ hidden
>
Leviathan
diff --git a/frontend/scripts/test-regressions.js b/frontend/scripts/test-regressions.js
index 0d19032..7de226d 100644
--- a/frontend/scripts/test-regressions.js
+++ b/frontend/scripts/test-regressions.js
@@ -23,6 +23,17 @@ function assertNotContains(file, needle, message) {
}
}
+function assertBefore(file, first, second, message) {
+ const text = read(file);
+ const firstIndex = text.indexOf(first);
+ const secondIndex = text.indexOf(second);
+ if (firstIndex === -1 || secondIndex === -1 || firstIndex >= secondIndex) {
+ throw new Error(
+ `${message}\nExpected ${JSON.stringify(first)} before ${JSON.stringify(second)} in ${file}`,
+ );
+ }
+}
+
function assertTreeNotContains(relDir, needle, message) {
const dir = path.join(repo, relDir);
const offenders = [];
@@ -74,6 +85,26 @@ assertContains(
"cancelScheduledFit",
"terminal teardown must cancel pending fit work",
);
+assertContains(
+ "frontend/src/js/terminal.js",
+ "this._sendTerminalSize(sessionId, term);",
+ "every completed terminal fit must publish its geometry to the backend PTY",
+);
+assertContains(
+ "frontend/src/js/terminal.js",
+ "[Exited - press enter to restart]",
+ "exited local shells must show the inline restart prompt",
+);
+assertContains(
+ "frontend/src/js/terminal.js",
+ "RestartLocalSession",
+ "pressing enter after a local shell exit must restart the existing session",
+);
+assertContains(
+ "frontend/src/js/app.js",
+ "!localExited &&",
+ "local shell exits must not show the disconnected overlay",
+);
assertNotContains(
"frontend/src/js/terminal.js",
"new ResizeObserver(() => {\n try {\n fitAddon.fit();",
@@ -114,6 +145,31 @@ assertContains(
"mode-leviathan",
"Leviathan mode toggle must be present beside Nexus Terminal",
);
+assertContains(
+ "frontend/index.html",
+ 'id="mode-leviathan"\n type="button"\n hidden',
+ "Leviathan mode toggle must be hidden before preferences load",
+);
+assertContains(
+ "frontend/src/js/app.js",
+ "prefs.leviathanEnabled === true",
+ "Leviathan mode must only be exposed by an explicit persisted preference",
+);
+assertContains(
+ "frontend/src/js/settings.js",
+ "KONAMI_SEQUENCE",
+ "the About window must recognise the Konami sequence",
+);
+assertContains(
+ "frontend/src/js/settings.js",
+ "_toggleLeviathanFromKonami",
+ "the About-window Konami sequence must toggle Leviathan",
+);
+assertContains(
+ "frontend/src/js/settings.js",
+ "SavePreferences(prefs)",
+ "the Konami toggle must persist the Leviathan preference immediately",
+);
assertContains(
"frontend/src/css/sidebar.css",
"user-select: none",
@@ -299,6 +355,17 @@ assertContains(
"setMode(\"leviathan\")",
"App mode switch must route to Leviathan",
);
+assertBefore(
+ "frontend/src/js/splitpane.js",
+ "localLabel.textContent = 'Local Machine';",
+ "profiles.forEach(function(profile)",
+ "split-pane connection choices must put Local Machine before saved profiles",
+);
+assertContains(
+ "frontend/src/js/splitpane.js",
+ "wailsService.ConnectLocal()",
+ "the Local Machine split-pane choice must open a real local shell",
+);
assertContains(
"internal/api/leviathan.go",
"LeviathanRunTool",
diff --git a/frontend/src/css/sidebar.css b/frontend/src/css/sidebar.css
index 2253040..0653691 100644
--- a/frontend/src/css/sidebar.css
+++ b/frontend/src/css/sidebar.css
@@ -66,6 +66,10 @@
text-align: right;
}
+#mode-leviathan[hidden] {
+ display: none;
+}
+
.sidebar-mode-toggle.is-active {
color: var(--text-primary);
}
diff --git a/frontend/src/js/app.js b/frontend/src/js/app.js
index 03faf58..5da303f 100644
--- a/frontend/src/js/app.js
+++ b/frontend/src/js/app.js
@@ -191,13 +191,23 @@
);
this.updateStatusBar();
+ const session = this.state.getSession(payload.sessionID);
+ const isLocal = session?.protocol === "local";
+ const localExited =
+ isLocal && payload.state === "disconnected";
const showDisconnectedOverlay =
- payload.state === "disconnected" || payload.state === "error";
+ !localExited &&
+ (payload.state === "disconnected" || payload.state === "error");
this.terminals.setDisconnectedOverlay(
payload.sessionID,
showDisconnectedOverlay,
payload.error,
);
+ if (localExited) {
+ this.terminals.setLocalExited(payload.sessionID, true);
+ } else if (isLocal && payload.state === "connected") {
+ this.terminals.setLocalExited(payload.sessionID, false);
+ }
if (payload.state === "error" && payload.error) {
this._handleSessionError(payload.sessionID, payload.error);
@@ -612,7 +622,10 @@
* @param {"nexus"|"leviathan"} mode
*/
setMode(mode) {
- this.mode = mode === "leviathan" ? "leviathan" : "nexus";
+ const leviathanEnabled =
+ this.state.preferences?.leviathanEnabled === true;
+ this.mode =
+ mode === "leviathan" && leviathanEnabled ? "leviathan" : "nexus";
const root = document.getElementById("app");
if (root) {
root.classList.toggle("mode-leviathan", this.mode === "leviathan");
@@ -1374,6 +1387,16 @@
prefs.showStatusBar === false ? "none" : "flex";
}
+ // Leviathan is an opt-in mode, revealed from the About window.
+ const leviathanEnabled = prefs.leviathanEnabled === true;
+ const leviathanButton = document.getElementById("mode-leviathan");
+ if (leviathanButton) {
+ leviathanButton.hidden = !leviathanEnabled;
+ }
+ if (!leviathanEnabled && this.mode === "leviathan") {
+ this.setMode("nexus");
+ }
+
// Refit any visible terminal after geometry changes.
requestAnimationFrame(() => {
this.terminals.fitActive();
diff --git a/frontend/src/js/settings.js b/frontend/src/js/settings.js
index 0f126c8..15bbc4b 100644
--- a/frontend/src/js/settings.js
+++ b/frontend/src/js/settings.js
@@ -18,6 +18,19 @@
{ key: "about", label: "About" },
];
+ const KONAMI_SEQUENCE = [
+ "ArrowUp",
+ "ArrowUp",
+ "ArrowDown",
+ "ArrowDown",
+ "ArrowLeft",
+ "ArrowRight",
+ "ArrowLeft",
+ "ArrowRight",
+ "b",
+ "a",
+ ];
+
class Settings {
/**
* @param {App} app
@@ -36,6 +49,10 @@
/** @type {Array