Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions electron/src/apps/local-app-launcher.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { spawn } from "node:child_process";
import { mkdir, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { decodeClientProtocolPayload as decodePayload } from "../shared/client-protocol";
import { electronLog } from "../shared/debug-log";

function decodePayload(raw) {
const value = String(raw || "");
const encoded = value.startsWith("jms2://") ? value.slice(7) : "";
if (!encoded) throw new Error("invalid local client URL scheme");
try {
return JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
} catch (error) {
throw new Error(`decode local client payload failed: ${error instanceof Error ? error.message : error}`);
}
}

function sanitizedName(raw) {
let decoded = String(raw || "");
try {
Expand Down Expand Up @@ -271,13 +261,13 @@
throw new Error("local client payload is missing required connection fields");
}
electronLog.info(
`launch ${payload.protocol} ${payload.endpoint.host}:${payload.endpoint.port} via ${application.display_name || application.launch_type}`

Check warning on line 264 in electron/src/apps/local-app-launcher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'payload.endpoint.host' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AaDB-oitUMlVUYDC1s3L&open=AaDB-oitUMlVUYDC1s3L&pullRequest=1714

Check warning on line 264 in electron/src/apps/local-app-launcher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'payload.endpoint.port' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AaDB-oitUMlVUYDC1s3M&open=AaDB-oitUMlVUYDC1s3M&pullRequest=1714
);
const values = valuesFor(payload);
if (application.use_ssh_helper) values.helper = this.helperCommand();
if (application.protocol_aliases?.[payload.protocol])
values.protocol = application.protocol_aliases[payload.protocol];
if (String(payload.command || "").trim()) {

Check warning on line 270 in electron/src/apps/local-app-launcher.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'payload.command || ""' will use Object's default stringification format ('[object Object]') when stringified.

See more on https://sonarcloud.io/project/issues?id=jumpserver_luna&issues=AaDB-oitUMlVUYDC1s3N&open=AaDB-oitUMlVUYDC1s3N&pullRequest=1714
if (!["terminal", "iterm2", "linux-terminal", "windows-terminal"].includes(application.launch_driver)) {
throw new Error("selected application cannot open command payloads");
}
Expand Down
27 changes: 26 additions & 1 deletion electron/src/desktop/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ import { FfmpegPluginManager } from "../replay/ffmpeg-plugin";
import { OfflineRecordingStore } from "../replay/offline-recordings";
import { ReplayTranscoder } from "../replay/transcoder";
import { readableToWebBody } from "../shared/bytes";
import { findClientProtocolUrl, normalizeClientProtocolUrl } from "../shared/client-protocol";
import {
decodeClientProtocolPayload,
findClientProtocolUrl,
isWebAssetClientProtocolPayload,
normalizeClientProtocolUrl
} from "../shared/client-protocol";
import {
activateDebugLogService,
DebugLogService,
Expand Down Expand Up @@ -1291,6 +1296,7 @@ async function handleInvoke(event, request) {
return applicationConfig.updateCustomTerminal({ ...args, path: resolveExecutablePath(args.path) });
}
if (command === "pull_up") return withIpcErrorLog("pull_up", () => localApplicationLauncher.launch(args.url));
if (command === "take_web_protocol_payloads") return takePendingWebProtocolPayloads();
if (command === "list_system_fonts") return listSystemFonts();
if (command === "cancel_transcode") return replayTranscoder.cancelCurrent(labelForWindow(win));
if (command === "transcode_replays") {
Expand Down Expand Up @@ -1409,8 +1415,21 @@ interface PendingProtocolUrl {
}

const pendingProtocolUrls: PendingProtocolUrl[] = [];
const pendingWebProtocolPayloads: Array<{
url: string;
payload: ReturnType<typeof decodeClientProtocolPayload>;
}> = [];
let startupFinished = false;

function queueWebProtocolPayload(url: string, payload: ReturnType<typeof decodeClientProtocolPayload>) {
if (pendingWebProtocolPayloads.some((pending) => pending.url === url)) return;
pendingWebProtocolPayloads.push({ url, payload });
}

function takePendingWebProtocolPayloads() {
return pendingWebProtocolPayloads.splice(0).map((pending) => pending.payload);
}

function describeProtocolUrl(rawUrl) {
const value = String(rawUrl || "");
if (isOAuthCallbackUrl(value)) return "auth-callback";
Expand All @@ -1420,6 +1439,12 @@ function describeProtocolUrl(rawUrl) {

async function processIncomingProtocolUrl(value: string) {
if (authService.handleCallback(value) || authService.isOAuthCallbackUrl(value)) return false;
const payload = decodeClientProtocolPayload(value);
if (isWebAssetClientProtocolPayload(payload)) {
queueWebProtocolPayload(value, payload);
if (startupFinished) sendMainWindowEvent("web-protocol-url", null);
return false;
}
await localApplicationLauncher.launch(value);
return true;
}
Expand Down
30 changes: 30 additions & 0 deletions electron/src/shared/client-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,36 @@ export function normalizeClientProtocolUrl(raw: unknown): string | undefined {
if (/^jms2:\/\//i.test(value)) return `${CLIENT_PROTOCOL}:${value.slice(value.indexOf(":") + 1)}`;
}

export interface ClientProtocolPayload {
protocol?: string;
asset?: { id?: unknown; [key: string]: unknown };
token?: { id?: unknown; protocol?: string; [key: string]: unknown };
file?: { content?: unknown; [key: string]: unknown };
endpoint?: { host?: unknown; port?: unknown; [key: string]: unknown };
[key: string]: unknown;
}

export function decodeClientProtocolPayload(raw: unknown): ClientProtocolPayload {
const value = normalizeClientProtocolUrl(raw);
const encoded = value?.startsWith(`${CLIENT_PROTOCOL}://`) ? value.slice(`${CLIENT_PROTOCOL}://`.length) : "";
if (!encoded) throw new Error("invalid local client URL scheme");
try {
const payload: unknown = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
if (!payload || Array.isArray(payload) || typeof payload !== "object") throw new Error("payload must be an object");
return payload as ClientProtocolPayload;
} catch (error) {
throw new Error(`decode local client payload failed: ${error instanceof Error ? error.message : error}`);
}
}

export function isWebAssetClientProtocolPayload(payload: ClientProtocolPayload) {
return (
["http", "https"].includes(String(payload.protocol || "").toLowerCase()) &&
typeof payload.asset?.id === "string" &&
Boolean(payload.asset.id.trim())
);
}

export function findClientProtocolUrl(argv: string[], additionalData?: unknown): string | undefined {
if (additionalData && typeof additionalData === "object" && "protocolUrl" in additionalData) {
const forwarded = normalizeClientProtocolUrl(additionalData.protocolUrl);
Expand Down
52 changes: 51 additions & 1 deletion electron/tests/client-protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,46 @@ import test from "node:test";
import ts from "typescript";
import {
CLIENT_AUTH_CALLBACK,
decodeClientProtocolPayload,
findClientProtocolUrl,
isWebAssetClientProtocolPayload,
normalizeClientProtocolUrl,
registerClientProtocol
} from "../src/shared/client-protocol.ts";

function loadTestDependency(require: NodeJS.Require, name: string) {
switch (name) {
case "../../../ui/utils/apiError":
return require("../../../ui/utils/apiError");
case "../package.json":
return require("../package.json");
case "../shared/client-protocol":
return require("../shared/client-protocol");
case "../shared/debug-log":
return require("../shared/debug-log");
case "../shared/url":
return require("../shared/url");
case "./oauth-callback":
return require("./oauth-callback");
case "./shared/product-name":
return require("./shared/product-name");
case "node:child_process":
return require("node:child_process");
case "node:crypto":
return require("node:crypto");
case "node:events":
return require("node:events");
case "node:fs/promises":
return require("node:fs/promises");
case "node:http":
return require("node:http");
case "node:path":
return require("node:path");
default:
throw new Error(`Unexpected test dependency: ${name}`);
}
}

// Execute the real entry/service code with Electron replaced; no desktop or OS
// registration is touched by these Windows lifecycle checks on other platforms.
function loadWithElectronMocks(relativePath: string, replacements: Record<string, unknown>, runtime = process) {
Expand All @@ -25,7 +60,8 @@ function loadWithElectronMocks(relativePath: string, replacements: Record<string
const exports: Record<string, any> = {};
runInNewContext(output.outputText, {
exports,
require: (name: string) => (Object.hasOwn(replacements, name) ? replacements[name] : require(name)),
require: (name: string) =>
Object.hasOwn(replacements, name) ? replacements[name] : loadTestDependency(require, name),
process: runtime,
console,
Buffer,
Expand Down Expand Up @@ -141,6 +177,20 @@ test("extracts Windows cold-start and forwarded URLs without changing their payl
assert.equal(normalizeClientProtocolUrl("jms2%3A%2F%2Fbad%ZZ"), undefined);
});

test("routes only HTTP and HTTPS asset payloads to the built-in web proxy", () => {
const encode = (payload: unknown) => `jms2://${Buffer.from(JSON.stringify(payload)).toString("base64")}`;
const http = decodeClientProtocolPayload(
encode({ protocol: "http", asset: { id: "asset-1" }, token: { id: "token" } })
);
const https = decodeClientProtocolPayload(encode({ protocol: "HTTPS", asset: { id: "asset-2" } }));
const ssh = decodeClientProtocolPayload(encode({ protocol: "ssh", asset: { id: "asset-3" } }));

assert.equal(isWebAssetClientProtocolPayload(http), true);
assert.equal(isWebAssetClientProtocolPayload(https), true);
assert.equal(isWebAssetClientProtocolPayload(ssh), false);
assert.equal(isWebAssetClientProtocolPayload({ protocol: "http", asset: {} }), false);
});

test("registers only jms2 and keeps the Windows development entry path as one argument", () => {
const calls: unknown[][] = [];
const app = {
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "Kubernetes connections use the koko Kubernetes UI workspace.",
"RemoteDesktop": "Remote desktop",
"RemoteDesktopDescription": "RDP/VNC connections use the Lion remote desktop workspace.",
"ClientProxy": "Client",
"BuiltinWebProxy": "Built-in Browser",
"BuiltinWebProxyDescription": "Website assets use the system WebView, with traffic routed through Koko Web Proxy."
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/es.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"Monitor": {
"Unavailable": "La sesión ha finalizado o no se puede supervisar.",

Check warning on line 3 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"supervisar" should be "supervisor".

Check warning on line 3 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"supervisar" should be "supervisor".
"PauseSent": "Se ha enviado la tarea de pausa",
"ResumeSent": "Se ha enviado la tarea de reanudación"
},
Expand All @@ -14,24 +14,24 @@
"Play": "Reproducir",
"Pause": "Pausar",
"Restart": "Reiniciar",
"HistoryCommands": "Historial de comandos",

Check warning on line 17 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".

Check warning on line 17 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"Historial" should be "Historical".

Check warning on line 17 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".

Check warning on line 17 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"Historial" should be "Historical".
"Playlist": "Lista de reproducción",
"SearchCommands": "Buscar comandos...",

Check warning on line 19 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".

Check warning on line 19 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".
"TextIndex": "Índice de texto",
"SearchIndex": "Buscar texto reconocido...",
"OCRTextEvidence": "Texto en pantalla (OCR)",
"OCREvidenceHint": "Observaciones OCR de los fotogramas, no acciones verificadas del usuario.",
"IndexBaseline": "Pantalla inicial (OCR)",

Check warning on line 24 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"inicial" should be "initial" or "indicial".

Check warning on line 24 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"inicial" should be "initial" or "indicial".
"IndexNoText": "No hay texto disponible",
"IndexEmpty": "No hay eventos indexados",
"IndexEmptyHint": "Esta grabación no contiene eventos de texto OCR.",
"IndexNoMatch": "No hay eventos coincidentes",
"IndexNoMatchHint": "Pruebe con otra palabra o frase.",

Check warning on line 29 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"frase" should be "phrase".

Check warning on line 29 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"frase" should be "phrase".
"IndexFailed": "No se pudo cargar el índice de texto",
"IndexPartUnavailable": "El segmento de grabación de este evento aún no está disponible.",
"NoCommands": "No hay registros de comandos",

Check warning on line 32 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".

Check warning on line 32 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".
"NoCommandsHint": "Esta sesión no tiene comandos grabados",

Check warning on line 33 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".

Check warning on line 33 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".
"CommandsFailed": "No se pudieron cargar los comandos",

Check warning on line 34 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".

Check warning on line 34 in i18n/locales/es.json

View workflow job for this annotation

GitHub Actions / Spell Check with Typos

"comandos" should be "commandos".
"CommandsFailedHint": "El historial de comandos no está disponible temporalmente",
"Loading": "Cargando...",
"HideRail": "Ocultar panel",
Expand Down Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "Las conexiones K8s usan el workspace de Kubernetes UI de koko.",
"RemoteDesktop": "Escritorio remoto",
"RemoteDesktopDescription": "Las conexiones RDP/VNC usan el workspace de escritorio remoto de Lion.",
"ClientProxy": "Cliente",
"BuiltinWebProxy": "Navegador integrado",
"BuiltinWebProxyDescription": "Los activos de sitios web usan el WebView del sistema y el tráfico pasa por Koko Web Proxy."
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "Les connexions Kubernetes utilisent l’espace d’interface Kubernetes Koko.",
"RemoteDesktop": "Bureau distant",
"RemoteDesktopDescription": "Les connexions RDP/VNC utilisent l’espace de bureau distant Lion.",
"ClientProxy": "Client",
"BuiltinWebProxy": "Navigateur intégré",
"BuiltinWebProxyDescription": "Les actifs web utilisent la WebView du système, avec un trafic acheminé via Koko Web Proxy."
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "K8s 接続は koko Kubernetes UI workspace を使用します。",
"RemoteDesktop": "リモートデスクトップ",
"RemoteDesktopDescription": "RDP/VNC 接続は Lion リモートデスクトップ workspace を使用します。",
"ClientProxy": "クライアント",
"BuiltinWebProxy": "内蔵ブラウザ",
"BuiltinWebProxyDescription": "Website 資産はシステム WebView を使用し、トラフィックは Koko Web Proxy 経由で統一します。"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "K8s 연결은 koko Kubernetes UI workspace를 사용합니다.",
"RemoteDesktop": "원격 데스크톱",
"RemoteDesktopDescription": "RDP/VNC 연결은 Lion 원격 데스크톱 workspace를 사용합니다.",
"ClientProxy": "클라이언트",
"BuiltinWebProxy": "내장 브라우저",
"BuiltinWebProxyDescription": "Website 자산은 시스템 WebView를 사용하며 트래픽은 Koko Web Proxy를 통해 전달됩니다."
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/pt_br.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "Conexões Kubernetes usam o workspace da UI Kubernetes do koko.",
"RemoteDesktop": "Área de trabalho remota",
"RemoteDesktopDescription": "Conexões RDP/VNC usam o workspace de área de trabalho remota do Lion.",
"ClientProxy": "Cliente",
"BuiltinWebProxy": "Navegador integrado",
"BuiltinWebProxyDescription": "Ativos de website usam o WebView do sistema, com o tráfego roteado pelo Koko Web Proxy."
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "Подключения K8s используют workspace Kubernetes UI koko.",
"RemoteDesktop": "Удаленный рабочий стол",
"RemoteDesktopDescription": "Подключения RDP/VNC идут через workspace удаленного рабочего стола Lion.",
"ClientProxy": "Клиент",
"BuiltinWebProxy": "Встроенный браузер",
"BuiltinWebProxyDescription": "Веб-активы используют системный WebView, трафик идет через Koko Web Proxy."
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "Kết nối K8s dùng workspace Kubernetes UI của koko.",
"RemoteDesktop": "Máy tính từ xa",
"RemoteDesktopDescription": "Kết nối RDP/VNC đi qua workspace máy tính từ xa Lion.",
"ClientProxy": "Ứng dụng khách",
"BuiltinWebProxy": "Trình duyệt tích hợp",
"BuiltinWebProxyDescription": "Tài sản website dùng WebView hệ thống, lưu lượng đi qua Koko Web Proxy."
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "K8s 连接使用 koko Kubernetes UI workspace。",
"RemoteDesktop": "远程桌面",
"RemoteDesktopDescription": "RDP/VNC 连接走 Lion 远程桌面 workspace。",
"ClientProxy": "客户端",
"BuiltinWebProxy": "内置浏览器",
"BuiltinWebProxyDescription": "Website 资产使用系统 WebView,流量统一通过 Koko Web Proxy。"
},
Expand Down
1 change: 1 addition & 0 deletions i18n/locales/zh_hant.json
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,7 @@
"KubernetesDescription": "K8s 連線使用 koko Kubernetes UI workspace。",
"RemoteDesktop": "遠端桌面",
"RemoteDesktopDescription": "RDP/VNC 連線走 Lion 遠端桌面 workspace。",
"ClientProxy": "用戶端",
"BuiltinWebProxy": "內建瀏覽器",
"BuiltinWebProxyDescription": "Website 資產使用系統 WebView,流量統一經由 Koko Web Proxy。"
},
Expand Down
50 changes: 50 additions & 0 deletions ui/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DEFAULT_DARK_THEME_PRESET, DEFAULT_LIGHT_THEME_PRESET } from "~/composa
import { useWorkspaceFeatures } from "~/composables/useWorkspaceFeatures";
import { registerAiTaskTabCloseConfirm } from "~/composables/useWorkspaceTabs";
import { desktopInvoke, desktopListen } from "~/shared/desktop/bridge";
import { useUserInfoStore } from "~/store/modules/userInfo";
import { normalizeLanguageCode, resolveLanguageFromSystem, toDjangoLanguageCode } from "~/utils";
import {
COMMUNITY_WORKSPACE_BRAND,
Expand All @@ -32,6 +33,8 @@ const LOCALE_PREFIX_RE = /^\/[a-z]{2}(?:-[A-Z]{2})?(?=\/|$)/;

const route = useRoute();
const authSession = useAuthSession();
const userInfoStore = useUserInfoStore();
const { handleWebClientProtocolPayload } = useAssetAction();
useWorkspaceFeatures(authSession.authReady);
const webWorkspaceBrand = useState<string>(WORKSPACE_BRAND_STATE_KEY, () => COMMUNITY_WORKSPACE_BRAND);
const webWorkspaceFavicon = useState<string>(WORKSPACE_FAVICON_STATE_KEY, () => "");
Expand Down Expand Up @@ -69,6 +72,7 @@ const unlistenPrimaryColor = ref<DesktopUnlistenFn | null>(null);
const unlistenTheme = ref<DesktopUnlistenFn | null>(null);
const unlistenFont = ref<DesktopUnlistenFn | null>(null);
const unlistenSettingsNavigate = ref<DesktopUnlistenFn | null>(null);
const unlistenWebProtocolUrl = ref<DesktopUnlistenFn | null>(null);
const { openSettings } = useSettingsWindow();
const {
confirmOpen: siteLeaveConfirmOpen,
Expand Down Expand Up @@ -278,6 +282,42 @@ async function applyLanguagePreference(pref: LanguagePreference) {
}
}

let drainingWebProtocols = false;
let webProtocolDrainRequested = false;

async function drainWebProtocolPayloads() {
webProtocolDrainRequested = true;
if (drainingWebProtocols || !isDesktopRuntime() || !userInfoStore.loggedIn) return;

drainingWebProtocols = true;
try {
while (webProtocolDrainRequested && userInfoStore.loggedIn) {
webProtocolDrainRequested = false;
const payloads =
await desktopInvoke<Array<{ protocol?: unknown; asset?: { id?: unknown } }>>("take_web_protocol_payloads");
for (const payload of payloads) {
try {
await handleWebClientProtocolPayload(payload);
} catch (error) {
console.error("open web asset from client protocol failed", error);
}
}
}
} catch (error) {
console.error("drain web client protocols failed", error);
} finally {
drainingWebProtocols = false;
if (webProtocolDrainRequested && userInfoStore.loggedIn) void drainWebProtocolPayloads();
}
}

watch(
() => userInfoStore.loggedIn,
(loggedIn) => {
if (loggedIn) void drainWebProtocolPayloads();
}
);

async function applyAfterHydration() {
if (hydrationPromise.value) {
try {
Expand Down Expand Up @@ -366,6 +406,15 @@ onMounted(async () => {
} catch (err) {
console.error("listen settings-navigate failed", err);
}

try {
unlistenWebProtocolUrl.value = await desktopListen("web-protocol-url", () => {
void drainWebProtocolPayloads();
});
void drainWebProtocolPayloads();
} catch (err) {
console.error("listen web-protocol-url failed", err);
}
});

onBeforeUnmount(() => {
Expand All @@ -374,6 +423,7 @@ onBeforeUnmount(() => {
unlistenTheme.value?.();
unlistenFont.value?.();
unlistenSettingsNavigate.value?.();
unlistenWebProtocolUrl.value?.();
});
</script>

Expand Down
15 changes: 15 additions & 0 deletions ui/components/Workspace/connectionSetupPane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ const launchSummary = computed(() => {
: t("ConnectionSetup.LaunchWithLocalClient");
});
const launchHint = computed(() => t("ConnectionSetup.LaunchHint"));
const downloadCenterUrl = computed(() =>
import.meta.client ? withWebSitePrefix("/core/download/") : "/core/download/"
);
const standaloneSessionWindow = computed(() => route.path.startsWith("/session/"));

const updateExternalLaunchState = async () => {
Expand Down Expand Up @@ -374,6 +377,18 @@ onMounted(loadAsset);
{{ launchedClientName }}
</div>
</div>

<UButton
:label="t('Setting.DownloadClient')"
icon="i-lucide-download"
:to="downloadCenterUrl"
external
target="_blank"
rel="noopener noreferrer"
color="neutral"
variant="outline"
class="mt-5"
/>
</section>
</div>

Expand Down
Loading
Loading