Files
Termix/plugins/workspaces/src/frontend/workspaceUtils.ts
T

164 lines
4.5 KiB
TypeScript

import type {
Host,
Tab,
TabType,
WorkspacePayload,
WorkspaceTabSnapshot,
} from "@/types/ui-types";
// "dashboard" is deliberately excluded: AppShell always keeps a dashboard
// tab alive as the fallback when every other tab closes, so it isn't a
// meaningful part of a saved arrangement and shouldn't count toward
// tabCount or be reopened on apply.
export const WORKSPACE_CAPTURABLE_TYPES: TabType[] = [
"terminal",
"rdp",
"vnc",
"telnet",
"host-metrics",
"proxmox-stats",
"files",
"docker",
"tunnel",
"network_graph",
"tmux_monitor",
"serial",
"homepage",
"fleet-inventory",
"termix-id",
"session-logs",
"snippets",
"macros",
"history",
"ssh-tools",
"automations",
"ai",
];
export const HOSTLESS_WORKSPACE_TAB_TYPES: TabType[] = [
"tunnel",
"network_graph",
"tmux_monitor",
"homepage",
"fleet-inventory",
"docker",
"termix-id",
"session-logs",
"snippets",
"macros",
"history",
"ssh-tools",
"automations",
"ai",
];
function createWorkspaceSlotId(): string {
if (typeof globalThis.crypto?.randomUUID === "function") {
return globalThis.crypto.randomUUID();
}
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}-${Math.random().toString(36).slice(2)}`;
}
/**
* Maps live tabs to WorkspaceTabSnapshots, generating a fresh slotId per tab.
* slotId is a stable key within the saved payload, distinct from Tab.id
* (which is regenerated every time a tab is opened).
*/
export function buildWorkspaceTabSnapshots(
tabs: Tab[],
genSlotId: () => string = createWorkspaceSlotId,
): { snapshots: WorkspaceTabSnapshot[]; slotIdByTabId: Map<string, string> } {
const capturable = tabs.filter((t) =>
WORKSPACE_CAPTURABLE_TYPES.includes(t.type),
);
const slotIdByTabId = new Map(capturable.map((t) => [t.id, genSlotId()]));
const snapshots: WorkspaceTabSnapshot[] = capturable.map((t) => ({
slotId: slotIdByTabId.get(t.id)!,
type: t.type,
hostSyncId: t.type === "serial" ? null : (t.host?.syncId ?? null),
hostNameSnapshot: t.host?.name ?? null,
label: t.label,
customLabel: t.customLabel,
initialFilePath: t.initialFilePath,
initialPath: t.initialPath,
fleetId: t.fleetId,
serialConfig: t.type === "serial" ? t.serialConfig : undefined,
}));
return { snapshots, slotIdByTabId };
}
/** Remaps a slotId-keyed array (paneTabIds shape) to live tab ids via a slotId->tabId map. */
export function remapSlotIds(
slotIds: (string | null)[],
slotIdToTabId: Map<string, string>,
): (string | null)[] {
return slotIds.map((slotId) =>
slotId != null ? (slotIdToTabId.get(slotId) ?? null) : null,
);
}
/**
* Resolves how to (re)open a single workspace-payload tab on apply.
*
* "singleton" covers every rail-singleton tab type (dashboard/tunnel/docker/
* etc.) whether or not a host was resolved for it - these always go through
* openSingletonTab, which accepts an optional preselected host. "host" is
* for genuinely per-host tab types (terminal/rdp/files/...), which always
* need a resolved host to open at all.
*/
export function resolveWorkspaceTabTarget(
snapshot: WorkspaceTabSnapshot,
allHosts: Host[],
):
| { kind: "serial" }
| { kind: "singleton"; host?: Host }
| { kind: "host"; host: Host }
| { kind: "skip" } {
if (snapshot.type === "serial") {
return snapshot.serialConfig ? { kind: "serial" } : { kind: "skip" };
}
let host: Host | undefined;
if (snapshot.hostSyncId) {
host = allHosts.find((h) => h.syncId === snapshot.hostSyncId);
if (!host) return { kind: "skip" };
}
if (HOSTLESS_WORKSPACE_TAB_TYPES.includes(snapshot.type)) {
return { kind: "singleton", host };
}
return host ? { kind: "host", host } : { kind: "skip" };
}
export function buildWorkspacePayload(input: {
tabs: Tab[];
activeTabId: string;
splitMode: WorkspacePayload["splitMode"];
paneTabIds: (string | null)[];
rowSizes: number[];
rowColSizes: number[][];
genSlotId?: () => string;
sidebar?: WorkspacePayload["sidebar"];
}): WorkspacePayload {
const { snapshots, slotIdByTabId } = buildWorkspaceTabSnapshots(
input.tabs,
input.genSlotId,
);
return {
version: 1,
tabs: snapshots,
activeSlotId: slotIdByTabId.get(input.activeTabId) ?? null,
splitMode: input.splitMode,
paneTabIds: input.paneTabIds.map((tabId) =>
tabId != null ? (slotIdByTabId.get(tabId) ?? null) : null,
),
rowSizes: input.rowSizes,
rowColSizes: input.rowColSizes,
...(input.sidebar ? { sidebar: input.sidebar } : {}),
};
}