CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | /**
* Generic WebviewViewProvider that embeds a Gluecron page in an iframe.
*
* We re-resolve the URL on every `resolveWebviewView` call so that
* workspace changes (different repo, different host) are picked up
* without a window reload.
*
* The chat view also passes `?embed=1` so the server can render a
* sidebar-friendly layout (no nav chrome).
*/
import * as vscode from "vscode";
import { getGluecronRepoInfo, hostUrl } from "../repo";
export type UrlBuilder = (repo: { owner: string; repo: string }) => string;
export class IframeView implements vscode.WebviewViewProvider {
constructor(
private readonly emptyState: string,
private readonly buildUrl: UrlBuilder
) {}
resolveWebviewView(
webviewView: vscode.WebviewView,
_ctx: vscode.WebviewViewResolveContext,
_token: vscode.CancellationToken
): void | Thenable<void> {
webviewView.webview.options = {
enableScripts: true,
enableForms: true,
enableCommandUris: false,
};
this.render(webviewView);
// Re-render on host changes (settings) — covers self-host switches.
const sub = vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration("gluecron.host")) {
this.render(webviewView);
}
});
webviewView.onDidDispose(() => sub.dispose());
}
private async render(view: vscode.WebviewView): Promise<void> {
const info = await getGluecronRepoInfo();
if (!info) {
view.webview.html = htmlShell(emptyHtml(this.emptyState));
return;
}
const url = this.buildUrl(info);
view.webview.html = htmlShell(iframeHtml(url));
}
}
export function chatViewProvider(): IframeView {
return new IframeView(
"Open a folder backed by a Gluecron repository to start chatting.",
({ owner, repo }) => hostUrl(`/${owner}/${repo}/chat?embed=1`)
);
}
export function pullsViewProvider(): IframeView {
return new IframeView(
"Open a Gluecron-hosted folder to see its pull requests.",
({ owner, repo }) => hostUrl(`/${owner}/${repo}/pulls?embed=1`)
);
}
export function issuesViewProvider(): IframeView {
return new IframeView(
"Open a Gluecron-hosted folder to see its issues.",
({ owner, repo }) => hostUrl(`/${owner}/${repo}/issues?embed=1`)
);
}
export function standupsViewProvider(): IframeView {
return new IframeView(
"Open a Gluecron-hosted folder to see AI standups.",
({ owner, repo }) => hostUrl(`/${owner}/${repo}/standups?embed=1`)
);
}
function htmlShell(body: string): string {
// Note: we deliberately allow `frame-src` to whatever HTTP host we
// embed, but we keep `default-src` tight so an injected script can't
// execute anything inside the webview shell itself.
return `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; frame-src http: https:;" />
<style>
html, body { margin: 0; padding: 0; height: 100%; background: var(--vscode-editor-background); color: var(--vscode-foreground); font-family: var(--vscode-font-family); }
iframe { width: 100%; height: 100%; border: 0; }
.empty { padding: 16px; font-size: 13px; line-height: 1.5; opacity: 0.8; }
</style>
</head>
<body>
${body}
</body>
</html>`;
}
function iframeHtml(url: string): string {
// Escape the URL for attribute context.
const safe = url.replace(/&/g, "&").replace(/"/g, """);
return `<iframe src="${safe}" sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"></iframe>`;
}
function emptyHtml(message: string): string {
const safe = message.replace(/&/g, "&").replace(/</g, "<");
return `<div class="empty">${safe}</div>`;
}
|