import type { ComponentType, Ref } from "react"; import type { HostDraft } from "@termix/plugin-sdk/frontend"; import type { Host, Tab } from "@/types/ui-types"; import { createRegistry } from "@/lib/registry"; import { viewOwner } from "@/plugin-host/view-ownership"; /** * The shell callbacks a registered tab may use. Same bag every tab gets, so * a plugin tab can do anything a core tab can without new positional props. */ export interface TabShellCallbacks { openTab: ( host: Host | null, type: string, options?: { label?: string; forceNewTab?: boolean; data?: Record; }, ) => void; openSingletonTab: ( type: string, options?: { label?: string; data?: Record }, ) => void; closeTab: (tabId: string) => void; renameTab: (tabId: string, label: string) => void; openRailView: (id: string) => void; /** Closes a rail view wherever it is shown, e.g. when its feature goes away. */ closeRailView: (id: string) => void; /** Opens the host editor for a new host with these fields filled in. */ openHostEditor?: (draft?: HostDraft) => void; /** Saves a quick-connect tab's host. Core terminal only. */ saveQuickConnect?: (tab: Tab, host: Host) => Promise; } export interface TabRenderProps { tab: Tab; host?: Host; /** The host in the shape SSH-facing components take. */ sshHost?: Record; label: string; isVisible: boolean; isFocusedPane: boolean; inSplit?: boolean; handleRef: Ref; shell: TabShellCallbacks; } export interface StandaloneViewProps { hostId?: string; view: string; params: URLSearchParams; } export interface TabTypeDef { /** The tab type. */ id: string; pluginId?: string; component: ComponentType; icon?: ComponentType<{ className?: string }>; titleKey?: string; requiresHost?: boolean; noHostMessageKey?: string; persistent?: boolean; singleton?: boolean; session?: boolean; hostless?: boolean; restore?: (host: Host) => boolean; activityTypes?: string[]; standalone?: ComponentType; standaloneViews?: string[]; panelFrame?: boolean; /** False keeps this tab out of saved layouts and workspaces. */ inLayouts?: boolean; /** Panels that type into a terminal act on the last one of these focused. */ commandTarget?: boolean; /** Paints its own background; its frame stays transparent. */ ownBackground?: boolean; /** Every open is a new tab. */ multiInstance?: boolean; preload?: () => Promise; } const registry = createRegistry(); export const registerTabType = registry.register; export const getTabType = registry.get; export const listTabTypes = registry.list; export const useTabTypes = registry.useList; export const resetTabTypes = registry.reset; /** Core tab types that are saved and reopened after login. */ const CORE_PERSISTENT = new Set(); /** Core tab types that hold a live session (close confirm, refresh). */ const CORE_SESSION = new Set(); /** Core tab types that restore without a host. */ const CORE_HOSTLESS = new Set(["dashboard"]); /** * A plugin tab type nobody registered right now: its plugin is off, failed or * still loading. It keeps the behaviour it had, so a saved tab survives the * plugin being disabled instead of being dropped by the next sync. */ function isOwnedButAbsent(type: string): boolean { return !registry.get(type) && !!viewOwner("tab", type); } export function isPersistentTabType(type: string): boolean { if (CORE_PERSISTENT.has(type)) return true; const def = registry.get(type); if (def) return !!def.persistent; return isOwnedButAbsent(type); } export function isSessionTabType(type: string): boolean { return CORE_SESSION.has(type) || !!registry.get(type)?.session; } function isHostlessTabType(type: string): boolean { return CORE_HOSTLESS.has(type) || !!registry.get(type)?.hostless; } /** Whether a saved tab of this type may be restored for this host. */ export function canRestoreTabType( type: string, host: Host | undefined, ): boolean { const def = registry.get(type); if (!host) return isHostlessTabType(type) || isOwnedButAbsent(type); return def?.restore ? def.restore(host) : true; } /** The tab type a recent-activity entry opens, if a registered tab claims it. */ export function tabTypeForActivity( activityType: string, ): TabTypeDef | undefined { return registry .list() .find( (def) => def.id === activityType || def.activityTypes?.includes(activityType), ); } /** The registered tab serving a `?view=` full-screen link. */ export function standaloneViewFor(view: string): TabTypeDef | undefined { return registry .list() .find( (def) => def.standalone && (def.id === view || def.standaloneViews?.includes(view)), ); }