Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading