feat: merge host editor terminal tabs into one owned by ssh-terminal

This commit is contained in:
LukeGus committed 2026-09-27 14:03:02 -05:00
1 parent fff373d5bd
commit c7fd900732
10 files changed
+162 -986

No files matched your search

-1
View File
@@ -344,7 +344,6 @@
"uploadFile": "Upload File",
"tabGeneral": "General",
"tabSsh": "SSH",
"tabTerminal": "Terminal",
"tabStats": "Host Metrics",
"tabSharing": "Sharing",
"tabAuthentication": "Authentication",
+11
View File
@@ -70,6 +70,12 @@ export { useTabs, useTabsSafe } from "@/shell/TabContext";
// Slots: a plugin can offer places for other plugins to fill.
export { ActionSlot, ComponentSlot } from "@/shell/ActionSlot";
// A plugin's manifest host settings, drawn inside its own host editor section.
export {
HostFeatureFields,
type HostFeatureFieldsProps,
} from "@/settings/HostPluginSections";
// Components other plugins offer by id (app.registerComponent), rendered with
// a fallback while their plugin is off.
export { PluginComponent } from "@/plugin-host/component-registry";
@@ -103,6 +109,11 @@ 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 { TerminalPreview } from "@/components/terminal-preview/TerminalPreview";
export {
HostTerminalSettings,
type HostTerminalSettingsProps,
} from "@/sidebar/HostTerminalSettings";
export { copyToClipboard, readFromClipboard } from "@/lib/clipboard";
export { RobustClipboardProvider } from "@/lib/clipboard-provider";
export { useTheme as useAppTheme } from "@/components/theme-provider";
+42
View File
@@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next";
import { SectionCard } from "@/components/section-card";
import { PluginIcon } from "@/lib/plugin-icon";
import { getPlugins, type PluginSummary } from "@/api/plugins-api";
import { usePluginScope } from "@/plugin-host/scope";
import { SettingsFieldRow } from "./SettingsFields";
import { hasVisibleFields, isFieldActive } from "./settings-fields-util";
@@ -112,6 +113,47 @@ export function HostPluginSections({
);
}
export interface HostFeatureFieldsProps {
// The host editor form, as handed to a host editor section.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
form: any;
updateForm: (
patch: (form: Record<string, unknown>) => Record<string, unknown>,
) => void;
}
/**
* The calling plugin's manifest host settings, for a plugin that registered
* its own host editor section and so gets no generated tab.
*/
export function HostFeatureFields({
form,
updateForm,
}: HostFeatureFieldsProps) {
const pluginId = usePluginScope();
const plugin = usePluginHostSections().find((p) => p.id === pluginId);
if (!plugin) return null;
const values = (form?.pluginSettings as HostPluginSettings | undefined) ?? {};
return (
<HostPluginSection
plugin={plugin}
values={values[plugin.id] ?? {}}
setValue={(key, value) =>
updateForm((current) => {
const all = (current.pluginSettings ?? {}) as HostPluginSettings;
return {
...current,
pluginSettings: {
...all,
[plugin.id]: { ...(all[plugin.id] ?? {}), [key]: value },
},
};
})
}
/>
);
}
function HostPluginSection({
plugin,
values,
File diff suppressed because it is too large. Load diff
+3 -9
View File
@@ -1,10 +1,10 @@
/* eslint-disable react-refresh/only-export-components */
import { useEffect, useRef, type ComponentType, type ReactNode } from "react";
import { KeyRound, Settings, SquareTerminal, Terminal } from "lucide-react";
import { KeyRound, Settings, Terminal } from "lucide-react";
import { byOrderThenId, createRegistry } from "@/lib/registry";
/** Core host editor tabs. Plugins add theirs through registerHostEditorSection. */
export type CoreHostTabId = "general" | "ssh" | "terminal-options";
export type CoreHostTabId = "general" | "ssh";
export type HostTabId = CoreHostTabId | (string & {});
export type CredentialTabId = "general" | "auth";
@@ -62,7 +62,7 @@ export const hostEditorSectionList = sections.list;
export const useHostEditorSections = sections.useList;
export const resetHostEditorSections = sections.reset;
const CORE_SSH_GROUP = new Set<string>(["ssh", "terminal-options"]);
const CORE_SSH_GROUP = new Set<string>(["ssh"]);
/** Whether a tab lives in the SSH group's second strip. */
export function isSshGroupTab(id: string): boolean {
@@ -149,12 +149,6 @@ export function makeHostSshSubTabs(
icon: <Settings className="size-3" />,
order: 0,
},
{
id: "terminal-options",
label: t("hosts.tabTerminal"),
icon: <SquareTerminal className="size-3" />,
order: 10,
},
],
"ssh",
t,
+1 -5
View File
@@ -244,11 +244,7 @@ describe("registries through the app object", () => {
it("adds a host editor section to the SSH group in order", async () => {
await mount();
const ids = makeHostSshSubTabs((key) => key).map((tab) => tab.id);
expect(ids.slice(0, 3)).toEqual([
"ssh",
"terminal-options",
"fixture-section",
]);
expect(ids.slice(0, 2)).toEqual(["ssh", "fixture-section"]);
expect(makeHostTabs((key) => key).map((tab) => tab.id)).not.toContain(
"fixture-section",
);
@@ -3,7 +3,13 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from "@testing-library/react";
import type { PluginSummary } from "@/api/plugins-api";
import {
hostFeatureTabId,
@@ -14,7 +20,16 @@ import {
registerHostEditorSection,
resetHostEditorSections,
} from "@/sidebar/HostManagerTabs";
import { HostPluginSections } from "@/settings/HostPluginSections";
import {
HostFeatureFields,
HostPluginSections,
} from "@/settings/HostPluginSections";
import { withPluginScope } from "@/plugin-host/scope";
const getPlugins = vi.fn();
vi.mock("@/api/plugins-api", () => ({
getPlugins: () => getPlugins(),
}));
vi.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
@@ -266,3 +281,43 @@ describe("HostPluginSections", () => {
expect(screen.getByDisplayValue("web")).toBeTruthy();
});
});
describe("HostFeatureFields", () => {
it("draws the calling plugin's fields and writes into its pluginSettings", async () => {
getPlugins.mockResolvedValue([
plugin(),
plugin({ id: "warpgate", name: "Warpgate" }),
]);
const form = {
pluginSettings: {
docker: { enableDocker: false },
warpgate: { enableDocker: true },
},
};
const updateForm = vi.fn();
const Scoped = withPluginScope("docker", HostFeatureFields);
render(<Scoped form={form} updateForm={updateForm} />);
await waitFor(() =>
expect(screen.getByText("docker:enableDocker.label")).toBeTruthy(),
);
expect(screen.getAllByText("Docker")).toHaveLength(1);
expect(screen.queryByText("Warpgate")).toBeNull();
fireEvent.click(screen.getAllByRole("button")[0]);
const patch = updateForm.mock.calls[0][0];
expect(patch(form).pluginSettings).toEqual({
docker: { enableDocker: true },
warpgate: { enableDocker: true },
});
});
it("renders nothing outside a plugin", async () => {
getPlugins.mockResolvedValue([plugin()]);
const { container } = render(
<HostFeatureFields form={{}} updateForm={vi.fn()} />,
);
await waitFor(() => expect(getPlugins).toHaveBeenCalled());
expect(container.innerHTML).toBe("");
});
});