mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
feat: move the terminal fully into the ssh-terminal plugin (B9)
This commit is contained in:
1 parent
535126fd1b
commit
79d77678c9
309 files changed
+41789
-14958
No files matched your search
@@ -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({
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user