diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7f438d0a2e3..16612308812 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -641,9 +641,18 @@ importers:
ui:
dependencies:
+ '@lit-labs/signals':
+ specifier: ^0.2.0
+ version: 0.2.0
+ '@lit/context':
+ specifier: ^1.1.6
+ version: 1.1.6
'@noble/ed25519':
specifier: 3.0.1
version: 3.0.1
+ '@novnc/novnc':
+ specifier: ^1.6.0
+ version: 1.6.0
dompurify:
specifier: ^3.3.3
version: 3.3.3
@@ -654,6 +663,9 @@ importers:
specifier: ^17.0.4
version: 17.0.4
devDependencies:
+ '@types/ws':
+ specifier: ^8.18.1
+ version: 8.18.1
'@vitest/browser-playwright':
specifier: 4.1.0
version: 4.1.0(playwright@1.58.2)(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.1.0)
@@ -669,6 +681,9 @@ importers:
vitest:
specifier: 4.1.0
version: 4.1.0(@opentelemetry/api@1.9.0)(@types/node@25.5.0)(@vitest/browser-playwright@4.1.0)(jsdom@29.0.0(@noble/hashes@2.0.1))(vite@8.0.0(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2))
+ ws:
+ specifier: ^8.19.0
+ version: 8.19.0
packages:
@@ -2068,6 +2083,9 @@ packages:
resolution: {integrity: sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA==}
engines: {node: '>=12.4.0'}
+ '@novnc/novnc@1.6.0':
+ resolution: {integrity: sha512-CJrmdSe9Yt2ZbLsJpVFoVkEu0KICEvnr3njW25Nz0jodaiFJtg8AYLGZogRYy0/N5HUWkGUsCmegKXYBSqwygw==}
+
'@octokit/app@16.1.2':
resolution: {integrity: sha512-8j7sEpUYVj18dxvh0KWj6W/l6uAiVRBl1JBDVRqH1VHKAO/G5eRVl4yEoYACjakWers1DjUkcCHyJNQK47JqyQ==}
engines: {node: '>= 20'}
@@ -8758,6 +8776,8 @@ snapshots:
'@nolyfill/domexception@1.0.28': {}
+ '@novnc/novnc@1.6.0': {}
+
'@octokit/app@16.1.2':
dependencies:
'@octokit/auth-app': 8.2.0
diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts
index dd5a659dbc9..439fc9ba277 100644
--- a/src/gateway/server-http.ts
+++ b/src/gateway/server-http.ts
@@ -802,6 +802,28 @@ export function createGatewayHttpServer(opts: {
rateLimiter,
}),
},
+ {
+ name: "debug-image",
+ run: async () => {
+ if (requestPath === "/api/debug-image") {
+ try {
+ const fs = await import("node:fs");
+ const img = fs.readFileSync(
+ "/home/fish/.openclaw/workspace/linux-desktop-control/images/desktop_screeshot_20260318_181311_annotated.png",
+ );
+ res.statusCode = 200;
+ res.setHeader("Content-Type", "image/png");
+ res.setHeader("Access-Control-Allow-Origin", "*");
+ res.end(img);
+ } catch {
+ res.statusCode = 404;
+ res.end("Not Found");
+ }
+ return true;
+ }
+ return false;
+ },
+ },
{
name: "sessions-kill",
run: () =>
@@ -977,6 +999,58 @@ export function attachGatewayUpgradeHandler(opts: {
if (scopedCanvas.rewrittenUrl) {
req.url = scopedCanvas.rewrittenUrl;
}
+
+ const url = new URL(req.url ?? "/", "http://localhost");
+ if (url.pathname === "/api/debug-vnc") {
+ const target = url.searchParams.get("target");
+ let host = process.env.OPENCLAW_VNC_HOST || "localhost";
+ let port = parseInt(process.env.OPENCLAW_VNC_PORT || "5900", 10);
+ if (target) {
+ if (target.includes(":")) {
+ const parts = target.split(":");
+ host = parts[0] || host;
+ const p = parseInt(parts[1] || "", 10);
+ if (!isNaN(p)) {
+ port = p;
+ }
+ } else {
+ host = target;
+ }
+ }
+
+ const net = await import("node:net");
+ // Reuse the same wss for upgrading, but handle connection directly
+ // Or better yet, we can create an ad-hoc WebSocketServer just for this upgrade
+ // to avoid mixing with gateway clients.
+ const { WebSocketServer: VncWss } = await import("ws");
+ const vncWss = new VncWss({ noServer: true, perMessageDeflate: false });
+ vncWss.handleUpgrade(req, socket, head, (ws) => {
+ const tcpSocket = net.connect(port, host);
+ tcpSocket.on("data", (data) => {
+ if (ws.readyState === ws.OPEN) {
+ ws.send(data);
+ }
+ });
+ ws.on("message", (data) => {
+ if (!tcpSocket.writable) {
+ return;
+ }
+ if (Buffer.isBuffer(data)) {
+ tcpSocket.write(data);
+ } else if (Array.isArray(data)) {
+ tcpSocket.write(Buffer.concat(data));
+ } else {
+ tcpSocket.write(Buffer.from(data));
+ }
+ });
+ ws.on("close", () => tcpSocket.end());
+ tcpSocket.on("close", () => ws.close());
+ tcpSocket.on("error", () => ws.close());
+ ws.on("error", () => tcpSocket.end());
+ });
+ return;
+ }
+
if (canvasHost) {
const url = new URL(req.url ?? "/", "http://localhost");
if (url.pathname === CANVAS_WS_PATH) {
diff --git a/test/fixtures/plugin-extension-import-boundary-inventory.json b/test/fixtures/plugin-extension-import-boundary-inventory.json
index 0894fe0d5b5..ead171321f9 100644
--- a/test/fixtures/plugin-extension-import-boundary-inventory.json
+++ b/test/fixtures/plugin-extension-import-boundary-inventory.json
@@ -31,14 +31,6 @@
"resolvedPath": "extensions/imessage/runtime-api.js",
"reason": "imports extension-owned file from src/plugins"
},
- {
- "file": "src/plugins/runtime/runtime-matrix.ts",
- "line": 4,
- "kind": "import",
- "specifier": "../../../extensions/matrix/runtime-api.js",
- "resolvedPath": "extensions/matrix/runtime-api.js",
- "reason": "imports extension-owned file from src/plugins"
- },
{
"file": "src/plugins/runtime/runtime-slack-ops.runtime.ts",
"line": 10,
diff --git a/ui/package.json b/ui/package.json
index 5d514f671cd..f1088b63b03 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -9,16 +9,21 @@
"test": "vitest run --config vitest.config.ts"
},
"dependencies": {
+ "@lit-labs/signals": "^0.2.0",
+ "@lit/context": "^1.1.6",
"@noble/ed25519": "3.0.1",
+ "@novnc/novnc": "^1.6.0",
"dompurify": "^3.3.3",
"lit": "^3.3.2",
"marked": "^17.0.4"
},
"devDependencies": {
+ "@types/ws": "^8.18.1",
"@vitest/browser-playwright": "4.1.0",
"jsdom": "^29.0.0",
"playwright": "^1.58.2",
"vite": "8.0.0",
- "vitest": "4.1.0"
+ "vitest": "4.1.0",
+ "ws": "^8.19.0"
}
}
diff --git a/ui/src/ui/app-render.helpers.ts b/ui/src/ui/app-render.helpers.ts
index d27fb221582..c9760d08ce3 100644
--- a/ui/src/ui/app-render.helpers.ts
+++ b/ui/src/ui/app-render.helpers.ts
@@ -326,6 +326,26 @@ export function renderChatControls(state: AppViewState) {
>
${renderCronFilterIcon(hiddenCronCount)}
+
`;
}
diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts
index c0535cd6c30..143b3b56632 100644
--- a/ui/src/ui/app-render.ts
+++ b/ui/src/ui/app-render.ts
@@ -380,6 +380,11 @@ export function renderApp(state: AppViewState) {
? rawDeliveryToSuggestions.filter((value) => isHttpUrl(value))
: rawDeliveryToSuggestions;
+ const gatewayHttpUrl = state.settings.gatewayUrl
+ .replace(/^ws:\/\//i, "http://")
+ .replace(/^wss:\/\//i, "https://");
+ const debugImageUrl = `${gatewayHttpUrl}/api/debug-image`;
+
return html`
${renderCommandPalette({
open: state.paletteOpen,
@@ -596,7 +601,7 @@ export function renderApp(state: AppViewState) {
: nothing
}
${
- state.tab === "config"
+ state.tab === "config" || state.tab === "chat"
? nothing
: html`