feat: convert serial console into the serial plugin

This commit is contained in:
LukeGus committed 2026-09-23 23:06:37 -05:00
1 parent 708eedb861
commit 791d8656a1
121 files changed
+2216 -1425

No files matched your search

-195
View File
@@ -1,195 +0,0 @@
import { getErrorMessage } from "../utils/error-message.js";
import { WebSocketServer, WebSocket, type RawData } from "ws";
import { SerialPort } from "serialport";
import { AuthManager } from "../utils/auth-manager.js";
import { DataCrypto } from "../utils/data-crypto.js";
import { sshLogger } from "../utils/logger.js";
import { parseWsMessage } from "../utils/ws-message.js";
import { extractWebSocketToken } from "../utils/ws-auth.js";
interface SerialConnectData {
path: string;
baudRate: number;
dataBits?: 5 | 6 | 7 | 8;
stopBits?: 1 | 2;
parity?: "none" | "even" | "odd";
}
const authManager = AuthManager.getInstance();
const wss = new WebSocketServer({ host: "127.0.0.1", port: 30011 });
wss.on("error", (error) => {
sshLogger.error("Serial WebSocket server error", error, {
operation: "wss_error",
});
});
wss.on("connection", async (ws: WebSocket, req) => {
let userId: string | undefined;
try {
const token = extractWebSocketToken(req);
if (!token) {
ws.close(1008, "Authentication required");
return;
}
const payload = await authManager.verifyJWTToken(token);
if (!payload?.userId || payload.pendingTOTP) {
ws.close(1008, "Authentication required");
return;
}
userId = payload.userId;
} catch {
ws.close(1008, "Authentication required");
return;
}
const dataKey = DataCrypto.getUserDataKey(userId);
if (!dataKey) {
ws.send(JSON.stringify({ type: "error", data: "Data locked" }));
ws.close(1008, "Data access required");
return;
}
let port: SerialPort | null = null;
const send = (msg: object) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
}
};
const cleanup = () => {
if (port?.isOpen) {
port.close();
}
port = null;
};
ws.on("message", async (raw: RawData) => {
let type: string;
let data: unknown;
try {
({ type, data } = parseWsMessage(raw));
} catch {
return;
}
try {
switch (type) {
case "list_ports": {
try {
const ports = await SerialPort.list();
send({ type: "ports_list", data: ports });
} catch (err) {
send({
type: "error",
data: getErrorMessage(err, "Failed to list ports"),
});
}
break;
}
case "connect": {
if (port?.isOpen) {
port.close();
port = null;
}
const cfg = data as SerialConnectData;
if (!cfg?.path || !cfg?.baudRate) {
send({ type: "error", data: "Missing port path or baud rate" });
break;
}
try {
port = new SerialPort({
path: cfg.path,
baudRate: cfg.baudRate,
dataBits: cfg.dataBits ?? 8,
stopBits: cfg.stopBits ?? 1,
parity: cfg.parity ?? "none",
autoOpen: false,
});
port.open((err) => {
if (err) {
sshLogger.error("Serial port open failed", err, {
operation: "serial_open",
path: cfg.path,
userId,
});
send({ type: "error", data: err.message });
port = null;
return;
}
sshLogger.info("Serial port opened", {
operation: "serial_open",
path: cfg.path,
baudRate: cfg.baudRate,
userId,
});
send({ type: "connected" });
});
port.on("data", (chunk: Buffer) => {
send({ type: "data", data: chunk.toString("binary") });
});
port.on("error", (err) => {
send({ type: "error", data: err.message });
});
port.on("close", () => {
send({ type: "disconnected" });
port = null;
});
} catch (err) {
send({
type: "error",
data: getErrorMessage(err, "Failed to open serial port"),
});
}
break;
}
case "input": {
if (!port?.isOpen) break;
const input = typeof data === "string" ? data : "";
if (!input) break;
port.write(Buffer.from(input, "binary"), (err) => {
if (err) {
send({ type: "error", data: err.message });
}
});
break;
}
case "disconnect": {
cleanup();
send({ type: "disconnected" });
break;
}
}
} catch (err) {
sshLogger.error("Error handling serial WebSocket message", err, {
operation: "serial_message_handler_error",
userId,
messageType: type,
});
send({ type: "error", data: "Failed to process message" });
}
});
ws.on("close", () => {
cleanup();
});
ws.on("error", () => {
cleanup();
});
});
+27 -1
View File
@@ -26,7 +26,7 @@ import * as serviceRegistry from "./service-registry.js";
import type { ServiceRegistration } from "./service-registry.js";
import * as secretRegistry from "./secret-registry.js";
import type { SecretRegistration } from "./secret-registry.js";
import { assertCapability } from "./permissions.js";
import { assertCapability, hasCapability } from "./permissions.js";
import { getActor, runAsActor } from "./actor.js";
import { DisposableBag } from "./disposables.js";
import {
@@ -322,6 +322,27 @@ export function createPluginContext(
},
);
const capabilitiesRequire = async (capability: string) => {
try {
await assertCapability(pluginId, capability, declared);
} catch (error) {
await writeAudit(
manifest,
{ action: "capability_require", details: () => capability },
{
success: false,
errorMessage: error instanceof Error ? error.message : String(error),
},
);
throw error;
}
await writeAudit(
manifest,
{ action: "capability_require", details: () => capability },
{ success: true },
);
};
return {
pluginId,
manifest,
@@ -626,6 +647,11 @@ export function createPluginContext(
require: (permission) => createRbacMiddleware(manifest, permission),
},
capabilities: {
has: (capability) => hasCapability(pluginId, capability, declared),
require: capabilitiesRequire,
},
disposables: {
add: (dispose) => {
handle.bag.add(dispose, "plugin resource");
+6 -6
View File
@@ -309,12 +309,12 @@ async function provisionLocalDesktopUserIfNeeded(): Promise<void> {
// plugins/ssh-terminal, plugins/docker, plugins/host-metrics,
// plugins/file-manager and plugins/tmux-monitor.
// Every other plugin (AI, Proxmox, Remote Desktop, Fleets, Automations,
// Network Topology, Workspaces, Web Endpoint, Tunnels) is absent for the
// same reason: each one serves its routes under /plugin-api/<id>/ through
// ctx.http on activate, so disabling it answers 503 instead of leaving a
// dead import here. Automations' scheduler and tunnel autostart also
// start from their own activate() rather than here.
await import("./hosts/serial.js");
// Network Topology, Workspaces, Web Endpoint, Tunnels, Serial) is absent
// for the same reason: each one serves its routes under /plugin-api/<id>/
// (or a WS route under /plugin-ws/<id>/) through ctx on activate, so
// disabling it answers 503 instead of leaving a dead import here.
// Automations' scheduler and tunnel autostart also start from their own
// activate() rather than here.
await import("./services/dashboard.js");
await import("./services/homepage.js");
@@ -0,0 +1,106 @@
/**
* ctx.capabilities: a generic check for privileged code no ctx method wraps,
* used first by the serial plugin to gate opening a physical device. Every
* `require` call is audited, allowed or refused, the same as a guarded ctx
* method.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
const grants = new Map<string, string[]>();
const auditEntries: Array<Record<string, unknown>> = [];
vi.mock("../../database/repositories/factory.js", () => ({
createCurrentPluginPermissionGrantRepository: () => ({
listByPlugin: async (pluginId: string) =>
(grants.get(pluginId) ?? []).map((capability) => ({
pluginId,
capability,
})),
}),
}));
vi.mock("../../utils/audit-logger.js", () => ({
logAudit: async (entry: Record<string, unknown>) => {
auditEntries.push(entry);
},
}));
import { createPluginContext, createPluginHandle } from "../../plugins/ctx.js";
import { invalidatePluginPermissionCache } from "../../plugins/permissions.js";
import type { PluginManifest } from "@termix/plugin-sdk/manifest";
function manifestFor(pluginId: string, capabilities: string[]): PluginManifest {
return {
id: pluginId,
name: pluginId,
version: "1.0.0",
description: "",
author: { name: "test" },
license: "MIT",
category: "Productivity",
engine: { termix: ">=2.9.0", api: "1" },
capabilities,
} as PluginManifest;
}
function contextFor(pluginId: string, capabilities: string[]) {
const manifest = manifestFor(pluginId, capabilities);
const handle = createPluginHandle(pluginId, { activate: () => {} });
return { ctx: createPluginContext(manifest, handle), handle };
}
beforeEach(() => {
grants.clear();
auditEntries.length = 0;
invalidatePluginPermissionCache();
});
describe("ctx.capabilities", () => {
it("has() is false when the manifest never declared the capability", async () => {
grants.set("demo", ["device:serial"]);
const { ctx } = contextFor("demo", []);
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(false);
});
it("has() is false when declared but not granted", async () => {
grants.set("demo", []);
const { ctx } = contextFor("demo", ["device:serial"]);
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(false);
});
it("has() is true when declared and granted", async () => {
grants.set("demo", ["device:serial"]);
const { ctx } = contextFor("demo", ["device:serial"]);
await expect(ctx.capabilities.has("device:serial")).resolves.toBe(true);
});
it("require() throws and audits a refusal without the capability", async () => {
grants.set("demo", []);
const { ctx } = contextFor("demo", []);
await expect(ctx.capabilities.require("device:serial")).rejects.toThrow(
/device:serial/,
);
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_capability_require",
success: false,
});
});
it("require() resolves and audits success when granted", async () => {
grants.set("demo", ["device:serial"]);
const { ctx } = contextFor("demo", ["device:serial"]);
await expect(
ctx.capabilities.require("device:serial"),
).resolves.toBeUndefined();
expect(auditEntries.at(-1)).toMatchObject({
action: "plugin_capability_require",
success: true,
});
});
});
-4
View File
@@ -30,10 +30,6 @@ const swaggerOptions: SwaggerJSDocOptions = {
url: "http://localhost:30006",
description: "Dashboard server",
},
{
url: "http://localhost:30011",
description: "Serial connection server",
},
],
components: {
securitySchemes: {
+1 -12
View File
@@ -292,14 +292,6 @@ export type KnownTabType =
*/
export type TabType = KnownTabType | (string & {});
export type SerialConfig = {
path: string;
baudRate: number;
dataBits: 5 | 6 | 7 | 8;
stopBits: 1 | 2;
parity: "none" | "even" | "odd";
};
export type Tab = {
id: string;
instanceId: string;
@@ -319,7 +311,6 @@ export type Tab = {
data?: Record<string, unknown>;
/** Which collab room a collab tab is showing. */
collabRoomId?: string;
serialConfig?: SerialConfig;
/** Present only on a split-screen container tab. Pane ids reference live child tabs. */
splitConfig?: SplitTabConfig;
/** Hides this session from the top-level tab bar while it belongs to a split tab. */
@@ -423,7 +414,7 @@ export type WorkspaceTabSnapshot = {
/** Stable key within the saved tab list, not the live Tab.id (which is regenerated on every open). */
slotId: string;
type: TabType;
/** Set for host-bound tab types, resolved by Host.syncId on apply. Never set for "serial". */
/** Set for host-bound tab types, resolved by Host.syncId on apply. */
hostSyncId?: string | null;
/** Denormalized snapshot for display and graceful-skip messaging if the host is later deleted. */
hostNameSnapshot?: string | null;
@@ -435,8 +426,6 @@ export type WorkspaceTabSnapshot = {
fleetId?: number;
/** The tab's plugin payload. */
data?: Record<string, unknown>;
/** Only present when type === "serial". Fully self-contained, no host resolution needed. */
serialConfig?: SerialConfig;
};
/** One dock's arrangement. `view` is a RailView, or null when the dock is closed. */
-70
View File
@@ -66,9 +66,6 @@ const QuickConnectPanel = lazy(() =>
default: m.QuickConnectPanel,
})),
);
const SerialPanel = lazy(() =>
import("@/sidebar/SerialPanel").then((m) => ({ default: m.SerialPanel })),
);
const SplitScreenPanel = lazy(() =>
import("@/sidebar/SplitScreenPanel").then((m) => ({
default: m.SplitScreenPanel,
@@ -146,7 +143,6 @@ import type {
HostFolder,
ThemeId,
FontSizeId,
SerialConfig,
WorkspacePayload,
} from "@/types/ui-types";
import { applyAccentColor, applyFontSize, PANE_COUNTS } from "@/lib/theme";
@@ -1263,12 +1259,6 @@ export function AppShell({
continue;
}
if (target.kind === "serial" && snapshot.serialConfig) {
const newTabId = openSerialTab(snapshot.serialConfig);
slotIdToNewTabId.set(snapshot.slotId, newTabId);
continue;
}
if (target.kind === "singleton") {
openSingletonTab(
snapshot.type,
@@ -1562,7 +1552,6 @@ export function AppShell({
savedLabel?: string;
initialFilePath?: string;
initialPath?: string;
serialConfig?: SerialConfig;
joinSharedSessionId?: string | null;
joinShareId?: string | null;
collabRoomId?: string;
@@ -1600,7 +1589,6 @@ export function AppShell({
const savedLabel = restore?.savedLabel;
const initialFilePath = restore?.initialFilePath;
const initialPath = restore?.initialPath;
const serialConfig = restore?.serialConfig;
const joinSharedSessionId = restore?.joinSharedSessionId ?? null;
const joinShareId = restore?.joinShareId ?? null;
// A saved label that doesn't match the bare host name or the auto-numbered pattern is a custom label
@@ -1628,7 +1616,6 @@ export function AppShell({
joinShareId,
initialFilePath,
initialPath,
serialConfig,
collabRoomId: restore?.collabRoomId,
},
];
@@ -1667,7 +1654,6 @@ export function AppShell({
joinShareId,
initialFilePath,
initialPath,
serialConfig,
collabRoomId: restore?.collabRoomId,
data: options?.data,
},
@@ -1717,53 +1703,6 @@ export function AppShell({
[loadHosts, t],
);
function openSerialTab(config: SerialConfig): string {
const pseudoHost: Host = {
id: `serial-${Date.now()}`,
name: config.path
? `${config.path} (${config.baudRate})`
: `Serial (${config.baudRate})`,
username: "",
ip: "",
port: 0,
folder: "",
online: false,
cpu: null,
ram: null,
lastAccess: new Date().toISOString(),
authType: "none",
enableTerminal: false,
enableCommandHistory: false,
enableTunnel: false,
enableFileManager: false,
enableDocker: false,
enableProxmox: false,
enableProxmoxStats: false,
enableTmuxMonitor: false,
enableTerminalToolbar: false,
enableAiAssistant: false,
enableSsh: false,
enableRdp: false,
enableVnc: false,
enableTelnet: false,
sshPort: 22,
rdpPort: 3389,
vncPort: 5900,
telnetPort: 23,
serverTunnels: [],
quickActions: [],
};
const instanceId =
typeof crypto.randomUUID === "function"
? crypto.randomUUID()
: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
return openTab(pseudoHost, "serial", {
instanceId,
restoredSessionId: null,
serialConfig: config,
});
}
/** A tab type that opens a fresh tab every time (a local shell). */
function openMultiInstanceTab(type: TabType): string {
const instanceId =
@@ -2500,15 +2439,6 @@ export function AppShell({
</div>
)}
{railView === "serial" && (
<SerialPanel
onConnect={(config) => {
openSerialTab(config);
if (isMobile) setSidebarOpen(false);
}}
/>
)}
{railView === "quick-connect" && (
<QuickConnectPanel
onConnect={(host, type) => {
-364
View File
@@ -1,364 +0,0 @@
import {
useEffect,
useRef,
useCallback,
forwardRef,
useImperativeHandle,
} from "react";
import { useXTerm } from "react-xtermjs";
import { FitAddon } from "@xterm/addon-fit";
import { useTranslation } from "react-i18next";
import { TriangleAlert } from "lucide-react";
import { isElectron } from "@/lib/electron";
import { websocketAuthProtocols } from "@/lib/ws-auth";
import { useTheme } from "@/components/theme-provider";
import { resolveTermixThemeColors } from "@/lib/terminal-look/terminal-theme";
import { DEFAULT_TERMINAL_CONFIG, TERMINAL_FONTS } from "@/lib/terminal-themes";
import { ensureTerminalFontsLoaded } from "@/lib/terminal-look/terminal-global-styles";
import type { SerialConfig } from "@/types/ui-types";
import type { SerialHandle } from "./serial-types";
type WebSerialPort = {
open(options: {
baudRate: number;
dataBits?: number;
stopBits?: number;
parity?: string;
}): Promise<void>;
close(): Promise<void>;
readable: ReadableStream<Uint8Array> | null;
writable: WritableStream<Uint8Array> | null;
};
interface SerialProps {
config: SerialConfig;
isVisible: boolean;
instanceId: string;
}
export const Serial = forwardRef<SerialHandle, SerialProps>(function Serial(
{ config, isVisible, instanceId },
ref,
) {
const { t } = useTranslation();
const { theme: appTheme } = useTheme();
const { instance: terminal, ref: xtermRef } = useXTerm();
const fitAddonRef = useRef<FitAddon | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const connectedRef = useRef(false);
const webSerialReaderRef = useRef<ReadableStreamDefaultReader | null>(null);
const webSerialWriterRef = useRef<WritableStreamDefaultWriter | null>(null);
const webSerialPortRef = useRef<WebSerialPort | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const useWebSerial = !isElectron();
const write = useCallback(
(text: string) => {
terminal?.write(text);
},
[terminal],
);
// ── Theme sync ─────────────────────────────────────────────────────────
useEffect(() => {
if (!terminal) return;
const themeColors = resolveTermixThemeColors("termix", appTheme);
const fontConfig = TERMINAL_FONTS.find(
(f) => f.value === DEFAULT_TERMINAL_CONFIG.fontFamily,
);
ensureTerminalFontsLoaded(fontConfig?.value ?? TERMINAL_FONTS[0].value);
terminal.options.theme = {
background: themeColors.background,
foreground: themeColors.foreground,
cursor: themeColors.cursor,
cursorAccent: themeColors.cursorAccent,
selectionBackground: themeColors.selectionBackground,
selectionForeground: themeColors.selectionForeground,
black: themeColors.black,
red: themeColors.red,
green: themeColors.green,
yellow: themeColors.yellow,
blue: themeColors.blue,
magenta: themeColors.magenta,
cyan: themeColors.cyan,
white: themeColors.white,
brightBlack: themeColors.brightBlack,
brightRed: themeColors.brightRed,
brightGreen: themeColors.brightGreen,
brightYellow: themeColors.brightYellow,
brightBlue: themeColors.brightBlue,
brightMagenta: themeColors.brightMagenta,
brightCyan: themeColors.brightCyan,
brightWhite: themeColors.brightWhite,
};
terminal.options.fontFamily =
fontConfig?.fallback ?? TERMINAL_FONTS[0].fallback;
terminal.options.fontSize = DEFAULT_TERMINAL_CONFIG.fontSize;
}, [terminal, appTheme]);
// ── WebSocket (Electron) path ──────────────────────────────────────────
const buildWsUrl = useCallback(() => {
// Serial is always local -- the device is physically attached to this
// desktop machine, so it never routes through a remote server.
return "ws://127.0.0.1:30011";
}, []);
const disconnectWs = useCallback(() => {
if (wsRef.current) {
wsRef.current.onclose = null;
wsRef.current.close();
wsRef.current = null;
}
connectedRef.current = false;
}, []);
const connectWs = useCallback(() => {
disconnectWs();
const url = buildWsUrl();
if (!url) {
write(`\r\n\x1b[31m${t("serial.errorNoServerUrl")}\x1b[0m\r\n`);
return;
}
const ws = new WebSocket(
url,
websocketAuthProtocols(localStorage.getItem("jwt")),
);
wsRef.current = ws;
ws.onopen = () => {
ws.send(JSON.stringify({ type: "connect", data: config }));
};
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data as string) as {
type: string;
data?: unknown;
};
switch (msg.type) {
case "connected":
connectedRef.current = true;
write(
`\x1b[32m${t("serial.connected", { path: config.path, baud: config.baudRate })}\x1b[0m\r\n`,
);
break;
case "data":
if (typeof msg.data === "string") write(msg.data);
break;
case "disconnected":
connectedRef.current = false;
write(`\r\n\x1b[33m${t("serial.disconnected")}\x1b[0m\r\n`);
break;
case "error":
write(
`\r\n\x1b[31m${t("serial.connectionError")}: ${msg.data as string}\x1b[0m\r\n`,
);
break;
}
} catch {
// ignore malformed messages
}
};
ws.onclose = () => {
connectedRef.current = false;
};
ws.onerror = () => {
write(`\r\n\x1b[31m${t("serial.wsError")}\x1b[0m\r\n`);
};
}, [buildWsUrl, config, disconnectWs, t, write]);
// ── Web Serial API path ────────────────────────────────────────────────
const disconnectWebSerial = useCallback(async () => {
try {
webSerialReaderRef.current?.cancel();
webSerialWriterRef.current?.releaseLock();
await webSerialPortRef.current?.close();
} catch {
// best-effort
}
webSerialReaderRef.current = null;
webSerialWriterRef.current = null;
webSerialPortRef.current = null;
connectedRef.current = false;
}, []);
const connectWebSerial = useCallback(async () => {
await disconnectWebSerial();
if (!("serial" in navigator)) {
write(`\r\n\x1b[31m${t("serial.notSupported")}\x1b[0m\r\n`);
return;
}
try {
const serial = navigator.serial as {
requestPort(): Promise<WebSerialPort>;
};
const port = await serial.requestPort();
await port.open({
baudRate: config.baudRate,
dataBits: config.dataBits,
stopBits: config.stopBits,
parity: config.parity === "none" ? "none" : config.parity,
});
webSerialPortRef.current = port;
connectedRef.current = true;
write(
`\x1b[32m${t("serial.connected", { path: config.path || "serial", baud: config.baudRate })}\x1b[0m\r\n`,
);
const reader = port.readable!.getReader();
webSerialReaderRef.current = reader;
const decoder = new TextDecoder();
(async () => {
try {
for (;;) {
const { value, done } = await reader.read();
if (done) break;
write(decoder.decode(value));
}
} catch {
// port closed
} finally {
reader.releaseLock();
connectedRef.current = false;
write(`\r\n\x1b[33m${t("serial.disconnected")}\x1b[0m\r\n`);
}
})();
const writer = port.writable!.getWriter();
webSerialWriterRef.current = writer;
} catch (err) {
if (err instanceof Error && err.name !== "NotFoundError") {
write(
`\r\n\x1b[31m${t("serial.connectionError")}: ${err.message}\x1b[0m\r\n`,
);
}
}
}, [config, disconnectWebSerial, t, write]);
// ── Unified connect/disconnect ─────────────────────────────────────────
const connect = useCallback(() => {
if (useWebSerial) {
connectWebSerial();
} else {
connectWs();
}
}, [useWebSerial, connectWebSerial, connectWs]);
const disconnect = useCallback(() => {
if (useWebSerial) {
disconnectWebSerial();
} else {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: "disconnect" }));
}
disconnectWs();
}
}, [useWebSerial, disconnectWebSerial, disconnectWs]);
useImperativeHandle(ref, () => ({
connect,
disconnect,
isConnected: () => connectedRef.current,
sendInput: (data: string) => {
if (useWebSerial) {
const encoder = new TextEncoder();
webSerialWriterRef.current?.write(encoder.encode(data)).catch(() => {});
} else if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: "input", data }));
}
},
}));
// ── Terminal setup ─────────────────────────────────────────────────────
useEffect(() => {
if (!terminal) return;
const fitAddon = new FitAddon();
fitAddonRef.current = fitAddon;
terminal.loadAddon(fitAddon);
terminal.options.cursorBlink = DEFAULT_TERMINAL_CONFIG.cursorBlink;
terminal.options.scrollback = DEFAULT_TERMINAL_CONFIG.scrollback;
terminal.onData((data) => {
if (!connectedRef.current) return;
if (useWebSerial) {
const encoder = new TextEncoder();
webSerialWriterRef.current?.write(encoder.encode(data)).catch(() => {});
} else if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({ type: "input", data }));
}
});
connect();
return () => {
disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [terminal, instanceId]);
// ── Fit on visibility change ───────────────────────────────────────────
useEffect(() => {
if (!isVisible || !fitAddonRef.current) return;
try {
fitAddonRef.current.fit();
} catch {
// ignore
}
}, [isVisible]);
// ── ResizeObserver ─────────────────────────────────────────────────────
useEffect(() => {
if (!containerRef.current || !terminal) return;
const observer = new ResizeObserver(() => {
try {
fitAddonRef.current?.fit();
} catch {
// ignore
}
});
observer.observe(containerRef.current);
return () => observer.disconnect();
}, [terminal]);
// ── Not supported (Firefox etc.) ──────────────────────────────────────
if (useWebSerial && !("serial" in navigator)) {
return (
<div className="flex flex-col items-center justify-center flex-1 gap-3 p-6 text-center">
<div className="size-10 rounded-full bg-muted/40 flex items-center justify-center">
<TriangleAlert className="size-5 text-muted-foreground/50" />
</div>
<div className="flex flex-col gap-1">
<span className="text-sm font-semibold text-foreground">
{t("serial.notSupportedTitle")}
</span>
<span className="text-xs text-muted-foreground max-w-xs">
{t("serial.notSupported")}
</span>
</div>
</div>
);
}
return (
<div ref={containerRef} className="flex h-full w-full">
<div ref={xtermRef} className="flex-1 min-h-0" />
</div>
);
});
-6
View File
@@ -1,6 +0,0 @@
export interface SerialHandle {
connect: () => void;
disconnect: () => void;
isConnected: () => boolean;
sendInput: (data: string) => void;
}
+8 -9
View File
@@ -11,13 +11,15 @@ interface OriginResolvableHost {
const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
/**
* Resolves which backend a given host's interactive connection (SSH,
* Docker console, Serial, RDP/VNC/Telnet) should dial: the desktop app's
* embedded local backend, or a connected remote sync server.
* Resolves which backend a given host's interactive connection (SSH, Docker
* console, RDP/VNC/Telnet) should dial: the desktop app's embedded local
* backend, or a connected remote sync server. A plugin whose connection is
* always local regardless of this setting (serial: the hardware is
* physically attached to this desktop machine) skips this helper entirely
* and calls app.wsUrl() without an origin option, which defaults to "local".
*
* Serial always resolves to "local" -- the hardware is physically attached
* to this desktop machine. Everything else follows the host's own override
* if set, falling back to the desktop-wide default.
* Everything else follows the host's own override if set, falling back to
* the desktop-wide default.
*
* RDP/VNC/Telnet are the exception to that fallback: left on Default they
* resolve to "remote" rather than following the desktop-wide setting. They
@@ -30,9 +32,6 @@ const GUACAMOLE_CONNECTION_TYPES = new Set(["rdp", "vnc", "telnet"]);
export async function resolveConnectionOrigin(
host: OriginResolvableHost,
): Promise<ConnectionOrigin> {
if (host.connectionType === "serial") {
return "local";
}
if (!isElectron()) {
return "local";
}
+4 -25
View File
@@ -709,7 +709,6 @@
"hostManager": "Host Manager",
"credentials": "Credentials",
"connections": "Connections",
"serial": "Serial",
"keepSideMenuOpen": "Keep Menu Open",
"collapseSideMenu": "Collapse Menu",
"roleAdministrator": "Administrator",
@@ -3092,30 +3091,6 @@
"openProxmoxStats": "Open Proxmox Stats",
"openTmuxMonitor": "Open Tmux Monitor"
},
"serial": {
"title": "Serial",
"portLabel": "Port",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "Baud Rate",
"dataBitsLabel": "Data",
"stopBitsLabel": "Stop",
"parityLabel": "Parity",
"parityNone": "None",
"parityEven": "Even",
"parityOdd": "Odd",
"connect": "Connect to Serial",
"disconnect": "Disconnect",
"refreshPorts": "Refresh ports",
"connected": "Connected to {{path}} at {{baud}} baud",
"disconnected": "Serial port disconnected",
"connectionError": "Failed to open serial port",
"wsError": "WebSocket error",
"errorNoServerUrl": "No server URL configured",
"notSupportedTitle": "Serial Not Supported",
"notSupported": "Serial connections require a browser with Web Serial API support (Chrome, Edge, or Firefox 151+), or the Termix desktop app.",
"hideHint": "You can hide the Serial tab under User Profile > Appearance > Sidebar > Navigation.",
"browserPickerHint": "Click Connect and your browser will open a port picker to choose the device."
},
"alerts": {
"addChannel": "Add Channel",
"editChannel": "Edit Channel",
@@ -3256,6 +3231,10 @@
"title": "Manage certificates",
"consequence": "It can request and replace the server certificate."
},
"device:serial": {
"title": "Access serial and USB devices",
"consequence": "It can open a physical serial port on the machine running Termix."
},
"hosts:write": {
"title": "Create and change hosts",
"consequence": "It can add hosts and edit the ones you already have."
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Gasheerbestuurder",
"credentials": "Geloofsbriewe",
"connections": "Verbindings",
"serial": "Reeks",
"keepSideMenuOpen": "Hou Kieslys Oop",
"collapseSideMenu": "Vou Kieslys in",
"roleAdministrator": "Administrateur",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Maak Proxmox-statistieke oop",
"openTmuxMonitor": "Maak Tmux-monitor oop"
},
"serial": {
"title": "Reeks",
"portLabel": "Hawe",
"portPlaceholder": "/dev/ttyUSB0 of COM3",
"baudRateLabel": "Baudtempo",
"dataBitsLabel": "Data",
"stopBitsLabel": "Stop",
"parityLabel": "Pariteit",
"parityNone": "Geen",
"parityEven": "Selfs",
"parityOdd": "Vreemd",
"connect": "Koppel aan serieel",
"disconnect": "Ontkoppel",
"refreshPorts": "Verfris poorte",
"connected": "Verbind aan {{path}} teen {{baud}} baud",
"disconnected": "Seriepoort ontkoppel",
"connectionError": "Kon nie die seriepoort oopmaak nie",
"wsError": "WebSocket-fout",
"errorNoServerUrl": "Geen bediener-URL gekonfigureer nie",
"notSupportedTitle": "Serienommer word nie ondersteun nie",
"notSupported": "Seriële verbindings vereis 'n blaaier met Web Serial API-ondersteuning (Chrome, Edge of Firefox 151+), of die Termix-lessenaarprogram.",
"hideHint": "Jy kan die Seriële oortjie versteek onder Gebruikersprofiel > Voorkoms > Sybalk > Navigasie.",
"browserPickerHint": "Klik op Verbind en jou blaaier sal 'n poortkieser oopmaak om die toestel te kies."
},
"alerts": {
"addChannel": "Voeg kanaal by",
"editChannel": "Wysig kanaal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "مدير المضيفين",
"credentials": "بيانات الاعتماد",
"connections": "الاتصالات",
"serial": "تسلسلي",
"keepSideMenuOpen": "أبقِ قائمة الطعام مفتوحة",
"collapseSideMenu": "قائمة قابلة للطي",
"roleAdministrator": "مسؤول",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "افتح إحصائيات Proxmox",
"openTmuxMonitor": "افتح شاشة Tmux"
},
"serial": {
"title": "منفذ تسلسلي",
"portLabel": "المنفذ",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "معدل الباود",
"dataBitsLabel": "البيانات",
"stopBitsLabel": "التوقف",
"parityLabel": "التكافؤ",
"parityNone": "بلا",
"parityEven": "زوجي",
"parityOdd": "فردي",
"connect": "اتصال",
"disconnect": "قطع الاتصال",
"refreshPorts": "تحديث المنافذ",
"connected": "متصل بـ {{path}} بسرعة {{baud}} باود",
"disconnected": "تم قطع الاتصال بالمنفذ التسلسلي",
"connectionError": "فشل في فتح المنفذ التسلسلي",
"wsError": "خطأ في WebSocket",
"errorNoServerUrl": "لم يتم تكوين عنوان URL للخادم",
"notSupportedTitle": "المنفذ التسلسلي غير مدعوم",
"notSupported": "تتطلب الاتصالات التسلسلية متصفحًا يدعم Web Serial API (Chrome، أو Edge، أو Firefox 151+)، أو تطبيق Termix لسطح المكتب.",
"hideHint": "يمكنك إخفاء تبويب المنفذ التسلسلي من الملف الشخصي > المظهر > الشريط الجانبي > التنقل.",
"browserPickerHint": "انقر على Connect وسيفتح متصفحك نافذة اختيار المنفذ لتحديد الجهاز."
},
"alerts": {
"addChannel": "إضافة قناة",
"editChannel": "تعديل القناة",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Мениджър на домакини",
"credentials": "Пълномощия",
"connections": "Връзки",
"serial": "Сериен",
"keepSideMenuOpen": "Запази менюто отворено",
"collapseSideMenu": "Свиване на менюто",
"roleAdministrator": "Администратор",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Отворете статистиката на Proxmox",
"openTmuxMonitor": "Отворете Tmux монитора"
},
"serial": {
"title": "Сериен",
"portLabel": "Порт",
"portPlaceholder": "/dev/ttyUSB0 или COM3",
"baudRateLabel": "Скорост на бодове",
"dataBitsLabel": "Данни",
"stopBitsLabel": "Спри",
"parityLabel": "Паритет",
"parityNone": "Няма",
"parityEven": "Дори",
"parityOdd": "Странно",
"connect": "Свързване към сериен",
"disconnect": "Изключване",
"refreshPorts": "Обновяване на портове",
"connected": "Свързан към {{path}} при {{baud}} бод",
"disconnected": "Серийният порт е прекъснат",
"connectionError": "Неуспешно отваряне на серийния порт",
"wsError": "Грешка в WebSocket",
"errorNoServerUrl": "Няма конфигуриран URL адрес на сървъра",
"notSupportedTitle": "Серийният номер не се поддържа",
"notSupported": "Серийните връзки изискват браузър с поддръжка на Web Serial API (Chrome, Edge или Firefox 151+) или настолното приложение Termix.",
"hideHint": "Можете да скриете раздела „Сериен номер“ в Потребителски профил > Външен вид > Странична лента > Навигация.",
"browserPickerHint": "Щракнете върху „Свързване“ и браузърът ви ще отвори инструмент за избор на порт, за да изберете устройството."
},
"alerts": {
"addChannel": "Добавяне на канал",
"editChannel": "Редактиране на канал",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "হোস্ট ম্যানেজার",
"credentials": "যোগ্যতা",
"connections": "সংযোগ",
"serial": "সিরিয়াল",
"keepSideMenuOpen": "মেনু খোলা রাখুন",
"collapseSideMenu": "মেনু সংকুচিত করুন",
"roleAdministrator": "প্রশাসক",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "ওপেন প্রক্সমক্স স্ট্যাটস",
"openTmuxMonitor": "ওপেন Tmux মনিটর"
},
"serial": {
"title": "সিরিয়াল",
"portLabel": "বন্দর",
"portPlaceholder": "/dev/ttyUSB0 অথবা COM3",
"baudRateLabel": "বড রেট",
"dataBitsLabel": "ডেটা",
"stopBitsLabel": "থামুন",
"parityLabel": "সমতা",
"parityNone": "কোনোটিই না",
"parityEven": "এমনকি",
"parityOdd": "অদ্ভুত",
"connect": "সিরিয়ালের সাথে সংযোগ করুন",
"disconnect": "সংযোগ বিচ্ছিন্ন করুন",
"refreshPorts": "পোর্টগুলি রিফ্রেশ করুন",
"connected": "{{path}} এর সাথে {{baud}} বাউডে সংযুক্ত।",
"disconnected": "সিরিয়াল পোর্ট সংযোগ বিচ্ছিন্ন",
"connectionError": "সিরিয়াল পোর্ট খুলতে ব্যর্থ হয়েছে",
"wsError": "ওয়েব সকেট ত্রুটি",
"errorNoServerUrl": "কোন সার্ভার URL কনফিগার করা নেই",
"notSupportedTitle": "সিরিয়াল সমর্থিত নয়",
"notSupported": "সিরিয়াল সংযোগের জন্য ওয়েব সিরিয়াল এপিআই সমর্থনসহ একটি ব্রাউজার (ক্রোম, এজ, বা ফায়ারফক্স ১৫১+), অথবা টারমিক্স ডেস্কটপ অ্যাপ প্রয়োজন।",
"hideHint": "আপনি ইউজার প্রোফাইল > অ্যাপিয়ারেন্স > সাইডবার > নেভিগেশন-এর অধীনে সিরিয়াল ট্যাবটি লুকাতে পারেন।",
"browserPickerHint": "কানেক্ট-এ ক্লিক করলে আপনার ব্রাউজারে ডিভাইসটি বেছে নেওয়ার জন্য একটি পোর্ট পিকার খুলবে।"
},
"alerts": {
"addChannel": "চ্যানেল যোগ করুন",
"editChannel": "চ্যানেল সম্পাদনা করুন",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Gestor d'amfitrions",
"credentials": "Credencials",
"connections": "Connexions",
"serial": "Sèrie",
"keepSideMenuOpen": "Mantenir el menú obert",
"collapseSideMenu": "Menú replegable",
"roleAdministrator": "Administrador/a",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Obre les estadístiques de Proxmox",
"openTmuxMonitor": "Obre el monitor Tmux"
},
"serial": {
"title": "Sèrie",
"portLabel": "Port",
"portPlaceholder": "/dev/ttyUSB0 o COM3",
"baudRateLabel": "velocitat de bauds",
"dataBitsLabel": "Dades",
"stopBitsLabel": "Atura",
"parityLabel": "Paritat",
"parityNone": "Cap",
"parityEven": "Fins i tot",
"parityOdd": "Estrany",
"connect": "Connecta't al serial",
"disconnect": "Desconnecta",
"refreshPorts": "Actualitza els ports",
"connected": "Connectat a {{path}} a {{baud}} baud",
"disconnected": "Port sèrie desconnectat",
"connectionError": "No s'ha pogut obrir el port sèrie",
"wsError": "Error de WebSocket",
"errorNoServerUrl": "No s'ha configurat cap URL del servidor",
"notSupportedTitle": "Número de sèrie no compatible",
"notSupported": "Les connexions en sèrie requereixen un navegador compatible amb l'API Web Serial (Chrome, Edge o Firefox 151+) o l'aplicació d'escriptori Termix.",
"hideHint": "Podeu amagar la pestanya Sèrie a Perfil d'usuari > Aparença > Barra lateral > Navegació.",
"browserPickerHint": "Feu clic a Connecta i el navegador obrirà un selector de ports per triar el dispositiu."
},
"alerts": {
"addChannel": "Afegeix un canal",
"editChannel": "Edita el canal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Hostitelský manažer",
"credentials": "Pověření",
"connections": "Připojení",
"serial": "Seriál",
"keepSideMenuOpen": "Ponechat nabídku otevřenou",
"collapseSideMenu": "Sbalit nabídku",
"roleAdministrator": "Správce",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Otevřít statistiky Proxmoxu",
"openTmuxMonitor": "Otevřít monitor Tmux"
},
"serial": {
"title": "Seriál",
"portLabel": "Přístav",
"portPlaceholder": "/dev/ttyUSB0 nebo COM3",
"baudRateLabel": "Přenosová rychlost",
"dataBitsLabel": "Data",
"stopBitsLabel": "Zastávka",
"parityLabel": "Parita",
"parityNone": "Žádný",
"parityEven": "Dokonce",
"parityOdd": "Zvláštní",
"connect": "Připojení k sériovému",
"disconnect": "Odpojit",
"refreshPorts": "Obnovit porty",
"connected": "Připojeno k {{path}} rychlostí {{baud}} baudu",
"disconnected": "Sériový port odpojen",
"connectionError": "Nepodařilo se otevřít sériový port",
"wsError": "Chyba WebSocketu",
"errorNoServerUrl": "Není nakonfigurována žádná adresa URL serveru",
"notSupportedTitle": "Sériové číslo není podporováno",
"notSupported": "Sériová připojení vyžadují prohlížeč s podporou webového sériového API (Chrome, Edge nebo Firefox 151+) nebo desktopovou aplikaci Termix.",
"hideHint": "Kartu Sériové číslo můžete skrýt v části Profil uživatele > Vzhled > Postranní panel > Navigace.",
"browserPickerHint": "Klikněte na tlačítko Připojit a prohlížeč otevře výběr portu pro výběr zařízení."
},
"alerts": {
"addChannel": "Přidat kanál",
"editChannel": "Upravit kanál",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Værtsadministrator",
"credentials": "Akkreditiver",
"connections": "Forbindelser",
"serial": "Seriel",
"keepSideMenuOpen": "Hold menuen åben",
"collapseSideMenu": "Skjul menu",
"roleAdministrator": "Administrator",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Åbn Proxmox-statistik",
"openTmuxMonitor": "Åbn Tmux-skærmen"
},
"serial": {
"title": "Seriel",
"portLabel": "Havn",
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
"baudRateLabel": "Baudhastighed",
"dataBitsLabel": "Data",
"stopBitsLabel": "Stop",
"parityLabel": "Paritet",
"parityNone": "Ingen",
"parityEven": "Endog",
"parityOdd": "Ulige",
"connect": "Tilslut til seriel",
"disconnect": "Afbryde",
"refreshPorts": "Opdater porte",
"connected": "Forbundet til {{path}} ved {{baud}} baud",
"disconnected": "Seriel port afbrudt",
"connectionError": "Kunne ikke åbne seriel port",
"wsError": "WebSocket-fejl",
"errorNoServerUrl": "Ingen server-URL konfigureret",
"notSupportedTitle": "Seriel understøttes ikke",
"notSupported": "Serielle forbindelser kræver en browser med Web Serial API-understøttelse (Chrome, Edge eller Firefox 151+) eller Termix-desktopappen.",
"hideHint": "Du kan skjule fanen Seriel under Brugerprofil > Udseende > Sidebjælke > Navigation.",
"browserPickerHint": "Klik på Opret forbindelse, og din browser åbner en portvælger, hvor du kan vælge enheden."
},
"alerts": {
"addChannel": "Tilføj kanal",
"editChannel": "Rediger kanal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Host-Manager",
"credentials": "Zugangsdaten",
"connections": "Verbindungen",
"serial": "Seriell",
"keepSideMenuOpen": "Menü geöffnet lassen",
"collapseSideMenu": "Menü ausklappen",
"roleAdministrator": "Administrator",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Proxmox-Statistiken öffnen",
"openTmuxMonitor": "Tmux-Monitor öffnen"
},
"serial": {
"title": "Seriell",
"portLabel": "Port",
"portPlaceholder": "/dev/ttyUSB0 oder COM3",
"baudRateLabel": "Baudrate",
"dataBitsLabel": "Daten",
"stopBitsLabel": "Stop",
"parityLabel": "Parität",
"parityNone": "Keine",
"parityEven": "Gerade",
"parityOdd": "Ungerade",
"connect": "Mit Seriell verbinden",
"disconnect": "Trennen",
"refreshPorts": "Ports aktualisieren",
"connected": "Verbunden mit {{path}} bei {{baud}} Baud",
"disconnected": "Serielle Schnittstelle getrennt",
"connectionError": "Fehler beim Öffnen der seriellen Schnittstelle",
"wsError": "WebSocket-Fehler",
"errorNoServerUrl": "Keine Server-URL konfiguriert",
"notSupportedTitle": "Seriell nicht unterstützt",
"notSupported": "Serielle Verbindungen erfordern einen Browser mit Web Serial API-Unterstützung (Chrome, Edge oder Firefox 151+) oder die Termix-Desktop-App.",
"hideHint": "Sie können den Seriell-Tab unter Benutzerprofil > Erscheinungsbild > Seitenleiste > Navigation ausblenden.",
"browserPickerHint": "Klicken Sie auf Verbinden, und Ihr Browser öffnet eine Port-Auswahl, um das Gerät auszuwählen."
},
"alerts": {
"addChannel": "Kanal hinzufügen",
"editChannel": "Kanal bearbeiten",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Διευθυντής Υποδοχής",
"credentials": "Διαπιστευτήρια",
"connections": "Συνδέσεις",
"serial": "Σειράς",
"keepSideMenuOpen": "Διατήρηση μενού ανοιχτού",
"collapseSideMenu": "Σύμπτυξη μενού",
"roleAdministrator": "Διαχειριστής",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Άνοιγμα στατιστικών Proxmox",
"openTmuxMonitor": "Άνοιγμα παρακολούθησης Tmux"
},
"serial": {
"title": "Σειράς",
"portLabel": "Λιμάνι",
"portPlaceholder": "/dev/ttyUSB0 ή COM3",
"baudRateLabel": "Ρυθμός Baud",
"dataBitsLabel": "Δεδομένα",
"stopBitsLabel": "Στάση",
"parityLabel": "Ισοτιμία",
"parityNone": "Κανένας",
"parityEven": "Ακόμη και",
"parityOdd": "Περιττός",
"connect": "Σύνδεση σε σειριακή σύνδεση",
"disconnect": "Αποσυνδέω",
"refreshPorts": "Ανανέωση θυρών",
"connected": "Συνδεδεμένο με {{path}} σε {{baud}} baud",
"disconnected": "Η σειριακή θύρα αποσυνδέθηκε",
"connectionError": "Αποτυχία ανοίγματος σειριακής θύρας",
"wsError": "Σφάλμα WebSocket",
"errorNoServerUrl": "Δεν έχει ρυθμιστεί διεύθυνση URL διακομιστή",
"notSupportedTitle": "Σειριακή Δεν Υποστηρίζεται",
"notSupported": "Οι σειριακές συνδέσεις απαιτούν ένα πρόγραμμα περιήγησης με υποστήριξη Web Serial API (Chrome, Edge ή Firefox 151+) ή την εφαρμογή Termix για υπολογιστές.",
"hideHint": "Μπορείτε να αποκρύψετε την καρτέλα Σειριακός αριθμός στην περιοχή Προφίλ χρήστη > Εμφάνιση > Πλαϊνή μπάρα > Πλοήγηση.",
"browserPickerHint": "Κάντε κλικ στην επιλογή Σύνδεση και το πρόγραμμα περιήγησής σας θα ανοίξει ένα πρόγραμμα επιλογής θυρών για να επιλέξετε τη συσκευή."
},
"alerts": {
"addChannel": "Προσθήκη καναλιού",
"editChannel": "Επεξεργασία καναλιού",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Gestor de hosts",
"credentials": "Credenciales",
"connections": "Conexiones",
"serial": "Serial",
"keepSideMenuOpen": "Mantener el menú abierto",
"collapseSideMenu": "Contraer menú",
"roleAdministrator": "Administrador",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Abrir estadísticas de Proxmox",
"openTmuxMonitor": "Abrir el monitor de Tmux"
},
"serial": {
"title": "Serial",
"portLabel": "Puerto",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "Baudios",
"dataBitsLabel": "Datos",
"stopBitsLabel": "Parada",
"parityLabel": "Paridad",
"parityNone": "Ninguna",
"parityEven": "Par",
"parityOdd": "Impar",
"connect": "Conectar a Serial",
"disconnect": "Desconectar",
"refreshPorts": "Actualizar puertos",
"connected": "Conectado a {{path}} a {{baud}} baudios",
"disconnected": "Puerto serie desconectado",
"connectionError": "Error al abrir el puerto serie",
"wsError": "Error WebSocket",
"errorNoServerUrl": "No se ha configurado URL de servidor",
"notSupportedTitle": "Serial no compatible",
"notSupported": "Las conexiones serie requieren un navegador compatible con Web Serial API (Chrome, Edge o Firefox 151+), o la aplicación de escritorio Termix.",
"hideHint": "Puedes ocultar la pestaña Serial en Perfil de usuario > Apariencia > Barra lateral > Navegación.",
"browserPickerHint": "Haz clic en Conectar y el navegador abrirá un selector de puertos para elegir el dispositivo."
},
"alerts": {
"addChannel": "Añadir canal",
"editChannel": "Editar canal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Isäntäpäällikkö",
"credentials": "Valtakirjat",
"connections": "Yhteydet",
"serial": "Sarjanumero",
"keepSideMenuOpen": "Pidä valikko auki",
"collapseSideMenu": "Pienennä valikko",
"roleAdministrator": "Ylläpitäjä",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Avaa Proxmox-tilastot",
"openTmuxMonitor": "Avaa Tmux-näyttö"
},
"serial": {
"title": "Sarjanumero",
"portLabel": "Portti",
"portPlaceholder": "/dev/ttyUSB0 tai COM3",
"baudRateLabel": "Baud-nopeus",
"dataBitsLabel": "Data",
"stopBitsLabel": "Stop",
"parityLabel": "Pariteetti",
"parityNone": "Ei mitään",
"parityEven": "Jopa",
"parityOdd": "Pariton",
"connect": "Yhdistä sarjaporttiin",
"disconnect": "Katkaise yhteys",
"refreshPorts": "Päivitä portit",
"connected": "Yhdistetty {{path}} -laitteeseen {{baud}} baudilla",
"disconnected": "Sarjaportti irrotettu",
"connectionError": "Sarjaportin avaaminen epäonnistui",
"wsError": "WebSocket-virhe",
"errorNoServerUrl": "Palvelimen URL-osoitetta ei ole määritetty",
"notSupportedTitle": "Sarjanumeroa ei tueta",
"notSupported": "Sarjaliikenne vaatii selaimen, jossa on Web Serial API -tuki (Chrome, Edge tai Firefox 151+), tai Termix-työpöytäsovelluksen.",
"hideHint": "Voit piilottaa Sarja-välilehden kohdassa Käyttäjäprofiili > Ulkoasu > Sivupalkki > Navigointi.",
"browserPickerHint": "Napsauta Yhdistä, niin selaimesi avaa porttivalitsimen laitteen valitsemiseksi."
},
"alerts": {
"addChannel": "Lisää kanava",
"editChannel": "Muokkaa kanavaa",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Gestionnaire d'hôtes",
"credentials": "Identifiants",
"connections": "Connexions",
"serial": "Série",
"keepSideMenuOpen": "Garder le menu ouvert",
"collapseSideMenu": "Réduire le menu",
"roleAdministrator": "Administrateur",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Ouvrir les statistiques de Proxmox",
"openTmuxMonitor": "Moniteur Open Tmux"
},
"serial": {
"title": "Série",
"portLabel": "Port",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "Débit en bauds",
"dataBitsLabel": "Données",
"stopBitsLabel": "Stop",
"parityLabel": "Parité",
"parityNone": "Aucune",
"parityEven": "Paire",
"parityOdd": "Impaire",
"connect": "Connecter",
"disconnect": "Déconnecter",
"refreshPorts": "Actualiser les ports",
"connected": "Connecté à {{path}} à {{baud}} bauds",
"disconnected": "Port série déconnecté",
"connectionError": "Échec de l'ouverture du port série",
"wsError": "Erreur WebSocket",
"errorNoServerUrl": "Aucune URL de serveur configurée",
"notSupportedTitle": "Port série non pris en charge",
"notSupported": "Les connexions série nécessitent un navigateur prenant en charge l'API Web Serial (Chrome, Edge, ou Firefox 151+), ou l'application de bureau Termix.",
"hideHint": "Vous pouvez masquer l'onglet Série sous Profil utilisateur > Apparence > Barre latérale > Navigation.",
"browserPickerHint": "Cliquez sur Connexion et votre navigateur ouvrira un sélecteur de port pour choisir le périphérique."
},
"alerts": {
"addChannel": "Ajouter un canal",
"editChannel": "Modifier le canal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "מנהל מארח",
"credentials": "אישורים",
"connections": "חיבורים",
"serial": "סִדרָתִי",
"keepSideMenuOpen": "שמור את התפריט פתוח",
"collapseSideMenu": "כווץ תפריט",
"roleAdministrator": "מְנַהֵל",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "פתח את סטטיסטיקות Proxmox",
"openTmuxMonitor": "פתח את צג Tmux"
},
"serial": {
"title": "סִדרָתִי",
"portLabel": "נָמָל",
"portPlaceholder": "/dev/ttyUSB0 או COM3",
"baudRateLabel": "קצב באוד",
"dataBitsLabel": "נְתוּנִים",
"stopBitsLabel": "לְהַפְסִיק",
"parityLabel": "שִׁוּוּי",
"parityNone": "אַף לֹא אֶחָד",
"parityEven": "אֲפִילוּ",
"parityOdd": "מְשׁוּנֶה",
"connect": "חיבור סריאלי",
"disconnect": "לְנַתֵק",
"refreshPorts": "רענון פורטים",
"connected": "מחובר ל- {{path}} ב- {{baud}} באוד",
"disconnected": "יציאה טורית מנותקת",
"connectionError": "נכשלה פתיחת הפורט הטורי",
"wsError": "שגיאת WebSocket",
"errorNoServerUrl": "לא הוגדרה כתובת URL של שרת",
"notSupportedTitle": "סריאלי לא נתמך",
"notSupported": "חיבורים טוריים דורשים דפדפן עם תמיכה ב-Web Serial API (Chrome, Edge או Firefox 151+), או את אפליקציית שולחן העבודה Termix.",
"hideHint": "ניתן להסתיר את הכרטיסייה 'סדרתי' תחת פרופיל משתמש > מראה > סרגל צד > ניווט.",
"browserPickerHint": "לחץ על התחבר והדפדפן שלך יפתח בורר פורטים לבחירת המכשיר."
},
"alerts": {
"addChannel": "הוסף ערוץ",
"editChannel": "עריכת ערוץ",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "होस्ट प्रबंधक",
"credentials": "क्रेडेंशियल्स",
"connections": "कनेक्शन्स",
"serial": "सीरियल",
"keepSideMenuOpen": "मेनू खुला रखें",
"collapseSideMenu": "मेनू को छोटा करें",
"roleAdministrator": "प्रशासक",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "प्रोक्समॉक्स के आँकड़े खोलें",
"openTmuxMonitor": "ओपन टीएमएक्स मॉनिटर"
},
"serial": {
"title": "सीरियल",
"portLabel": "पोर्ट",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "बॉड दर",
"dataBitsLabel": "डेटा",
"stopBitsLabel": "स्टॉप",
"parityLabel": "पैरिटी",
"parityNone": "कोई नहीं",
"parityEven": "सम",
"parityOdd": "विषम",
"connect": "सीरियल कनेक्ट करें",
"disconnect": "डिस्कनेक्ट करें",
"refreshPorts": "पोर्ट रीफ़्रेश करें",
"connected": "{{path}} से {{baud}} बॉड पर कनेक्टेड",
"disconnected": "सीरियल पोर्ट डिस्कनेक्ट हो गया",
"connectionError": "सीरियल पोर्ट खोलने में विफल",
"wsError": "WebSocket त्रुटि",
"errorNoServerUrl": "कोई सर्वर URL कॉन्फ़िगर नहीं है",
"notSupportedTitle": "सीरियल समर्थित नहीं",
"notSupported": "सीरियल कनेक्शन के लिए Web Serial API समर्थन वाले ब्राउज़र (Chrome, Edge, या Firefox 151+) या Termix डेस्कटॉप ऐप की आवश्यकता है।",
"hideHint": "आप सीरियल टैब को User Profile > Appearance > Sidebar > Navigation में छिपा सकते हैं।",
"browserPickerHint": "Connect पर क्लिक करें और आपका ब्राउज़र डिवाइस चुनने के लिए एक पोर्ट पिकर खोलेगा।"
},
"alerts": {
"addChannel": "चैनल जोड़ें",
"editChannel": "चैनल संपादित करें",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Házigazda-kezelő",
"credentials": "Hitelesítő adatok",
"connections": "Kapcsolatok",
"serial": "Sorozatszám",
"keepSideMenuOpen": "Menü nyitva tartása",
"collapseSideMenu": "Menü összecsukása",
"roleAdministrator": "Adminisztrátor",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Proxmox statisztikák megnyitása",
"openTmuxMonitor": "Tmux monitor megnyitása"
},
"serial": {
"title": "Sorozatszám",
"portLabel": "Kikötő",
"portPlaceholder": "/dev/ttyUSB0 vagy COM3",
"baudRateLabel": "Baud sebesség",
"dataBitsLabel": "Adat",
"stopBitsLabel": "Stop",
"parityLabel": "Paritás",
"parityNone": "Egyik sem",
"parityEven": "Még",
"parityOdd": "Páratlan",
"connect": "Csatlakozás soros porthoz",
"disconnect": "Leválasztás",
"refreshPorts": "Frissítse a portokat",
"connected": "{{path}} -hoz csatlakoztatva {{baud}} baud sebességgel",
"disconnected": "Soros port leválasztva",
"connectionError": "Nem sikerült megnyitni a soros portot",
"wsError": "WebSocket hiba",
"errorNoServerUrl": "Nincs konfigurálva szerver URL",
"notSupportedTitle": "Sorozatszám Nem támogatott",
"notSupported": "A soros kapcsolatokhoz Web Serial API-t támogató böngésző (Chrome, Edge vagy Firefox 151+), vagy a Termix asztali alkalmazás szükséges.",
"hideHint": "A Sorozat fület elrejtheted a Felhasználói profil > Megjelenés > Oldalsáv > Navigáció alatt.",
"browserPickerHint": "Kattintson a Csatlakozás gombra, és a böngészője megnyit egy portválasztót az eszköz kiválasztásához."
},
"alerts": {
"addChannel": "Csatorna hozzáadása",
"editChannel": "Csatorna szerkesztése",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Manajer Host",
"credentials": "Kredensial",
"connections": "Koneksi",
"serial": "Serial",
"keepSideMenuOpen": "Biarkan Menu Tetap Terbuka",
"collapseSideMenu": "Ciutkan Menu",
"roleAdministrator": "Administrator",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Buka Statistik Proxmox",
"openTmuxMonitor": "Buka Monitor Tmux"
},
"serial": {
"title": "Serial",
"portLabel": "Pelabuhan",
"portPlaceholder": "/dev/ttyUSB0 atau COM3",
"baudRateLabel": "Kecepatan Baud",
"dataBitsLabel": "Data",
"stopBitsLabel": "Berhenti",
"parityLabel": "Keseimbangan",
"parityNone": "Tidak ada",
"parityEven": "Bahkan",
"parityOdd": "Aneh",
"connect": "Hubungkan ke Serial",
"disconnect": "Memutuskan",
"refreshPorts": "Penyegaran port",
"connected": "Terhubung ke {{path}} pada kecepatan {{baud}} baud",
"disconnected": "Port serial terputus",
"connectionError": "Gagal membuka port serial",
"wsError": "Kesalahan WebSocket",
"errorNoServerUrl": "Tidak ada URL server yang dikonfigurasi.",
"notSupportedTitle": "Nomor Seri Tidak Didukung",
"notSupported": "Koneksi serial memerlukan browser dengan dukungan Web Serial API (Chrome, Edge, atau Firefox 151+), atau aplikasi desktop Termix.",
"hideHint": "Anda dapat menyembunyikan tab Serial di bawah Profil Pengguna > Tampilan > Bilah Samping > Navigasi.",
"browserPickerHint": "Klik Sambungkan dan browser Anda akan membuka pemilih port untuk memilih perangkat."
},
"alerts": {
"addChannel": "Tambahkan Saluran",
"editChannel": "Edit Saluran",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Gestione host",
"credentials": "Credenziali",
"connections": "Connessioni",
"serial": "Seriale",
"keepSideMenuOpen": "Mantieni il menu aperto",
"collapseSideMenu": "Comprimi menu",
"roleAdministrator": "Amministratore",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Statistiche di Proxmox aperte",
"openTmuxMonitor": "Monitor Tmux aperto"
},
"serial": {
"title": "Seriale",
"portLabel": "Porta",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "Baud rate",
"dataBitsLabel": "Dati",
"stopBitsLabel": "Stop",
"parityLabel": "Parità",
"parityNone": "Nessuna",
"parityEven": "Pari",
"parityOdd": "Dispari",
"connect": "Connetti alla Seriale",
"disconnect": "Disconnetti",
"refreshPorts": "Aggiorna porte",
"connected": "Connesso a {{path}} a {{baud}} baud",
"disconnected": "Porta seriale disconnessa",
"connectionError": "Impossibile aprire la porta seriale",
"wsError": "Errore WebSocket",
"errorNoServerUrl": "Nessun URL del server configurato",
"notSupportedTitle": "Seriale non supportato",
"notSupported": "Le connessioni seriali richiedono un browser con supporto Web Serial API (Chrome, Edge o Firefox 151+), oppure l'app desktop Termix.",
"hideHint": "Puoi nascondere la scheda Seriale in Profilo Utente > Aspetto > Barra laterale > Navigazione.",
"browserPickerHint": "Fai clic su Connetti e il browser aprirà un selettore di porte per scegliere il dispositivo."
},
"alerts": {
"addChannel": "Aggiungi canale",
"editChannel": "Modifica canale",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "ホストマネージャー",
"credentials": "認証情報",
"connections": "接続",
"serial": "シリアル",
"keepSideMenuOpen": "メニューを開いたままにする",
"collapseSideMenu": "メニューを折りたたむ",
"roleAdministrator": "管理者",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Proxmox統計を開く",
"openTmuxMonitor": "Tmux Monitorを開く"
},
"serial": {
"title": "シリアル",
"portLabel": "ポート",
"portPlaceholder": "/dev/ttyUSB0 または COM3",
"baudRateLabel": "ボーレート",
"dataBitsLabel": "データ",
"stopBitsLabel": "ストップ",
"parityLabel": "パリティ",
"parityNone": "なし",
"parityEven": "偶数",
"parityOdd": "奇数",
"connect": "シリアルに接続",
"disconnect": "切断",
"refreshPorts": "ポートを更新",
"connected": "{{path}} に {{baud}} ボーで接続しました",
"disconnected": "シリアルポートが切断されました",
"connectionError": "シリアルポートを開けませんでした",
"wsError": "WebSocket エラー",
"errorNoServerUrl": "サーバー URL が設定されていません",
"notSupportedTitle": "シリアル非対応",
"notSupported": "シリアル接続には Web Serial API をサポートするブラウザ (Chrome, Edge, Firefox 151+) または Termix デスクトップアプリが必要です。",
"hideHint": "シリアルタブは「ユーザープロファイル > 外観 > サイドバー > ナビゲーション」で非表示にできます。",
"browserPickerHint": "接続をクリックすると、ブラウザがポートピッカーを開き、デバイスを選択できます。"
},
"alerts": {
"addChannel": "チャンネルを追加",
"editChannel": "チャンネルを編集",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "호스트 관리자",
"credentials": "자격 증명",
"connections": "연결",
"serial": "시리얼",
"keepSideMenuOpen": "메뉴를 열어 두세요",
"collapseSideMenu": "메뉴 접기",
"roleAdministrator": "관리자",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Proxmox 통계 열기",
"openTmuxMonitor": "Tmux 모니터 열기"
},
"serial": {
"title": "시리얼",
"portLabel": "포트",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "보레이트",
"dataBitsLabel": "데이터",
"stopBitsLabel": "스톱",
"parityLabel": "패리티",
"parityNone": "없음",
"parityEven": "짝수",
"parityOdd": "홀수",
"connect": "시리얼 연결",
"disconnect": "연결 해제",
"refreshPorts": "포트 새로 고침",
"connected": "{{path}}에 {{baud}} 보로 연결됨",
"disconnected": "시리얼 포트 연결 해제됨",
"connectionError": "시리얼 포트를 열지 못했습니다.",
"wsError": "WebSocket 오류",
"errorNoServerUrl": "서버 URL이 구성되지 않았습니다.",
"notSupportedTitle": "시리얼 미지원",
"notSupported": "시리얼 연결에는 Web Serial API를 지원하는 브라우저(Chrome, Edge, Firefox 151+) 또는 Termix 데스크톱 앱이 필요합니다.",
"hideHint": "Serial 탭은 사용자 프로필 > 모양 > 사이드바 > 탐색에서 숨길 수 있습니다.",
"browserPickerHint": "연결 버튼을 클릭하면 브라우저에서 장치 선택을 위한 포트 선택기가 열립니다."
},
"alerts": {
"addChannel": "채널 추가",
"editChannel": "채널 편집",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Hostmanager",
"credentials": "Referenties",
"connections": "Verbindingen",
"serial": "Serieel",
"keepSideMenuOpen": "Menu open houden",
"collapseSideMenu": "Menu inklappen",
"roleAdministrator": "Beheerder",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Open Proxmox-statistieken",
"openTmuxMonitor": "Open Tmux Monitor"
},
"serial": {
"title": "Serieel",
"portLabel": "Haven",
"portPlaceholder": "/dev/ttyUSB0 of COM3",
"baudRateLabel": "Baudrate",
"dataBitsLabel": "Gegevens",
"stopBitsLabel": "Stop",
"parityLabel": "Pariteit",
"parityNone": "Geen",
"parityEven": "Zelfs",
"parityOdd": "Vreemd",
"connect": "Verbinden via serieel",
"disconnect": "Verbinding verbreken",
"refreshPorts": "Vernieuw poorten",
"connected": "Verbonden met {{path}} met {{baud}} baud",
"disconnected": "Seriële poort losgekoppeld",
"connectionError": "Het openen van de seriële poort is mislukt.",
"wsError": "WebSocket-fout",
"errorNoServerUrl": "Geen server-URL geconfigureerd",
"notSupportedTitle": "Serienummer niet ondersteund",
"notSupported": "Voor seriële verbindingen is een browser met Web Serial API-ondersteuning vereist (Chrome, Edge of Firefox 151+), of de Termix-desktopapp.",
"hideHint": "Je kunt het tabblad 'Serienummer' verbergen via Gebruikersprofiel > Uiterlijk > Zijbalk > Navigatie.",
"browserPickerHint": "Klik op Verbinden en uw browser opent een poortkiezer waarmee u het apparaat kunt selecteren."
},
"alerts": {
"addChannel": "Kanaal toevoegen",
"editChannel": "Bewerk kanaal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Vertsadministrator",
"credentials": "Legitimasjon",
"connections": "Tilkoblinger",
"serial": "Seriell",
"keepSideMenuOpen": "Hold menyen åpen",
"collapseSideMenu": "Skjul menyen",
"roleAdministrator": "Administrator",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Åpne Proxmox-statistikk",
"openTmuxMonitor": "Åpne Tmux-skjermen"
},
"serial": {
"title": "Seriell",
"portLabel": "Havn",
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
"baudRateLabel": "Baudhastighet",
"dataBitsLabel": "Data",
"stopBitsLabel": "Stoppe",
"parityLabel": "Paritet",
"parityNone": "Ingen",
"parityEven": "Til og med",
"parityOdd": "Merkelig",
"connect": "Koble til seriell",
"disconnect": "Frakople",
"refreshPorts": "Oppdater porter",
"connected": "Koblet til {{path}} ved {{baud}} baud",
"disconnected": "Seriell port frakoblet",
"connectionError": "Klarte ikke å åpne serieporten",
"wsError": "WebSocket-feil",
"errorNoServerUrl": "Ingen server-URL konfigurert",
"notSupportedTitle": "Serienummer støttes ikke",
"notSupported": "Serielle tilkoblinger krever en nettleser med støtte for Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivebordsappen.",
"hideHint": "Du kan skjule Seriell-fanen under Brukerprofil > Utseende > Sidefelt > Navigasjon.",
"browserPickerHint": "Klikk på Koble til, så åpner nettleseren din en portvelger der du kan velge enheten."
},
"alerts": {
"addChannel": "Legg til kanal",
"editChannel": "Rediger kanal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Menedżer hostów",
"credentials": "Poświadczenia",
"connections": "Połączenia",
"serial": "Port szeregowy",
"keepSideMenuOpen": "Zachowaj otwarte menu",
"collapseSideMenu": "Zwiń menu",
"roleAdministrator": "Administrator",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Otwórz statystyki Proxmox",
"openTmuxMonitor": "Otwórz monitor Tmux"
},
"serial": {
"title": "Port szeregowy",
"portLabel": "Port",
"portPlaceholder": "/dev/ttyUSB0 lub COM3",
"baudRateLabel": "Szybkość transmisji",
"dataBitsLabel": "Dane",
"stopBitsLabel": "Stop",
"parityLabel": "Parzystość",
"parityNone": "Brak",
"parityEven": "Parzysta",
"parityOdd": "Nieparzysta",
"connect": "Połącz z portem szeregowym",
"disconnect": "Odłącz",
"refreshPorts": "Odśwież porty",
"connected": "Połączono z {{path}} z szybkością {{baud}} bodów",
"disconnected": "Port szeregowy rozłączony",
"connectionError": "Nie udało się otworzyć portu szeregowego",
"wsError": "Błąd WebSocket",
"errorNoServerUrl": "Brak skonfigurowanego adresu URL serwera",
"notSupportedTitle": "Port szeregowy nieobsługiwany",
"notSupported": "Połączenia szeregowe wymagają przeglądarki obsługującej Web Serial API (Chrome, Edge lub Firefox 151+) albo aplikacji desktopowej Termix.",
"hideHint": "Kartę Port szeregowy można ukryć w: Profil użytkownika > Wygląd > Pasek boczny > Nawigacja.",
"browserPickerHint": "Kliknij Połącz, a przeglądarka otworzy okno wyboru portu, w którym wskażesz urządzenie."
},
"alerts": {
"addChannel": "Dodaj kanał",
"editChannel": "Edytuj kanał",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Gerente de Hospedagem",
"credentials": "Credenciais",
"connections": "Conexões",
"serial": "Serial",
"keepSideMenuOpen": "Mantenha o menu aberto",
"collapseSideMenu": "Recolher menu",
"roleAdministrator": "Administrador",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Estatísticas do Open Proxmox",
"openTmuxMonitor": "Abra o monitor Tmux"
},
"serial": {
"title": "Serial",
"portLabel": "Porta",
"portPlaceholder": "/dev/ttyUSB0 ou COM3",
"baudRateLabel": "Taxa de transmissão (Baud rate)",
"dataBitsLabel": "Dados",
"stopBitsLabel": "Parar",
"parityLabel": "Paridade",
"parityNone": "Nenhum",
"parityEven": "Até",
"parityOdd": "Chance",
"connect": "Conectar à porta serial",
"disconnect": "Desconectar",
"refreshPorts": "Atualizar portas",
"connected": "Conectado a {{path}} na taxa de transmissão de {{baud}}",
"disconnected": "Porta serial desconectada",
"connectionError": "Falha ao abrir a porta serial",
"wsError": "Erro no WebSocket",
"errorNoServerUrl": "Nenhuma URL de servidor configurada",
"notSupportedTitle": "Serial não suportado",
"notSupported": "As conexões seriais exigem um navegador com suporte à API Web Serial (Chrome, Edge ou Firefox 151+) ou o aplicativo Termix para desktop.",
"hideHint": "Você pode ocultar a guia Serial em Perfil do Usuário > Aparência > Barra lateral > Navegação.",
"browserPickerHint": "Clique em Conectar e seu navegador abrirá um seletor de portas para que você escolha o dispositivo."
},
"alerts": {
"addChannel": "Adicionar canal",
"editChannel": "Editar canal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Gestor de Máquinas",
"credentials": "Credenciais",
"connections": "Ligações",
"serial": "Serial",
"keepSideMenuOpen": "Mantenha o menu aberto",
"collapseSideMenu": "Recolher menu",
"roleAdministrator": "Administrador",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Estatísticas do Open Proxmox",
"openTmuxMonitor": "Abra o monitor Tmux"
},
"serial": {
"title": "Serial",
"portLabel": "Porta",
"portPlaceholder": "/dev/ttyUSB0 ou COM3",
"baudRateLabel": "Taxa Baud",
"dataBitsLabel": "Dados",
"stopBitsLabel": "Stop",
"parityLabel": "Paridade",
"parityNone": "Nenhuma",
"parityEven": "Par",
"parityOdd": "Ímpar",
"connect": "Ligar à Serial",
"disconnect": "Desligar",
"refreshPorts": "Atualizar portas",
"connected": "Ligado a {{path}} a {{baud}} baud",
"disconnected": "Porta série desligada",
"connectionError": "Falha ao abrir a porta série",
"wsError": "Erro de WebSocket",
"errorNoServerUrl": "Nenhum URL de servidor configurado",
"notSupportedTitle": "Série não suportada",
"notSupported": "As ligações série requerem um navegador com suporte à Web Serial API (Chrome, Edge ou Firefox 151+) ou a aplicação de ambiente de trabalho Termix.",
"hideHint": "Pode ocultar o separador Série em Perfil do Utilizador > Aparência > Barra Lateral > Navegação.",
"browserPickerHint": "Clique em Ligar e o seu navegador abrirá um seletor de porta para escolher o dispositivo."
},
"alerts": {
"addChannel": "Adicionar Canal",
"editChannel": "Editar Canal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Manager de gazdă",
"credentials": "Acreditări",
"connections": "Conexiuni",
"serial": "Serial",
"keepSideMenuOpen": "Mențineți meniul deschis",
"collapseSideMenu": "Restrângeți meniul",
"roleAdministrator": "Administrator",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Deschideți statisticile Proxmox",
"openTmuxMonitor": "Deschideți monitorul Tmux"
},
"serial": {
"title": "Serial",
"portLabel": "Port",
"portPlaceholder": "/dev/ttyUSB0 sau COM3",
"baudRateLabel": "Rată de baud",
"dataBitsLabel": "Date",
"stopBitsLabel": "Stop",
"parityLabel": "Paritate",
"parityNone": "Nici unul",
"parityEven": "Chiar",
"parityOdd": "Ciudat",
"connect": "Conectare la serial",
"disconnect": "Deconecta",
"refreshPorts": "Reîmprospătați porturile",
"connected": "Conectat la {{path}} la {{baud}} baud",
"disconnected": "Portul serial deconectat",
"connectionError": "Nu s-a putut deschide portul serial",
"wsError": "Eroare WebSocket",
"errorNoServerUrl": "Nu este configurată nicio adresă URL a serverului",
"notSupportedTitle": "Număr de serie neacceptat",
"notSupported": "Conexiunile seriale necesită un browser cu suport Web Serial API (Chrome, Edge sau Firefox 151+) sau aplicația desktop Termix.",
"hideHint": "Puteți ascunde fila Serial din Profil utilizator > Aspect > Bara laterală > Navigare.",
"browserPickerHint": "Faceți clic pe Conectare și browserul dvs. va deschide un selector de porturi pentru a alege dispozitivul."
},
"alerts": {
"addChannel": "Adăugați canal",
"editChannel": "Editați canalul",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Менеджер хостов",
"credentials": "Учетные данные",
"connections": "Подключения",
"serial": "Последовательный порт",
"keepSideMenuOpen": "Оставить меню открытым",
"collapseSideMenu": "Свернуть меню",
"roleAdministrator": "Администратор",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Откройте статистику Proxmox",
"openTmuxMonitor": "Открыть монитор Tmux"
},
"serial": {
"title": "Последовательный порт",
"portLabel": "Порт",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "Скорость передачи",
"dataBitsLabel": "Биты данных",
"stopBitsLabel": "Стоп-биты",
"parityLabel": "Чётность",
"parityNone": "Нет",
"parityEven": "Чётный",
"parityOdd": "Нечётный",
"connect": "Подключиться",
"disconnect": "Отключиться",
"refreshPorts": "Обновить порты",
"connected": "Подключено к {{path}} на скорости {{baud}} бод",
"disconnected": "Последовательный порт отключен",
"connectionError": "Ошибка открытия последовательного порта",
"wsError": "Ошибка WebSocket",
"errorNoServerUrl": "Не настроен URL сервера",
"notSupportedTitle": "Последовательный порт не поддерживается",
"notSupported": "Последовательные подключения требуют браузер с поддержкой Web Serial API (Chrome, Edge или Firefox 151+) или десктопное приложение Termix.",
"hideHint": "Вы можете скрыть вкладку «Serial» в разделе «Профиль пользователя» > «Оформление» > «Боковая панель» > «Навигация».",
"browserPickerHint": "Нажмите «Подключиться», и браузер откроет окно выбора порта для выбора устройства."
},
"alerts": {
"addChannel": "Добавить канал",
"editChannel": "Изменить канал",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Менаџер домаћина",
"credentials": "Акредитиви",
"connections": "Везе",
"serial": "Серијски",
"keepSideMenuOpen": "Остави мени отвореним",
"collapseSideMenu": "Сакриј мени",
"roleAdministrator": "Администратор",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Отвори Proxmox статистику",
"openTmuxMonitor": "Отвори Tmux монитор"
},
"serial": {
"title": "Серијски",
"portLabel": "Лука",
"portPlaceholder": "/dev/ttyUSB0 или COM3",
"baudRateLabel": "Брзина преноса података",
"dataBitsLabel": "Подаци",
"stopBitsLabel": "Заустави",
"parityLabel": "Паритет",
"parityNone": "Ниједан",
"parityEven": "Чак",
"parityOdd": "Чудно",
"connect": "Повежи се са серијским",
"disconnect": "Прекини везу",
"refreshPorts": "Освежи портове",
"connected": "Повезано са {{path}} брзином {{baud}} бауда",
"disconnected": "Серијски порт је искључен",
"connectionError": "Отварање серијског порта није успело",
"wsError": "Грешка WebSocket-а",
"errorNoServerUrl": "Није конфигурисан URL сервера",
"notSupportedTitle": "Серијски број није подржан",
"notSupported": "Серијске везе захтевају прегледач са подршком за Web Serial API (Chrome, Edge или Firefox 151+) или Termix десктоп апликацију.",
"hideHint": "Картицу Серијски број можете сакрити под Кориснички профил > Изглед > Бочна трака > Навигација.",
"browserPickerHint": "Кликните на Повежи се и ваш прегледач ће отворити бирач портова за избор уређаја."
},
"alerts": {
"addChannel": "Додај канал",
"editChannel": "Измени канал",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Värdhanterare",
"credentials": "Referenser",
"connections": "Anslutningar",
"serial": "Serie",
"keepSideMenuOpen": "Håll menyn öppen",
"collapseSideMenu": "Minimera menyn",
"roleAdministrator": "Administratör",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Öppna Proxmox-statistik",
"openTmuxMonitor": "Öppna Tmux-monitorn"
},
"serial": {
"title": "Serie",
"portLabel": "Hamn",
"portPlaceholder": "/dev/ttyUSB0 eller COM3",
"baudRateLabel": "Baudhastighet",
"dataBitsLabel": "Data",
"stopBitsLabel": "Stopp",
"parityLabel": "Paritet",
"parityNone": "Ingen",
"parityEven": "Även",
"parityOdd": "Udda",
"connect": "Anslut till seriellt",
"disconnect": "Koppla från",
"refreshPorts": "Uppdatera portar",
"connected": "Ansluten till {{path}} vid {{baud}} baud",
"disconnected": "Seriell port frånkopplad",
"connectionError": "Misslyckades med att öppna serieporten",
"wsError": "WebSocket-fel",
"errorNoServerUrl": "Ingen server-URL konfigurerad",
"notSupportedTitle": "Seriellt stöds inte",
"notSupported": "Seriella anslutningar kräver en webbläsare med stöd för Web Serial API (Chrome, Edge eller Firefox 151+), eller Termix-skrivbordsappen.",
"hideHint": "Du kan dölja fliken Seriell under Användarprofil > Utseende > Sidofält > Navigering.",
"browserPickerHint": "Klicka på Anslut så öppnar din webbläsare en portväljare där du kan välja enheten."
},
"alerts": {
"addChannel": "Lägg till kanal",
"editChannel": "Redigera kanal",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "ผู้จัดการโฮสต์",
"credentials": "คุณสมบัติ",
"connections": "การเชื่อมต่อ",
"serial": "ซีเรียล",
"keepSideMenuOpen": "เปิดเมนูค้างไว้",
"collapseSideMenu": "ยุบเมนู",
"roleAdministrator": "ผู้ดูแลระบบ",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "เปิดดูสถิติ Proxmox",
"openTmuxMonitor": "เปิด Tmux Monitor"
},
"serial": {
"title": "ซีเรียล",
"portLabel": "ท่าเรือ",
"portPlaceholder": "/dev/ttyUSB0 หรือ COM3",
"baudRateLabel": "อัตราบิต",
"dataBitsLabel": "ข้อมูล",
"stopBitsLabel": "หยุด",
"parityLabel": "ความเท่าเทียมกัน",
"parityNone": "ไม่มี",
"parityEven": "สม่ำเสมอ",
"parityOdd": "แปลก",
"connect": "เชื่อมต่อกับพอร์ตอนุกรม",
"disconnect": "ตัดการเชื่อมต่อ",
"refreshPorts": "พอร์ตรีเฟรช",
"connected": "เชื่อมต่อกับ {{path}} ที่ความเร็ว {{baud}} บอด",
"disconnected": "พอร์ตอนุกรมถูกตัดการเชื่อมต่อ",
"connectionError": "ไม่สามารถเปิดพอร์ตอนุกรมได้",
"wsError": "ข้อผิดพลาด WebSocket",
"errorNoServerUrl": "ไม่ได้กำหนดค่า URL เซิร์ฟเวอร์",
"notSupportedTitle": "ไม่รองรับหมายเลขซีเรียล",
"notSupported": "การเชื่อมต่อแบบอนุกรมจำเป็นต้องใช้เบราว์เซอร์ที่รองรับ Web Serial API (Chrome, Edge หรือ Firefox เวอร์ชัน 151 ขึ้นไป) หรือแอปพลิเคชัน Termix บนเดสก์ท็อป",
"hideHint": "คุณสามารถซ่อนแท็บ Serial ได้ที่ โปรไฟล์ผู้ใช้ > ลักษณะที่ปรากฏ > แถบด้านข้าง > การนำทาง",
"browserPickerHint": "คลิก \"เชื่อมต่อ\" แล้วเบราว์เซอร์ของคุณจะเปิดตัวเลือกพอร์ตเพื่อเลือกอุปกรณ์"
},
"alerts": {
"addChannel": "เพิ่มช่องทาง",
"editChannel": "แก้ไขช่อง",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Ana Bilgisayar Yöneticisi",
"credentials": "Kimlik Bilgileri",
"connections": "Bağlantılar",
"serial": "Seri",
"keepSideMenuOpen": "Menüyü Açık Tutun",
"collapseSideMenu": "Menüyü Daralt",
"roleAdministrator": "Yönetici",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Proxmox İstatistiklerini Aç",
"openTmuxMonitor": "Tmux Monitörünü Aç"
},
"serial": {
"title": "Seri",
"portLabel": "Port",
"portPlaceholder": "/dev/ttyUSB0 veya COM3",
"baudRateLabel": "Baud Hızı",
"dataBitsLabel": "Veri",
"stopBitsLabel": "Durdurma",
"parityLabel": "Eşlik",
"parityNone": "Yok",
"parityEven": "Çift",
"parityOdd": "Tek",
"connect": "Seri Bağlantıya Bağlan",
"disconnect": "Bağlantıyı Kes",
"refreshPorts": "Portları yenile",
"connected": "{{path}} ile {{baud}} baud hızında bağlandı.",
"disconnected": "Seri port bağlantısı kesildi.",
"connectionError": "Seri bağlantı noktası açılamadı",
"wsError": "WebSocket hatası",
"errorNoServerUrl": "Sunucu URL'si yapılandırılmamış",
"notSupportedTitle": "Seri Bağlantı Desteklenmiyor",
"notSupported": "Seri bağlantılar, Web Serial API desteği olan bir tarayıcı (Chrome, Edge veya Firefox 151+) ya da Termix masaüstü uygulaması gerektirir.",
"hideHint": "Seri sekmesini Kullanıcı Profili > Görünüm > Kenar Çubuğu > Gezinme altından gizleyebilirsiniz.",
"browserPickerHint": "Bağlan'a tıklayın, tarayıcınız cihazı seçmek için bir bağlantı noktası seçici açacaktır."
},
"alerts": {
"addChannel": "Kanal Ekle",
"editChannel": "Kanalı Düzenle",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Менеджер хостингу",
"credentials": "Облікові дані",
"connections": "З'єднання",
"serial": "Серійний",
"keepSideMenuOpen": "Залишити меню відкритим",
"collapseSideMenu": "Згорнути меню",
"roleAdministrator": "Адміністратор",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Відкрити статистику Proxmox",
"openTmuxMonitor": "Відкрити монітор Tmux"
},
"serial": {
"title": "Серійний",
"portLabel": "Порт",
"portPlaceholder": "/dev/ttyUSB0 або COM3",
"baudRateLabel": "Швидкість передачі даних",
"dataBitsLabel": "Дані",
"stopBitsLabel": "СТІЙ",
"parityLabel": "Паритет",
"parityNone": "Жоден",
"parityEven": "Навіть",
"parityOdd": "Дивний",
"connect": "Підключення до послідовного порту",
"disconnect": "Відключитися",
"refreshPorts": "Оновити порти",
"connected": "Підключено до {{path}} зі швидкістю {{baud}} бод",
"disconnected": "Послідовний порт відключено",
"connectionError": "Не вдалося відкрити послідовний порт",
"wsError": "Помилка WebSocket",
"errorNoServerUrl": "URL-адреса сервера не налаштована",
"notSupportedTitle": "Серійний номер не підтримується",
"notSupported": "Для послідовних з’єднань потрібен браузер із підтримкою Web Serial API (Chrome, Edge або Firefox 151+) або настільний додаток Termix.",
"hideHint": "Ви можете приховати вкладку «Серійний номер» у розділі «Профіль користувача» > «Зовнішній вигляд» > «Бічна панель» > «Навігація».",
"browserPickerHint": "Натисніть «Підключитися», і ваш браузер відкриє вибір порту для вибору пристрою."
},
"alerts": {
"addChannel": "Додати канал",
"editChannel": "Редагувати канал",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "Quản lý máy chủ",
"credentials": "Thông tin đăng nhập",
"connections": "Kết nối",
"serial": "Nối tiếp",
"keepSideMenuOpen": "Giữ menu luôn mở",
"collapseSideMenu": "Menu thu gọn",
"roleAdministrator": "Quản trị viên",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "Mở thống kê Proxmox",
"openTmuxMonitor": "Mở Tmux Monitor"
},
"serial": {
"title": "Cổng nối tiếp",
"portLabel": "Cổng",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "Tốc độ Baud",
"dataBitsLabel": "Dữ liệu",
"stopBitsLabel": "Bit dừng",
"parityLabel": "Kiểm tra chẵn lẻ",
"parityNone": "Không",
"parityEven": "Chẵn",
"parityOdd": "Lẻ",
"connect": "Kết nối cổng nối tiếp",
"disconnect": "Ngắt kết nối",
"refreshPorts": "Làm mới cổng",
"connected": "Đã kết nối tới {{path}} với tốc độ {{baud}} baud",
"disconnected": "Đã ngắt kết nối cổng nối tiếp",
"connectionError": "Không thể mở cổng nối tiếp",
"wsError": "Lỗi WebSocket",
"errorNoServerUrl": "Chưa cấu hình URL máy chủ",
"notSupportedTitle": "Không hỗ trợ Serial",
"notSupported": "Kết nối nối tiếp yêu cầu trình duyệt hỗ trợ Web Serial API (Chrome, Edge, hoặc Firefox 151+), hoặc ứng dụng Termix desktop.",
"hideHint": "Bạn có thể ẩn tab Serial trong Hồ sơ người dùng > Giao diện > Thanh bên > Điều hướng.",
"browserPickerHint": "Nhấn Kết nối và trình duyệt sẽ mở bộ chọn cổng để chọn thiết bị."
},
"alerts": {
"addChannel": "Thêm kênh",
"editChannel": "Sửa kênh",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "主机管理器",
"credentials": "凭据",
"connections": "连接",
"serial": "串口",
"keepSideMenuOpen": "保持菜单打开",
"collapseSideMenu": "折叠菜单",
"roleAdministrator": "管理员",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "打开 Proxmox 统计数据",
"openTmuxMonitor": "打开 Tmux 监视器"
},
"serial": {
"title": "串口",
"portLabel": "端口",
"portPlaceholder": "/dev/ttyUSB0 或 COM3",
"baudRateLabel": "波特率",
"dataBitsLabel": "数据",
"stopBitsLabel": "停止位",
"parityLabel": "奇偶校验",
"parityNone": "无",
"parityEven": "偶校验",
"parityOdd": "奇校验",
"connect": "连接串口",
"disconnect": "断开",
"refreshPorts": "刷新端口",
"connected": "已连接到 {{path}},波特率 {{baud}}",
"disconnected": "串口已断开",
"connectionError": "打开串口失败",
"wsError": "WebSocket 错误",
"errorNoServerUrl": "未配置服务器 URL",
"notSupportedTitle": "不支持串口",
"notSupported": "串口连接需要支持 Web Serial API 的浏览器(Chrome、Edge 或 Firefox 151+),或者 Termix 桌面应用。",
"hideHint": "您可以在用户资料 > 外观 > 侧边栏 > 导航中隐藏串口标签页。",
"browserPickerHint": "点击连接,您的浏览器将打开端口选择器以选择设备。"
},
"alerts": {
"addChannel": "添加渠道",
"editChannel": "编辑渠道",
-25
View File
@@ -713,7 +713,6 @@
"hostManager": "主機管理員",
"credentials": "憑證",
"connections": "連線",
"serial": "序列",
"keepSideMenuOpen": "保持菜單打開",
"collapseSideMenu": "折疊菜單",
"roleAdministrator": "管理員",
@@ -3055,30 +3054,6 @@
"openProxmoxStats": "開啟 Proxmox 統計數據",
"openTmuxMonitor": "打開 Tmux Monitor"
},
"serial": {
"title": "序列",
"portLabel": "連接埠",
"portPlaceholder": "/dev/ttyUSB0 or COM3",
"baudRateLabel": "鮑率",
"dataBitsLabel": "資料位元",
"stopBitsLabel": "停止位元",
"parityLabel": "同位檢查",
"parityNone": "無",
"parityEven": "偶",
"parityOdd": "奇",
"connect": "連線至序列埠",
"disconnect": "中斷連線",
"refreshPorts": "重新整理連接埠",
"connected": "已連線至 {{path}},鮑率 {{baud}}",
"disconnected": "序列埠已中斷連線",
"connectionError": "開啟序列埠失敗",
"wsError": "WebSocket 錯誤",
"errorNoServerUrl": "未設定伺服器網址",
"notSupportedTitle": "不支援序列埠",
"notSupported": "序列連線需要支援 Web Serial API 的瀏覽器 (Chrome、Edge 或 Firefox 151+) 或 Termix 桌面應用程式。",
"hideHint": "您可以在「使用者設定檔 > 外觀 > 側邊欄 > 導覽」中隱藏序列索引標籤。",
"browserPickerHint": "按一下連線,瀏覽器會開啟連接埠選擇器讓您選取裝置。"
},
"alerts": {
"addChannel": "新增通道",
"editChannel": "編輯通道",
+10 -5
View File
@@ -30,17 +30,22 @@ import {
Share2,
} from "lucide-react";
import { tabIcon } from "@/shell/tabUtils";
import { isSessionTabType } from "@/shell/tab-registry";
import { isSessionTabType, isShareableTabType } from "@/shell/tab-registry";
import { isElectron } from "@/lib/electron";
import type { Tab, TabType, SplitMode } from "@/types/ui-types";
import { SPLIT_MODES, PANE_COUNTS } from "@/lib/theme";
/**
* Tabs holding a live connection that can be refreshed and shared: the
* registered session tabs. Serial sessions are not shareable.
* Tabs holding a live connection that can be refreshed: the registered
* session tabs.
*/
function isConnectionTab(type: TabType): boolean {
return type !== "serial" && isSessionTabType(type);
return isSessionTabType(type);
}
/** Connection tabs that also offer "Share session". */
function isShareableConnectionTab(type: TabType): boolean {
return isConnectionTab(type) && isShareableTabType(type);
}
export function TabBar({
@@ -467,7 +472,7 @@ export function TabBar({
<RefreshCw className="size-3" />
</button>
)}
{isConnectionTab(tab.type) && onOpenShare && (
{isShareableConnectionTab(tab.type) && onOpenShare && (
<button
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
+2 -12
View File
@@ -19,7 +19,6 @@ import { getTabType } from "./tab-registry";
* not a meaningful part of an arrangement.
*/
const CORE_CAPTURABLE = new Set([
"serial",
"homepage",
"termix-id",
"session-logs",
@@ -52,10 +51,7 @@ const CORE_UNSAVED = new Set([
*/
export function isUnregisteredPluginTabType(type: string): boolean {
return (
!CORE_UNSAVED.has(type) &&
!CORE_CAPTURABLE.has(type) &&
type !== "serial" &&
!getTabType(type)
!CORE_UNSAVED.has(type) && !CORE_CAPTURABLE.has(type) && !getTabType(type)
);
}
@@ -94,14 +90,13 @@ export function buildLayoutTabSnapshots(
const snapshots: WorkspaceTabSnapshot[] = capturable.map((tab) => ({
slotId: slotIdByTabId.get(tab.id)!,
type: tab.type,
hostSyncId: tab.type === "serial" ? null : (tab.host?.syncId ?? null),
hostSyncId: tab.host?.syncId ?? null,
hostNameSnapshot: tab.host?.name ?? null,
label: tab.label,
customLabel: tab.customLabel,
initialFilePath: tab.initialFilePath,
initialPath: tab.initialPath,
data: tab.data,
serialConfig: tab.type === "serial" ? tab.serialConfig : undefined,
}));
return { snapshots, slotIdByTabId };
@@ -139,14 +134,9 @@ export function resolveLayoutTabTarget(
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);
+8 -1
View File
@@ -61,6 +61,8 @@ export interface TabTypeDef {
persistent?: boolean;
singleton?: boolean;
session?: boolean;
/** Whether a session tab offers "Share session". Default true. */
shareable?: boolean;
hostless?: boolean;
restore?: (host: Host) => boolean;
activityTypes?: string[];
@@ -91,7 +93,7 @@ export const resetTabTypes = registry.reset;
/** Core tab types that are saved and reopened after login. */
const CORE_PERSISTENT = new Set<string>();
/** Core tab types that hold a live session (close confirm, refresh). */
const CORE_SESSION = new Set(["serial"]);
const CORE_SESSION = new Set<string>();
/** Core tab types that restore without a host. */
const CORE_HOSTLESS = new Set(["dashboard"]);
@@ -115,6 +117,11 @@ export function isSessionTabType(type: string): boolean {
return CORE_SESSION.has(type) || !!registry.get(type)?.session;
}
/** Whether a session tab of this type offers "Share session". Default true. */
export function isShareableTabType(type: string): boolean {
return registry.get(type)?.shareable !== false;
}
export function isHostlessTabType(type: string): boolean {
return CORE_HOSTLESS.has(type) || !!registry.get(type)?.hostless;
}
-21
View File
@@ -6,7 +6,6 @@ import {
LayoutPanelLeft,
Server,
Settings,
Usb,
User,
Fingerprint,
Hammer,
@@ -15,7 +14,6 @@ import {
} from "lucide-react";
import { lazy, Suspense } from "react";
import { useTranslation } from "react-i18next";
import type { SerialHandle } from "@/features/serial/serial-types";
import type { Tab, TabType } from "@/types/ui-types";
import { hostToSSHHost } from "@/lib/host-to-ssh-host";
import { PluginViewPlaceholder } from "@/plugin-host/PluginViewPlaceholder";
@@ -46,11 +44,6 @@ const HomepageCanvas = lazy(() =>
default: m.HomepageCanvas,
})),
);
const Serial = lazy(() =>
import("@/features/serial/Serial").then((m) => ({
default: m.Serial,
})),
);
// Rail panels promoted to full tabs.
const TermixIdPanel = lazy(() =>
import("@/sidebar/TermixIdPanel").then((m) => ({ default: m.TermixIdPanel })),
@@ -134,8 +127,6 @@ export function tabIcon(type: TabType) {
return <User className="size-3.5" />;
case "admin-settings":
return <Settings className="size-3.5" />;
case "serial":
return <Usb className="size-3.5" />;
case "homepage":
return <LayoutGrid className="size-3.5" />;
case "collab":
@@ -237,18 +228,6 @@ export function renderTabContent(tab: Tab, context: TabRenderContext) {
/>,
);
case "serial":
if (!tab.serialConfig)
return <EmptyState icon={Usb} messageKey="serial.notSupportedTitle" />;
return withTabSuspense(
<Serial
ref={tab.terminalRef as React.Ref<SerialHandle>}
config={tab.serialConfig}
isVisible={isVisible}
instanceId={tab.instanceId}
/>,
);
case "homepage":
return withTabSuspense(<HomepageCanvas />);
-1
View File
@@ -21,7 +21,6 @@ export type CoreRailView =
| "credentials"
| "termix-id"
| "quick-connect"
| "serial"
| ToolsTab
| "connections"
| "session-logs"
-335
View File
@@ -1,335 +0,0 @@
import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { RefreshCw, Usb, TriangleAlert } from "lucide-react";
import { Input } from "@/components/input";
import { isElectron } from "@/lib/electron";
import { websocketAuthProtocols } from "@/lib/ws-auth";
import type { SerialConfig } from "@/types/ui-types";
import { Select2 } from "@/components/select2";
const BAUD_RATES = [
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400, 460800,
921600,
];
interface SerialPanelProps {
onConnect: (config: SerialConfig) => void;
}
// In Chrome/Edge the Web Serial API shows its own native port picker on requestPort(),
// so getPorts() only returns previously-granted ports. We never need the user to type
// a path — the browser handles selection at connect time.
const useWebSerial = !isElectron() && "serial" in navigator;
const serialUnsupported = !isElectron() && !("serial" in navigator);
export function SerialPanel({ onConnect }: SerialPanelProps) {
const { t } = useTranslation();
const [path, setPath] = useState("");
const [baudRate, setBaudRate] = useState(115200);
const [dataBits, setDataBits] = useState<5 | 6 | 7 | 8>(8);
const [stopBits, setStopBits] = useState<1 | 2>(1);
const [parity, setParity] = useState<"none" | "even" | "odd">("none");
// Electron only: ports discovered from the backend
const [availablePorts, setAvailablePorts] = useState<string[]>([]);
const [loadingPorts, setLoadingPorts] = useState(false);
const buildWsUrl = () => {
// Serial is always local -- the device is physically attached to this
// desktop machine, so it never routes through a remote server.
return "ws://127.0.0.1:30011";
};
const refreshPorts = useCallback(() => {
if (!isElectron()) return;
setLoadingPorts(true);
const url = buildWsUrl();
if (!url) {
setLoadingPorts(false);
return;
}
const ws = new WebSocket(
url,
websocketAuthProtocols(localStorage.getItem("jwt")),
);
ws.onopen = () => ws.send(JSON.stringify({ type: "list_ports" }));
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data as string) as {
type: string;
data?: { path: string }[];
};
if (msg.type === "ports_list" && Array.isArray(msg.data)) {
const paths = msg.data.map((p) => p.path);
setAvailablePorts(paths);
if (!path && paths.length > 0) setPath(paths[0]);
}
} catch {
// ignore
} finally {
ws.close();
setLoadingPorts(false);
}
};
ws.onerror = () => setLoadingPorts(false);
}, [path]);
useEffect(() => {
if (isElectron()) refreshPorts();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const connect = () => {
// For Web Serial, path is irrelevant — the browser picker handles it.
// We pass an empty string; Serial.tsx calls requestPort() itself.
if (useWebSerial) {
onConnect({ path: "", baudRate, dataBits, stopBits, parity });
return;
}
const finalPath = path.trim();
if (!finalPath) return;
onConnect({ path: finalPath, baudRate, dataBits, stopBits, parity });
};
// ── Not supported (Firefox, Safari, etc.) ─────────────────────────────
if (serialUnsupported) {
return (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<div className="flex flex-col gap-3 p-3">
<div className="flex flex-col gap-2 rounded border border-destructive/30 bg-destructive/5 p-3">
<div className="flex items-center gap-2">
<TriangleAlert className="size-3.5 text-destructive shrink-0" />
<span className="text-xs font-semibold text-destructive">
{t("serial.notSupportedTitle")}
</span>
</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
{t("serial.notSupported")}
</p>
<p className="text-[11px] text-muted-foreground leading-relaxed">
{t("serial.hideHint")}
</p>
</div>
</div>
</div>
);
}
// ── Web Serial (Chrome / Edge) ─────────────────────────────────────────
if (useWebSerial) {
return (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<div className="flex flex-col gap-3 p-3">
<p className="text-[11px] text-muted-foreground leading-relaxed">
{t("serial.browserPickerHint")}
</p>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.baudRateLabel")}
</label>
<Select2
value={baudRate}
onChange={(e) => setBaudRate(Number(e.target.value))}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
{BAUD_RATES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</Select2>
</div>
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.dataBitsLabel")}
</label>
<Select2
value={dataBits}
onChange={(e) =>
setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8)
}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
{([5, 6, 7, 8] as const).map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</Select2>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.stopBitsLabel")}
</label>
<Select2
value={stopBits}
onChange={(e) => setStopBits(Number(e.target.value) as 1 | 2)}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
<option value={1}>1</option>
<option value={2}>2</option>
</Select2>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.parityLabel")}
</label>
<Select2
value={parity}
onChange={(e) =>
setParity(e.target.value as "none" | "even" | "odd")
}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
<option value="none">{t("serial.parityNone")}</option>
<option value="even">{t("serial.parityEven")}</option>
<option value="odd">{t("serial.parityOdd")}</option>
</Select2>
</div>
</div>
<button
onClick={connect}
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors mt-1"
>
<Usb className="size-3.5" />
{t("serial.connect")}
</button>
</div>
</div>
);
}
// ── Electron (backend serialport) ─────────────────────────────────────
return (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
<div className="flex flex-col gap-3 p-3">
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.portLabel")}
</label>
<button
onClick={refreshPorts}
className="text-muted-foreground hover:text-foreground transition-colors"
title={t("serial.refreshPorts")}
>
<RefreshCw
className={`size-3 ${loadingPorts ? "animate-spin" : ""}`}
/>
</button>
</div>
{availablePorts.length > 0 && (
<Select2
value={path}
onChange={(e) => setPath(e.target.value)}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
<option value="">{t("serial.portPlaceholder")}</option>
{availablePorts.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</Select2>
)}
<Input
placeholder={t("serial.portPlaceholder")}
value={path}
onChange={(e) => setPath(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") connect();
}}
className="h-7 text-xs font-mono"
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.baudRateLabel")}
</label>
<Select2
value={baudRate}
onChange={(e) => setBaudRate(Number(e.target.value))}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
{BAUD_RATES.map((r) => (
<option key={r} value={r}>
{r}
</option>
))}
</Select2>
</div>
<div className="grid grid-cols-3 gap-2">
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.dataBitsLabel")}
</label>
<Select2
value={dataBits}
onChange={(e) =>
setDataBits(Number(e.target.value) as 5 | 6 | 7 | 8)
}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
{([5, 6, 7, 8] as const).map((b) => (
<option key={b} value={b}>
{b}
</option>
))}
</Select2>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.stopBitsLabel")}
</label>
<Select2
value={stopBits}
onChange={(e) => setStopBits(Number(e.target.value) as 1 | 2)}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
<option value={1}>1</option>
<option value={2}>2</option>
</Select2>
</div>
<div className="flex flex-col gap-1">
<label className="text-[10px] font-semibold uppercase tracking-widest text-muted-foreground">
{t("serial.parityLabel")}
</label>
<Select2
value={parity}
onChange={(e) =>
setParity(e.target.value as "none" | "even" | "odd")
}
className="flex h-7 w-full border border-border bg-background px-2.5 py-1 text-xs outline-none focus:ring-1 focus:ring-ring"
>
<option value="none">{t("serial.parityNone")}</option>
<option value="even">{t("serial.parityEven")}</option>
<option value="odd">{t("serial.parityOdd")}</option>
</Select2>
</div>
</div>
<button
onClick={connect}
disabled={!path.trim()}
className="flex items-center justify-center gap-1.5 h-7 w-full border border-accent-brand/40 bg-accent-brand/10 text-accent-brand text-xs font-semibold hover:bg-accent-brand/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed mt-1"
>
<Usb className="size-3.5" />
{t("serial.connect")}
</button>
</div>
</div>
);
}
-3
View File
@@ -8,7 +8,6 @@ import {
ScrollText,
Server,
Settings,
Usb,
User,
Zap,
Presentation,
@@ -104,7 +103,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
separatorAfter: true,
mobilePrimary: true,
},
{ id: "serial", icon: Usb, labelKey: "nav.serial", separatorAfter: true },
{
id: "ssh-tools",
icon: Hammer,
@@ -122,7 +120,6 @@ export const RAIL_ITEMS: RailItemDef[] = [
promotable: true,
rightDockable: true,
},
{
id: "session-logs",
icon: ScrollText,
@@ -57,16 +57,6 @@ describe("resolveConnectionOrigin", () => {
).resolves.toBe("local");
});
it("always resolves serial to local, even with a remote override", async () => {
win.IS_ELECTRON = true;
await expect(
resolveConnectionOrigin({
connectionType: "serial",
connectionOrigin: "remote",
}),
).resolves.toBe("local");
});
it("resolves to local outside Electron regardless of connectionType", async () => {
await expect(
resolveConnectionOrigin({
+1 -58
View File
@@ -128,34 +128,6 @@ describe("buildWorkspaceTabSnapshots", () => {
expect(snapshots).toHaveLength(1);
expect(snapshots[0].type).toBe("terminal");
});
it("captures serialConfig only for serial tabs", () => {
const tabs: Tab[] = [
makeTab({
id: "t1",
type: "serial",
serialConfig: {
path: "/dev/ttyUSB0",
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: "none",
},
}),
makeTab({ id: "t2", type: "terminal", host: makeHost() }),
];
const { snapshots } = buildWorkspaceTabSnapshots(tabs);
expect(snapshots[0].serialConfig).toEqual({
path: "/dev/ttyUSB0",
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: "none",
});
expect(snapshots[0].hostSyncId).toBeNull();
expect(snapshots[1].serialConfig).toBeUndefined();
});
});
describe("remapSlotIds", () => {
@@ -175,35 +147,6 @@ describe("remapSlotIds", () => {
describe("resolveWorkspaceTabTarget", () => {
const hosts: Host[] = [makeHost({ id: "1", syncId: "sync-web-01" })];
it("resolves a serial snapshot with a config as 'serial'", () => {
const snapshot: WorkspaceTabSnapshot = {
slotId: "s1",
type: "serial",
label: "Serial",
serialConfig: {
path: "/dev/ttyUSB0",
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: "none",
},
};
expect(resolveWorkspaceTabTarget(snapshot, hosts)).toEqual({
kind: "serial",
});
});
it("skips a serial snapshot missing its config", () => {
const snapshot: WorkspaceTabSnapshot = {
slotId: "s1",
type: "serial",
label: "Serial",
};
expect(resolveWorkspaceTabTarget(snapshot, hosts)).toEqual({
kind: "skip",
});
});
it("resolves a host-bound snapshot by matching syncId", () => {
const snapshot: WorkspaceTabSnapshot = {
slotId: "s1",
@@ -253,7 +196,7 @@ describe("resolveWorkspaceTabTarget", () => {
expect((result as { host?: Host }).host?.syncId).toBe("sync-web-01");
});
it("skips a non-singleton, non-serial, non-plugin type with no resolvable host", () => {
it("skips a non-singleton, non-plugin type with no resolvable host", () => {
const snapshot: WorkspaceTabSnapshot = {
slotId: "s1",
type: "host-manager",
+3 -3
View File
@@ -48,8 +48,9 @@ describe("RAIL_ITEMS", () => {
// Guards against a destination silently disappearing now that the rail,
// the settings toggles, the sidebar titles and the mobile bar all read
// from this one list.
// "sftp" moved to the file-manager plugin's own registerRailItem, and
// "port-forwarding" to the tunnels plugin's.
// "sftp" moved to the file-manager plugin's own registerRailItem,
// "port-forwarding" to the tunnels plugin's, and "serial" to the serial
// plugin's.
expect(RAIL_ITEMS.map((item) => item.id)).toEqual([
"hosts",
"credentials",
@@ -57,7 +58,6 @@ describe("RAIL_ITEMS", () => {
"connections",
"collab",
"quick-connect",
"serial",
"ssh-tools",
"macros",
"session-logs",