feat: move the terminal fully into the ssh-terminal plugin (B9)

This commit is contained in:
LukeGus committed 2026-09-23 21:41:22 -05:00
1 parent 535126fd1b
commit 79d77678c9
309 files changed
+41789 -14958

No files matched your search

+8
View File
@@ -31,6 +31,7 @@ import {
registerSlotContribution,
} from "@/shell/action-registry";
import { pluginWsUrl } from "@/lib/plugin-transport";
import { registerPluginComponent } from "./component-registry";
import type { WidgetTypeDefinition } from "@/types/homepage-types";
import type { LucideIcon } from "lucide-react";
import {
@@ -168,6 +169,9 @@ export function createPluginApp(
standaloneViews: options.standaloneViews,
panelFrame: options.panelFrame,
inLayouts: options.inLayouts,
commandTarget: options.commandTarget,
ownBackground: options.ownBackground,
multiInstance: options.multiInstance,
preload: options.preload,
}),
);
@@ -305,6 +309,10 @@ export function createPluginApp(
invokeAction: (id, ...args) => invokeAction(id, ...args),
registerComponent(id, component) {
return track(registerPluginComponent(id, scoped(component)));
},
registerSshAuthEditor(editor) {
return track(
registerSshAuthEditor({
+4 -1
View File
@@ -26,6 +26,7 @@ import { tabsApi, useShellHosts } from "./shell-bridge";
import { invokeAction } from "@/shell/action-registry";
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
import { useActionSlot } from "@/hooks/use-action-slot";
import { usePluginComponent } from "./component-registry";
/**
* Core permission groups, mirroring RESERVED_PERMISSION_PREFIXES in the SDK
@@ -272,7 +273,9 @@ export const pluginHostBridge: PluginHostBridge = {
};
},
useSlotContributions: (slotId) => useActionSlot(slotId),
useSlotContributions: (slotId, context) => useActionSlot(slotId, context),
usePluginComponent: (id) => usePluginComponent(id),
};
/** Installs the bridge. Idempotent. */
+80
View File
@@ -0,0 +1,80 @@
import {
createElement,
useSyncExternalStore,
type ComponentType,
type ReactNode,
} from "react";
/**
* Components a plugin offers other code by id, through app.registerComponent.
*
* The owner decides what the component is and the caller decides where it
* goes: the ssh-terminal plugin offers "terminal.view", and the collab room,
* the homepage SSH widget and the file manager render it without importing
* the terminal. While the owner is off the id resolves to nothing and the
* caller's fallback shows instead.
*/
type AnyComponent = ComponentType<Record<string, unknown>>;
const components = new Map<string, AnyComponent>();
const listeners = new Set<() => void>();
let version = 0;
function notify(): void {
version += 1;
for (const listener of listeners) listener();
}
export function registerPluginComponent(
id: string,
component: AnyComponent,
): () => void {
components.set(id, component);
notify();
return () => {
if (components.get(id) === component) {
components.delete(id);
notify();
}
};
}
export function getPluginComponent(id: string): AnyComponent | undefined {
return components.get(id);
}
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}
/** The component registered under an id, re-rendering when that changes. */
export function usePluginComponent(id: string): AnyComponent | undefined {
useSyncExternalStore(subscribe, () => version);
return components.get(id);
}
/**
* Renders the component another plugin registered under `id`, with the rest
* of the props passed through, or `fallback` while nobody has registered it.
*/
export function PluginComponent({
id,
fallback = null,
...props
}: {
id: string;
fallback?: ReactNode;
[prop: string]: unknown;
}): ReactNode {
const Component = usePluginComponent(id);
if (!Component) return fallback as ReactNode;
return createElement(Component, props);
}
/** Test seam. */
export function resetPluginComponents(): void {
components.clear();
notify();
}
+58
View File
@@ -66,3 +66,61 @@ export { useTabs, useTabsSafe } from "@/shell/TabContext";
// Slots: a plugin can offer places for other plugins to fill.
export { ActionSlot, ComponentSlot } from "@/shell/ActionSlot";
// Components other plugins offer by id (app.registerComponent), rendered with
// a fallback while their plugin is off.
export { PluginComponent } from "@/plugin-host/component-registry";
// More primitives and hooks.
export { cn } from "@/lib/utils";
export {
runAdaptivePolling,
getPollingEnvironmentMultiplier,
} from "@/lib/adaptive-polling";
export * from "@/components/sheet";
export { useConfirmation } from "@/hooks/use-confirmation";
export { useIsMobile } from "@/hooks/use-mobile";
export { PassphraseDialog } from "@/ssh/dialogs/PassphraseDialog";
export { OPKSSHDialog } from "@/ssh/dialogs/OPKSSHDialog";
export { HostKeyVerificationDialog } from "@/ssh/dialogs/HostKeyVerificationDialog";
// Terminal look: themes, fonts and clipboard, shared by every terminal-like
// surface (the SSH and local terminals, the docker console, serial).
export * from "@/lib/terminal-themes";
export { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme";
export { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles";
export * from "@/lib/terminal-look/terminal-font-zoom";
export { copyToClipboard, readFromClipboard } from "@/lib/clipboard";
export { RobustClipboardProvider } from "@/lib/clipboard-provider";
export { useTheme as useAppTheme } from "@/components/theme-provider";
// Keyboard handling the shell shares with a terminal, so app shortcuts keep
// working while a terminal has focus.
export { findMatchingKeybinding } from "@/lib/keybinding-match";
export { globalShortcutHandler } from "@/lib/global-shortcut-handler";
export { isMacPlatform, isTabJumpHotkey } from "@/lib/tab-jump-hotkey";
export type * from "@/types/keybindings";
// Connection helpers: which backend a host's session dials, and the pieces
// the desktop app needs to reach it.
export { isElectron } from "@/lib/electron";
export {
resolveConnectionOrigin,
type ConnectionOrigin,
} from "@/lib/connection-origin";
export { pluginWsUrl } from "@/lib/plugin-transport";
export { hydrateLocalSharedHostAuth } from "@/lib/remote-server-api";
export { useConnectionDefaults } from "@/contexts/ConnectionDefaultsContext";
// Core APIs a terminal-like surface calls: recent activity, the host's stored
// password for sudo autofill, the open tab record, the user's keybindings and
// host terminal options.
export { getCookie } from "@/main-axios";
export { logActivity } from "@/api/dashboard-api";
export { getHostPassword } from "@/api/credentials-api";
export {
patchOpenTab,
getUserPreferences,
parseCustomKeybindings,
} from "@/api/open-tabs-api";
export { setHostAutoTmux } from "@/api/host-terminal-config-api";
-1
View File
@@ -89,7 +89,6 @@ export const shell: TabShellCallbacks = {
openSingletonTab: (...args) => callbacks?.openSingletonTab(...args),
closeTab: (...args) => callbacks?.closeTab(...args),
renameTab: (...args) => callbacks?.renameTab(...args),
openTerminalTab: (...args) => callbacks?.openTerminalTab(...args),
openRailView: (...args) => callbacks?.openRailView(...args),
closeRailView: (...args) => callbacks?.closeRailView(...args),
saveQuickConnect: (...args) =>
-1
View File
@@ -68,7 +68,6 @@ function recordingShell(calls: ShellCall[]): TabShellCallbacks {
openSingletonTab: record("openSingletonTab"),
closeTab: record("closeTab"),
renameTab: record("renameTab"),
openTerminalTab: record("openTerminalTab"),
openRailView: record("openRailView"),
closeRailView: record("closeRailView"),
saveQuickConnect: async (...args) => {