Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

iframe-view.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

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