Переглянути джерело

Fall back to js bundle when local webview app not running in development

cte 11 місяців тому
батько
коміт
2ad57ba057

+ 1 - 0
jest.config.js

@@ -34,6 +34,7 @@ module.exports = {
 	transformIgnorePatterns: [
 		"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)",
 	],
+	roots: ["<rootDir>/src", "<rootDir>/webview-ui/src"],
 	modulePathIgnorePatterns: [".vscode-test"],
 	reporters: [["jest-simple-dot-reporter", {}]],
 	setupFiles: [],

+ 1 - 1
src/activate/registerCommands.ts

@@ -75,7 +75,7 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterComman
 		dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
 	}
 
-	tabProvider.resolveWebviewView(panel)
+	await tabProvider.resolveWebviewView(panel)
 
 	// Lock the editor group so clicking on files doesn't open them over the panel
 	await delay(100)

+ 17 - 10
src/core/webview/ClineProvider.ts

@@ -256,11 +256,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		await visibleProvider.initClineWithTask(prompt)
 	}
 
-	resolveWebviewView(
-		webviewView: vscode.WebviewView | vscode.WebviewPanel,
-		//context: vscode.WebviewViewResolveContext<unknown>, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden
-		//token: vscode.CancellationToken
-	): void | Thenable<void> {
+	async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
 		this.outputChannel.appendLine("Resolving webview view")
 		this.view = webviewView
 
@@ -277,7 +273,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 		webviewView.webview.html =
 			this.context.extensionMode === vscode.ExtensionMode.Development
-				? this.getHMRHtmlContent(webviewView.webview)
+				? await this.getHMRHtmlContent(webviewView.webview)
 				: this.getHtmlContent(webviewView.webview)
 
 		// Sets up an event listener to listen for messages passed from the webview view context
@@ -402,9 +398,22 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		await this.view?.webview.postMessage(message)
 	}
 
-	private getHMRHtmlContent(webview: vscode.Webview): string {
-		const nonce = getNonce()
+	private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
+		const localPort = "5173"
+		const localServerUrl = `localhost:${localPort}`
+
+		// Check if local dev server is running.
+		try {
+			await axios.get(`http://${localServerUrl}`)
+		} catch (error) {
+			vscode.window.showErrorMessage(
+				"Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.",
+			)
+
+			return this.getHtmlContent(webview)
+		}
 
+		const nonce = getNonce()
 		const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
 		const codiconsUri = getUri(webview, this.context.extensionUri, [
 			"node_modules",
@@ -415,8 +424,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		])
 
 		const file = "src/index.tsx"
-		const localPort = "5173"
-		const localServerUrl = `localhost:${localPort}`
 		const scriptUri = `http://${localServerUrl}/${file}`
 
 		const reactRefresh = /*html*/ `

+ 49 - 28
src/core/webview/__tests__/ClineProvider.test.ts

@@ -1,13 +1,17 @@
-import { ClineProvider } from "../ClineProvider"
+// npx jest src/core/webview/__tests__/ClineProvider.test.ts
+
 import * as vscode from "vscode"
+import axios from "axios"
+
+import { ClineProvider } from "../ClineProvider"
 import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
 import { setSoundEnabled } from "../../../utils/sound"
-import { defaultModeSlug, modes } from "../../../shared/modes"
-import { addCustomInstructions } from "../../prompts/sections/custom-instructions"
-import { experimentDefault, experiments } from "../../../shared/experiments"
+import { defaultModeSlug } from "../../../shared/modes"
+import { experimentDefault } from "../../../shared/experiments"
 
 // Mock custom-instructions module
 const mockAddCustomInstructions = jest.fn()
+
 jest.mock("../../prompts/sections/custom-instructions", () => ({
 	addCustomInstructions: mockAddCustomInstructions,
 }))
@@ -202,7 +206,6 @@ describe("ClineProvider", () => {
 	let mockOutputChannel: vscode.OutputChannel
 	let mockWebviewView: vscode.WebviewView
 	let mockPostMessage: jest.Mock
-	let visibilityChangeCallback: (e?: unknown) => void
 
 	beforeEach(() => {
 		// Reset mocks
@@ -270,13 +273,13 @@ describe("ClineProvider", () => {
 				return { dispose: jest.fn() }
 			}),
 			onDidChangeVisibility: jest.fn().mockImplementation((callback) => {
-				visibilityChangeCallback = callback
 				return { dispose: jest.fn() }
 			}),
 		} as unknown as vscode.WebviewView
 
 		provider = new ClineProvider(mockContext, mockOutputChannel)
-		// @ts-ignore - accessing private property for testing
+
+		// @ts-ignore - Accessing private property for testing.
 		provider.customModesManager = mockCustomModesManager
 	})
 
@@ -288,18 +291,36 @@ describe("ClineProvider", () => {
 		expect(ClineProvider.getVisibleInstance()).toBe(provider)
 	})
 
-	test("resolveWebviewView sets up webview correctly", () => {
-		provider.resolveWebviewView(mockWebviewView)
+	test("resolveWebviewView sets up webview correctly", async () => {
+		await provider.resolveWebviewView(mockWebviewView)
 
 		expect(mockWebviewView.webview.options).toEqual({
 			enableScripts: true,
 			localResourceRoots: [mockContext.extensionUri],
 		})
+
+		expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
+	})
+
+	test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => {
+		provider = new ClineProvider(
+			{ ...mockContext, extensionMode: vscode.ExtensionMode.Development },
+			mockOutputChannel,
+		)
+		;(axios.get as jest.Mock).mockRejectedValueOnce(new Error("Network error"))
+
+		await provider.resolveWebviewView(mockWebviewView)
+
+		expect(mockWebviewView.webview.options).toEqual({
+			enableScripts: true,
+			localResourceRoots: [mockContext.extensionUri],
+		})
+
 		expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
 	})
 
 	test("postMessageToWebview sends message to webview", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 
 		const mockState: ExtensionState = {
 			version: "1.0.0",
@@ -341,7 +362,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("handles webviewDidLaunch message", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 
 		// Get the message handler from onDidReceiveMessage
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
@@ -420,7 +441,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("handles writeDelayMs message", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		await messageHandler({ type: "writeDelayMs", value: 2000 })
@@ -430,7 +451,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("updates sound utility when sound setting changes", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 
 		// Get the message handler from onDidReceiveMessage
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
@@ -470,7 +491,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("loads saved API config when switching modes", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		// Mock ConfigManager methods
@@ -491,7 +512,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("saves current config when switching to mode without config", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		// Mock ConfigManager methods
@@ -519,7 +540,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("saves config as default for current mode when loading config", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		provider.configManager = {
@@ -540,7 +561,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("handles request delay settings messages", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		// Test alwaysApproveResubmit
@@ -555,7 +576,7 @@ describe("ClineProvider", () => {
 	})
 
 	test("handles updatePrompt message correctly", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		// Mock existing prompts
@@ -650,7 +671,7 @@ describe("ClineProvider", () => {
 		)
 	})
 	test("handles mode-specific custom instructions updates", async () => {
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		// Mock existing prompts
@@ -707,7 +728,7 @@ describe("ClineProvider", () => {
 
 		// Create new provider with updated mock context
 		provider = new ClineProvider(mockContext, mockOutputChannel)
-		provider.resolveWebviewView(mockWebviewView)
+		await provider.resolveWebviewView(mockWebviewView)
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		provider.configManager = {
@@ -732,10 +753,10 @@ describe("ClineProvider", () => {
 	})
 
 	describe("deleteMessage", () => {
-		beforeEach(() => {
+		beforeEach(async () => {
 			// Mock window.showInformationMessage
 			;(vscode.window.showInformationMessage as jest.Mock) = jest.fn()
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 		})
 
 		test('handles "Just this message" deletion correctly', async () => {
@@ -861,9 +882,9 @@ describe("ClineProvider", () => {
 	})
 
 	describe("getSystemPrompt", () => {
-		beforeEach(() => {
+		beforeEach(async () => {
 			mockPostMessage.mockClear()
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 			// Reset and setup mock
 			mockAddCustomInstructions.mockClear()
 			mockAddCustomInstructions.mockImplementation(
@@ -1111,7 +1132,7 @@ describe("ClineProvider", () => {
 			})
 
 			// Resolve webview and trigger getSystemPrompt
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 			const architectHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 			await architectHandler({ type: "getSystemPrompt" })
 
@@ -1125,9 +1146,9 @@ describe("ClineProvider", () => {
 	})
 
 	describe("handleModeSwitch", () => {
-		beforeEach(() => {
+		beforeEach(async () => {
 			// Set up webview for each test
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 		})
 
 		test("loads saved API config when switching modes", async () => {
@@ -1188,7 +1209,7 @@ describe("ClineProvider", () => {
 
 	describe("updateCustomMode", () => {
 		test("updates both file and state when updating custom mode", async () => {
-			provider.resolveWebviewView(mockWebviewView)
+			await provider.resolveWebviewView(mockWebviewView)
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 			// Mock CustomModesManager methods

+ 1 - 1
src/test/task.test.ts

@@ -31,7 +31,7 @@ suite("Roo Code Task", () => {
 
 		try {
 			// Initialize provider with panel.
-			provider.resolveWebviewView(panel)
+			await provider.resolveWebviewView(panel)
 
 			// Wait for webview to launch.
 			let startTime = Date.now()