Skip to content

Commit e70954f

Browse files
authored
♻️ refactor(webview): move webview HTML generation to WebviewHTMLManager (#2494)
- 【What】Move the logic for generating webview HTML content from ClineProvider to a new WebviewHTMLManager class. - 【Why】This improves code organization and maintainability by separating concerns related to webview HTML generation. - 【Why】This allows for easier testing and modification of the HTML generation logic without affecting the ClineProvider class.
1 parent 405e599 commit e70954f

File tree

2 files changed

+186
-186
lines changed

2 files changed

+186
-186
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 6 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { supportPrompt } from "../../shared/support-prompt"
2828
import { GlobalFileNames } from "../../shared/globalFileNames"
2929
import { HistoryItem } from "../../shared/HistoryItem"
3030
import { ExtensionMessage } from "../../shared/ExtensionMessage"
31-
import { Mode, PromptComponent, defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
31+
import { Mode, PromptComponent, defaultModeSlug } from "../../shared/modes"
3232
import { experimentDefault } from "../../shared/experiments"
3333
import { formatLanguage } from "../../shared/language"
3434
import { Terminal, TERMINAL_SHELL_INTEGRATION_TIMEOUT } from "../../integrations/terminal/Terminal"
@@ -47,8 +47,7 @@ import { CustomModesManager } from "../config/CustomModesManager"
4747
import { buildApiHandler } from "../../api"
4848
import { ACTION_NAMES } from "../CodeActionProvider"
4949
import { Cline, ClineOptions } from "../Cline"
50-
import { getNonce } from "./getNonce"
51-
import { getUri } from "./getUri"
50+
import { WebviewHTMLManager } from "./WebviewHTMLManager"
5251
import { telemetryService } from "../../services/telemetry/TelemetryService"
5352
import { getWorkspacePath } from "../../utils/path"
5453
import { webviewMessageHandler } from "./webviewMessageHandler"
@@ -82,6 +81,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
8281
public readonly contextProxy: ContextProxy
8382
public readonly providerSettingsManager: ProviderSettingsManager
8483
public readonly customModesManager: CustomModesManager
84+
private readonly webviewHTMLManager: WebviewHTMLManager
8585

8686
constructor(
8787
readonly context: vscode.ExtensionContext,
@@ -92,6 +92,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
9292

9393
this.log("ClineProvider instantiated")
9494
this.contextProxy = new ContextProxy(context)
95+
this.webviewHTMLManager = new WebviewHTMLManager(this.contextProxy)
9596
ClineProvider.activeInstances.add(this)
9697

9798
// Register this provider with the telemetry service to enable it to add
@@ -374,8 +375,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
374375

375376
webviewView.webview.html =
376377
this.contextProxy.extensionMode === vscode.ExtensionMode.Development
377-
? await this.getHMRHtmlContent(webviewView.webview)
378-
: this.getHtmlContent(webviewView.webview)
378+
? await this.webviewHTMLManager.getHMRHtmlContent(webviewView.webview)
379+
: this.webviewHTMLManager.getHtmlContent(webviewView.webview)
379380

380381
// Sets up an event listener to listen for messages passed from the webview view context
381382
// and executes code based on the message that is recieved
@@ -578,187 +579,6 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
578579
await this.view?.webview.postMessage(message)
579580
}
580581

581-
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
582-
// Try to read the port from the file
583-
let localPort = "5173" // Default fallback
584-
try {
585-
const fs = require("fs")
586-
const path = require("path")
587-
const portFilePath = path.resolve(__dirname, "../.vite-port")
588-
589-
if (fs.existsSync(portFilePath)) {
590-
localPort = fs.readFileSync(portFilePath, "utf8").trim()
591-
console.log(`[ClineProvider:Vite] Using Vite server port from ${portFilePath}: ${localPort}`)
592-
} else {
593-
console.log(
594-
`[ClineProvider:Vite] Port file not found at ${portFilePath}, using default port: ${localPort}`,
595-
)
596-
}
597-
} catch (err) {
598-
console.error("[ClineProvider:Vite] Failed to read Vite port file:", err)
599-
// Continue with default port if file reading fails
600-
}
601-
602-
const localServerUrl = `localhost:${localPort}`
603-
604-
// Check if local dev server is running.
605-
try {
606-
await axios.get(`http://${localServerUrl}`)
607-
} catch (error) {
608-
vscode.window.showErrorMessage(t("common:errors.hmr_not_running"))
609-
610-
return this.getHtmlContent(webview)
611-
}
612-
613-
const nonce = getNonce()
614-
615-
const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
616-
"webview-ui",
617-
"build",
618-
"assets",
619-
"index.css",
620-
])
621-
622-
const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
623-
"node_modules",
624-
"@vscode",
625-
"codicons",
626-
"dist",
627-
"codicon.css",
628-
])
629-
630-
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
631-
632-
const file = "src/index.tsx"
633-
const scriptUri = `http://${localServerUrl}/${file}`
634-
635-
const reactRefresh = /*html*/ `
636-
<script nonce="${nonce}" type="module">
637-
import RefreshRuntime from "http://localhost:${localPort}/@react-refresh"
638-
RefreshRuntime.injectIntoGlobalHook(window)
639-
window.$RefreshReg$ = () => {}
640-
window.$RefreshSig$ = () => (type) => type
641-
window.__vite_plugin_react_preamble_installed__ = true
642-
</script>
643-
`
644-
645-
const csp = [
646-
"default-src 'none'",
647-
`font-src ${webview.cspSource}`,
648-
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
649-
`img-src ${webview.cspSource} data:`,
650-
`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
651-
`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
652-
]
653-
654-
return /*html*/ `
655-
<!DOCTYPE html>
656-
<html lang="en">
657-
<head>
658-
<meta charset="utf-8">
659-
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
660-
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
661-
<link rel="stylesheet" type="text/css" href="${stylesUri}">
662-
<link href="${codiconsUri}" rel="stylesheet" />
663-
<script nonce="${nonce}">
664-
window.IMAGES_BASE_URI = "${imagesUri}"
665-
</script>
666-
<title>Roo Code</title>
667-
</head>
668-
<body>
669-
<div id="root"></div>
670-
${reactRefresh}
671-
<script type="module" src="${scriptUri}"></script>
672-
</body>
673-
</html>
674-
`
675-
}
676-
677-
/**
678-
* Defines and returns the HTML that should be rendered within the webview panel.
679-
*
680-
* @remarks This is also the place where references to the React webview build files
681-
* are created and inserted into the webview HTML.
682-
*
683-
* @param webview A reference to the extension webview
684-
* @param extensionUri The URI of the directory containing the extension
685-
* @returns A template string literal containing the HTML that should be
686-
* rendered within the webview panel
687-
*/
688-
private getHtmlContent(webview: vscode.Webview): string {
689-
// Get the local path to main script run in the webview,
690-
// then convert it to a uri we can use in the webview.
691-
692-
// The CSS file from the React build output
693-
const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
694-
"webview-ui",
695-
"build",
696-
"assets",
697-
"index.css",
698-
])
699-
// The JS file from the React build output
700-
const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
701-
702-
// The codicon font from the React build output
703-
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
704-
// we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
705-
// don't forget to add font-src ${webview.cspSource};
706-
const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
707-
"node_modules",
708-
"@vscode",
709-
"codicons",
710-
"dist",
711-
"codicon.css",
712-
])
713-
714-
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
715-
716-
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
717-
718-
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
719-
// const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css"))
720-
721-
// // Same for stylesheet
722-
// const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css"))
723-
724-
// Use a nonce to only allow a specific script to be run.
725-
/*
726-
content security policy of your webview to only allow scripts that have a specific nonce
727-
create a content security policy meta tag so that only loading scripts with a nonce is allowed
728-
As your extension grows you will likely want to add custom styles, fonts, and/or images to your webview. If you do, you will need to update the content security policy meta tag to explicity allow for these resources. E.g.
729-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${webview.cspSource}; font-src ${webview.cspSource}; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}';">
730-
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
731-
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
732-
733-
in meta tag we add nonce attribute: A cryptographic nonce (only used once) to allow scripts. The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.
734-
*/
735-
const nonce = getNonce()
736-
737-
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
738-
return /*html*/ `
739-
<!DOCTYPE html>
740-
<html lang="en">
741-
<head>
742-
<meta charset="utf-8">
743-
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
744-
<meta name="theme-color" content="#000000">
745-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}' https://us-assets.i.posthog.com; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
746-
<link rel="stylesheet" type="text/css" href="${stylesUri}">
747-
<link href="${codiconsUri}" rel="stylesheet" />
748-
<script nonce="${nonce}">
749-
window.IMAGES_BASE_URI = "${imagesUri}"
750-
</script>
751-
<title>Roo Code</title>
752-
</head>
753-
<body>
754-
<noscript>You need to enable JavaScript to run this app.</noscript>
755-
<div id="root"></div>
756-
<script nonce="${nonce}" type="module" src="${scriptUri}"></script>
757-
</body>
758-
</html>
759-
`
760-
}
761-
762582
/**
763583
* Sets up an event listener to listen for messages passed from the webview context and
764584
* executes code based on the message that is recieved.

0 commit comments

Comments
 (0)