mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 06:35:24 +00:00
feat: ship the ssh terminal as a first-party in-process plugin
Adds a hardcoded process:transport-owner tier so the terminal can own its WebSocket server and ssh2 clients, which cannot cross the worker boundary.
This commit is contained in:
1 parent
e040b12045
commit
c94a453f1e
43 files changed
+2426
-38
No files matched your search
@@ -46,6 +46,7 @@ import { canEditHost } from "@/sidebar/host-permissions";
|
||||
import { RAIL_ITEMS, RAIL_UTILITY_ITEMS } from "@/sidebar/rail-items";
|
||||
import { useAiAvailability } from "@/hooks/use-ai-availability";
|
||||
import { useSnippetRunner } from "@/hooks/use-snippet-runner.tsx";
|
||||
import { isTabTypeAvailable } from "@/shell/pluginLoader";
|
||||
|
||||
interface CommandPaletteProps {
|
||||
isOpen: boolean;
|
||||
@@ -87,11 +88,12 @@ function getSshActions(host: Host): {
|
||||
}[] {
|
||||
const metricsEnabled = host.statsConfig?.metricsEnabled !== false;
|
||||
return [
|
||||
host.enableTerminal !== false && {
|
||||
type: "terminal",
|
||||
icon: Terminal,
|
||||
label: "Terminal",
|
||||
},
|
||||
host.enableTerminal !== false &&
|
||||
isTabTypeAvailable("terminal") && {
|
||||
type: "terminal",
|
||||
icon: Terminal,
|
||||
label: "Terminal",
|
||||
},
|
||||
host.enableFileManager && {
|
||||
type: "files",
|
||||
icon: FolderSearch,
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Applies backend plugin state to the shell.
|
||||
*
|
||||
* Two kinds of contributed surface exist, and they are removed differently:
|
||||
*
|
||||
* - A tab that only exists because a plugin registered it goes away with
|
||||
* unregisterTabComponent / unregisterRailItem.
|
||||
* - A tab that ships as a built-in code path but is OWNED by a plugin (the
|
||||
* terminal) cannot, because it was never registered. Callers ask
|
||||
* isTabTypeAvailable() before offering it instead.
|
||||
*
|
||||
* The terminal is in the second group because its tab takes around a dozen
|
||||
* props (host, split-pane state, file-manager and editor callbacks) that the
|
||||
* generic { tab } plugin surface has no way to supply. Rewiring that was a
|
||||
* bigger regression risk to a working terminal than gating the entry points.
|
||||
*/
|
||||
|
||||
import { unregisterRailItem } from "@/sidebar/rail-items";
|
||||
import { unregisterTabComponent } from "@/shell/tabUtils";
|
||||
import { getPlugins, type PluginSummary } from "@/api/plugins-api";
|
||||
|
||||
/**
|
||||
* Built-in tab types owned by a plugin, keyed by plugin id. The terminal has
|
||||
* no rail item: it opens from a host, the command palette or quick connect.
|
||||
*/
|
||||
const BUILT_IN_TABS_BY_PLUGIN: Record<string, string[]> = {
|
||||
"ssh-terminal": ["terminal"],
|
||||
};
|
||||
|
||||
let pluginEnabled = new Map<string, boolean>();
|
||||
let disabledTabTypes = new Set<string>();
|
||||
|
||||
export function applyPluginState(plugins: PluginSummary[]): void {
|
||||
const nextEnabled = new Map<string, boolean>();
|
||||
const nextDisabledTabs = new Set<string>();
|
||||
|
||||
for (const plugin of plugins) {
|
||||
nextEnabled.set(plugin.id, plugin.enabled);
|
||||
|
||||
const builtInTabs = BUILT_IN_TABS_BY_PLUGIN[plugin.id];
|
||||
if (builtInTabs) {
|
||||
if (!plugin.enabled) {
|
||||
for (const tabId of builtInTabs) nextDisabledTabs.add(tabId);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Plugin-registered surfaces: only removal happens here. Registration is
|
||||
// the plugin's own frontend entry being imported.
|
||||
if (!plugin.enabled) {
|
||||
for (const tab of plugin.contributes?.tabs ?? []) {
|
||||
unregisterRailItem(tab.id);
|
||||
unregisterTabComponent(tab.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginEnabled = nextEnabled;
|
||||
disabledTabTypes = nextDisabledTabs;
|
||||
}
|
||||
|
||||
export async function refreshPluginState(): Promise<PluginSummary[]> {
|
||||
const plugins = await getPlugins();
|
||||
applyPluginState(plugins);
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a built-in tab type may still be opened. Call sites that offer a
|
||||
* connection action (host item, command palette, quick connect) check this so
|
||||
* a disabled plugin stops offering the tab it owns.
|
||||
*/
|
||||
export function isTabTypeAvailable(tabType: string): boolean {
|
||||
return !disabledTabTypes.has(tabType);
|
||||
}
|
||||
|
||||
export function isPluginEnabled(pluginId: string): boolean {
|
||||
// Unknown means not yet loaded; assume enabled so the UI does not blank out
|
||||
// a working feature while the first request is in flight.
|
||||
return pluginEnabled.get(pluginId) ?? true;
|
||||
}
|
||||
|
||||
/** Test seam. */
|
||||
export function resetPluginState(): void {
|
||||
pluginEnabled = new Map();
|
||||
disabledTabTypes = new Set();
|
||||
}
|
||||
Reference in new issue
Block a user