feat: implement local terminal support with shell configuration and UI integration

This commit is contained in:
Andy Dixon
2026-05-21 13:13:06 +01:00
parent 3ff04feb92
commit a7cf28b404
21 changed files with 626 additions and 3 deletions
+10
View File
@@ -467,6 +467,16 @@
color: var(--danger);
}
.settings-shell-row {
display: flex;
gap: var(--space-2);
align-items: center;
}
.settings-shell-row .form-input {
flex: 1;
}
.form-success {
font-size: var(--text-xs);
color: var(--success);
+21
View File
@@ -253,6 +253,12 @@
.sidebar-item .protocol-indicator.proto-github::before {
content: "GH";
}
.sidebar-item .protocol-indicator.proto-vnc::before {
content: "⊞";
}
.sidebar-item .protocol-indicator.proto-local::before {
content: "›_";
}
.sidebar-item .protocol-indicator.proto-ssh {
background: rgba(78, 205, 196, 0.18);
@@ -270,6 +276,21 @@
background: rgba(124, 138, 255, 0.18);
color: var(--protocol-github);
}
.sidebar-item .protocol-indicator.proto-vnc {
background: rgba(232, 93, 74, 0.18);
color: var(--protocol-vnc);
}
.sidebar-item .protocol-indicator.proto-local {
background: rgba(139, 195, 74, 0.18);
color: var(--protocol-local);
}
/* Local machine item separator */
.sidebar-item-local {
border-bottom: 1px solid var(--border-subtle);
margin: var(--space-2) var(--space-2) var(--space-2);
padding-bottom: var(--space-2);
}
/* Text block (name + host) */
.sidebar-item-text {
+8
View File
@@ -114,6 +114,14 @@
background: rgba(124, 138, 255, 0.15);
color: var(--protocol-github);
}
.tab-badge.badge-vnc {
background: rgba(232, 93, 74, 0.15);
color: var(--protocol-vnc);
}
.tab-badge.badge-local {
background: rgba(139, 195, 74, 0.15);
color: var(--protocol-local);
}
/* -----------------------------------------------------------------------
Status dot (connection state)
+1
View File
@@ -58,6 +58,7 @@
--protocol-raw: #ab47bc;
--protocol-github: #7c8aff;
--protocol-vnc: #e85d4a;
--protocol-local: #8bc34a;
/* -----------------------------------------------------------------------
Spacing scale (4 px base unit)
+27
View File
@@ -541,6 +541,9 @@
this._bindClick("btn-new-tab", () => this.profileEditor.openNew());
this._bindClick("btn-broadcast", () => this.toggleBroadcast());
// Wire the "Local Machine" sidebar button
this.sidebar.onConnectLocal = () => this.connectLocal();
// Right-click on broadcast button opens target selector
const broadcastBtn = document.getElementById("btn-broadcast");
if (broadcastBtn) {
@@ -809,6 +812,30 @@
}
}
/**
* Opens a local terminal session.
*/
async connectLocal() {
try {
const sessionInfo = await window.go.api.Service.ConnectLocal();
this.state.addSession(sessionInfo);
this.tabs.addTab(sessionInfo);
const panelId = `panel-${sessionInfo.id}`;
const prefs = this.state.preferences || {};
this.terminals.createTerminal(sessionInfo.id, panelId, {
terminalFontFamily: prefs.defaultFontFamily,
terminalFontSize: prefs.defaultFontSize,
terminalScrollback: prefs.defaultScrollback,
terminalCursorStyle: prefs.defaultCursorStyle,
});
this.notifications.info("Opening local terminal…");
} catch (err) {
this.notifications.error("Local terminal failed: " + err);
}
}
/**
* Opens a quick connect dialog for ad-hoc connections without saving a profile.
*/
+8
View File
@@ -107,6 +107,14 @@
action: () => this.app.quickConnect(),
});
// Local terminal
cmds.push({
id: "local-terminal",
label: "Open Local Terminal",
section: "Actions",
action: () => this.app.connectLocal(),
});
// Tab actions
cmds.push({
id: "close-tab",
+46
View File
@@ -53,6 +53,12 @@
this._knownHosts = [];
}
try {
this._defaultShell = await window.go.api.Service.GetDefaultShell();
} catch (_) {
this._defaultShell = "";
}
this._render();
}
@@ -281,6 +287,45 @@
{ min: 180, max: 500 },
),
);
// Local shell path with browse button
const shellGroup = createElement("div", "form-group settings-field");
const shellLabel = createElement("label", "form-label", {
textContent: "Local shell",
for: "setting-localShell",
});
shellGroup.appendChild(shellLabel);
const shellRow = createElement("div", "settings-shell-row");
const shellInput = createElement("input", "form-input", {
type: "text",
id: "setting-localShell",
name: "setting-localShell",
value: p.localShell || "",
placeholder: this._defaultShell || "/bin/zsh",
});
shellRow.appendChild(shellInput);
const browseBtn = createElement("button", "btn btn-sm btn-secondary", {
textContent: "Browse…",
type: "button",
});
browseBtn.addEventListener("click", async () => {
try {
const path = await window.go.api.Service.BrowseForShell();
if (path) {
shellInput.value = path;
}
} catch (_) {}
});
shellRow.appendChild(browseBtn);
shellGroup.appendChild(shellRow);
const shellHint = createElement("span", "form-hint", {
textContent: "Leave empty to use system default",
});
shellGroup.appendChild(shellHint);
body.appendChild(shellGroup);
}
/**
@@ -1050,6 +1095,7 @@
"defaultScrollback",
"defaultCursorStyle",
"useNativePaste",
"localShell",
];
for (const field of fields) {
+33
View File
@@ -68,6 +68,9 @@
this.el.appendChild(this._renderBulkActions());
}
// Local Machine quick-connect button
this.el.appendChild(this._renderLocalMachineItem());
// Favourites
const favourites = profiles.filter((p) => p.favourite);
if (favourites.length > 0) {
@@ -531,6 +534,36 @@
);
}
/** @private */
_renderLocalMachineItem() {
const item = createElement("div", "sidebar-item sidebar-item-local");
item.addEventListener("mousedown", (e) => e.preventDefault());
const indicator = createElement(
"span",
"protocol-indicator proto-local",
{
title: "Local",
},
);
item.appendChild(indicator);
const textWrapper = createElement("div", "sidebar-item-text");
const nameEl = createElement("span", "sidebar-item-name", {
textContent: "Local Machine",
});
textWrapper.appendChild(nameEl);
item.appendChild(textWrapper);
item.addEventListener("click", () => {
if (this.onConnectLocal) {
this.onConnectLocal();
}
});
return item;
}
/** @private */
_renderBulkActions() {
const bar = createElement("div", "sidebar-bulk-actions");
+3 -1
View File
@@ -480,7 +480,9 @@
const protoLabel =
sessionInfo.protocol === "github"
? "GitHub"
: sessionInfo.protocol.toUpperCase();
: sessionInfo.protocol === "local"
? "Local"
: sessionInfo.protocol.toUpperCase();
const badge = createElement(
"span",
`tab-badge badge-${sessionInfo.protocol}`,
+2
View File
@@ -229,6 +229,8 @@
return "proto-github";
case "vnc":
return "proto-vnc";
case "local":
return "proto-local";
default:
return "";
}