[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,288 @@
"use strict";
const { randomUUID } = require("node:crypto");
const path = require("node:path");
const { assertJsonValue } = require("./contributionService.cjs");
const { PLUGIN_PROTOCOL_SCHEME } = require("./constants.cjs");
const VIEW_CHANNELS = Object.freeze({
postMessage: "netcatty-plugin-view:post-message",
executeCommand: "netcatty-plugin-view:execute-command",
getState: "netcatty-plugin-view:get-state",
getEnvironment: "netcatty-plugin-view:get-environment",
setState: "netcatty-plugin-view:set-state",
});
function normalizeBounds(bounds) {
const value = bounds ?? { x: 0, y: 0, width: 480, height: 320 };
const normalized = {};
for (const key of ["x", "y", "width", "height"]) {
if (!Number.isSafeInteger(value[key])) throw new TypeError("Plugin view bounds must use safe integers");
normalized[key] = value[key];
}
if (normalized.width < 1 || normalized.height < 1 || normalized.width > 16_384 || normalized.height > 16_384) {
throw new TypeError("Plugin view bounds are invalid");
}
return normalized;
}
class PluginViewHost {
constructor(options) {
this.electron = options.electron;
this.protocol = options.protocol;
this.packageStore = options.packageStore;
this.database = options.database;
this.contributionService = options.contributionService;
this.preloadPath = options.preloadPath ?? path.join(__dirname, "runtime", "viewPreload.cjs");
this.instances = new Map();
this.byWebContentsId = new Map();
this.closeListeners = new Set();
this.environment = { locale: "en", theme: "system", reducedMotion: false, highContrast: false };
this.subscriptions = [];
this.#registerIpc();
this.subscriptions.push(this.contributionService.onDidPostViewMessage((event) => {
for (const instance of this.instances.values()) {
if (instance.pluginId === event.pluginId && instance.viewId === event.viewId) {
instance.contents.send("netcatty-plugin-view:message", event.message);
}
}
}));
this.subscriptions.push(this.contributionService.onDidChange?.((event) => {
if (!["plugin-disabled", "runtime-stopped", "runtime-error", "runtime-quarantined"].includes(event.reason)
|| !event.pluginId) return;
for (const instance of [...this.instances.values()]) {
if (instance.pluginId === event.pluginId) void this.close(instance.instanceId, undefined, event.reason);
}
}));
}
onDidClose(listener) {
this.closeListeners.add(listener);
return { dispose: () => this.closeListeners.delete(listener) };
}
#registerIpc() {
const ipcMain = this.electron.ipcMain;
if (!ipcMain?.handle) return;
const instanceFor = (event) => {
const instance = this.byWebContentsId.get(event.sender?.id);
if (!instance || instance.contents !== event.sender) throw new Error("Untrusted plugin view sender");
return instance;
};
ipcMain.handle(VIEW_CHANNELS.postMessage, async (event, message) => {
const instance = instanceFor(event);
assertJsonValue(message, "view message");
await this.contributionService.runtimeSupervisor.notify(instance.pluginId, "plugin.view.message", {
viewId: instance.viewId,
message,
});
return null;
});
ipcMain.handle(VIEW_CHANNELS.executeCommand, (event, payload) => {
const instance = instanceFor(event);
return this.contributionService.executeCommand(payload?.command, payload?.args, {
source: "view",
callerPluginId: instance.pluginId,
context: { "netcatty.view": instance.viewId },
});
});
ipcMain.handle(VIEW_CHANNELS.getState, (event) => {
const instance = instanceFor(event);
return this.database.getViewState(instance.pluginId, instance.viewId, instance.scopeId);
});
ipcMain.handle(VIEW_CHANNELS.getEnvironment, (event) => {
instanceFor(event);
return structuredClone(this.environment);
});
ipcMain.handle(VIEW_CHANNELS.setState, (event, state) => {
const instance = instanceFor(event);
assertJsonValue(state, "view state");
this.database.setViewState(instance.pluginId, instance.viewId, instance.scopeId, state);
return null;
});
}
#ownedInstance(instanceId, sender) {
const instance = this.instances.get(instanceId);
if (!instance) throw new Error(`Plugin view was not found: ${instanceId}`);
if (sender) {
const owner = this.electron.BrowserWindow.fromWebContents(sender);
if (!owner || instance.owner !== owner) throw new Error("Plugin view belongs to another window");
}
return instance;
}
async open(payload, sender) {
if (!payload || typeof payload !== "object") throw new TypeError("Plugin view request is invalid");
const { plugin, view, identity } = await this.contributionService.activateView(
payload.viewId,
{ context: payload.context },
);
const activation = Object.freeze({
pluginId: plugin.id,
pluginVersion: plugin.activeVersion,
viewId: view.id,
runtimeId: identity?.runtimeId ?? null,
});
if (!activation.runtimeId) throw new Error("Plugin view runtime identity is unavailable");
const scopeId = typeof payload.scopeId === "string" && payload.scopeId.length <= 256 ? payload.scopeId : null;
if (!scopeId) throw new TypeError("Plugin view scope ID is required");
const owner = this.electron.BrowserWindow.fromWebContents(sender);
if (!owner || owner.isDestroyed()) throw new Error("Plugin view owner is unavailable");
const instanceId = typeof payload.instanceId === "string" && payload.instanceId.length <= 128
? payload.instanceId
: randomUUID();
if (this.instances.has(instanceId)) throw new Error(`Plugin view already exists: ${instanceId}`);
const packageRoot = await this.packageStore.preparePackageRoot(plugin);
this.contributionService.assertViewActivationCurrent(activation, { context: payload.context });
const partition = `netcatty-plugin-view-${randomUUID()}`;
const pluginSession = this.electron.session.fromPartition(partition, { cache: false });
let registration;
let sessionRegistration;
try {
registration = this.protocol.registerView({
pluginId: plugin.id,
packageRoot,
entry: view.entry,
});
sessionRegistration = this.protocol.registerSession(pluginSession);
await pluginSession.setProxy({
mode: "fixed_servers",
proxyRules: "http=127.0.0.1:9;https=127.0.0.1:9;socks=127.0.0.1:9",
proxyBypassRules: "<-loopback>",
});
pluginSession.on("will-download", (event) => event.preventDefault());
pluginSession.setPermissionCheckHandler?.(() => false);
pluginSession.setPermissionRequestHandler?.((_contents, _permission, callback) => callback(false));
pluginSession.setDevicePermissionHandler?.(() => false);
pluginSession.webRequest.onBeforeRequest((details, callback) => {
try {
const url = new URL(details.url);
callback({ cancel: url.protocol !== `${PLUGIN_PROTOCOL_SCHEME}:` || url.hostname !== registration.token });
} catch { callback({ cancel: true }); }
});
const contentsView = new this.electron.WebContentsView({
webPreferences: {
sandbox: true,
nodeIntegration: false,
nodeIntegrationInSubFrames: false,
nodeIntegrationInWorker: false,
contextIsolation: true,
webSecurity: true,
devTools: false,
disableDialogs: true,
navigateOnDragDrop: false,
session: pluginSession,
preload: this.preloadPath,
},
});
const contents = contentsView.webContents;
contents.setWindowOpenHandler(() => ({ action: "deny" }));
contents.on("will-navigate", (event, url) => {
if (url !== registration.url) event.preventDefault();
});
contents.on("will-attach-webview", (event) => event.preventDefault());
const ownerClosed = () => { void this.close(instanceId, undefined, "owner-closed"); };
const instance = {
instanceId,
pluginId: plugin.id,
viewId: view.id,
scopeId,
owner,
ownerClosed,
view: contentsView,
contents,
registration,
sessionRegistration,
state: "opening",
attached: false,
};
this.instances.set(instanceId, instance);
this.byWebContentsId.set(contents.id, instance);
owner.once?.("closed", ownerClosed);
contents.once("destroyed", () => { void this.close(instanceId, undefined, "contents-destroyed"); });
await contents.loadURL(registration.url);
this.contributionService.assertViewActivationCurrent(activation, { context: payload.context });
if (this.instances.get(instanceId) !== instance || instance.state !== "opening"
|| owner.isDestroyed() || contents.isDestroyed()) {
throw new Error("Plugin view ownership changed while opening");
}
owner.contentView.addChildView(contentsView);
instance.attached = true;
contentsView.setBounds(normalizeBounds(payload.bounds));
instance.state = "open";
contents.send("netcatty-plugin-view:environment", this.environment);
return { instanceId };
} catch (error) {
if (this.instances.has(instanceId)) await this.close(instanceId, undefined, "open-failed");
else {
try { registration?.dispose(); } catch {}
try { sessionRegistration?.dispose(); } catch {}
}
throw error;
}
}
setBounds(instanceId, bounds, sender) {
const instance = this.#ownedInstance(instanceId, sender);
instance.view.setBounds(normalizeBounds(bounds));
}
setVisible(instanceId, visible, sender) {
if (typeof visible !== "boolean") throw new TypeError("Plugin view visibility must be a boolean");
const instance = this.#ownedInstance(instanceId, sender);
instance.view.setVisible(visible);
}
async postMessage(instanceId, message, sender) {
const instance = this.#ownedInstance(instanceId, sender);
assertJsonValue(message, "view message");
instance.contents.send("netcatty-plugin-view:message", message);
}
setEnvironment(environment) {
assertJsonValue(environment, "environment");
this.environment = { ...environment };
for (const instance of this.instances.values()) {
instance.contents.send("netcatty-plugin-view:environment", this.environment);
}
}
async close(instanceId, sender, reason = sender ? "renderer-requested" : "host-requested") {
const instance = this.instances.get(instanceId);
if (!instance) return;
if (sender) this.#ownedInstance(instanceId, sender);
this.instances.delete(instanceId);
this.byWebContentsId.delete(instance.contents.id);
try { instance.owner.removeListener?.("closed", instance.ownerClosed); } catch {}
if (instance.attached) {
try { instance.owner.contentView.removeChildView(instance.view); } catch {}
}
try { instance.registration.dispose(); } catch {}
try { instance.sessionRegistration.dispose(); } catch {}
try {
if (!instance.contents.isDestroyed()) {
if (typeof instance.contents.close === "function") instance.contents.close();
else instance.contents.destroy?.();
}
} catch {}
const event = Object.freeze({
instanceId: instance.instanceId,
pluginId: instance.pluginId,
viewId: instance.viewId,
reason,
});
for (const listener of this.closeListeners) {
try { listener(event); } catch {}
}
}
async shutdown() {
await Promise.all([...this.instances.keys()].map((instanceId) => this.close(instanceId)));
for (const subscription of this.subscriptions.splice(0)) subscription?.dispose?.();
this.closeListeners.clear();
}
}
module.exports = { PluginViewHost, VIEW_CHANNELS, normalizeBounds };