mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
fix: build desktop session share links on the linked server (#1350)
This commit is contained in:
1 parent
d30875483d
commit
ab43563247
11 files changed
+153
-29
No files matched your search
@@ -639,6 +639,8 @@ const GuacamoleAppInner = React.forwardRef<
|
|||||||
sessionId: guacamoleConnectionId,
|
sessionId: guacamoleConnectionId,
|
||||||
protocol: resolvedProtocol,
|
protocol: resolvedProtocol,
|
||||||
tabInstanceId: tabId,
|
tabInstanceId: tabId,
|
||||||
|
origin:
|
||||||
|
sessionLookup?.origin === "remote" ? "remote" : "local",
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export interface RemoteDesktopToolbarContext {
|
|||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
protocol: "rdp" | "vnc" | "telnet";
|
protocol: "rdp" | "vnc" | "telnet";
|
||||||
tabInstanceId?: string;
|
tabInstanceId?: string;
|
||||||
|
/** The backend the session runs on. */
|
||||||
|
origin?: "local" | "remote";
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODIFIER_KEYSYMS = {
|
const MODIFIER_KEYSYMS = {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"searchUsersPlaceholder": "Search users...",
|
"searchUsersPlaceholder": "Search users...",
|
||||||
"noUsersFound": "No users found",
|
"noUsersFound": "No users found",
|
||||||
"linkCreated": "Share link created",
|
"linkCreated": "Share link created",
|
||||||
|
"linkNeedsServer": "Share links only work for sessions on a Termix server. Connect this host through your linked server to share it with a link.",
|
||||||
"linkCopied": "Link copied to clipboard",
|
"linkCopied": "Link copied to clipboard",
|
||||||
"copyLink": "Copy link",
|
"copyLink": "Copy link",
|
||||||
"shareCreated": "Session shared. It will appear in their Connections tab.",
|
"shareCreated": "Session shared. It will appear in their Connections tab.",
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import {
|
|||||||
type SessionSharePermissionLevel,
|
type SessionSharePermissionLevel,
|
||||||
type SessionShareRecord,
|
type SessionShareRecord,
|
||||||
getDirectory,
|
getDirectory,
|
||||||
|
type SessionOrigin,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
import { guestViewUrl } from "./meeting-backend";
|
||||||
|
|
||||||
const EXPIRY_PRESETS = [
|
const EXPIRY_PRESETS = [
|
||||||
{ key: "oneHour", hours: 1 },
|
{ key: "oneHour", hours: 1 },
|
||||||
@@ -44,6 +46,8 @@ export function ShareSessionModal({
|
|||||||
sessionId,
|
sessionId,
|
||||||
protocol,
|
protocol,
|
||||||
tabInstanceId,
|
tabInstanceId,
|
||||||
|
origin,
|
||||||
|
resolvePublicUrl,
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -51,6 +55,8 @@ export function ShareSessionModal({
|
|||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
protocol: SessionShareProtocol;
|
protocol: SessionShareProtocol;
|
||||||
tabInstanceId?: string;
|
tabInstanceId?: string;
|
||||||
|
origin?: SessionOrigin;
|
||||||
|
resolvePublicUrl?: (origin?: SessionOrigin) => Promise<string | null>;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [mode, setMode] = useState<"link" | "user">("link");
|
const [mode, setMode] = useState<"link" | "user">("link");
|
||||||
@@ -83,8 +89,8 @@ export function ShareSessionModal({
|
|||||||
if (!open || sharesLoaded) return;
|
if (!open || sharesLoaded) return;
|
||||||
setSharesLoaded(true);
|
setSharesLoaded(true);
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getDirectory().catch(() => ({ users: [] })),
|
getDirectory(origin).catch(() => ({ users: [] })),
|
||||||
getActiveSessionShares(hostId).catch(() => ({ shares: [] })),
|
getActiveSessionShares(hostId, origin).catch(() => ({ shares: [] })),
|
||||||
]).then(([usersRes, sharesRes]) => {
|
]).then(([usersRes, sharesRes]) => {
|
||||||
setUsers(
|
setUsers(
|
||||||
(usersRes.users ?? []).map((u) => ({
|
(usersRes.users ?? []).map((u) => ({
|
||||||
@@ -94,7 +100,7 @@ export function ShareSessionModal({
|
|||||||
);
|
);
|
||||||
setShares(sharesRes.shares ?? []);
|
setShares(sharesRes.shares ?? []);
|
||||||
});
|
});
|
||||||
}, [open, hostId, sharesLoaded]);
|
}, [open, hostId, origin, sharesLoaded]);
|
||||||
|
|
||||||
const filteredUsers = useMemo(() => {
|
const filteredUsers = useMemo(() => {
|
||||||
const q = search.trim().toLowerCase();
|
const q = search.trim().toLowerCase();
|
||||||
@@ -113,7 +119,7 @@ export function ShareSessionModal({
|
|||||||
|
|
||||||
async function refreshShares() {
|
async function refreshShares() {
|
||||||
try {
|
try {
|
||||||
const res = await getActiveSessionShares(hostId);
|
const res = await getActiveSessionShares(hostId, origin);
|
||||||
setShares(res.shares ?? []);
|
setShares(res.shares ?? []);
|
||||||
} catch {
|
} catch {
|
||||||
// silently ignore
|
// silently ignore
|
||||||
@@ -127,21 +133,33 @@ export function ShareSessionModal({
|
|||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
try {
|
try {
|
||||||
const result = await createSessionShare({
|
const base =
|
||||||
hostId,
|
mode === "link"
|
||||||
sessionId,
|
? resolvePublicUrl
|
||||||
tabInstanceId,
|
? await resolvePublicUrl(origin)
|
||||||
protocol,
|
: window.location.href
|
||||||
shareType: mode,
|
: null;
|
||||||
targetUserId:
|
if (mode === "link" && !guestViewUrl(base, "shared", "check")) {
|
||||||
mode === "user" ? (selectedUserId ?? undefined) : undefined,
|
toast.error(t("sessionSharing.linkNeedsServer"));
|
||||||
permissionLevel,
|
return;
|
||||||
expiryHours,
|
}
|
||||||
});
|
const result = await createSessionShare(
|
||||||
|
{
|
||||||
|
hostId,
|
||||||
|
sessionId,
|
||||||
|
tabInstanceId,
|
||||||
|
protocol,
|
||||||
|
shareType: mode,
|
||||||
|
targetUserId:
|
||||||
|
mode === "user" ? (selectedUserId ?? undefined) : undefined,
|
||||||
|
permissionLevel,
|
||||||
|
expiryHours,
|
||||||
|
},
|
||||||
|
origin,
|
||||||
|
);
|
||||||
|
|
||||||
if (mode === "link" && result.linkToken) {
|
if (mode === "link" && result.linkToken) {
|
||||||
const url = `${window.location.origin}${window.location.pathname}?view=shared&token=${result.linkToken}`;
|
setCreatedLink(guestViewUrl(base, "shared", result.linkToken));
|
||||||
setCreatedLink(url);
|
|
||||||
toast.success(t("sessionSharing.linkCreated"));
|
toast.success(t("sessionSharing.linkCreated"));
|
||||||
} else {
|
} else {
|
||||||
toast.success(t("sessionSharing.shareCreated"));
|
toast.success(t("sessionSharing.shareCreated"));
|
||||||
@@ -172,7 +190,7 @@ export function ShareSessionModal({
|
|||||||
|
|
||||||
async function handleRevoke(shareId: string) {
|
async function handleRevoke(shareId: string) {
|
||||||
try {
|
try {
|
||||||
await revokeSessionShare(shareId);
|
await revokeSessionShare(shareId, origin);
|
||||||
setShares((prev) => prev.filter((s) => s.id !== shareId));
|
setShares((prev) => prev.filter((s) => s.id !== shareId));
|
||||||
toast.success(t("sessionSharing.revoked"));
|
toast.success(t("sessionSharing.revoked"));
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -5,16 +5,24 @@ import type { PluginApiClient } from "@termix/plugin-sdk/frontend";
|
|||||||
* Guest pages use it too: the two guest routes are public.
|
* Guest pages use it too: the two guest routes are public.
|
||||||
*/
|
*/
|
||||||
let client: PluginApiClient | null = null;
|
let client: PluginApiClient | null = null;
|
||||||
|
let clientFor: ((origin?: unknown) => PluginApiClient) | null = null;
|
||||||
|
|
||||||
export function bindApi(api: PluginApiClient | null): void {
|
export function bindApi(
|
||||||
|
api: PluginApiClient | null,
|
||||||
|
apiFor?: (origin?: unknown) => PluginApiClient,
|
||||||
|
): void {
|
||||||
client = api;
|
client = api;
|
||||||
|
clientFor = api ? (apiFor ?? null) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function api(): PluginApiClient {
|
/** The session lives on the backend its tab connected through. */
|
||||||
|
function api(origin?: SessionOrigin): PluginApiClient {
|
||||||
if (!client) throw new Error("Session sharing is not running");
|
if (!client) throw new Error("Session sharing is not running");
|
||||||
return client;
|
return origin && clientFor ? clientFor(origin) : client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SessionOrigin = "local" | "remote";
|
||||||
|
|
||||||
interface HttpError {
|
interface HttpError {
|
||||||
response?: { status?: number; data?: { error?: string } };
|
response?: { status?: number; data?: { error?: string } };
|
||||||
}
|
}
|
||||||
@@ -134,25 +142,33 @@ export interface CreateSessionShareResponse {
|
|||||||
|
|
||||||
export function createSessionShare(
|
export function createSessionShare(
|
||||||
request: CreateSessionShareRequest,
|
request: CreateSessionShareRequest,
|
||||||
|
origin?: SessionOrigin,
|
||||||
): Promise<CreateSessionShareResponse> {
|
): Promise<CreateSessionShareResponse> {
|
||||||
return call(
|
return call(
|
||||||
() => api().post<CreateSessionShareResponse>("/create", request),
|
() => api(origin).post<CreateSessionShareResponse>("/create", request),
|
||||||
"Failed to create session share",
|
"Failed to create session share",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getActiveSessionShares(
|
export function getActiveSessionShares(
|
||||||
hostId: number,
|
hostId: number,
|
||||||
|
origin?: SessionOrigin,
|
||||||
): Promise<{ shares: SessionShareRecord[] }> {
|
): Promise<{ shares: SessionShareRecord[] }> {
|
||||||
return call(
|
return call(
|
||||||
() => api().get<{ shares: SessionShareRecord[] }>(`/host/${hostId}/active`),
|
() =>
|
||||||
|
api(origin).get<{ shares: SessionShareRecord[] }>(
|
||||||
|
`/host/${hostId}/active`,
|
||||||
|
),
|
||||||
"Failed to fetch active session shares",
|
"Failed to fetch active session shares",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function revokeSessionShare(shareId: string): Promise<unknown> {
|
export function revokeSessionShare(
|
||||||
|
shareId: string,
|
||||||
|
origin?: SessionOrigin,
|
||||||
|
): Promise<unknown> {
|
||||||
return call(
|
return call(
|
||||||
() => api().delete(`/${encodeURIComponent(shareId)}`),
|
() => api(origin).delete(`/${encodeURIComponent(shareId)}`),
|
||||||
"Failed to revoke session share",
|
"Failed to revoke session share",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -169,13 +185,13 @@ export interface DirectoryRole {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Users and roles to share with or invite. */
|
/** Users and roles to share with or invite. */
|
||||||
export function getDirectory(): Promise<{
|
export function getDirectory(origin?: SessionOrigin): Promise<{
|
||||||
users: DirectoryUser[];
|
users: DirectoryUser[];
|
||||||
roles: DirectoryRole[];
|
roles: DirectoryRole[];
|
||||||
}> {
|
}> {
|
||||||
return call(
|
return call(
|
||||||
() =>
|
() =>
|
||||||
api().get<{ users: DirectoryUser[]; roles: DirectoryRole[] }>(
|
api(origin).get<{ users: DirectoryUser[]; roles: DirectoryRole[] }>(
|
||||||
"/directory",
|
"/directory",
|
||||||
),
|
),
|
||||||
"Failed to list users and roles",
|
"Failed to list users and roles",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import SharedSessionView from "./SharedSessionView";
|
|||||||
import { ParticipantsOverlay } from "./ParticipantsOverlay";
|
import { ParticipantsOverlay } from "./ParticipantsOverlay";
|
||||||
import {
|
import {
|
||||||
ShareDialogHost,
|
ShareDialogHost,
|
||||||
|
bindShareApp,
|
||||||
closeShareDialog,
|
closeShareDialog,
|
||||||
openShareDialog,
|
openShareDialog,
|
||||||
type ShareTarget,
|
type ShareTarget,
|
||||||
@@ -217,6 +218,7 @@ interface RemoteDesktopToolbarContext {
|
|||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
protocol: "rdp" | "vnc" | "telnet";
|
protocol: "rdp" | "vnc" | "telnet";
|
||||||
tabInstanceId?: string;
|
tabInstanceId?: string;
|
||||||
|
origin?: "local" | "remote";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRemoteDesktopContext(
|
function isRemoteDesktopContext(
|
||||||
@@ -232,10 +234,12 @@ function isRemoteDesktopContext(
|
|||||||
|
|
||||||
export function activate(app: TermixApp): void {
|
export function activate(app: TermixApp): void {
|
||||||
meetingApp = app;
|
meetingApp = app;
|
||||||
bindApi(app.api);
|
bindApi(app.api, app.apiFor);
|
||||||
|
bindShareApp(app);
|
||||||
app.onDispose(() => {
|
app.onDispose(() => {
|
||||||
closeShareDialog();
|
closeShareDialog();
|
||||||
bindApi(null);
|
bindApi(null);
|
||||||
|
bindShareApp(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rooms and share links draw remote desktop streams through this slot.
|
// Rooms and share links draw remote desktop streams through this slot.
|
||||||
@@ -281,6 +285,7 @@ export function activate(app: TermixApp): void {
|
|||||||
sessionId: context.sessionId,
|
sessionId: context.sessionId,
|
||||||
protocol: context.protocol,
|
protocol: context.protocol,
|
||||||
tabInstanceId: context.tabInstanceId,
|
tabInstanceId: context.tabInstanceId,
|
||||||
|
origin: context.origin,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -32,13 +32,32 @@ export async function connectMeetings(app: TermixApp) {
|
|||||||
export function meetingGuestUrl(
|
export function meetingGuestUrl(
|
||||||
publicUrl: string | null,
|
publicUrl: string | null,
|
||||||
token: string,
|
token: string,
|
||||||
|
): string | null {
|
||||||
|
return guestViewUrl(publicUrl, "collab-guest", token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A guest link on a server others can open, never a file:// desktop page. */
|
||||||
|
export function guestViewUrl(
|
||||||
|
publicUrl: string | null,
|
||||||
|
view: string,
|
||||||
|
token: string,
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!publicUrl) return null;
|
if (!publicUrl) return null;
|
||||||
const url = new URL(publicUrl);
|
const url = new URL(publicUrl);
|
||||||
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
||||||
url.search = "";
|
url.search = "";
|
||||||
url.hash = "";
|
url.hash = "";
|
||||||
url.searchParams.set("view", "collab-guest");
|
url.searchParams.set("view", view);
|
||||||
url.searchParams.set("token", token);
|
url.searchParams.set("token", token);
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Where guests reach a session that runs on the given backend. */
|
||||||
|
export async function sessionPublicUrl(
|
||||||
|
app: TermixApp,
|
||||||
|
origin: "local" | "remote" | undefined,
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!app.desktop.available) return window.location.href;
|
||||||
|
if (origin !== "remote") return null;
|
||||||
|
return (await app.desktop.remoteServerUrl()) || null;
|
||||||
|
}
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
import { useSyncExternalStore } from "react";
|
import { useSyncExternalStore } from "react";
|
||||||
|
import type { TermixApp } from "@termix/plugin-sdk/frontend";
|
||||||
import { ShareSessionModal } from "./ShareSessionModal";
|
import { ShareSessionModal } from "./ShareSessionModal";
|
||||||
import type { SessionShareProtocol } from "./api";
|
import type { SessionShareProtocol } from "./api";
|
||||||
|
import { sessionPublicUrl } from "./meeting-backend";
|
||||||
|
|
||||||
|
let shareApp: TermixApp | null = null;
|
||||||
|
|
||||||
|
export function bindShareApp(app: TermixApp | null): void {
|
||||||
|
shareApp = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveShareBase(
|
||||||
|
origin: "local" | "remote" | undefined,
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!shareApp) return Promise.resolve(window.location.href);
|
||||||
|
return sessionPublicUrl(shareApp, origin);
|
||||||
|
}
|
||||||
|
|
||||||
/** What the share dialog needs to share one live session. */
|
/** What the share dialog needs to share one live session. */
|
||||||
export interface ShareTarget {
|
export interface ShareTarget {
|
||||||
@@ -8,6 +23,8 @@ export interface ShareTarget {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
protocol: SessionShareProtocol;
|
protocol: SessionShareProtocol;
|
||||||
tabInstanceId?: string;
|
tabInstanceId?: string;
|
||||||
|
/** The backend the session runs on; a desktop can be on either. */
|
||||||
|
origin?: "local" | "remote";
|
||||||
}
|
}
|
||||||
|
|
||||||
let current: ShareTarget | null = null;
|
let current: ShareTarget | null = null;
|
||||||
@@ -45,6 +62,8 @@ export function ShareDialogHost() {
|
|||||||
sessionId={target.sessionId}
|
sessionId={target.sessionId}
|
||||||
protocol={target.protocol}
|
protocol={target.protocol}
|
||||||
tabInstanceId={target.tabInstanceId}
|
tabInstanceId={target.tabInstanceId}
|
||||||
|
origin={target.origin}
|
||||||
|
resolvePublicUrl={resolveShareBase}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { TermixApp } from "@termix/plugin-sdk/frontend";
|
||||||
|
import {
|
||||||
|
guestViewUrl,
|
||||||
|
sessionPublicUrl,
|
||||||
|
} from "../../src/frontend/meeting-backend";
|
||||||
|
|
||||||
|
function desktop(url: string | null): TermixApp {
|
||||||
|
return {
|
||||||
|
desktop: { available: true, remoteServerUrl: async () => url },
|
||||||
|
} as unknown as TermixApp;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("session share links", () => {
|
||||||
|
it("builds the link on the linked server for a remote session", async () => {
|
||||||
|
const base = await sessionPublicUrl(
|
||||||
|
desktop("https://termix.example/"),
|
||||||
|
"remote",
|
||||||
|
);
|
||||||
|
expect(guestViewUrl(base, "shared", "tok")).toBe(
|
||||||
|
"https://termix.example/?view=shared&token=tok",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gives no link for a session on the desktop's own backend", async () => {
|
||||||
|
expect(
|
||||||
|
await sessionPublicUrl(desktop("https://termix.example/"), "local"),
|
||||||
|
).toBeNull();
|
||||||
|
expect(
|
||||||
|
guestViewUrl("file:///C:/app/index.html", "shared", "tok"),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses the page address in a browser", async () => {
|
||||||
|
const app = { desktop: { available: false } } as unknown as TermixApp;
|
||||||
|
expect(await sessionPublicUrl(app, undefined)).toBe(window.location.href);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -281,6 +281,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
const webSocketRef = useRef<WebSocket | null>(null);
|
const webSocketRef = useRef<WebSocket | null>(null);
|
||||||
const terminalInputDisposableRef = useRef<{ dispose(): void } | null>(null);
|
const terminalInputDisposableRef = useRef<{ dispose(): void } | null>(null);
|
||||||
const localEchoRef = useRef<TerminalLocalEcho | null>(null);
|
const localEchoRef = useRef<TerminalLocalEcho | null>(null);
|
||||||
|
const sessionOriginRef = useRef<"local" | "remote">("local");
|
||||||
const customKeybindingsRef = useRef<CustomKeybinding[]>([]);
|
const customKeybindingsRef = useRef<CustomKeybinding[]>([]);
|
||||||
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
const resizeTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||||
const wasDisconnectedBySSH = useRef(false);
|
const wasDisconnectedBySSH = useRef(false);
|
||||||
@@ -785,6 +786,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
sessionId: sessionIdRef.current,
|
sessionId: sessionIdRef.current,
|
||||||
protocol: "ssh" as const,
|
protocol: "ssh" as const,
|
||||||
tabInstanceId: hostConfig.instanceId,
|
tabInstanceId: hostConfig.instanceId,
|
||||||
|
origin: sessionOriginRef.current,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
}),
|
}),
|
||||||
@@ -1642,6 +1644,7 @@ const TerminalInner = forwardRef<TerminalHandle, SSHTerminalProps>(
|
|||||||
"local" | "remote" | null | undefined,
|
"local" | "remote" | null | undefined,
|
||||||
})
|
})
|
||||||
: "local";
|
: "local";
|
||||||
|
sessionOriginRef.current = origin === "remote" ? "remote" : "local";
|
||||||
const resolvedUrl = await pluginWsUrl("ssh-terminal", "/terminal", {
|
const resolvedUrl = await pluginWsUrl("ssh-terminal", "/terminal", {
|
||||||
origin,
|
origin,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface TerminalSlotApi {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
protocol: "ssh";
|
protocol: "ssh";
|
||||||
tabInstanceId?: string;
|
tabInstanceId?: string;
|
||||||
|
origin?: "local" | "remote";
|
||||||
} | null;
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user