From fff373d5bdbb880574e6ae0f19cd55fd6bb85280 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sun, 27 Sep 2026 14:02:40 -0500 Subject: [PATCH] chore: remove references to plugin sdk ARCHITECTURE.md --- .gitignore | 2 - packages/plugin-sdk/README.md | 1 - packages/plugin-sdk/package.json | 3 +- packages/plugin-sdk/src/backend.ts | 5 +- packages/plugin-sdk/src/manifest.ts | 3 +- packages/plugin-sdk/src/vitest-preset.ts | 3 +- plugins/acme-ssl/README.md | 2 - plugins/ai/README.md | 2 - plugins/alerts/README.md | 2 - plugins/automations/README.md | 2 - plugins/docker/README.md | 2 - plugins/file-manager/README.md | 2 - plugins/fleets/README.md | 2 - plugins/homepage/README.md | 2 - plugins/host-metrics/README.md | 2 - plugins/ldap/README.md | 2 - plugins/network-topology/README.md | 2 - plugins/opkssh/README.md | 2 - plugins/proxmox/README.md | 2 - plugins/remote-desktop/README.md | 2 - plugins/secret-sources/README.md | 2 - plugins/serial/README.md | 4 +- plugins/session-recording/README.md | 2 - plugins/session-sharing/README.md | 2 - plugins/snippets/README.md | 2 - plugins/ssh-terminal/README.md | 2 - .../frontend/settings/HostTerminalSection.tsx | 21 + plugins/sso/README.md | 2 - plugins/step-ca/README.md | 2 - plugins/tailscale/README.md | 2 - plugins/termix-identity/README.md | 2 - plugins/tmux-monitor/README.md | 2 - plugins/totp/README.md | 2 - plugins/tunnels/README.md | 2 - plugins/vault/README.md | 2 - plugins/wake-on-lan/README.md | 2 - plugins/warpgate/README.md | 2 - plugins/web-endpoint/README.md | 2 - plugins/webauthn/README.md | 2 - plugins/workspaces/README.md | 2 - src/backend/plugins/ctx.ts | 2 +- src/backend/plugins/loader.ts | 2 +- src/backend/tests/plugins/ctx-db.test.ts | 5 +- src/ui/plugin-host/sdk-ui.ts | 3 +- src/ui/sidebar/HostTerminalSettings.tsx | 916 ++++++++++++++++++ .../sidebar/HostTerminalSettings.test.tsx | 60 ++ 46 files changed, 1008 insertions(+), 86 deletions(-) create mode 100644 plugins/ssh-terminal/src/frontend/settings/HostTerminalSection.tsx create mode 100644 src/ui/sidebar/HostTerminalSettings.tsx create mode 100644 src/ui/tests/sidebar/HostTerminalSettings.test.tsx diff --git a/.gitignore b/.gitignore index 812a34091..458fe27ac 100644 --- a/.gitignore +++ b/.gitignore @@ -46,5 +46,3 @@ electron/build-info.cjs /scripts/auto-fix-blocklist.json /scripts/auto-fix-state.json /scripts/auto-fix-report-*.json - -/packages/plugin-sdk/ARCHITECTURE.md diff --git a/packages/plugin-sdk/README.md b/packages/plugin-sdk/README.md index c98b02321..6fcb25e1d 100644 --- a/packages/plugin-sdk/README.md +++ b/packages/plugin-sdk/README.md @@ -16,4 +16,3 @@ npx termix-plugin sign # needs TERMIX_PLUGIN_SIGNING_KEY Start from [Termix-Plugin-Template](https://github.com/Termix-SSH/Termix-Plugin-Template). -The full contract is in [ARCHITECTURE.md](./ARCHITECTURE.md). diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index 2082699e0..1e4f6ca24 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -8,7 +8,7 @@ "url": "git+https://github.com/Termix-SSH/Termix.git", "directory": "packages/plugin-sdk" }, - "homepage": "https://github.com/Termix-SSH/Termix/blob/main/packages/plugin-sdk/ARCHITECTURE.md", + "homepage": "https://github.com/Termix-SSH/Termix/tree/main/packages/plugin-sdk#readme", "type": "module", "engines": { "node": ">=22" @@ -77,7 +77,6 @@ "cli", "schema", "tsconfig.plugin.json", - "ARCHITECTURE.md", "README.md" ], "scripts": { diff --git a/packages/plugin-sdk/src/backend.ts b/packages/plugin-sdk/src/backend.ts index e8bc958cc..95fdb9e9f 100644 --- a/packages/plugin-sdk/src/backend.ts +++ b/packages/plugin-sdk/src/backend.ts @@ -96,9 +96,8 @@ export interface PluginFiles { * Scoped by name: every table carries the p__ prefix, and `refs` exposes * users, ssh_data, roles and user_roles read-only so a plugin can join against * them without being able to write them. In-process code could reach around - * all of this - see "What this protects, and what it does not" in - * ARCHITECTURE.md. The capability, the prefix, lint and review are the - * contract, not a sandbox. + * all of this. The capability, the prefix, lint and review are the contract, + * not a sandbox. */ export interface PluginDatabase { /** Registers a definition and returns its queryable table object. */ diff --git a/packages/plugin-sdk/src/manifest.ts b/packages/plugin-sdk/src/manifest.ts index 6e6db333c..6de760243 100644 --- a/packages/plugin-sdk/src/manifest.ts +++ b/packages/plugin-sdk/src/manifest.ts @@ -420,8 +420,7 @@ export interface PluginManifest { * never bundles them: they stay a real dependency in the plugin's own * package.json and are resolved from node_modules at runtime instead, * because esbuild bundling a native addon breaks the relative path it uses - * to locate its compiled binary. See ARCHITECTURE.md under "native - * dependencies". + * to locate its compiled binary. */ nativeDependencies?: string[]; /** How a linked desktop treats this plugin. Defaults to "mirror". */ diff --git a/packages/plugin-sdk/src/vitest-preset.ts b/packages/plugin-sdk/src/vitest-preset.ts index c93c01dff..72b25b65a 100644 --- a/packages/plugin-sdk/src/vitest-preset.ts +++ b/packages/plugin-sdk/src/vitest-preset.ts @@ -6,8 +6,7 @@ * import { pluginVitestConfig } from "@termix/plugin-sdk/vitest-preset"; * export default pluginVitestConfig(import.meta.url); * - * Two projects, matching the layout in ARCHITECTURE.md: tests/backend runs in - * node, tests/frontend in jsdom. + * Two projects: tests/backend runs in node, tests/frontend in jsdom. */ import fs from "node:fs"; diff --git a/plugins/acme-ssl/README.md b/plugins/acme-ssl/README.md index f85c53bd1..4f510fe65 100644 --- a/plugins/acme-ssl/README.md +++ b/plugins/acme-ssl/README.md @@ -39,5 +39,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/ai/README.md b/plugins/ai/README.md index 15b9a7ad8..2d072244e 100644 --- a/plugins/ai/README.md +++ b/plugins/ai/README.md @@ -48,5 +48,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/alerts/README.md b/plugins/alerts/README.md index 39e274acd..75d25acac 100644 --- a/plugins/alerts/README.md +++ b/plugins/alerts/README.md @@ -37,5 +37,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/automations/README.md b/plugins/automations/README.md index f3c2e7449..dd841c35c 100644 --- a/plugins/automations/README.md +++ b/plugins/automations/README.md @@ -35,5 +35,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/docker/README.md b/plugins/docker/README.md index 1c4f089e0..3cd0b37bf 100644 --- a/plugins/docker/README.md +++ b/plugins/docker/README.md @@ -35,5 +35,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/file-manager/README.md b/plugins/file-manager/README.md index 1591025cb..4a4b0064f 100644 --- a/plugins/file-manager/README.md +++ b/plugins/file-manager/README.md @@ -36,5 +36,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/fleets/README.md b/plugins/fleets/README.md index 468598ab6..651814268 100644 --- a/plugins/fleets/README.md +++ b/plugins/fleets/README.md @@ -30,5 +30,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/homepage/README.md b/plugins/homepage/README.md index 5a9c3b2e6..05d5e9a95 100644 --- a/plugins/homepage/README.md +++ b/plugins/homepage/README.md @@ -27,5 +27,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/host-metrics/README.md b/plugins/host-metrics/README.md index 6369bd961..0d40ede3c 100644 --- a/plugins/host-metrics/README.md +++ b/plugins/host-metrics/README.md @@ -50,5 +50,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/ldap/README.md b/plugins/ldap/README.md index 971e1b0b0..aad1b98eb 100644 --- a/plugins/ldap/README.md +++ b/plugins/ldap/README.md @@ -25,5 +25,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/network-topology/README.md b/plugins/network-topology/README.md index deb77594f..0e80064ee 100644 --- a/plugins/network-topology/README.md +++ b/plugins/network-topology/README.md @@ -27,5 +27,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/opkssh/README.md b/plugins/opkssh/README.md index 246167ac6..2403cc451 100644 --- a/plugins/opkssh/README.md +++ b/plugins/opkssh/README.md @@ -38,5 +38,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/proxmox/README.md b/plugins/proxmox/README.md index db60425fd..4df68d975 100644 --- a/plugins/proxmox/README.md +++ b/plugins/proxmox/README.md @@ -25,5 +25,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/remote-desktop/README.md b/plugins/remote-desktop/README.md index 78448efea..148596406 100644 --- a/plugins/remote-desktop/README.md +++ b/plugins/remote-desktop/README.md @@ -66,5 +66,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/secret-sources/README.md b/plugins/secret-sources/README.md index f820fb7d4..933ee3975 100644 --- a/plugins/secret-sources/README.md +++ b/plugins/secret-sources/README.md @@ -21,5 +21,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/serial/README.md b/plugins/serial/README.md index 5592a1f59..57680eb47 100644 --- a/plugins/serial/README.md +++ b/plugins/serial/README.md @@ -10,7 +10,7 @@ Open a serial console to a device plugged into your computer. ## Setup -The desktop app uses the `serialport` npm package, which has a native binary. The build does not bundle it. See "native dependencies" in `packages/plugin-sdk/ARCHITECTURE.md` for how Docker, Electron and dev each get a working copy. +The desktop app uses the `serialport` npm package, which has a native binary. The build does not bundle it. ## Development @@ -19,5 +19,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/session-recording/README.md b/plugins/session-recording/README.md index 9fd006886..cd4e575c2 100644 --- a/plugins/session-recording/README.md +++ b/plugins/session-recording/README.md @@ -37,5 +37,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/session-sharing/README.md b/plugins/session-sharing/README.md index d5a3da019..6b253d9e9 100644 --- a/plugins/session-sharing/README.md +++ b/plugins/session-sharing/README.md @@ -41,5 +41,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/snippets/README.md b/plugins/snippets/README.md index 7db72bc71..fe4a582b7 100644 --- a/plugins/snippets/README.md +++ b/plugins/snippets/README.md @@ -37,5 +37,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/ssh-terminal/README.md b/plugins/ssh-terminal/README.md index 510525f4e..e490b922a 100644 --- a/plugins/ssh-terminal/README.md +++ b/plugins/ssh-terminal/README.md @@ -52,5 +52,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/ssh-terminal/src/frontend/settings/HostTerminalSection.tsx b/plugins/ssh-terminal/src/frontend/settings/HostTerminalSection.tsx new file mode 100644 index 000000000..592f62c2b --- /dev/null +++ b/plugins/ssh-terminal/src/frontend/settings/HostTerminalSection.tsx @@ -0,0 +1,21 @@ +import type { HostEditorSectionProps } from "@termix/plugin-sdk/frontend"; +import { HostFeatureFields, HostTerminalSettings } from "@termix/plugin-sdk/ui"; + +/** The host editor's Terminal tab: this plugin's switches, then the look. */ +export function HostTerminalSection({ + form, + setField, + updateForm, + snippets, +}: HostEditorSectionProps) { + return ( + <> + + + + ); +} diff --git a/plugins/sso/README.md b/plugins/sso/README.md index 86c0991db..2daad778b 100644 --- a/plugins/sso/README.md +++ b/plugins/sso/README.md @@ -26,5 +26,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/step-ca/README.md b/plugins/step-ca/README.md index 8aaabba4a..26ad7f607 100644 --- a/plugins/step-ca/README.md +++ b/plugins/step-ca/README.md @@ -29,5 +29,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/tailscale/README.md b/plugins/tailscale/README.md index 517ad6e19..e85eee27b 100644 --- a/plugins/tailscale/README.md +++ b/plugins/tailscale/README.md @@ -28,5 +28,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/termix-identity/README.md b/plugins/termix-identity/README.md index 21f50f444..7e1dbf799 100644 --- a/plugins/termix-identity/README.md +++ b/plugins/termix-identity/README.md @@ -33,5 +33,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/tmux-monitor/README.md b/plugins/tmux-monitor/README.md index f2036ebd5..80ca29dbb 100644 --- a/plugins/tmux-monitor/README.md +++ b/plugins/tmux-monitor/README.md @@ -33,5 +33,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/totp/README.md b/plugins/totp/README.md index d9b30974d..1d7e68c09 100644 --- a/plugins/totp/README.md +++ b/plugins/totp/README.md @@ -15,5 +15,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/tunnels/README.md b/plugins/tunnels/README.md index 00851c728..92369548f 100644 --- a/plugins/tunnels/README.md +++ b/plugins/tunnels/README.md @@ -33,5 +33,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/vault/README.md b/plugins/vault/README.md index 766ea881b..753e7820b 100644 --- a/plugins/vault/README.md +++ b/plugins/vault/README.md @@ -36,5 +36,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/wake-on-lan/README.md b/plugins/wake-on-lan/README.md index c72bd6b4d..dcf43af66 100644 --- a/plugins/wake-on-lan/README.md +++ b/plugins/wake-on-lan/README.md @@ -31,5 +31,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/warpgate/README.md b/plugins/warpgate/README.md index 0c00e5477..b50904d78 100644 --- a/plugins/warpgate/README.md +++ b/plugins/warpgate/README.md @@ -21,5 +21,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/web-endpoint/README.md b/plugins/web-endpoint/README.md index b7bba86da..6552da6ea 100644 --- a/plugins/web-endpoint/README.md +++ b/plugins/web-endpoint/README.md @@ -36,5 +36,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/webauthn/README.md b/plugins/webauthn/README.md index 0bdee9c0c..4625d344a 100644 --- a/plugins/webauthn/README.md +++ b/plugins/webauthn/README.md @@ -15,5 +15,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/plugins/workspaces/README.md b/plugins/workspaces/README.md index 3afec8233..10b326444 100644 --- a/plugins/workspaces/README.md +++ b/plugins/workspaces/README.md @@ -26,5 +26,3 @@ npm run build # build into dist/ npm run test # run this plugin's tests npm run typecheck # type-check this plugin ``` - -See `packages/plugin-sdk/ARCHITECTURE.md` for the plugin contract. diff --git a/src/backend/plugins/ctx.ts b/src/backend/plugins/ctx.ts index 6631a2f43..e88edea86 100644 --- a/src/backend/plugins/ctx.ts +++ b/src/backend/plugins/ctx.ts @@ -12,7 +12,7 @@ * - It does NOT contain malicious code. A plugin runs in the server process * and can import any module core can. Guarding ctx does not change that, * and this file does not pretend otherwise. Trust comes from signing, - * review and the kill list. See ARCHITECTURE.md. + * review and the kill list. * * The actor never comes from plugin code. It comes from AsyncLocalStorage, * set by a request or by ctx.asUser, so a plugin cannot name a user and be diff --git a/src/backend/plugins/loader.ts b/src/backend/plugins/loader.ts index f0e2f3296..c9d38834e 100644 --- a/src/backend/plugins/loader.ts +++ b/src/backend/plugins/loader.ts @@ -9,7 +9,7 @@ * * What that means honestly: a plugin has the same reach as core. The * capability gate on ctx makes privileged calls declared and auditable, not - * impossible. See ARCHITECTURE.md. + * impossible. */ import crypto from "node:crypto"; diff --git a/src/backend/tests/plugins/ctx-db.test.ts b/src/backend/tests/plugins/ctx-db.test.ts index 4b8ceb2d8..3feaf4b60 100644 --- a/src/backend/tests/plugins/ctx-db.test.ts +++ b/src/backend/tests/plugins/ctx-db.test.ts @@ -1,9 +1,8 @@ /** * ctx.db and the kv caps. * - * db:own was in the catalog from A1 but nothing checked it, which ARCHITECTURE - * calls the anti-pattern outright. These assert that it is checked where the - * privileged thing happens, and audited either way. + * db:own was in the catalog from A1 but nothing checked it. These assert that + * it is checked where the privileged thing happens, and audited either way. */ import { beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/src/ui/plugin-host/sdk-ui.ts b/src/ui/plugin-host/sdk-ui.ts index 631d375e0..6bff121a7 100644 --- a/src/ui/plugin-host/sdk-ui.ts +++ b/src/ui/plugin-host/sdk-ui.ts @@ -8,8 +8,7 @@ * The list is deliberate. It covers what the bundled plugins use today: the * shadcn primitives, the composites for metrics, connection screens and * settings rows, and the contexts a connection surface needs. Adding to it is - * a contract change; update ARCHITECTURE.md with it. Removing from it breaks - * plugins. + * a contract change. Removing from it breaks plugins. */ // Primitives diff --git a/src/ui/sidebar/HostTerminalSettings.tsx b/src/ui/sidebar/HostTerminalSettings.tsx new file mode 100644 index 000000000..3c6716b21 --- /dev/null +++ b/src/ui/sidebar/HostTerminalSettings.tsx @@ -0,0 +1,916 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Info, Palette, Plus, X, Zap } from "lucide-react"; +import { toast } from "sonner"; +import { Select2 } from "@/components/select2"; +import { Button } from "@/components/button"; +import { Input } from "@/components/input"; +import { Slider } from "@/components/slider"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/tooltip"; +import { SectionCard, SettingRow, FakeSwitch } from "@/components/section-card"; +import { TerminalPreview } from "@/components/terminal-preview/TerminalPreview"; +import { + TERMINAL_THEMES, + TERMINAL_FONTS, + BELL_STYLES, + FAST_SCROLL_MODIFIERS, + CURSOR_STYLES, +} from "@/lib/terminal-themes"; +import { + TERMINAL_FONT_ZOOM_MIN, + TERMINAL_FONT_ZOOM_MAX, +} from "@/lib/terminal-look/terminal-font-zoom"; +import { + getUserPreferences, + saveUserPreferences, + parseCustomThemes, + type SavedCustomTheme, +} from "@/api/open-tabs-api"; +import { useTabsSafe } from "@/shell/TabContext"; +import type { + HostBackspaceMode, + HostBellStyle, + HostCursorStyle, + HostEditorForm, + HostFastScrollModifier, +} from "./HostEditorData"; + +const CUSTOM_FONT_OPTION = "__custom__"; + +export interface HostTerminalSettingsProps { + // The host editor form, as handed to a host editor section. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + form: any; + setField: (key: string, value: unknown) => void; + snippets?: unknown[]; +} + +/** The host editor's terminal appearance and behavior cards. */ +export function HostTerminalSettings({ + form: rawForm, + setField: rawSetField, + snippets: rawSnippets, +}: HostTerminalSettingsProps) { + const { t } = useTranslation(); + const form = rawForm as HostEditorForm; + const setField = rawSetField as ( + key: K, + value: HostEditorForm[K], + ) => void; + const snippets = (rawSnippets ?? []) as { id: number; name: string }[]; + const { setPreviewTerminalTheme } = useTabsSafe(); + const [isCustomFont, setIsCustomFont] = useState( + () => !TERMINAL_FONTS.some((f) => f.value === form.fontFamily), + ); + const [savedThemes, setSavedThemes] = useState([]); + const [savingTheme, setSavingTheme] = useState(false); + + useEffect(() => { + getUserPreferences() + .then((prefs) => setSavedThemes(parseCustomThemes(prefs.customThemes))) + .catch(() => {}); + }, []); + + const handleSaveAsGlobalTheme = async () => { + const colors = form.customThemeColors; + if (!colors) return; + const name = window.prompt(t("hosts.saveGlobalThemeNamePrompt")); + if (!name || !name.trim()) return; + setSavingTheme(true); + try { + const newTheme: SavedCustomTheme = { + id: `theme-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + name: name.trim(), + colors, + }; + const updated = [...savedThemes, newTheme]; + await saveUserPreferences({ + customThemes: JSON.stringify(updated), + }); + setSavedThemes(updated); + toast.success(t("hosts.saveGlobalThemeSuccess")); + } catch { + toast.error(t("hosts.saveGlobalThemeError")); + } finally { + setSavingTheme(false); + } + }; + + const handleDeleteGlobalTheme = async (id: string) => { + const updated = savedThemes.filter((theme) => theme.id !== id); + try { + await saveUserPreferences({ + customThemes: JSON.stringify(updated), + }); + setSavedThemes(updated); + } catch { + toast.error(t("hosts.saveGlobalThemeError")); + } + }; + + const handleApplyGlobalTheme = (id: string) => { + const theme = savedThemes.find((entry) => entry.id === id); + if (!theme) return; + setField("customThemeColors", { ...theme.colors }); + }; + + return ( + <> + } + > +
+ + setField("inheritTerminalAppearance", value)} + /> + +
+
+ + +
+
+
+ + { + const newTheme = e.target.value; + setField("theme", newTheme); + setPreviewTerminalTheme(newTheme); + if (newTheme === "custom" && !form.customThemeColors) { + setField("customThemeColors", { + ...TERMINAL_THEMES.termixDark.colors, + }); + } + }} + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + {Object.entries(TERMINAL_THEMES) + .filter( + ([key]) => key !== "termixDark" && key !== "termixLight", + ) + .map(([key, theme]) => ( + + ))} + +
+
+
+ + + + + + + + {t("hosts.fontFamilyCustomHint")} + + + +
+ { + if (e.target.value === CUSTOM_FONT_OPTION) { + setIsCustomFont(true); + setField("fontFamily", ""); + } else { + setIsCustomFont(false); + setField("fontFamily", e.target.value); + } + }} + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring font-mono" + > + {TERMINAL_FONTS.map((f) => ( + + ))} + + + {isCustomFont && ( + setField("fontFamily", e.target.value)} + placeholder={t("hosts.fontFamilyCustomPlaceholder")} + className="h-9 text-xs font-mono" + /> + )} +
+
+
+ + + {form.fontSize}px + +
+ setField("fontSize", v)} + /> +
+
+ + + setField("cursorStyle", e.target.value as HostCursorStyle) + } + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + {CURSOR_STYLES.map((s) => ( + + ))} + +
+
+
+ + + {form.letterSpacing}px + +
+ setField("letterSpacing", v)} + /> +
+
+
+ + + {form.lineHeight.toFixed(1)} + +
+ setField("lineHeight", v)} + /> +
+
+ + + setField("bellStyle", e.target.value as HostBellStyle) + } + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + {BELL_STYLES.map((b) => ( + + ))} + +
+
+ + + setField( + "backspaceMode", + e.target.value as HostBackspaceMode, + ) + } + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + + + +
+
+
+ {form.theme === "custom" && ( +
+
+
+ + +
+ {savedThemes.length === 0 ? ( +

+ {t("hosts.noSavedThemes")} +

+ ) : ( +
+ {savedThemes.map((theme) => ( +
+ + +
+ ))} +
+ )} +
+
+ + +
+ {( + [ + ["background", "customThemeBackground"], + ["foreground", "customThemeForeground"], + ["cursor", "customThemeCursor"], + ["cursorAccent", "customThemeCursorAccent"], + ["selectionBackground", "customThemeSelection"], + ] as const + ).map(([key, labelKey]) => ( +
+ +
+ + setField("customThemeColors", { + ...(form.customThemeColors ?? + TERMINAL_THEMES.termixDark.colors), + [key]: e.target.value, + }) + } + className="h-7 w-10 cursor-pointer border border-border bg-background p-0.5" + /> + + {form.customThemeColors?.[key] ?? + TERMINAL_THEMES.termixDark.colors[key]} + +
+
+ ))} + +
+ {( + [ + ["black", "customThemeBlack"], + ["brightBlack", "customThemeBrightBlack"], + ["red", "customThemeRed"], + ["brightRed", "customThemeBrightRed"], + ["green", "customThemeGreen"], + ["brightGreen", "customThemeBrightGreen"], + ["yellow", "customThemeYellow"], + ["brightYellow", "customThemeBrightYellow"], + ["blue", "customThemeBlue"], + ["brightBlue", "customThemeBrightBlue"], + ["magenta", "customThemeMagenta"], + ["brightMagenta", "customThemeBrightMagenta"], + ["cyan", "customThemeCyan"], + ["brightCyan", "customThemeBrightCyan"], + ["white", "customThemeWhite"], + ["brightWhite", "customThemeBrightWhite"], + ] as const + ).map(([key, labelKey]) => ( +
+ +
+ + setField("customThemeColors", { + ...(form.customThemeColors ?? + TERMINAL_THEMES.termixDark.colors), + [key]: e.target.value, + }) + } + className="h-7 w-10 cursor-pointer border border-border bg-background p-0.5" + /> + + {form.customThemeColors?.[key] ?? + TERMINAL_THEMES.termixDark.colors[key]} + +
+
+ ))} +
+
+ )} + + setField("cursorBlink", v)} + /> + + + setField("rightClickSelectsWord", v)} + /> + + + setField("macOptionIsMeta", v)} + /> + + + setField("syntaxHighlighting", v)} + /> + + {form.syntaxHighlighting && ( +
+ {( + [ + [ + "logLevels", + "syntaxCategoryLogLevels", + "syntaxCategoryLogLevelsDesc", + ], + ["paths", "syntaxCategoryPaths", "syntaxCategoryPathsDesc"], + [ + "timestamps", + "syntaxCategoryTimestamps", + "syntaxCategoryTimestampsDesc", + ], + [ + "ipAddresses", + "syntaxCategoryIpAddresses", + "syntaxCategoryIpAddressesDesc", + ], + ["urls", "syntaxCategoryUrls", "syntaxCategoryUrlsDesc"], + [ + "numbers", + "syntaxCategoryNumbers", + "syntaxCategoryNumbersDesc", + ], + ] as const + ).map(([key, labelKey, descKey]) => ( + + + setField("syntaxHighlightingOptions", { + ...form.syntaxHighlightingOptions, + [key]: v, + }) + } + /> + + ))} +
+ )} +
+ +

+ {t("hosts.backgroundImageDesc")} +

+ setField("backgroundImage", e.target.value)} + placeholder="https://example.com/image.jpg" + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring font-mono" + /> +
+ {form.backgroundImage && ( +
+
+ + + {Math.round(form.backgroundImageOpacity * 100)}% + +
+ setField("backgroundImageOpacity", v)} + /> +
+ )} +
+
+ + } + > +
+
+
+ + + {form.scrollback.toLocaleString()}{" "} + {t("hosts.scrollbackMaxLines")} + +
+ setField("scrollback", v)} + /> +
+ + {t("hosts.enableAutoTmuxDesc")}{" "} + + {t("hosts.docsLink")} + + + } + > + setField("autoTmux", v)} + /> + + + setField("agentForwarding", v)} + /> + + + setField("useSSHTitle", v)} + /> + + + setField("autoMosh", v)} + /> + +
+ + + setField( + "localEcho", + e.target.value as "default" | "off" | "auto" | "on", + ) + } + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + + + + + +

+ {t("hosts.localEchoDesc")}{" "} + + {t("hosts.docsLink")} + +

+
+
+ + + setField( + "linkClickBehavior", + e.target.value as "default" | "confirm" | "direct", + ) + } + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + + + + +

+ {t("hosts.linkClickBehaviorDesc")} +

+
+
+
+ + {t("hosts.environmentVariablesLabel")} + + +
+ {form.environmentVariables.length === 0 && ( +

+ {t("hosts.noEnvVars")} +

+ )} +
+ {form.environmentVariables.map((ev, i) => ( +
+ { + const updated = [...form.environmentVariables]; + updated[i] = { + ...updated[i], + key: e.target.value, + }; + setField("environmentVariables", updated); + }} + /> + { + const updated = [...form.environmentVariables]; + updated[i] = { + ...updated[i], + value: e.target.value, + }; + setField("environmentVariables", updated); + }} + /> + +
+ ))} +
+
+
+
+ + + setField( + "fastScrollModifier", + e.target.value as HostFastScrollModifier, + ) + } + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + {FAST_SCROLL_MODIFIERS.map((m) => ( + + ))} + +
+
+
+ + + {form.fastScrollSensitivity} + +
+ setField("fastScrollSensitivity", v)} + /> +
+
+ {form.autoMosh && ( +
+ + setField("moshCommand", e.target.value)} + /> +
+ )} +
+
+ + + setField( + "startupSnippetId", + e.target.value ? Number(e.target.value) : null, + ) + } + className="flex h-9 w-full border border-border bg-background px-3 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" + > + + {snippets.map((s) => ( + + ))} + +
+
+
+
+ + ); +} diff --git a/src/ui/tests/sidebar/HostTerminalSettings.test.tsx b/src/ui/tests/sidebar/HostTerminalSettings.test.tsx new file mode 100644 index 000000000..81fd3556c --- /dev/null +++ b/src/ui/tests/sidebar/HostTerminalSettings.test.tsx @@ -0,0 +1,60 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { cleanup, fireEvent, render, screen } from "@testing-library/react"; +import { HostTerminalSettings } from "@/sidebar/HostTerminalSettings"; +import { createHostEditorForm } from "@/sidebar/HostEditorData"; + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})); + +vi.mock("@/api/open-tabs-api", () => ({ + getUserPreferences: vi.fn().mockResolvedValue({ customThemes: null }), + saveUserPreferences: vi.fn(), + parseCustomThemes: () => [], +})); + +vi.mock("@/components/theme-provider", () => ({ + useTheme: () => ({ theme: "dark" }), +})); + +globalThis.ResizeObserver ??= class { + observe() {} + unobserve() {} + disconnect() {} +} as unknown as typeof ResizeObserver; + +afterEach(cleanup); + +function renderSettings(setField = vi.fn()) { + const form = createHostEditorForm(undefined); + render( + , + ); + return setField; +} + +describe("HostTerminalSettings", () => { + it("renders the appearance and behavior cards", () => { + renderSettings(); + expect(screen.getByText("hosts.terminalAppearance")).toBeTruthy(); + expect(screen.getByText("hosts.behaviorAndAdvanced")).toBeTruthy(); + }); + + it("leaves the keepalive fields to the SSH tab", () => { + renderSettings(); + expect(screen.queryByText("hosts.keepaliveIntervalLabel")).toBeNull(); + }); + + it("sets the startup snippet from the offered snippets", () => { + const setField = renderSettings(); + const select = screen + .getByRole("option", { name: "deploy" }) + .closest("select")!; + fireEvent.change(select, { target: { value: "7" } }); + expect(setField).toHaveBeenCalledWith("startupSnippetId", 7); + }); +});