vscode: revamp terminal UX, fix sizing, add cross-platform packaging #6

Merged
andydixon merged 1 commits from vscode-ext-ux-and-cross-platform into master 2026-06-23 20:59:36 +02:00
9 changed files with 440 additions and 85 deletions
+7
View File
@@ -6,6 +6,13 @@
build/bin/
dist/
# ── VSCode extensions: platform-specific build artifacts (never commit) ────
# The Go bridge is compiled per-host by vscode/build.sh and staged into each
# extension's bin/ — committing it ships a binary for the wrong OS/arch.
vscode/bin/
vscode/dist/
vscode/*/bin/
# ── Build platform resources (tracked — icons, plists, manifests) ─────────
# build/darwin/, build/windows/, build/linux/ are tracked for icons & metadata
Binary file not shown.
Binary file not shown.
+23 -7
View File
@@ -69,6 +69,10 @@ class Bridge {
if (!fs.existsSync(bridgePath)) {
throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`);
}
// VSIX extraction does not always preserve the executable bit, so ensure it.
if (process.platform !== 'win32') {
try { fs.chmodSync(bridgePath, 0o755); } catch (_) { /* best effort */ }
}
this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], {
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true
@@ -776,12 +780,19 @@ class ToolPanel {
h1 { margin: 0; font-size: 15px; }
.title-wrap { min-width: 0; }
.file-state { color: var(--vscode-descriptionForeground); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
header button, .actions button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); }
header button:hover, .actions button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
main { display: grid; grid-template-columns: minmax(260px, 360px) 1fr; height: calc(100vh - 58px); }
textarea { width: calc(100% - 24px); height: calc(100% - 66px); margin: 12px; resize: none; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); font-family: var(--vscode-editor-font-family); }
.left { border-right: 1px solid var(--vscode-panel-border); }
.actions { display: flex; gap: 6px; padding: 0 12px 12px; }
button { padding: 5px 10px; }
.left { border-right: 1px solid var(--vscode-panel-border); display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.params-head { padding: 12px 12px 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--vscode-descriptionForeground); }
textarea { flex: 1; margin: 0 12px; resize: none; color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 8px; box-sizing: border-box; font-family: var(--vscode-editor-font-family, monospace); font-size: 12px; }
textarea:focus { outline: none; border-color: var(--vscode-focusBorder); }
.actions { display: flex; gap: 6px; padding: 10px 12px; }
button { padding: 5px 10px; cursor: pointer; }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); border-color: var(--vscode-button-border, transparent); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
#out { margin: 0; padding: 12px; overflow: auto; height: calc(100% - 24px); box-sizing: border-box; }
.out-empty { color: var(--vscode-descriptionForeground); }
table { border-collapse: collapse; width: 100%; margin: 0 0 12px; }
th, td { border: 1px solid var(--vscode-panel-border); padding: 4px 6px; text-align: left; vertical-align: top; font-family: var(--vscode-editor-font-family); font-size: 12px; }
th { background: var(--vscode-editorWidget-background); position: sticky; top: 0; }
@@ -800,15 +811,20 @@ class ToolPanel {
</header>
<main>
<section class="left">
<div class="params-head">Parameters (JSON)</div>
<textarea id="params" spellcheck="false">${escapeHtml(JSON.stringify(JSON.parse(defaults), null, 2))}</textarea>
<div class="actions"><button id="run">Run</button></div>
<div class="actions"><button id="run" class="primary">Run</button></div>
</section>
<section id="out">Run the tool to see results.</section>
<section id="out"><span class="out-empty">Adjust the parameters and select Run to see results.</span></section>
</main>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const initialFileState = ${initialFileState};
document.getElementById('run').onclick = () => vscode.postMessage({ type: 'run', params: document.getElementById('params').value });
const runTool = () => vscode.postMessage({ type: 'run', params: document.getElementById('params').value });
document.getElementById('run').onclick = runTool;
document.getElementById('params').addEventListener('keydown', event => {
if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { event.preventDefault(); runTool(); }
});
document.getElementById('undo').onclick = () => vscode.postMessage({ type: 'undo' });
document.getElementById('redo').onclick = () => vscode.postMessage({ type: 'redo' });
document.getElementById('revert').onclick = () => vscode.postMessage({ type: 'revert' });
+7 -9
View File
@@ -1,10 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M5.5 4.2h8.7l4.3 4.3v11.3H5.5V4.2Z"/>
<path d="M14.2 4.2v4.3h4.3"/>
<path d="M8.4 8.2h2.2"/>
<path d="M8.4 11.4h4.9"/>
<path d="M8.4 14.6h2.9"/>
<path d="m15.1 14.3 1.5 1.5"/>
<circle cx="13.9" cy="13.1" r="2.1"/>
<path d="M8.4 17.8h6.8"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M13.4 3.5H5a1.5 1.5 0 0 0-1.5 1.5v14A1.5 1.5 0 0 0 5 20.5h5.2"/>
<path d="M6.6 7.3h7"/>
<path d="M6.6 10.4h4.4"/>
<path d="M6.6 13.5h2.2"/>
<circle cx="15.3" cy="14.1" r="3.7"/>
<path d="m18 16.8 2.8 2.8"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.
+209 -61
View File
@@ -22,6 +22,10 @@ class Bridge {
if (!fs.existsSync(bridgePath)) {
throw new Error(`Bridge executable not found: ${bridgePath}. Run vscode/build.sh first.`);
}
// VSIX extraction does not always preserve the executable bit, so ensure it.
if (process.platform !== 'win32') {
try { fs.chmodSync(bridgePath, 0o755); } catch (_) { /* best effort */ }
}
this.proc = cp.spawn(bridgePath, ['-listen', '127.0.0.1:0'], {
stdio: ['ignore', 'pipe', 'pipe'],
@@ -508,8 +512,6 @@ class TerminalPanel {
await this.bridge.rpc('WriteSession', [this.session.id, msg.data]);
} else if (msg.type === 'resize') {
await this.bridge.rpc('ResizeSession', [this.session.id, msg.cols, msg.rows]);
} else if (msg.type === 'sftp') {
new SFTPPanel(this.context, this.bridge, this.session);
}
} catch (err) {
this.panel.webview.postMessage({ type: 'status', text: err.message });
@@ -536,94 +538,191 @@ class TerminalPanel {
const fitJs = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm-addon-fit.min.js'));
const xtermCss = webview.asWebviewUri(vscode.Uri.joinPath(mediaRoot, 'xterm.css'));
const nonce = String(Date.now());
const sftpStyle = String(this.profile.protocol || this.session.protocol || '').toLowerCase() === 'ssh' ? '' : 'display:none';
const terminalFontFamily = JSON.stringify(this.profile.terminalFontFamily || 'Menlo, Monaco, Consolas, "Courier New", monospace');
const terminalFontFamily = JSON.stringify(this.profile.terminalFontFamily || '');
const terminalFontSize = Number(this.profile.terminalFontSize || 13);
const terminalScrollback = Number(this.profile.terminalScrollback || 10000);
const terminalCursorStyle = JSON.stringify(this.profile.terminalCursorStyle || 'block');
return `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; script-src ${webview.cspSource} 'nonce-${nonce}';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource} 'unsafe-inline'; script-src ${webview.cspSource} 'nonce-${nonce}'; font-src ${webview.cspSource};">
<link rel="stylesheet" href="${xtermCss}">
<style>
:root { color-scheme: light dark; }
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
background: #0f1720;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
background: var(--vscode-terminal-background, var(--vscode-panel-background, var(--vscode-editor-background)));
}
/* Full-bleed terminal: occupies 100% of the tab, no chrome stealing space. */
#terminal {
position: fixed;
position: absolute;
inset: 0;
width: 100vw;
height: 100vh;
background: #0f1720;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
padding: 4px 6px;
box-sizing: border-box;
}
.xterm, .xterm-viewport, .xterm-screen {
width: 100% !important;
height: 100% !important;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace !important;
.xterm { height: 100%; }
.xterm-viewport::-webkit-scrollbar { width: 10px; }
.xterm-viewport::-webkit-scrollbar-thumb {
background: var(--vscode-scrollbarSlider-background, rgba(121, 121, 121, .4));
}
.xterm-viewport::-webkit-scrollbar-thumb:hover {
background: var(--vscode-scrollbarSlider-hoverBackground, rgba(100, 100, 100, .7));
}
/* Floating, auto-hiding status pill following VSCode badge styling. */
#status {
position: fixed;
right: 10px;
top: 8px;
z-index: 2;
color: #a7f3d0;
font: 12px Menlo, Monaco, Consolas, "Courier New", monospace;
position: absolute;
right: 12px;
top: 10px;
z-index: 3;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 9px;
border-radius: 11px;
font: 11px/1.6 var(--vscode-font-family);
color: var(--vscode-badge-foreground);
background: var(--vscode-badge-background);
box-shadow: 0 1px 4px rgba(0, 0, 0, .35);
pointer-events: none;
opacity: .9;
opacity: 0;
transition: opacity .25s ease;
}
#sftp {
position: fixed;
left: 10px;
top: 8px;
z-index: 2;
border: 1px solid rgba(148, 163, 184, .35);
background: rgba(15, 23, 32, .82);
color: #dbeafe;
padding: 4px 9px;
font: 12px Menlo, Monaco, Consolas, "Courier New", monospace;
cursor: pointer;
#status.show { opacity: 1; }
#status .dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--vscode-charts-yellow, #e2c08d);
}
#sftp:hover { background: rgba(30, 41, 59, .95); }
#status.ok .dot { background: var(--vscode-charts-green, #89d185); }
#status.bad { color: var(--vscode-statusBarItem-errorForeground, #fff); background: var(--vscode-statusBarItem-errorBackground, #c72e2e); }
#status.bad .dot { background: currentColor; }
</style>
</head>
<body>
<div id="status">connecting</div>
<button id="sftp" style="${sftpStyle}">SFTP</button>
<div id="status" class="show"><span class="dot"></span><span id="statusText">Connecting…</span></div>
<div id="terminal"></div>
<script nonce="${nonce}" src="${xtermJs}"></script>
<script nonce="${nonce}" src="${fitJs}"></script>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
// Resolve a VSCode theme colour from the CSS variables injected into the webview.
const rootStyle = getComputedStyle(document.documentElement);
function cssVar(name, fallback) {
const value = rootStyle.getPropertyValue(name);
return value && value.trim() ? value.trim() : fallback;
}
const background = cssVar('--vscode-terminal-background', cssVar('--vscode-editor-background', '#1e1e1e'));
const foreground = cssVar('--vscode-terminal-foreground', cssVar('--vscode-editor-foreground', '#d4d4d4'));
// Build the xterm palette straight from the active VSCode colour theme.
const theme = {
background,
foreground,
cursor: cssVar('--vscode-terminalCursor-foreground', foreground),
cursorAccent: cssVar('--vscode-terminalCursor-background', background),
selectionBackground: cssVar('--vscode-terminal-selectionBackground', 'rgba(128,128,128,0.35)'),
black: cssVar('--vscode-terminal-ansiBlack', '#000000'),
red: cssVar('--vscode-terminal-ansiRed', '#cd3131'),
green: cssVar('--vscode-terminal-ansiGreen', '#0dbc79'),
yellow: cssVar('--vscode-terminal-ansiYellow', '#e5e510'),
blue: cssVar('--vscode-terminal-ansiBlue', '#2472c8'),
magenta: cssVar('--vscode-terminal-ansiMagenta', '#bc3fbc'),
cyan: cssVar('--vscode-terminal-ansiCyan', '#11a8cd'),
white: cssVar('--vscode-terminal-ansiWhite', '#e5e5e5'),
brightBlack: cssVar('--vscode-terminal-ansiBrightBlack', '#666666'),
brightRed: cssVar('--vscode-terminal-ansiBrightRed', '#f14c4c'),
brightGreen: cssVar('--vscode-terminal-ansiBrightGreen', '#23d18b'),
brightYellow: cssVar('--vscode-terminal-ansiBrightYellow', '#f5f543'),
brightBlue: cssVar('--vscode-terminal-ansiBrightBlue', '#3b8eea'),
brightMagenta: cssVar('--vscode-terminal-ansiBrightMagenta', '#d670d6'),
brightCyan: cssVar('--vscode-terminal-ansiBrightCyan', '#29b8db'),
brightWhite: cssVar('--vscode-terminal-ansiBrightWhite', '#e5e5e5')
};
document.body.style.background = background;
// Prefer a profile override, otherwise inherit VSCode's terminal/editor monospace font.
const fontFamily = ${terminalFontFamily}
|| cssVar('--vscode-terminal-font-family', '')
|| cssVar('--vscode-editor-font-family', 'Menlo, Monaco, "Cascadia Code", "Courier New", monospace');
const term = new Terminal({
cursorBlink: true,
cursorStyle: ${terminalCursorStyle},
scrollback: ${terminalScrollback},
fontFamily: ${terminalFontFamily},
fontFamily,
fontSize: ${terminalFontSize},
theme: { background: '#0f1720' }
lineHeight: 1.1,
letterSpacing: 0,
allowProposedApi: true,
theme
});
const fit = new FitAddon.FitAddon();
term.loadAddon(fit);
term.open(document.getElementById('terminal'));
fit.fit();
term.onData(data => vscode.postMessage({ type: 'input', data: btoa(unescape(encodeURIComponent(data))) }));
document.getElementById('sftp').onclick = () => vscode.postMessage({ type: 'sftp' });
function resize() {
fit.fit();
vscode.postMessage({ type: 'resize', cols: term.cols, rows: term.rows });
const container = document.getElementById('terminal');
term.open(container);
let lastCols = 0;
let lastRows = 0;
// Push the current terminal geometry to the backend PTY. Pass force=true to
// re-send even when the dimensions are unchanged (needed right after the
// connection completes, since the remote PTY is created at a default size).
function resize(force) {
try { fit.fit(); } catch (_) { return; }
if (force || term.cols !== lastCols || term.rows !== lastRows) {
lastCols = term.cols;
lastRows = term.rows;
vscode.postMessage({ type: 'resize', cols: term.cols, rows: term.rows });
}
}
window.addEventListener('resize', resize);
setTimeout(resize, 50);
// Refit whenever the tab/container changes size — handles splits, panel drags
// and the editor being shown after retainContextWhenHidden.
if (window.ResizeObserver) {
new ResizeObserver(() => resize()).observe(container);
}
window.addEventListener('resize', () => resize());
document.addEventListener('visibilitychange', () => { if (!document.hidden) resize(); });
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => resize());
requestAnimationFrame(() => resize());
setTimeout(() => resize(), 60);
term.focus();
term.onData(data => vscode.postMessage({ type: 'input', data: btoa(unescape(encodeURIComponent(data))) }));
const statusEl = document.getElementById('status');
const statusText = document.getElementById('statusText');
let hideTimer = null;
function setStatus(text) {
const value = String(text || '').trim();
if (!value) return;
const lower = value.toLowerCase();
const connected = /connect(ed)?$|ready|open|online/.test(lower) && !/dis|fail|error|refus|lost|closed/.test(lower);
const failed = /dis|fail|error|refus|lost|closed|timeout/.test(lower);
statusText.textContent = value.charAt(0).toUpperCase() + value.slice(1);
statusEl.classList.toggle('ok', connected && !failed);
statusEl.classList.toggle('bad', failed);
statusEl.classList.add('show');
clearTimeout(hideTimer);
// Healthy states fade out so they don't sit on top of the terminal; problems stay visible.
if (connected && !failed) hideTimer = setTimeout(() => statusEl.classList.remove('show'), 1600);
return connected && !failed;
}
window.addEventListener('message', event => {
const msg = event.data || {};
if (msg.type === 'data') term.write(Uint8Array.from(atob(msg.data), c => c.charCodeAt(0)));
if (msg.type === 'status') document.getElementById('status').textContent = msg.text || '';
if (msg.type === 'status') {
// The remote PTY is allocated at a default size before our first resize
// can land, so re-send the real geometry once the session is connected.
if (setStatus(msg.text)) { resize(true); setTimeout(() => resize(true), 250); }
}
});
</script>
</body>
@@ -699,13 +798,18 @@ class RawPanel {
.send { color: var(--vscode-terminal-ansiGreen); }
.recv { color: var(--vscode-terminal-ansiCyan); }
.hex { color: var(--vscode-descriptionForeground); }
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--vscode-focusBorder); }
</style>
</head>
<body>
<header>
<select id="format"><option value="text">Text</option><option value="hex">Hex</option><option value="escaped">Escaped</option></select>
<input id="input" autocomplete="off" spellcheck="false">
<button id="send">Send</button>
<button id="send" class="primary">Send</button>
<button id="clear">Clear</button>
<button id="save">Save</button>
<span id="status">connecting</span>
@@ -858,6 +962,12 @@ class GitHubPanel {
.repo { border: 1px solid var(--vscode-panel-border); padding: 8px; }
.repo h3 { margin: 0 0 4px; font-size: 14px; }
.meta { color: var(--vscode-descriptionForeground); font-size: 12px; }
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
input:focus, select:focus { outline: none; border-color: var(--vscode-focusBorder); }
.repo p { display: flex; gap: 6px; margin: 8px 0 0; }
</style>
</head>
<body>
@@ -951,13 +1061,18 @@ class IRCPanel {
#target { width: 140px; }
#message { flex: 1; }
#log { height: calc(100vh - 92px); overflow: auto; padding: 8px; font-family: var(--vscode-editor-font-family), monospace; white-space: pre-wrap; }
.status { color: var(--vscode-descriptionForeground); }
.status { color: var(--vscode-descriptionForeground); align-self: center; }
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); padding: 5px 10px; cursor: pointer; }
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
input:focus { outline: none; border-color: var(--vscode-focusBorder); }
</style>
</head>
<body>
<header><input id="room" placeholder="#room"><button id="join">Join</button><button id="part">Part</button><button id="list">List</button><span id="status" class="status">connecting</span></header>
<main id="log"></main>
<footer><input id="target" value="server"><input id="message" autocomplete="off" spellcheck="false"><button id="send">Send</button></footer>
<footer><input id="target" value="server"><input id="message" autocomplete="off" spellcheck="false"><button id="send" class="primary">Send</button></footer>
<script nonce="${nonce}">
const vscode = acquireVsCodeApi();
const log = document.getElementById('log');
@@ -1041,7 +1156,8 @@ class VNCPanel {
html, body { margin: 0; width: 100%; height: 100%; color: var(--vscode-foreground); background: #111; font: var(--vscode-font-size) var(--vscode-font-family); }
#toolbar { display: flex; gap: 8px; align-items: center; height: 36px; padding: 0 8px; box-sizing: border-box; background: var(--vscode-sideBar-background); }
#screen { height: calc(100% - 36px); overflow: auto; display: flex; align-items: center; justify-content: center; }
button { padding: 4px 10px; }
button { padding: 4px 10px; border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
#status { margin-right: auto; color: var(--vscode-descriptionForeground); }
</style>
</head>
@@ -1161,8 +1277,12 @@ class ProfileEditorPanel {
.grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px; }
label { display: grid; gap: 4px; }
input, select, textarea { color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 5px 7px; box-sizing: border-box; }
input:focus, select:focus, textarea:focus { outline: none; border-color: var(--vscode-focusBorder); }
textarea { width: 100%; min-height: 240px; font-family: var(--vscode-editor-font-family), monospace; }
button { padding: 5px 12px; }
button { padding: 5px 12px; border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
.hint, #status { color: var(--vscode-descriptionForeground); }
.danger { color: var(--vscode-errorForeground); }
</style>
@@ -1171,7 +1291,7 @@ class ProfileEditorPanel {
<header>
<h1>${this.isNew ? 'New Connection Profile' : 'Edit Connection Profile'}</h1>
<button id="test">Test Host</button>
<button id="save">Save</button>
<button id="save" class="primary">Save</button>
</header>
<main>
<section>
@@ -1399,14 +1519,20 @@ class SettingsPanel {
.grid { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 10px; }
label { display: grid; gap: 4px; }
input, select { color: var(--vscode-input-foreground); background: var(--vscode-input-background); border: 1px solid var(--vscode-input-border); padding: 5px 7px; }
input:focus, select:focus { outline: none; border-color: var(--vscode-focusBorder); }
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); padding: 5px 12px; cursor: pointer; }
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
table { width: 100%; border-collapse: collapse; }
td, th { border: 1px solid var(--vscode-panel-border); padding: 5px 7px; text-align: left; }
td button { padding: 2px 8px; }
.hint, #status { color: var(--vscode-descriptionForeground); }
.danger { color: var(--vscode-errorForeground); }
</style>
</head>
<body>
<header><h1>Nexus Settings</h1><button id="save">Save</button></header>
<header><h1>Nexus Settings</h1><button id="save" class="primary">Save</button></header>
<main>
<section>
<h2>General</h2>
@@ -1520,12 +1646,27 @@ class SFTPPanel {
}
async init() {
try {
this.cwd = await this.bridge.rpc('SFTPPwd', [this.session.id]);
} catch (_) {
this.cwd = '/';
// A freshly opened SFTP session may be created before the underlying SSH
// connection finishes. SFTPPwd resolves realpath(".") which the server maps
// to the user's home directory, so retry until the session is ready and land
// the browser in the home folder instead of falling back to the filesystem root.
this.panel.webview.postMessage({ type: 'status', text: 'Connecting…' });
let home = '';
for (let attempt = 0; attempt < 30 && !home; attempt++) {
try {
const pwd = await this.bridge.rpc('SFTPPwd', [this.session.id]);
if (pwd && pwd.startsWith('/')) { home = pwd; break; }
} catch (_) {
// SSH session is still connecting; back off and retry.
}
await new Promise(resolve => setTimeout(resolve, 300));
}
this.cwd = home || '/';
try {
await this.list(this.cwd);
} catch (err) {
this.panel.webview.postMessage({ type: 'error', error: err.message || String(err) });
}
await this.list(this.cwd || '/');
}
async list(remotePath) {
@@ -1595,6 +1736,12 @@ class SFTPPanel {
.muted, #status { color: var(--vscode-descriptionForeground); }
.danger { color: var(--vscode-errorForeground); }
main { height: calc(100vh - 43px); overflow: auto; }
button { border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); cursor: pointer; }
button:hover { background: var(--vscode-button-secondaryHoverBackground, var(--vscode-button-secondaryBackground)); }
button.primary { background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
button.primary:hover { background: var(--vscode-button-hoverBackground, var(--vscode-button-background)); }
td button { padding: 2px 8px; }
#path:focus { outline: none; border-color: var(--vscode-focusBorder); }
</style>
</head>
<body>
@@ -1662,6 +1809,7 @@ class SFTPPanel {
window.addEventListener('message', event => {
const msg = event.data || {};
if (msg.type === 'entries') { cwd = msg.cwd || '/'; entries = msg.entries || []; render(); }
if (msg.type === 'status') setStatus(msg.text);
if (msg.type === 'error') setStatus(msg.error, true);
});
</script>
+4 -8
View File
@@ -1,9 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2.8 20.2 7.4v9.2L12 21.2l-8.2-4.6V7.4L12 2.8Z"/>
<path d="M7.2 8.6h9.6a1.6 1.6 0 0 1 1.6 1.6v5.6a1.6 1.6 0 0 1-1.6 1.6H7.2a1.6 1.6 0 0 1-1.6-1.6v-5.6a1.6 1.6 0 0 1 1.6-1.6Z"/>
<path d="m8.3 11 2 2-2 2"/>
<path d="M12 15h3.7"/>
<path d="M12 2.8v3.1"/>
<path d="M4.1 7.6 6.8 9"/>
<path d="m17.2 9 2.7-1.4"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2.5 20.5 7.25v9.5L12 21.5 3.5 16.75v-9.5L12 2.5Z"/>
<path d="m8.5 9.7 2.8 2.3-2.8 2.3"/>
<path d="M12.6 15h3.1"/>
</svg>

Before

Width:  |  Height:  |  Size: 501 B

After

Width:  |  Height:  |  Size: 301 B

+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env bash
#
# Build platform-specific VSIX packages for the Nexus and Leviathan VSCode
# extensions, covering Windows, macOS and Linux on both x64 and arm64.
#
# Each extension embeds a Go "bridge" binary that is OS/arch specific, so we
# produce one VSIX per target using VSCode's platform-specific extension
# mechanism (`vsce package --target <target>`). VSCode then installs the VSIX
# matching the user's platform automatically (e.g. from the Marketplace, or via
# `code --install-extension <file>.vsix`).
#
# The bridge is pure Go (CGO_ENABLED=0), so every target cross-compiles from any
# host with just the Go toolchain — no C compilers or per-OS toolchains needed.
#
# Usage:
# vscode/package-all.sh # build every target
# vscode/package-all.sh linux-x64 ... # build only the named vsce target(s)
#
# Optional macOS code signing + notarization (darwin targets only):
# Runs only on a macOS host with Xcode command line tools, and only when the
# relevant env vars are set — otherwise it is skipped with a warning and the
# darwin VSIXs are produced unsigned (fine for local use, blocked by Gatekeeper
# when distributed to other machines).
#
# MACOS_SIGN_IDENTITY "Developer ID Application: Name (TEAMID)" — enables signing.
#
# Then, to also notarize, provide EITHER a stored notarytool profile:
# MACOS_NOTARY_PROFILE profile name from `xcrun notarytool store-credentials`
# OR an Apple ID + app-specific password:
# MACOS_NOTARY_APPLE_ID Apple ID email
# MACOS_NOTARY_TEAM_ID Apple Developer team id
# MACOS_NOTARY_PASSWORD app-specific password
#
# Example:
# MACOS_SIGN_IDENTITY="Developer ID Application: Andy Dixon (AB12CD34EF)" \
# MACOS_NOTARY_PROFILE="nexus-notary" \
# vscode/package-all.sh darwin-x64 darwin-arm64
#
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VSCODE_DIR="$ROOT_DIR/vscode"
OUT_DIR="$VSCODE_DIR/dist"
EXTENSIONS=(nexus leviathan)
# vsce target -> GOOS/GOARCH
ALL_TARGETS=(
"win32-x64:windows/amd64"
"win32-arm64:windows/arm64"
"darwin-x64:darwin/amd64"
"darwin-arm64:darwin/arm64"
"linux-x64:linux/amd64"
"linux-arm64:linux/arm64"
)
# Optional positional filter: only build the vsce targets passed as arguments.
SELECTED=("$@")
target_selected() {
[ ${#SELECTED[@]} -eq 0 ] && return 0
local want="$1"
for t in "${SELECTED[@]}"; do [ "$t" = "$want" ] && return 0; done
return 1
}
run_vsce() {
if command -v vsce >/dev/null 2>&1; then
vsce "$@"
else
npx --yes @vscode/vsce "$@"
fi
}
# Codesign (hardened runtime) and optionally notarize a single darwin bridge
# binary. Opt-in via env vars and skipped with a warning when it can't run, so
# the cross-platform build still succeeds on non-macOS hosts. Signing the staged
# binary before it is copied into each extension means both VSIXs inherit the
# signature (and notarization, which is keyed to the binary's hash).
macos_sign() {
local bin="$1"
if [ -z "${MACOS_SIGN_IDENTITY:-}" ]; then
echo "[vscode] (skip signing: MACOS_SIGN_IDENTITY not set — shipping unsigned)"
return 0
fi
if [ "$(uname -s)" != "Darwin" ] || ! command -v codesign >/dev/null 2>&1; then
echo "[vscode] WARNING: cannot sign darwin bridge here (needs macOS + Xcode tools); shipping unsigned"
return 0
fi
echo "[vscode] codesign ($MACOS_SIGN_IDENTITY)"
codesign --force --options runtime --timestamp --sign "$MACOS_SIGN_IDENTITY" "$bin"
codesign --verify --strict --verbose=2 "$bin"
# Notarize only when credentials are provided.
local notary_args=()
if [ -n "${MACOS_NOTARY_PROFILE:-}" ]; then
notary_args=(--keychain-profile "$MACOS_NOTARY_PROFILE")
elif [ -n "${MACOS_NOTARY_APPLE_ID:-}" ] && [ -n "${MACOS_NOTARY_TEAM_ID:-}" ] && [ -n "${MACOS_NOTARY_PASSWORD:-}" ]; then
notary_args=(--apple-id "$MACOS_NOTARY_APPLE_ID" --team-id "$MACOS_NOTARY_TEAM_ID" --password "$MACOS_NOTARY_PASSWORD")
else
echo "[vscode] (skip notarization: no notary credentials set)"
return 0
fi
if ! command -v xcrun >/dev/null 2>&1; then
echo "[vscode] WARNING: xcrun not found; cannot notarize"
return 0
fi
echo "[vscode] notarize (submitting to Apple, waiting for result…)"
local zip="${bin}.notarize.zip"
# notarytool requires an archive; a bare Mach-O binary cannot be submitted directly.
ditto -c -k --keepParent "$bin" "$zip"
xcrun notarytool submit "$zip" "${notary_args[@]}" --wait
rm -f "$zip"
# A standalone executable cannot be stapled (stapler only supports app/dmg/pkg);
# Gatekeeper verifies the notarization online via the now-registered hash, which
# matches the exact bytes embedded in every VSIX.
echo "[vscode] notarized (online check; standalone binaries are not stapleable)"
}
echo "[vscode] Building frontend assets (platform independent)"
if [ ! -d "$ROOT_DIR/frontend/node_modules" ]; then
npm --prefix "$ROOT_DIR/frontend" install
fi
npm --prefix "$ROOT_DIR/frontend" run build
echo "[vscode] Staging shared media assets"
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm.min.js" "$VSCODE_DIR/nexus/media/xterm.min.js"
cp "$ROOT_DIR/frontend/dist/src/js/lib/xterm-addon-fit.min.js" "$VSCODE_DIR/nexus/media/xterm-addon-fit.min.js"
cp "$ROOT_DIR/frontend/dist/src/css/lib/xterm.css" "$VSCODE_DIR/nexus/media/xterm.css"
cp "$ROOT_DIR/frontend/dist/src/js/lib/novnc.min.js" "$VSCODE_DIR/nexus/media/novnc.min.js"
rm -rf "$OUT_DIR"
mkdir -p "$OUT_DIR"
cd "$ROOT_DIR"
VERSION="$(node -p "require('$VSCODE_DIR/nexus/package.json').version")"
for entry in "${ALL_TARGETS[@]}"; do
VSCE_TARGET="${entry%%:*}"
GOPLATFORM="${entry##*:}"
GOOS="${GOPLATFORM%%/*}"
GOARCH="${GOPLATFORM##*/}"
if ! target_selected "$VSCE_TARGET"; then
continue
fi
BIN_NAME="nexus-vscode-bridge"
[ "$GOOS" = "windows" ] && BIN_NAME="nexus-vscode-bridge.exe"
echo "[vscode] Building bridge for $VSCE_TARGET ($GOOS/$GOARCH)"
STAGE_BIN="$VSCODE_DIR/bin/$VSCE_TARGET/$BIN_NAME"
mkdir -p "$(dirname "$STAGE_BIN")"
GOOS="$GOOS" GOARCH="$GOARCH" CGO_ENABLED=0 \
go build -trimpath -o "$STAGE_BIN" ./cmd/vscode-bridge
# Sign + notarize macOS bridges before they are copied into the extensions.
if [ "$GOOS" = "darwin" ]; then
macos_sign "$STAGE_BIN"
fi
for EXT in "${EXTENSIONS[@]}"; do
EXT_BIN_DIR="$VSCODE_DIR/$EXT/bin"
# Ship exactly one binary per VSIX so the package can't carry a wrong-arch
# (or wrong-OS) bridge alongside the right one.
rm -rf "$EXT_BIN_DIR"
mkdir -p "$EXT_BIN_DIR"
cp "$STAGE_BIN" "$EXT_BIN_DIR/$BIN_NAME"
chmod 0755 "$EXT_BIN_DIR/$BIN_NAME"
OUT_VSIX="$OUT_DIR/$EXT-$VERSION-$VSCE_TARGET.vsix"
( cd "$VSCODE_DIR/$EXT" && run_vsce package --target "$VSCE_TARGET" \
--no-dependencies --allow-missing-repository -o "$OUT_VSIX" >/dev/null )
echo "[vscode] wrote $(basename "$OUT_VSIX")"
done
done
# Leave the working tree with a host-native bridge staged so a plain
# `code --install-extension` of an unpacked extension still works locally.
HOST_OS="$(go env GOOS)"; HOST_ARCH="$(go env GOARCH)"
HOST_BIN="nexus-vscode-bridge"; [ "$HOST_OS" = "windows" ] && HOST_BIN="nexus-vscode-bridge.exe"
for EXT in "${EXTENSIONS[@]}"; do
rm -rf "$VSCODE_DIR/$EXT/bin"; mkdir -p "$VSCODE_DIR/$EXT/bin"
GOOS="$HOST_OS" GOARCH="$HOST_ARCH" CGO_ENABLED=0 \
go build -trimpath -o "$VSCODE_DIR/$EXT/bin/$HOST_BIN" ./cmd/vscode-bridge
done
echo "[vscode] Done. Packages in $OUT_DIR:"
ls -1 "$OUT_DIR"