import type { App } from "@modelcontextprotocol/ext-apps"; import "./viewer.css"; import { VideoPlayer } from "./video-player.js"; declare const CROSSDESK_APPS: boolean; declare const CROSSDESK_LOGO: string; type Data = Record; type Invoke = (name: string, args?: Data) => Promise; const root = document.querySelector("#app")!; root.innerHTML = `

CrossDesk MCP

等待连接

远程画面

待连接

未连接设备

选择设备并输入访问密码。

——
等待设备连接等待授权
`; const element = (id: string) => document.getElementById(id) as T; const input = (id: string) => element(id); const select = (id: string) => element(id); let invoke: Invoke; let app: App | undefined; let context: Data | undefined; let status: Data = {}; let savedDevices: Data[] = []; let controlBusy = false; let disconnectedByUser = false; let frameSize = "", frameRate = ""; let player: VideoPlayer | undefined; let videoStarting = false, videoFailed = false, videoEpoch = 0; let videoSession: string | undefined; let videoGeneration: number | undefined; let imageAt = 0; let busy = false, polling = false, started = false; const messages: Record = { VIDEO_UNSUPPORTED: "当前浏览器不支持此视频格式,请使用支持 WebCodecs 的浏览器。", VIDEO_UNAVAILABLE: "视频通道暂不可用。可重新加载画面;内嵌面板受限时请在浏览器中观看。", VIDEO_INVALID: "视频格式发生变化,请重新加载画面。", VIDEO_LIMIT: "观看面板数量已达上限,请关闭其他面板。", AUTH_FAILED: "访问密码错误,请重新输入。", AUTH_REQUIRED: "请输入访问密码,或在本机 setup 中配置凭据。", CONNECTION_FAILED: "连接失败,请检查设备是否在线及服务器设置。", CONNECTION_TIMEOUT: "等待远端画面超时,请检查远端状态后重试。", CONNECTION_LOST: "远程连接已中断。请检查远端状态并重新连接。", SESSION_LIMIT: "已有活动连接,请先断开当前电脑。", DEVICE_BUSY: "另一实例正在使用此设备。", CREDENTIAL_STORE_UNAVAILABLE: "系统凭据库不可用,密码未保存。", PAIRING_REQUIRED: "请在终端运行 Agent 工具结果中的 setup --open-panel 配对命令,再打开面板。", PANEL_UNAUTHORIZED: "面板授权已失效,请重新打开。", VIEW_UNAUTHORIZED: "此面板已失效,请使用最新打开的面板。", CONTROL_DENIED: "当前会话没有控制权限,或已被人工暂停。", NATIVE_PACKAGE_MISSING: "缺少对应平台的 CLI 运行组件,请检查包安装是否启用了 optional dependencies。", INVALID_ARGUMENT: "请检查设备 ID、服务器和输入内容。", DEVICE_ALIAS_CONFLICT: "这个备注已属于另一台设备,请换一个备注。", }; function text(id: string, value: string) { if (element(id).textContent !== value) element(id).textContent = value; } function platformName(platform?: string) { return ({ macos: "macOS", windows: "Windows", linux: "Linux", android: "Android", ios: "iOS" } as Record)[platform ?? ""] ?? "远程电脑"; } function resetMetrics() { imageAt = 0; frameSize = frameRate = ""; text("resolution", "—"); text("fps", "—"); element("focus-view").disabled = true; } function setFocus(focused: boolean) { root.classList.toggle("focus-mode", focused); const button = element("focus-view"); const label = focused ? "还原布局" : "放大画面"; button.textContent = label; if (focused) setDetails(false); button.setAttribute("aria-pressed", String(focused)); button.setAttribute("aria-label", label); button.title = label; } function setDetails(open: boolean) { root.classList.toggle("details-open", open); element("details-toggle").setAttribute("aria-expanded", String(open)); text("details-toggle", open ? "收起信息" : "连接信息"); } element("details-toggle").onclick = () => setDetails(!root.classList.contains("details-open")); element("focus-view").onclick = () => setFocus(!root.classList.contains("focus-mode")); window.addEventListener("keydown", (event) => { if (event.key === "Escape" && root.classList.contains("focus-mode")) setFocus(false); }); function hidePassword() { input("password").type = "password"; text("password-toggle", "显示"); element("password-toggle").setAttribute("aria-label", "显示密码"); element("password-toggle").setAttribute("aria-pressed", "false"); } element("password-toggle").onclick = () => { if (input("password").type === "text") return hidePassword(); input("password").type = "text"; text("password-toggle", "隐藏"); element("password-toggle").setAttribute("aria-label", "隐藏密码"); element("password-toggle").setAttribute("aria-pressed", "true"); }; select("mode").onchange = () => { text("mode-description", select("mode").value === "control" ? "Agent 可以操作键鼠,你可以随时暂停。" : "Agent 可以查看屏幕,无法操作键鼠。"); }; function showError(error: unknown) { const code = error instanceof Error ? error.message : "INTERNAL_ERROR"; element("error").textContent = messages[code] ?? `操作未完成(${/^[A-Z_]+$/.test(code) ? code : "CONNECTION_ERROR"})`; element("error").hidden = false; } function check(result: Data): Data { if (result.error) throw new Error(result.error.code ?? "INTERNAL_ERROR"); return result; } async function devices() { const result = await invoke("devices"); if (!started && result.defaults) { input("server").value = result.defaults.server; input("port").value = String(result.defaults.port); } const chosen = select("device").value || context?.device_alias || ""; select("device").replaceChildren(new Option("连接新设备", "")); savedDevices = result.devices ?? []; for (const d of savedDevices) select("device").add( new Option(`${d.device_alias} · ${d.device_id}`, d.device_alias), ); select("device").value = [...select("device").options].some( (o) => o.value === chosen, ) ? chosen : ""; deviceChanged(); } function deviceChanged() { const saved = !!select("device").value; element("new-device").hidden = saved; element("new-device-label").hidden = saved; input("server").disabled = input("port").disabled = saved; input("password").placeholder = saved ? "留空使用已保存的密码" : "输入远端访问密码"; } select("device").onchange = deviceChanged; input("save").onchange = () => { for (const name of ["remember", "authorize"]) { input(name).disabled = !input("save").checked; if (!input("save").checked) input(name).checked = false; } }; function renderStatus(s: Data) { const connected = s.state === "ready" || s.state === "waiting_frame"; const lost = ["closed", "failed"].includes(s.state); if (lost && !disconnectedByUser && ["ready", "waiting_frame"].includes(status.state)) showError(new Error("CONNECTION_LOST")); const changedSession = s.session_id !== status.session_id; status = s; const connecting = busy || s.state === "waiting_frame"; root.dataset.connected = String(connected); root.dataset.connecting = String(connecting); root.dataset.controlling = String(connected && s.control === true && s.control_allowed === true && !s.human_paused); element("connection").dataset.state = connected ? "online" : connecting ? "connecting" : lost && !disconnectedByUser ? "offline" : "idle"; text("connection-label", connected ? "已连接" : connecting ? "正在连接" : lost ? "未连接" : "等待连接"); element("connect-form").hidden = connected; element("session-card").hidden = !connected; text("sidebar-title", connected ? "连接信息" : "连接设备"); text("device-name", connected ? s.device_alias || "远程电脑" : "远程画面"); if (!connected || changedSession) setDetails(false); text("permission", connected ? s.human_paused ? "控制已暂停" : s.control_allowed ? "允许 Agent 操作" : "仅查看画面" : "等待授权"); element("permission").dataset.state = !connected ? "idle" : s.human_paused ? "paused" : "active"; const platform = platformName(s.platform); text("session-info", connected ? `${platform} · 显示器 ${(s.display ?? 0) + 1}` : "等待设备连接"); text("session-device", s.device_alias || "远程电脑"); text("session-platform", platform); const device = savedDevices.find((d) => d.device_alias === s.device_alias); element("session-device-id-row").hidden = !device; text("session-device-id", device ? String(device.device_id).replace(/(\d{3})(?=\d)/g, "$1 ") : ""); text("session-access", s.control_allowed ? "查看与操作" : "仅查看"); text("session-display", `显示器 ${(s.display ?? 0) + 1}`); text("session-agent", s.human_paused ? "已暂停" : s.control_allowed ? "允许操作" : "仅可观察"); element("session-agent").dataset.paused = String(!!s.human_paused); element("pause").disabled = !connected || !s.control_allowed || controlBusy; const pauseLabel = s.human_paused ? "恢复控制" : "暂停控制"; text("pause-label", pauseLabel); element("disconnect").disabled = !s.session_id || controlBusy || busy; element("connect").disabled = busy || connected || !invoke; element("connection-fields").disabled = busy; element("connect").setAttribute("aria-busy", String(busy)); text("connect-label", busy ? "正在连接…" : "连接"); if (!connected) { element("frame").hidden = true; element("empty").hidden = false; element("stale").hidden = true; resetMetrics(); if (root.classList.contains("focus-mode")) setFocus(false); } else if (changedSession) { resetMetrics(); element("frame").hidden = true; element("empty").hidden = false; } if (!imageAt && !videoFailed) { text("empty-title", connecting || connected ? "正在建立远程连接" : lost ? "会话已结束" : "未连接设备"); text("empty-description", connecting || connected ? "正在等待远端画面,请稍候。" : lost ? "请重新连接设备。" : "选择设备并输入访问密码。"); text("video-state", connecting || connected ? "等待画面" : "待连接"); element("video-state").dataset.state = "idle"; } } async function connect() { if (!invoke || busy || ["ready", "waiting_frame"].includes(status.state)) return; element("error").hidden = true; disconnectedByUser = false; busy = true; renderStatus(status); const saved = select("device").value; let args: Data = { mode: select("mode").value, display_index: Number(input("display").value), save_device: input("save").checked, remember_password: input("remember").checked, persistent_authorization: input("authorize").checked, ...(saved ? { device_alias: saved } : { device_id: input("device-id").value.replace(/\s/g, ""), server: input("server").value, port: Number(input("port").value), ...(input("label").value ? { label: input("label").value } : {}), }), }; if (!saved || input("password").value) args.password = input("password").value; input("password").value = ""; hidePassword(); try { const result = await invoke("connect", args); args = {}; renderStatus(result); if (result.persistence_error) showError(new Error(result.persistence_error)); await devices(); } catch (error) { args = {}; showError(error); } finally { busy = false; renderStatus(status); } } element("connect").onclick = () => { void connect(); }; element("connect-form").addEventListener("keydown", (event) => { if (event.key === "Enter" && event.target instanceof HTMLInputElement) { event.preventDefault(); void connect(); } }); element("pause").onclick = async () => { if (controlBusy) return; controlBusy = true; renderStatus(status); try { await invoke("control", { operation: status.human_paused ? "resume" : "pause", }); renderStatus(await invoke("status")); } catch (e) { showError(e); } finally { controlBusy = false; renderStatus(status); } }; element("disconnect").onclick = async () => { if (controlBusy) return; controlBusy = true; disconnectedByUser = true; renderStatus(status); try { await closePreview(); await invoke("control", { operation: "disconnect" }); element("error").hidden = true; resetMetrics(); renderStatus(await invoke("status")); } catch (e) { disconnectedByUser = false; showError(e); } finally { controlBusy = false; renderStatus(status); } }; async function closePreview() { ++videoEpoch; player?.stop(); player = undefined; videoSession = undefined; videoGeneration = undefined; videoStarting = videoFailed = false; element("video-retry").hidden = true; element("video-browser").hidden = true; } async function startPreview() { if (player || videoStarting || videoFailed || document.hidden) return; const epoch = ++videoEpoch; videoSession = status.session_id; videoGeneration = status.generation; videoStarting = true; const failed = (error: Error) => { if (epoch !== videoEpoch) return; videoFailed = true; showError(error); element("video-retry").hidden = false; element("video-browser").hidden = !CROSSDESK_APPS; element("stale").hidden = !imageAt; text("video-state", "画面不可用"); element("video-state").dataset.state = "stale"; if (!imageAt) { text("empty-title", "暂时无法显示画面"); text("empty-description", "会话仍然保留。请尝试重新加载画面。"); } }; try { const grant = await invoke("video_open"); if (epoch !== videoEpoch || document.hidden) return; player = new VideoPlayer(element("frame"), (info) => { if (epoch !== videoEpoch) return; imageAt = Date.now(); element("frame").hidden = false; element("empty").hidden = true; element("stale").hidden = true; const size = `${info.width} × ${info.height}`; const rate = info.fps > 0 ? `${info.fps.toFixed(1)} FPS` : "测量中"; if (size !== frameSize) { frameSize = size; text("resolution", size); } if (rate !== frameRate) { frameRate = rate; text("fps", rate); } element("focus-view").disabled = false; text("video-state", "实时"); element("video-state").dataset.state = "live"; }, failed); player.start(grant.url, grant.ticket, grant.protocol, grant.decoder); } catch (error) { failed(error instanceof Error ? error : new Error("VIDEO_UNAVAILABLE")); } finally { if (epoch === videoEpoch) videoStarting = false; } } element("video-retry").onclick = async () => { await closePreview(); element("error").hidden = true; await startPreview(); }; element("video-browser").onclick = async () => { try { const result = await app!.callServerTool({name: "crossdesk_open_browser", arguments: {view_id: context!.view_id}}); if (result.isError) throw new Error("VIDEO_UNAVAILABLE"); started = false; await closePreview(); element("error").hidden = true; for (const name of ["connect", "pause", "disconnect"]) element(name).disabled = true; element("panel-status").hidden = false; element("panel-status").textContent = "浏览器观看面板已打开。"; } catch (error) { showError(error); } }; async function poll() { if (polling || !invoke || !started || document.hidden || busy || controlBusy) return; polling = true; try { renderStatus(await invoke("status")); if (videoSession && (videoSession !== status.session_id || videoGeneration !== status.generation)) { if (videoFailed) element("error").hidden = true; await closePreview(); } if (status.state !== "ready" || document.hidden) { await closePreview(); return; } await startPreview(); const stale = !!imageAt && Date.now() - imageAt > 2000; element("stale").hidden = !stale; if (stale) { text("video-state", "等待画面"); element("video-state").dataset.state = "stale"; text("fps", "—"); frameRate = ""; } } catch (error) { showError(error); } finally { polling = false; } } document.addEventListener("visibilitychange", () => { if (document.hidden) void closePreview(); else void poll(); }); window.addEventListener("pagehide", () => { void closePreview(); }); setInterval(() => { void poll(); }, 1000); async function ready(next: Data) { if ( started && context?.view_id === next.view_id && context?.view_generation === next.view_generation ) return; if (started) await closePreview(); context = next; element("panel-status").hidden = true; started = false; await invoke("ready"); await devices(); started = true; element("connect").disabled = false; await poll(); } async function start() { if (CROSSDESK_APPS) { const { App } = await import("@modelcontextprotocol/ext-apps"); app = new App({ name: "CrossDesk MCP", version: "0.1.0" }, {}); const names: Record = { ready: "crossdesk_ui_ready", devices: "crossdesk_ui_list_devices", status: "crossdesk_ui_get_session", connect: "crossdesk_ui_connect", video_open: "crossdesk_video_open", control: "crossdesk_viewer_control", }; let connected = false, initial: Data | undefined; invoke = async (name, args = {}) => { const result = await app!.callServerTool({ name: names[name]!, arguments: { ...args, view_id: context!.view_id, view_generation: context!.view_generation, ui_token: context!.ui_token, }, }); const data = result.structuredContent ?? JSON.parse((result.content?.[0] as Data)?.text ?? "{}"); return check(data); }; app.ontoolresult = (result) => { const meta = result._meta?.["crossdesk/ui"] as Data | undefined; if (!meta) return; initial = { ...(result.structuredContent ?? {}), ...meta }; if (connected) void ready(initial).catch(showError); }; app.onteardown = async () => { await closePreview(); return {}; }; await app.connect(); connected = true; if (initial) await ready(initial); } else { let csrf = ""; const request = async (path: string, body?: Data) => check( await ( await fetch("/api/" + path, { method: body ? "POST" : "GET", credentials: "same-origin", headers: body ? { "Content-Type": "application/json", "X-CrossDesk-CSRF": csrf } : {}, body: body ? JSON.stringify(body) : undefined, cache: "no-store", }) ).json(), ); const bootstrap = new URLSearchParams(location.hash.slice(1)).get( "bootstrap", ); history.replaceState(null, "", location.pathname); const result = bootstrap ? await request("bootstrap", { token: bootstrap }) : await request("context"); csrf = result.csrf; invoke = (name, args = {}) => request(name, { ...args, view_id: context!.view_id }); await ready(bootstrap ? result : result.views.at(-1)); } } void start().catch(showError);