Merge pull request #757 from RooVetGit/cte/hmr-dx

Fall back to js bundle when local webview app not running in development
This commit is contained in:
Chris Estreich 2025-02-03 12:31:34 -08:00 committed by GitHub
commit bf7ec3180d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 69 additions and 40 deletions

View file

@ -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: [],

View file

@ -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)

View file

@ -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*/ `

View file

@ -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

View file

@ -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()