Skip to content

Commit bf7ec31

Browse files
authored
Merge pull request RooCodeInc#757 from RooVetGit/cte/hmr-dx
Fall back to js bundle when local webview app not running in development
2 parents f2d7cbe + 2ad57ba commit bf7ec31

File tree

5 files changed

+69
-40
lines changed

5 files changed

+69
-40
lines changed

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ module.exports = {
3434
transformIgnorePatterns: [
3535
"node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)",
3636
],
37+
roots: ["<rootDir>/src", "<rootDir>/webview-ui/src"],
3738
modulePathIgnorePatterns: [".vscode-test"],
3839
reporters: [["jest-simple-dot-reporter", {}]],
3940
setupFiles: [],

src/activate/registerCommands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit<RegisterComman
7575
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "rocket.png"),
7676
}
7777

78-
tabProvider.resolveWebviewView(panel)
78+
await tabProvider.resolveWebviewView(panel)
7979

8080
// Lock the editor group so clicking on files doesn't open them over the panel
8181
await delay(100)

src/core/webview/ClineProvider.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
256256
await visibleProvider.initClineWithTask(prompt)
257257
}
258258

259-
resolveWebviewView(
260-
webviewView: vscode.WebviewView | vscode.WebviewPanel,
261-
//context: vscode.WebviewViewResolveContext<unknown>, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden
262-
//token: vscode.CancellationToken
263-
): void | Thenable<void> {
259+
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
264260
this.outputChannel.appendLine("Resolving webview view")
265261
this.view = webviewView
266262

@@ -277,7 +273,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
277273

278274
webviewView.webview.html =
279275
this.context.extensionMode === vscode.ExtensionMode.Development
280-
? this.getHMRHtmlContent(webviewView.webview)
276+
? await this.getHMRHtmlContent(webviewView.webview)
281277
: this.getHtmlContent(webviewView.webview)
282278

283279
// 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 {
402398
await this.view?.webview.postMessage(message)
403399
}
404400

405-
private getHMRHtmlContent(webview: vscode.Webview): string {
406-
const nonce = getNonce()
401+
private async getHMRHtmlContent(webview: vscode.Webview): Promise<string> {
402+
const localPort = "5173"
403+
const localServerUrl = `localhost:${localPort}`
404+
405+
// Check if local dev server is running.
406+
try {
407+
await axios.get(`http://${localServerUrl}`)
408+
} catch (error) {
409+
vscode.window.showErrorMessage(
410+
"Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.",
411+
)
412+
413+
return this.getHtmlContent(webview)
414+
}
407415

416+
const nonce = getNonce()
408417
const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
409418
const codiconsUri = getUri(webview, this.context.extensionUri, [
410419
"node_modules",
@@ -415,8 +424,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
415424
])
416425

417426
const file = "src/index.tsx"
418-
const localPort = "5173"
419-
const localServerUrl = `localhost:${localPort}`
420427
const scriptUri = `http://${localServerUrl}/${file}`
421428

422429
const reactRefresh = /*html*/ `

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
import { ClineProvider } from "../ClineProvider"
1+
// npx jest src/core/webview/__tests__/ClineProvider.test.ts
2+
23
import * as vscode from "vscode"
4+
import axios from "axios"
5+
6+
import { ClineProvider } from "../ClineProvider"
37
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
48
import { setSoundEnabled } from "../../../utils/sound"
5-
import { defaultModeSlug, modes } from "../../../shared/modes"
6-
import { addCustomInstructions } from "../../prompts/sections/custom-instructions"
7-
import { experimentDefault, experiments } from "../../../shared/experiments"
9+
import { defaultModeSlug } from "../../../shared/modes"
10+
import { experimentDefault } from "../../../shared/experiments"
811

912
// Mock custom-instructions module
1013
const mockAddCustomInstructions = jest.fn()
14+
1115
jest.mock("../../prompts/sections/custom-instructions", () => ({
1216
addCustomInstructions: mockAddCustomInstructions,
1317
}))
@@ -202,7 +206,6 @@ describe("ClineProvider", () => {
202206
let mockOutputChannel: vscode.OutputChannel
203207
let mockWebviewView: vscode.WebviewView
204208
let mockPostMessage: jest.Mock
205-
let visibilityChangeCallback: (e?: unknown) => void
206209

207210
beforeEach(() => {
208211
// Reset mocks
@@ -270,13 +273,13 @@ describe("ClineProvider", () => {
270273
return { dispose: jest.fn() }
271274
}),
272275
onDidChangeVisibility: jest.fn().mockImplementation((callback) => {
273-
visibilityChangeCallback = callback
274276
return { dispose: jest.fn() }
275277
}),
276278
} as unknown as vscode.WebviewView
277279

278280
provider = new ClineProvider(mockContext, mockOutputChannel)
279-
// @ts-ignore - accessing private property for testing
281+
282+
// @ts-ignore - Accessing private property for testing.
280283
provider.customModesManager = mockCustomModesManager
281284
})
282285

@@ -288,18 +291,36 @@ describe("ClineProvider", () => {
288291
expect(ClineProvider.getVisibleInstance()).toBe(provider)
289292
})
290293

291-
test("resolveWebviewView sets up webview correctly", () => {
292-
provider.resolveWebviewView(mockWebviewView)
294+
test("resolveWebviewView sets up webview correctly", async () => {
295+
await provider.resolveWebviewView(mockWebviewView)
293296

294297
expect(mockWebviewView.webview.options).toEqual({
295298
enableScripts: true,
296299
localResourceRoots: [mockContext.extensionUri],
297300
})
301+
302+
expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
303+
})
304+
305+
test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => {
306+
provider = new ClineProvider(
307+
{ ...mockContext, extensionMode: vscode.ExtensionMode.Development },
308+
mockOutputChannel,
309+
)
310+
;(axios.get as jest.Mock).mockRejectedValueOnce(new Error("Network error"))
311+
312+
await provider.resolveWebviewView(mockWebviewView)
313+
314+
expect(mockWebviewView.webview.options).toEqual({
315+
enableScripts: true,
316+
localResourceRoots: [mockContext.extensionUri],
317+
})
318+
298319
expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")
299320
})
300321

301322
test("postMessageToWebview sends message to webview", async () => {
302-
provider.resolveWebviewView(mockWebviewView)
323+
await provider.resolveWebviewView(mockWebviewView)
303324

304325
const mockState: ExtensionState = {
305326
version: "1.0.0",
@@ -341,7 +362,7 @@ describe("ClineProvider", () => {
341362
})
342363

343364
test("handles webviewDidLaunch message", async () => {
344-
provider.resolveWebviewView(mockWebviewView)
365+
await provider.resolveWebviewView(mockWebviewView)
345366

346367
// Get the message handler from onDidReceiveMessage
347368
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
@@ -420,7 +441,7 @@ describe("ClineProvider", () => {
420441
})
421442

422443
test("handles writeDelayMs message", async () => {
423-
provider.resolveWebviewView(mockWebviewView)
444+
await provider.resolveWebviewView(mockWebviewView)
424445
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
425446

426447
await messageHandler({ type: "writeDelayMs", value: 2000 })
@@ -430,7 +451,7 @@ describe("ClineProvider", () => {
430451
})
431452

432453
test("updates sound utility when sound setting changes", async () => {
433-
provider.resolveWebviewView(mockWebviewView)
454+
await provider.resolveWebviewView(mockWebviewView)
434455

435456
// Get the message handler from onDidReceiveMessage
436457
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
@@ -470,7 +491,7 @@ describe("ClineProvider", () => {
470491
})
471492

472493
test("loads saved API config when switching modes", async () => {
473-
provider.resolveWebviewView(mockWebviewView)
494+
await provider.resolveWebviewView(mockWebviewView)
474495
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
475496

476497
// Mock ConfigManager methods
@@ -491,7 +512,7 @@ describe("ClineProvider", () => {
491512
})
492513

493514
test("saves current config when switching to mode without config", async () => {
494-
provider.resolveWebviewView(mockWebviewView)
515+
await provider.resolveWebviewView(mockWebviewView)
495516
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
496517

497518
// Mock ConfigManager methods
@@ -519,7 +540,7 @@ describe("ClineProvider", () => {
519540
})
520541

521542
test("saves config as default for current mode when loading config", async () => {
522-
provider.resolveWebviewView(mockWebviewView)
543+
await provider.resolveWebviewView(mockWebviewView)
523544
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
524545

525546
provider.configManager = {
@@ -540,7 +561,7 @@ describe("ClineProvider", () => {
540561
})
541562

542563
test("handles request delay settings messages", async () => {
543-
provider.resolveWebviewView(mockWebviewView)
564+
await provider.resolveWebviewView(mockWebviewView)
544565
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
545566

546567
// Test alwaysApproveResubmit
@@ -555,7 +576,7 @@ describe("ClineProvider", () => {
555576
})
556577

557578
test("handles updatePrompt message correctly", async () => {
558-
provider.resolveWebviewView(mockWebviewView)
579+
await provider.resolveWebviewView(mockWebviewView)
559580
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
560581

561582
// Mock existing prompts
@@ -650,7 +671,7 @@ describe("ClineProvider", () => {
650671
)
651672
})
652673
test("handles mode-specific custom instructions updates", async () => {
653-
provider.resolveWebviewView(mockWebviewView)
674+
await provider.resolveWebviewView(mockWebviewView)
654675
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
655676

656677
// Mock existing prompts
@@ -707,7 +728,7 @@ describe("ClineProvider", () => {
707728

708729
// Create new provider with updated mock context
709730
provider = new ClineProvider(mockContext, mockOutputChannel)
710-
provider.resolveWebviewView(mockWebviewView)
731+
await provider.resolveWebviewView(mockWebviewView)
711732
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
712733

713734
provider.configManager = {
@@ -732,10 +753,10 @@ describe("ClineProvider", () => {
732753
})
733754

734755
describe("deleteMessage", () => {
735-
beforeEach(() => {
756+
beforeEach(async () => {
736757
// Mock window.showInformationMessage
737758
;(vscode.window.showInformationMessage as jest.Mock) = jest.fn()
738-
provider.resolveWebviewView(mockWebviewView)
759+
await provider.resolveWebviewView(mockWebviewView)
739760
})
740761

741762
test('handles "Just this message" deletion correctly', async () => {
@@ -861,9 +882,9 @@ describe("ClineProvider", () => {
861882
})
862883

863884
describe("getSystemPrompt", () => {
864-
beforeEach(() => {
885+
beforeEach(async () => {
865886
mockPostMessage.mockClear()
866-
provider.resolveWebviewView(mockWebviewView)
887+
await provider.resolveWebviewView(mockWebviewView)
867888
// Reset and setup mock
868889
mockAddCustomInstructions.mockClear()
869890
mockAddCustomInstructions.mockImplementation(
@@ -1111,7 +1132,7 @@ describe("ClineProvider", () => {
11111132
})
11121133

11131134
// Resolve webview and trigger getSystemPrompt
1114-
provider.resolveWebviewView(mockWebviewView)
1135+
await provider.resolveWebviewView(mockWebviewView)
11151136
const architectHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
11161137
await architectHandler({ type: "getSystemPrompt" })
11171138

@@ -1125,9 +1146,9 @@ describe("ClineProvider", () => {
11251146
})
11261147

11271148
describe("handleModeSwitch", () => {
1128-
beforeEach(() => {
1149+
beforeEach(async () => {
11291150
// Set up webview for each test
1130-
provider.resolveWebviewView(mockWebviewView)
1151+
await provider.resolveWebviewView(mockWebviewView)
11311152
})
11321153

11331154
test("loads saved API config when switching modes", async () => {
@@ -1188,7 +1209,7 @@ describe("ClineProvider", () => {
11881209

11891210
describe("updateCustomMode", () => {
11901211
test("updates both file and state when updating custom mode", async () => {
1191-
provider.resolveWebviewView(mockWebviewView)
1212+
await provider.resolveWebviewView(mockWebviewView)
11921213
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
11931214

11941215
// Mock CustomModesManager methods

src/test/task.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ suite("Roo Code Task", () => {
3131

3232
try {
3333
// Initialize provider with panel.
34-
provider.resolveWebviewView(panel)
34+
await provider.resolveWebviewView(panel)
3535

3636
// Wait for webview to launch.
3737
let startTime = Date.now()

0 commit comments

Comments
 (0)