init
This commit is contained in:
parent
5b06619c67
commit
2712bb0f3d
@ -283,6 +283,26 @@ export function renderChatControls(state: AppViewState) {
|
||||
>
|
||||
${renderCronFilterIcon(hiddenCronCount)}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--sm btn--icon ${state.showClawComputer ? "active" : ""}"
|
||||
@click=${() => state.toggleClawComputer()}
|
||||
title="远程桌面"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -1316,127 +1316,163 @@ export function renderApp(state: AppViewState) {
|
||||
|
||||
${
|
||||
state.tab === "chat"
|
||||
? renderChat({
|
||||
sessionKey: state.sessionKey,
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.chatAttachments = [];
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatRunId = null;
|
||||
state.chatQueue = [];
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
void loadChatHistory(state);
|
||||
void refreshChatAvatar(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
showThinking,
|
||||
loading: state.chatLoading,
|
||||
sending: state.chatSending,
|
||||
compactionStatus: state.compactionStatus,
|
||||
fallbackStatus: state.fallbackStatus,
|
||||
assistantAvatarUrl: chatAvatarUrl,
|
||||
messages: state.chatMessages,
|
||||
toolMessages: state.chatToolMessages,
|
||||
streamSegments: state.chatStreamSegments,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
draft: state.chatMessage,
|
||||
queue: state.chatQueue,
|
||||
connected: state.connected,
|
||||
canSend: state.connected,
|
||||
disabledReason: chatDisabledReason,
|
||||
error: state.lastError,
|
||||
sessions: state.sessionsResult,
|
||||
focusMode: chatFocus,
|
||||
onRefresh: () => {
|
||||
state.resetToolStream();
|
||||
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
|
||||
},
|
||||
onToggleFocusMode: () => {
|
||||
if (state.onboarding) {
|
||||
return;
|
||||
}
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
chatFocusMode: !state.settings.chatFocusMode,
|
||||
});
|
||||
},
|
||||
onChatScroll: (event) => state.handleChatScroll(event),
|
||||
getDraft: () => state.chatMessage,
|
||||
onDraftChange: (next) => (state.chatMessage = next),
|
||||
onRequestUpdate: requestHostUpdate,
|
||||
attachments: state.chatAttachments,
|
||||
onAttachmentsChange: (next) => (state.chatAttachments = next),
|
||||
onSend: () => state.handleSendChat(),
|
||||
canAbort: Boolean(state.chatRunId),
|
||||
onAbort: () => void state.handleAbortChat(),
|
||||
onQueueRemove: (id) => state.removeQueuedMessage(id),
|
||||
onNewSession: () => state.handleSendChat("/new", { restoreDraft: true }),
|
||||
onClearHistory: async () => {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await state.client.request("sessions.reset", { key: state.sessionKey });
|
||||
state.chatMessages = [];
|
||||
state.chatStream = null;
|
||||
state.chatRunId = null;
|
||||
await loadChatHistory(state);
|
||||
} catch (err) {
|
||||
state.lastError = String(err);
|
||||
}
|
||||
},
|
||||
agentsList: state.agentsList,
|
||||
currentAgentId: resolvedAgentId ?? "main",
|
||||
onAgentChange: (agentId: string) => {
|
||||
state.sessionKey = buildAgentMainSessionKey({ agentId });
|
||||
? html`
|
||||
<div style="flex: 1; display: flex; flex-direction: column; min-width: 0; padding: 12px 16px 32px; overflow: hidden; gap: 24px;">
|
||||
|
||||
|
||||
${renderChat({
|
||||
sessionKey: state.sessionKey,
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.chatAttachments = [];
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatRunId = null;
|
||||
state.chatQueue = [];
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
void loadChatHistory(state);
|
||||
void refreshChatAvatar(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
showThinking,
|
||||
loading: state.chatLoading,
|
||||
sending: state.chatSending,
|
||||
compactionStatus: state.compactionStatus,
|
||||
fallbackStatus: state.fallbackStatus,
|
||||
assistantAvatarUrl: chatAvatarUrl,
|
||||
messages: state.chatMessages,
|
||||
toolMessages: state.chatToolMessages,
|
||||
streamSegments: state.chatStreamSegments,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
draft: state.chatMessage,
|
||||
queue: state.chatQueue,
|
||||
connected: state.connected,
|
||||
canSend: state.connected,
|
||||
disabledReason: chatDisabledReason,
|
||||
error: state.lastError,
|
||||
sessions: state.sessionsResult,
|
||||
focusMode: chatFocus,
|
||||
onRefresh: () => {
|
||||
state.resetToolStream();
|
||||
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
|
||||
},
|
||||
onToggleFocusMode: () => {
|
||||
if (state.onboarding) {
|
||||
return;
|
||||
}
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
chatFocusMode: !state.settings.chatFocusMode,
|
||||
});
|
||||
},
|
||||
onChatScroll: (event) => state.handleChatScroll(event),
|
||||
getDraft: () => state.chatMessage,
|
||||
onDraftChange: (next) => (state.chatMessage = next),
|
||||
onRequestUpdate: requestHostUpdate,
|
||||
attachments: state.chatAttachments,
|
||||
onAttachmentsChange: (next) => (state.chatAttachments = next),
|
||||
onSend: () => state.handleSendChat(),
|
||||
canAbort: Boolean(state.chatRunId),
|
||||
onAbort: () => void state.handleAbortChat(),
|
||||
onQueueRemove: (id) => state.removeQueuedMessage(id),
|
||||
onNewSession: () => state.handleSendChat("/new", { restoreDraft: true }),
|
||||
onClearHistory: async () => {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await state.client.request("sessions.reset", { key: state.sessionKey });
|
||||
state.chatMessages = [];
|
||||
state.chatStream = null;
|
||||
state.chatRunId = null;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: state.sessionKey,
|
||||
lastActiveSessionKey: state.sessionKey,
|
||||
});
|
||||
void loadChatHistory(state);
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
onNavigateToAgent: () => {
|
||||
state.agentsSelectedId = resolvedAgentId;
|
||||
state.setTab("agents" as import("./navigation.ts").Tab);
|
||||
},
|
||||
onSessionSelect: (key: string) => {
|
||||
state.setSessionKey(key);
|
||||
state.chatMessages = [];
|
||||
void loadChatHistory(state);
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
showNewMessages: state.chatNewMessagesBelow && !state.chatManualRefreshInFlight,
|
||||
onScrollToBottom: () => state.scrollToBottom(),
|
||||
// Sidebar props for tool output viewing
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
sidebarContent: state.sidebarContent,
|
||||
sidebarError: state.sidebarError,
|
||||
splitRatio: state.splitRatio,
|
||||
onOpenSidebar: (content: string) => state.handleOpenSidebar(content),
|
||||
onCloseSidebar: () => state.handleCloseSidebar(),
|
||||
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
||||
assistantName: state.assistantName,
|
||||
assistantAvatar: state.assistantAvatar,
|
||||
basePath: state.basePath ?? "",
|
||||
})
|
||||
await loadChatHistory(state);
|
||||
} catch (err) {
|
||||
state.lastError = String(err);
|
||||
}
|
||||
},
|
||||
agentsList: state.agentsList,
|
||||
currentAgentId: resolvedAgentId ?? "main",
|
||||
onAgentChange: (agentId: string) => {
|
||||
state.sessionKey = buildAgentMainSessionKey({ agentId });
|
||||
state.chatMessages = [];
|
||||
state.chatStream = null;
|
||||
state.chatRunId = null;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: state.sessionKey,
|
||||
lastActiveSessionKey: state.sessionKey,
|
||||
});
|
||||
void loadChatHistory(state);
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
onNavigateToAgent: () => {
|
||||
state.agentsSelectedId = resolvedAgentId;
|
||||
state.setTab("agents" as import("./navigation.ts").Tab);
|
||||
},
|
||||
onSessionSelect: (key: string) => {
|
||||
state.setSessionKey(key);
|
||||
state.chatMessages = [];
|
||||
void loadChatHistory(state);
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
showNewMessages: state.chatNewMessagesBelow && !state.chatManualRefreshInFlight,
|
||||
onScrollToBottom: () => state.scrollToBottom(),
|
||||
// Sidebar props for tool output viewing
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
sidebarContent: state.sidebarContent,
|
||||
sidebarError: state.sidebarError,
|
||||
splitRatio: state.splitRatio,
|
||||
onOpenSidebar: (content: string) => state.handleOpenSidebar(content),
|
||||
onCloseSidebar: () => state.handleCloseSidebar(),
|
||||
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
||||
assistantName: state.assistantName,
|
||||
assistantAvatar: state.assistantAvatar,
|
||||
basePath: state.basePath ?? "",
|
||||
})}
|
||||
</div>
|
||||
|
||||
${
|
||||
state.showClawComputer
|
||||
? html`
|
||||
<resizable-divider
|
||||
mode="pixels"
|
||||
orientation="vertical"
|
||||
.initialWidth=${state.clawComputerWidth}
|
||||
.minWidth=${400}
|
||||
.maxWidth=${900}
|
||||
@resize=${(e: CustomEvent) => {
|
||||
if (e.detail.width !== undefined) {
|
||||
state.setClawComputerWidth(e.detail.width);
|
||||
}
|
||||
}}
|
||||
></resizable-divider>
|
||||
<div class="claw-computer-panel" style="width: ${state.clawComputerWidth}px;">
|
||||
<div class="claw-computer-header">
|
||||
<span>🖥️ 远程桌面</span>
|
||||
<button class="claw-computer-close" @click=${() => state.toggleClawComputer()}>×</button>
|
||||
</div>
|
||||
<claw-computer-panel
|
||||
.vncUrl=${"127.0.0.1:5901"}
|
||||
.vncToken=${"123456"}
|
||||
></claw-computer-panel>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
|
||||
${
|
||||
state.tab === "config"
|
||||
? renderConfig({
|
||||
|
||||
@ -364,4 +364,8 @@ export type AppViewState = {
|
||||
handleOpenSidebar: (content: string) => void;
|
||||
handleCloseSidebar: () => void;
|
||||
handleSplitRatioChange: (ratio: number) => void;
|
||||
showClawComputer: boolean;
|
||||
toggleClawComputer: () => void;
|
||||
clawComputerWidth: number;
|
||||
setClawComputerWidth: (width: number) => void;
|
||||
};
|
||||
|
||||
236
ui/src/ui/components/claw-computer-panel.ts
Normal file
236
ui/src/ui/components/claw-computer-panel.ts
Normal file
@ -0,0 +1,236 @@
|
||||
// ui/src/ui/components/claw-computer-panel.ts
|
||||
import { LitElement, html, css } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { createRef, ref, Ref } from "lit/directives/ref.js";
|
||||
|
||||
interface RFBInstance {
|
||||
disconnect(): void;
|
||||
addEventListener(event: string, callback: (e?: unknown) => void): void;
|
||||
scaleViewport: boolean;
|
||||
clipViewport: boolean;
|
||||
resize?(): void;
|
||||
}
|
||||
|
||||
@customElement("claw-computer-panel")
|
||||
export class ClawComputerPanel extends LitElement {
|
||||
@state() status = "🖥️ Claw Computer 未配置";
|
||||
@state() isConnected = false;
|
||||
@state() isFitted = true;
|
||||
@state() password = "";
|
||||
|
||||
private rfb: RFBInstance | null = null;
|
||||
private RFBConstructor: unknown = null;
|
||||
private screenRef: Ref<HTMLDivElement> = createRef<HTMLDivElement>();
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #0a0a0a;
|
||||
color: #eee;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
}
|
||||
h2 {
|
||||
text-align: center;
|
||||
margin: 0 0 16px 0;
|
||||
color: #ddd;
|
||||
}
|
||||
.controls {
|
||||
background: #1a1a1a;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: #222;
|
||||
border: 1px solid #444;
|
||||
color: #eee;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 12px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
button {
|
||||
padding: 10px 16px;
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
background: #007fff;
|
||||
}
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.active {
|
||||
background: #0088ff !important;
|
||||
}
|
||||
.screen-container {
|
||||
flex: 1;
|
||||
min-height: 400px;
|
||||
background: #000;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.status {
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
min-height: 24px;
|
||||
}
|
||||
`;
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<h2>Claw Computer - VNC 遠端畫面</h2>
|
||||
<div class="controls">
|
||||
<label>VNC 密碼(如果有):</label>
|
||||
<input type="password" placeholder="留空 = 無密碼"
|
||||
.value=${this.password}
|
||||
@input=${(e: Event) => {
|
||||
this.password = (e.target as HTMLInputElement).value;
|
||||
}}
|
||||
@keydown=${this.handlePasswordKeydown} />
|
||||
<div class="btn-group">
|
||||
<button @click=${this.connect} ?disabled=${this.isConnected}>連接</button>
|
||||
<button @click=${this.disconnect} ?disabled=${!this.isConnected}>斷開</button>
|
||||
<button @click=${this.toggleFullscreen}>全螢幕</button>
|
||||
</div>
|
||||
<div style="margin-top:12px;">
|
||||
<strong>視窗縮放模式:</strong>
|
||||
<button class=${this.isFitted ? "active" : ""} @click=${() => this.setFitMode(true)}>適配視窗</button>
|
||||
<button class=${!this.isFitted ? "active" : ""} @click=${() => this.setFitMode(false)}>1:1 原尺寸</button>
|
||||
</div>
|
||||
<div class="status" style="color: ${this.isConnected ? "#4caf50" : "#aaa"}">
|
||||
${this.status}
|
||||
</div>
|
||||
</div>
|
||||
<div class="screen-container">
|
||||
<div ${ref(this.screenRef)} class="screen"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async loadRFB() {
|
||||
if (this.RFBConstructor) {
|
||||
return;
|
||||
}
|
||||
const module = await import("@novnc/novnc/lib/rfb.js");
|
||||
this.RFBConstructor = (module as unknown as { default?: unknown }).default || module;
|
||||
}
|
||||
|
||||
private connect = async () => {
|
||||
await this.loadRFB();
|
||||
|
||||
const url = "ws://localhost:8081";
|
||||
if (this.rfb) {
|
||||
this.rfb.disconnect();
|
||||
}
|
||||
|
||||
this.status = "正在連接...";
|
||||
|
||||
const screen = this.screenRef.value;
|
||||
if (!screen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const Constructor = this.RFBConstructor as new (
|
||||
target: HTMLElement,
|
||||
url: string,
|
||||
options?: unknown,
|
||||
) => RFBInstance;
|
||||
this.rfb = new Constructor(screen, url, {
|
||||
credentials: { password: this.password || undefined },
|
||||
resizeSession: true,
|
||||
clipViewport: true,
|
||||
scaleViewport: this.isFitted,
|
||||
});
|
||||
|
||||
this.rfb.addEventListener("connect", () => {
|
||||
this.isConnected = true;
|
||||
this.status = "已連線成功 ✓(改變視窗大小會自動適配)";
|
||||
setTimeout(() => this.rfb?.resize?.(), 100);
|
||||
});
|
||||
|
||||
this.rfb.addEventListener("disconnect", () => {
|
||||
this.isConnected = false;
|
||||
this.status = "連線中斷";
|
||||
this.rfb = null;
|
||||
});
|
||||
};
|
||||
|
||||
private disconnect = () => {
|
||||
if (this.rfb) {
|
||||
this.rfb.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
private setFitMode(fitted: boolean) {
|
||||
this.isFitted = fitted;
|
||||
if (this.rfb) {
|
||||
this.rfb.scaleViewport = fitted;
|
||||
this.rfb.clipViewport = true;
|
||||
setTimeout(() => this.rfb?.resize?.(), 50);
|
||||
}
|
||||
}
|
||||
|
||||
private handleResize = () => {
|
||||
if (this.rfb && this.isConnected) {
|
||||
setTimeout(() => this.rfb?.resize?.(), 80);
|
||||
}
|
||||
};
|
||||
|
||||
private toggleFullscreen = () => {
|
||||
const container = this.shadowRoot?.querySelector(".screen-container");
|
||||
if (container) {
|
||||
void (container as HTMLElement).requestFullscreen?.();
|
||||
}
|
||||
};
|
||||
|
||||
private handlePasswordKeydown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") {
|
||||
void this.connect();
|
||||
}
|
||||
};
|
||||
|
||||
firstUpdated() {
|
||||
window.addEventListener("resize", this.handleResize);
|
||||
void this.loadRFB();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this.handleResize);
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
33
ui/src/ui/server/proxy.ts
Normal file
33
ui/src/ui/server/proxy.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import http from "http";
|
||||
// server/proxy.ts
|
||||
import { WebSocket, WebSocketServer } from "ws";
|
||||
|
||||
const PORT = 8081;
|
||||
const TARGET_VNC = "ws://10.75.171.0:25900"; // ← 改成你的真實 VNC 位址
|
||||
|
||||
const server = http.createServer();
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
wss.on("connection", (clientWs) => {
|
||||
console.log("[Proxy] Client connected");
|
||||
|
||||
const targetWs = new WebSocket(TARGET_VNC);
|
||||
|
||||
targetWs.on("open", () => console.log("[Proxy] 已連線到真實 VNC"));
|
||||
|
||||
clientWs.on("message", (data) => targetWs.readyState === WebSocket.OPEN && targetWs.send(data));
|
||||
targetWs.on("message", (data) => clientWs.readyState === WebSocket.OPEN && clientWs.send(data));
|
||||
|
||||
const cleanup = () => {
|
||||
targetWs.close();
|
||||
clientWs.close();
|
||||
};
|
||||
clientWs.on("close", cleanup);
|
||||
targetWs.on("close", cleanup);
|
||||
clientWs.on("error", cleanup);
|
||||
targetWs.on("error", cleanup);
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`✅ noVNC Proxy 啟動成功 → ws://localhost:${PORT}`);
|
||||
});
|
||||
22
ui/src/ui/types/novnc.d.ts
vendored
Normal file
22
ui/src/ui/types/novnc.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
// novnc.d.ts
|
||||
declare module "@novnc/novnc/lib/rfb.js" {
|
||||
export default class RFB {
|
||||
constructor(target: HTMLElement, url: string, options?: unknown);
|
||||
addEventListener(event: string, callback: (e?: unknown) => void): void;
|
||||
disconnect(): void;
|
||||
scaleViewport: boolean;
|
||||
clipViewport: boolean;
|
||||
resize?(): void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@novnc/novnc" {
|
||||
export default class RFB {
|
||||
constructor(target: HTMLElement, url: string, options?: unknown);
|
||||
addEventListener(event: string, callback: (e?: unknown) => void): void;
|
||||
disconnect(): void;
|
||||
scaleViewport: boolean;
|
||||
clipViewport: boolean;
|
||||
resize?(): void;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user