Compare commits

...
Author SHA1 Message Date
Sarah Fortune 3fc5626e98 Fix webview on IntelliJ
The ExternalWebviewProvider has to return /something/ for `getWebview()` or
the rest of the code thinks that is not set up and it won't generate the HTML
for IntelliJ.

The Vscode webview panel, `resolveWebview()` and other Vscode specific parts are
planned to be moved out of the WebviewProvider and into VscodeWebviewProvider,
but that depends other changes to how the webview is initialized in extension.ts
to need to happen first.

Move the WebviewProvider out of index.ts and into a file name `WebviewProvider`,
this follows best practises.
2025-08-08 13:15:00 +01:00
4 changed files with 361 additions and 362 deletions
+357
View File
@@ -0,0 +1,357 @@
import axios from "axios"
import * as vscode from "vscode"
import { getNonce } from "./getNonce"
import { WebviewProviderType } from "@/shared/webview/types"
import { Controller } from "@core/controller/index"
import { findLast } from "@shared/array"
import { readFile } from "fs/promises"
import path from "node:path"
import { v4 as uuidv4 } from "uuid"
import { Uri } from "vscode"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
export abstract class WebviewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
private static activeInstances: Set<WebviewProvider> = new Set()
private static clientIdMap = new Map<WebviewProvider, string>()
controller: Controller
private clientId: string
private static lastActiveControllerId: string | null = null
constructor(
readonly context: vscode.ExtensionContext,
private readonly providerType: WebviewProviderType,
) {
WebviewProvider.activeInstances.add(this)
this.clientId = uuidv4()
WebviewProvider.clientIdMap.set(this, this.clientId)
// Create controller with cache service
this.controller = new Controller(context, this.clientId)
WebviewProvider.setLastActiveControllerId(this.controller.id)
}
// Add a method to get the client ID
public getClientId(): string {
return this.clientId
}
// Add a static method to get the client ID for a specific instance
public static getClientIdForInstance(instance: WebviewProvider): string | undefined {
return WebviewProvider.clientIdMap.get(instance)
}
async dispose() {
await this.controller.dispose()
WebviewProvider.activeInstances.delete(this)
// Remove from client ID map
WebviewProvider.clientIdMap.delete(this)
}
public static getVisibleInstance(): WebviewProvider | undefined {
return findLast(Array.from(WebviewProvider.activeInstances), (instance) => instance.isVisible() === true)
}
public static getActiveInstance(): WebviewProvider | undefined {
return Array.from(WebviewProvider.activeInstances).find((instance) => {
const webview = instance.getWebview()
if (webview && webview.viewType === "claude-dev.TabPanelProvider" && "active" in webview) {
return webview.active === true
}
return false
})
}
public static getAllInstances(): WebviewProvider[] {
return Array.from(WebviewProvider.activeInstances)
}
public static getSidebarInstance() {
return Array.from(WebviewProvider.activeInstances).find(
(instance) => instance.providerType === WebviewProviderType.SIDEBAR,
)
}
public static getTabInstances(): WebviewProvider[] {
return Array.from(WebviewProvider.activeInstances).filter((instance) => instance.providerType === WebviewProviderType.TAB)
}
public static getLastActiveInstance(): WebviewProvider | undefined {
const lastActiveId = WebviewProvider.getLastActiveControllerId()
if (!lastActiveId) {
return undefined
}
return Array.from(WebviewProvider.activeInstances).find((instance) => instance.controller.id === lastActiveId)
}
/**
* Gets the last active controller ID with performance optimization
* @returns The last active controller ID or null
*/
public static getLastActiveControllerId(): string | null {
return WebviewProvider.lastActiveControllerId || WebviewProvider.getSidebarInstance()?.controller.id || null
}
/**
* Sets the last active controller ID with validation and performance optimization
* @param controllerId The controller ID to set as last active
*/
public static setLastActiveControllerId(controllerId: string | null): void {
// Only update if the value is actually different to avoid unnecessary operations
if (WebviewProvider.lastActiveControllerId !== controllerId) {
WebviewProvider.lastActiveControllerId = controllerId
}
}
public static async disposeAllInstances() {
const instances = Array.from(WebviewProvider.activeInstances)
for (const instance of instances) {
await instance.dispose()
}
}
/**
* Initializes and sets up the webview when it's first created.
*
* @param webviewView - The webview view or panel instance to be resolved
* @returns A promise that resolves when the webview has been fully initialized
*/
abstract resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): Promise<void>
/**
* Gets the current webview instance.
*
* @returns The webview instance (WebviewView, WebviewPanel, or similar)
*/
abstract getWebview(): any
/**
* Converts a local URI to a webview URI that can be used within the webview.
*
* @param uri - The local URI to convert
* @returns A URI that can be used within the webview
*/
abstract getWebviewUri(uri: Uri): Uri
/**
* Gets the Content Security Policy source for the webview.
*
* @returns The CSP source string to be used in the webview's Content-Security-Policy
*/
abstract getCspSource(): string
/**
* Checks if the webview is currently visible to the user.
*
* @returns True if the webview is visible, false otherwise
*/
abstract isVisible(): boolean
/**
* Defines and returns the HTML that should be rendered within the webview panel.
*
* @remarks This is also the place where references to the React webview build files
* are created and inserted into the webview HTML.
*
* @param webview A reference to the extension webview
* @param extensionUri The URI of the directory containing the extension
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
public getHtmlContent(): string {
// Get the local path to main script run in the webview,
// then convert it to a uri we can use in the webview.
// The CSS file from the React build output
const stylesUri = this.getExtensionUri("webview-ui", "build", "assets", "index.css")
// The JS file from the React build output
const scriptUri = this.getExtensionUri("webview-ui", "build", "assets", "index.js")
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
// 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
// don't forget to add font-src ${webview.cspSource};
const codiconsUri = this.getExtensionUri("node_modules", "@vscode", "codicons", "dist", "codicon.css")
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
// const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css"))
// // Same for stylesheet
// const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css"))
// Use a nonce to only allow a specific script to be run.
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
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 explicitly allow for these resources. E.g.
<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}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
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.
*/
const nonce = getNonce()
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
connect-src https://*.posthog.com https://*.cline.bot https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com;
font-src ${this.getCspSource()} data:;
style-src ${this.getCspSource()} 'unsafe-inline';
img-src ${this.getCspSource()} https: data:;
script-src 'nonce-${nonce}' 'unsafe-eval';">
<title>Cline</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
<script src="http://localhost:8097"></script>
</body>
</html>
`
}
/**
* Reads the Vite dev server port from the generated port file to avoid conflicts
* Returns a Promise that resolves to the port number
* If the file doesn't exist or can't be read, it resolves to the default port
*/
private getDevServerPort(): Promise<number> {
const DEFAULT_PORT = 25463
const portFilePath = path.join(__dirname, "..", "webview-ui", ".vite-port")
return readFile(portFilePath, "utf8")
.then((portFile) => {
const port = parseInt(portFile.trim()) || DEFAULT_PORT
console.info(`[getDevServerPort] Using dev server port ${port} from .vite-port file`)
return port
})
.catch((err) => {
console.warn(
`[getDevServerPort] Port file not found or couldn't be read at ${portFilePath}, using default port: ${DEFAULT_PORT}`,
)
return DEFAULT_PORT
})
}
/**
* Connects to the local Vite dev server to allow HMR, with fallback to the bundled assets
*
* @param webview A reference to the extension webview
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
protected async getHMRHtmlContent(): Promise<string> {
const localPort = await this.getDevServerPort()
const localServerUrl = `localhost:${localPort}`
// Check if local dev server is running.
try {
await axios.get(`http://${localServerUrl}`)
} catch (error) {
// Only show the error message when in development mode.
if (process.env.IS_DEV) {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message:
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
})
}
return this.getHtmlContent()
}
const nonce = getNonce()
const stylesUri = this.getExtensionUri("webview-ui", "build", "assets", "index.css")
const codiconsUri = this.getExtensionUri("node_modules", "@vscode", "codicons", "dist", "codicon.css")
const scriptEntrypoint = "src/main.tsx"
const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
const reactRefresh = /*html*/ `
<script nonce="${nonce}" type="module">
import RefreshRuntime from "http://${localServerUrl}/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
`
const csp = [
"default-src 'none'",
`font-src ${this.getCspSource()}`,
`style-src ${this.getCspSource()} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${this.getCspSource()} https: data:`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
${process.env.IS_DEV ? '<script src="http://localhost:8097"></script>' : ""}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Cline</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
${reactRefresh}
<script type="module" src="${scriptUri}"></script>
</body>
</html>
`
}
/**
* A helper function which will get the webview URI of a given file or resource in the extension directory.
*
* @remarks This URI can be used within a webview's HTML as a link to the
* given file/resource.
*
* @param pathList An array of strings representing the path to a file/resource in the extension directory.
* @returns A URI pointing to the file/resource
*/
private getExtensionUri(...pathList: string[]): Uri {
if (!this.getWebview()) {
throw Error("webview is not initialized.")
}
return this.getWebviewUri(Uri.joinPath(this.context.extensionUri, ...pathList))
}
}
+1 -358
View File
@@ -1,358 +1 @@
import axios from "axios"
import * as vscode from "vscode"
import { getNonce } from "./getNonce"
import { WebviewProviderType } from "@/shared/webview/types"
import { Controller } from "@core/controller/index"
import { findLast } from "@shared/array"
import { readFile } from "fs/promises"
import path from "node:path"
import { v4 as uuidv4 } from "uuid"
import { Uri } from "vscode"
import { ExtensionMessage } from "@/shared/ExtensionMessage"
import { HostProvider } from "@/hosts/host-provider"
import { ShowMessageType } from "@/shared/proto/host/window"
export abstract class WebviewProvider {
public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
public static readonly tabPanelId = "claude-dev.TabPanelProvider"
private static activeInstances: Set<WebviewProvider> = new Set()
private static clientIdMap = new Map<WebviewProvider, string>()
controller: Controller
private clientId: string
private static lastActiveControllerId: string | null = null
constructor(
readonly context: vscode.ExtensionContext,
private readonly providerType: WebviewProviderType,
) {
WebviewProvider.activeInstances.add(this)
this.clientId = uuidv4()
WebviewProvider.clientIdMap.set(this, this.clientId)
// Create controller with cache service
this.controller = new Controller(context, this.clientId)
WebviewProvider.setLastActiveControllerId(this.controller.id)
}
// Add a method to get the client ID
public getClientId(): string {
return this.clientId
}
// Add a static method to get the client ID for a specific instance
public static getClientIdForInstance(instance: WebviewProvider): string | undefined {
return WebviewProvider.clientIdMap.get(instance)
}
async dispose() {
await this.controller.dispose()
WebviewProvider.activeInstances.delete(this)
// Remove from client ID map
WebviewProvider.clientIdMap.delete(this)
}
public static getVisibleInstance(): WebviewProvider | undefined {
return findLast(Array.from(WebviewProvider.activeInstances), (instance) => instance.isVisible() === true)
}
public static getActiveInstance(): WebviewProvider | undefined {
return Array.from(WebviewProvider.activeInstances).find((instance) => {
const webview = instance.getWebview()
if (webview && webview.viewType === "claude-dev.TabPanelProvider" && "active" in webview) {
return webview.active === true
}
return false
})
}
public static getAllInstances(): WebviewProvider[] {
return Array.from(WebviewProvider.activeInstances)
}
public static getSidebarInstance() {
return Array.from(WebviewProvider.activeInstances).find(
(instance) => instance.providerType === WebviewProviderType.SIDEBAR,
)
}
public static getTabInstances(): WebviewProvider[] {
return Array.from(WebviewProvider.activeInstances).filter((instance) => instance.providerType === WebviewProviderType.TAB)
}
public static getLastActiveInstance(): WebviewProvider | undefined {
const lastActiveId = WebviewProvider.getLastActiveControllerId()
if (!lastActiveId) {
return undefined
}
return Array.from(WebviewProvider.activeInstances).find((instance) => instance.controller.id === lastActiveId)
}
/**
* Gets the last active controller ID with performance optimization
* @returns The last active controller ID or null
*/
public static getLastActiveControllerId(): string | null {
return WebviewProvider.lastActiveControllerId || WebviewProvider.getSidebarInstance()?.controller.id || null
}
/**
* Sets the last active controller ID with validation and performance optimization
* @param controllerId The controller ID to set as last active
*/
public static setLastActiveControllerId(controllerId: string | null): void {
// Only update if the value is actually different to avoid unnecessary operations
if (WebviewProvider.lastActiveControllerId !== controllerId) {
WebviewProvider.lastActiveControllerId = controllerId
}
}
public static async disposeAllInstances() {
const instances = Array.from(WebviewProvider.activeInstances)
for (const instance of instances) {
await instance.dispose()
}
}
/**
* Initializes and sets up the webview when it's first created.
*
* @param webviewView - The webview view or panel instance to be resolved
* @returns A promise that resolves when the webview has been fully initialized
*/
abstract resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): Promise<void>
/**
* Gets the current webview instance.
*
* @returns The webview instance (WebviewView, WebviewPanel, or similar)
*/
abstract getWebview(): vscode.WebviewPanel | vscode.WebviewView | undefined
/**
* Converts a local URI to a webview URI that can be used within the webview.
*
* @param uri - The local URI to convert
* @returns A URI that can be used within the webview
*/
abstract getWebviewUri(uri: Uri): Uri
/**
* Gets the Content Security Policy source for the webview.
*
* @returns The CSP source string to be used in the webview's Content-Security-Policy
*/
abstract getCspSource(): string
/**
* Checks if the webview is currently visible to the user.
*
* @returns True if the webview is visible, false otherwise
*/
abstract isVisible(): boolean
/**
* Defines and returns the HTML that should be rendered within the webview panel.
*
* @remarks This is also the place where references to the React webview build files
* are created and inserted into the webview HTML.
*
* @param webview A reference to the extension webview
* @param extensionUri The URI of the directory containing the extension
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
public getHtmlContent(): string {
// Get the local path to main script run in the webview,
// then convert it to a uri we can use in the webview.
// The CSS file from the React build output
const stylesUri = this.getExtensionUri("webview-ui", "build", "assets", "index.css")
// The JS file from the React build output
const scriptUri = this.getExtensionUri("webview-ui", "build", "assets", "index.js")
// The codicon font from the React build output
// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
// 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
// don't forget to add font-src ${webview.cspSource};
const codiconsUri = this.getExtensionUri("node_modules", "@vscode", "codicons", "dist", "codicon.css")
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
// const styleVSCodeUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "vscode.css"))
// // Same for stylesheet
// const stylesheetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.css"))
// Use a nonce to only allow a specific script to be run.
/*
content security policy of your webview to only allow scripts that have a specific nonce
create a content security policy meta tag so that only loading scripts with a nonce is allowed
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 explicitly allow for these resources. E.g.
<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}';">
- 'unsafe-inline' is required for styles due to vscode-webview-toolkit's dynamic style injection
- since we pass base64 images to the webview, we need to specify img-src ${webview.cspSource} data:;
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.
*/
const nonce = getNonce()
// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<meta http-equiv="Content-Security-Policy" content="default-src 'none';
connect-src https://*.posthog.com https://*.cline.bot https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com;
font-src ${this.getCspSource()} data:;
style-src ${this.getCspSource()} 'unsafe-inline';
img-src ${this.getCspSource()} https: data:;
script-src 'nonce-${nonce}' 'unsafe-eval';">
<title>Cline</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
<script src="http://localhost:8097"></script>
</body>
</html>
`
}
/**
* Reads the Vite dev server port from the generated port file to avoid conflicts
* Returns a Promise that resolves to the port number
* If the file doesn't exist or can't be read, it resolves to the default port
*/
private getDevServerPort(): Promise<number> {
const DEFAULT_PORT = 25463
const portFilePath = path.join(__dirname, "..", "webview-ui", ".vite-port")
return readFile(portFilePath, "utf8")
.then((portFile) => {
const port = parseInt(portFile.trim()) || DEFAULT_PORT
console.info(`[getDevServerPort] Using dev server port ${port} from .vite-port file`)
return port
})
.catch((err) => {
console.warn(
`[getDevServerPort] Port file not found or couldn't be read at ${portFilePath}, using default port: ${DEFAULT_PORT}`,
)
return DEFAULT_PORT
})
}
/**
* Connects to the local Vite dev server to allow HMR, with fallback to the bundled assets
*
* @param webview A reference to the extension webview
* @returns A template string literal containing the HTML that should be
* rendered within the webview panel
*/
protected async getHMRHtmlContent(): Promise<string> {
const localPort = await this.getDevServerPort()
const localServerUrl = `localhost:${localPort}`
// Check if local dev server is running.
try {
await axios.get(`http://${localServerUrl}`)
} catch (error) {
// Only show the error message when in development mode.
if (process.env.IS_DEV) {
HostProvider.window.showMessage({
type: ShowMessageType.ERROR,
message:
"Cline: Local webview dev server is not running, HMR will not work. Please run 'npm run dev:webview' before launching the extension to enable HMR. Using bundled assets.",
})
}
return this.getHtmlContent()
}
const nonce = getNonce()
const stylesUri = this.getExtensionUri("webview-ui", "build", "assets", "index.css")
const codiconsUri = this.getExtensionUri("node_modules", "@vscode", "codicons", "dist", "codicon.css")
const scriptEntrypoint = "src/main.tsx"
const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
const reactRefresh = /*html*/ `
<script nonce="${nonce}" type="module">
import RefreshRuntime from "http://${localServerUrl}/@react-refresh"
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
`
const csp = [
"default-src 'none'",
`font-src ${this.getCspSource()}`,
`style-src ${this.getCspSource()} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${this.getCspSource()} https: data:`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]
return /*html*/ `
<!DOCTYPE html>
<html lang="en">
<head>
${process.env.IS_DEV ? '<script src="http://localhost:8097"></script>' : ""}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Cline</title>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" nonce="${nonce}">
// Inject the provider type
window.WEBVIEW_PROVIDER_TYPE = ${JSON.stringify(this.providerType)};
// Inject the client ID
window.clineClientId = "${this.clientId}";
</script>
${reactRefresh}
<script type="module" src="${scriptUri}"></script>
</body>
</html>
`
}
/**
* A helper function which will get the webview URI of a given file or resource in the extension directory.
*
* @remarks This URI can be used within a webview's HTML as a link to the
* given file/resource.
*
* @param pathList An array of strings representing the path to a file/resource in the extension directory.
* @returns A URI pointing to the file/resource
*/
private getExtensionUri(...pathList: string[]): Uri {
if (!this.getWebview()) {
throw Error("webview is not initialized.")
}
return this.getWebviewUri(Uri.joinPath(this.context.extensionUri, ...pathList))
}
}
export { WebviewProvider } from "./WebviewProvider"
+2 -3
View File
@@ -1,8 +1,7 @@
import { ExtensionMessage } from "@/shared/ExtensionMessage"
import { WebviewProviderType } from "@/shared/webview/types"
import { WebviewProvider } from "@/core/webview"
import * as vscode from "vscode"
import { URI } from "vscode-uri"
import { WebviewProvider } from "@core/webview"
export class ExternalWebviewProvider extends WebviewProvider {
// This hostname cannot be changed without updating the external webview handler.
@@ -25,7 +24,7 @@ export class ExternalWebviewProvider extends WebviewProvider {
return true
}
override getWebview() {
return undefined
return {}
}
override resolveWebviewView(_: any): Promise<void> {
+1 -1
View File
@@ -1,4 +1,4 @@
import { WebviewProvider } from "@core/webview"
import { WebviewProvider } from "@/core/webview"
import { HostBridgeClientProvider } from "./host-provider-types"
import { WebviewProviderType } from "@/shared/webview/types"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"