andy-stack_vaultkeeper-ai/Services/WebViewerService.ts
Andrew Beal 3bc2b34aa2 feat: add web viewer content retrieval tool for AI agents
Add new get_web_viewer_content tool that allows AI agents to retrieve
text content or screenshots from open web views. Refactor AIToolResponse
to use AIToolResponsePayload class for structured responses with attachment
support. Update tool service to handle binary file attachments consistently
across read_vault_files and new web viewer tool.
2026-04-08 21:00:23 +01:00

86 lines
No EOL
2.9 KiB
TypeScript

import type VaultkeeperAIPlugin from "main";
import { Resolve } from "./DependencyService";
import { Services } from "./Services";
import type { WebviewElement } from "Types/WebviewElement";
export class WebViewerService {
private readonly plugin: VaultkeeperAIPlugin;
public constructor() {
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
}
public async getWebViewContent(urlHint?: string): Promise<string | null> {
const webviewElement = this.getWebviewElement(urlHint);
if (!webviewElement) {
return null;
}
if (await this.waitForLoad(webviewElement)) {
const pageContent = await webviewElement.executeJavaScript(
'document.body.innerText'
);
return pageContent as string ?? "Failed to retrieve page content";
}
return "";
}
public async takeScreenshot(urlHint?: string, stripDataUrl: boolean = false): Promise<string | null> {
const webviewElement = this.getWebviewElement(urlHint);
if (!webviewElement) {
return null;
}
if (await this.waitForLoad(webviewElement)) {
const screenshot = await webviewElement.capturePage();
const dataUrl = screenshot.toDataURL();
return stripDataUrl ? dataUrl.split(',')[1] : dataUrl;
}
return null;
}
private getWebviewElement(urlHint?: string): WebviewElement | null {
const leaves = this.plugin.app.workspace.getLeavesOfType('webviewer');
if (leaves.length <= 0) {
return null;
}
let webviewElement: WebviewElement | null = null;
if (urlHint) {
for (const leaf of leaves) {
const element = leaf.view.containerEl.querySelector('webview');
if (this.isWebviewElement(element) && element.getURL().contains(urlHint)) {
webviewElement = element;
}
}
} else {
const leaf = leaves[0];
const element = leaf.view.containerEl.querySelector('webview');
if (this.isWebviewElement(element)) {
webviewElement = element;
}
}
return webviewElement;
}
private async waitForLoad(webviewElement: WebviewElement, timeoutMs = 10000): Promise<boolean> {
const start = Date.now();
while (webviewElement.isLoading()) {
if (Date.now() - start > timeoutMs) {
return false;
}
await new Promise(r => setTimeout(r, 200));
}
return true;
}
private isWebviewElement(element: Element | null): element is WebviewElement {
return element !== null
&& typeof (element as WebviewElement).isLoading === 'function'
&& typeof (element as WebviewElement).executeJavaScript === 'function';
}
}