Skip to content

Commit

Permalink
Merge pull request RooVetGit#757 from RooVetGit/cte/hmr-dx
Browse files Browse the repository at this point in the history
Fall back to js bundle when local webview app not running in development
  • Loading branch information
cte authored Feb 3, 2025
2 parents f2d7cbe + 2ad57ba commit bf7ec31
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 40 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
2 changes: 1 addition & 1 deletion src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 17 additions & 10 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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*/ `
Expand Down
77 changes: 49 additions & 28 deletions src/core/webview/__tests__/ClineProvider.test.ts
Original file line number Diff line number Diff line change
@@ -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,
}))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
})

Expand All @@ -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",
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 })
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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" })

Expand All @@ -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 () => {
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/test/task.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit bf7ec31

Please sign in to comment.