Skip to content

Commit fffff4d

Browse files
committed
fix: jetbrains webview ui assets
1 parent 507c7aa commit fffff4d

File tree

5 files changed

+171
-11
lines changed

5 files changed

+171
-11
lines changed

.changeset/cool-cycles-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kilo-code": patch
3+
---
4+
5+
Fix Jetbrains webview

jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/webview/LocalResHandler.kt

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,58 @@ class LocalCefResHandle(val resourceBasePath: String, val request: CefRequest?)
3434
private var offset = 0
3535

3636
init {
37-
val requestPath = request?.url?.decodeURLPart()?.replace("http://localhost:", "")?.substringAfter("/")?.substringBefore("?")
38-
logger.info("init LocalCefResHandle,requestPath:$requestPath,resourceBasePath:$resourceBasePath")
37+
val requestPath = request?.url?.let { url ->
38+
val withoutProtocol = url.substringAfter("http://localhost:")
39+
val withoutPort = withoutProtocol.substringAfter("/")
40+
withoutPort.substringBefore("?").decodeURLPart()
41+
}
42+
logger.info("=== LocalCefResHandle INIT ===")
43+
logger.info("Full request URL: ${request?.url}")
44+
logger.info("Decoded request path: $requestPath")
45+
logger.info("Resource base path: $resourceBasePath")
46+
3947
requestPath?.let {
4048
val filePath = if (requestPath.isEmpty()) {
4149
"$resourceBasePath/index.html"
4250
} else {
4351
"$resourceBasePath/$requestPath"
4452
}
53+
54+
logger.info("Constructed file path: $filePath")
4555
file = File(filePath)
56+
57+
// If file doesn't exist, try alternative paths
58+
if (!file!!.exists()) {
59+
if (!requestPath.startsWith("webview-ui/build/")) {
60+
val alternativePath = "$resourceBasePath/webview-ui/build/$requestPath"
61+
file = File(alternativePath)
62+
}
63+
}
64+
65+
logger.info("File exists: ${file!!.exists()}, Is file: ${file!!.isFile}, Absolute path: ${file!!.absolutePath}")
4666

4767
if (file!!.exists() && file!!.isFile) {
4868
try {
4969
fileContent = file!!.readBytes()
70+
logger.info("Successfully read file content, size: ${fileContent!!.size} bytes")
5071
} catch (e: Exception) {
51-
logger.warn("cannot get fileContent,e:$e")
72+
logger.error("Failed to read file content", e)
5273
file = null
5374
fileContent = null
5475
}
5576
} else {
77+
logger.warn("File not found or not a file: ${file!!.absolutePath}")
78+
// List directory contents to help debug
79+
val parentDir = file!!.parentFile
80+
if (parentDir?.exists() == true) {
81+
logger.info("Parent directory exists: ${parentDir.absolutePath}")
82+
logger.info("Parent directory contents: ${parentDir.listFiles()?.joinToString(", ") { it.name }}")
83+
} else {
84+
logger.warn("Parent directory does not exist: ${parentDir?.absolutePath}")
85+
}
5686
file = null
5787
fileContent = null
5888
}
59-
logger.info("init LocalCefResHandle,filePath:$filePath,file:$file,exists:${file?.exists()}")
6089
}
6190
}
6291

@@ -72,6 +101,9 @@ class LocalCefResHandle(val resourceBasePath: String, val request: CefRequest?)
72101
return when {
73102
filePath.endsWith(".html", true) -> "text/html"
74103
filePath.endsWith(".css", true) -> "text/css"
104+
filePath.endsWith(".js.map", true) -> "application/json"
105+
filePath.endsWith(".css.map", true) -> "application/json"
106+
filePath.endsWith(".map", true) -> "application/json"
75107
filePath.endsWith(".js", true) -> "application/javascript"
76108
filePath.endsWith(".json", true) -> "application/json"
77109
filePath.endsWith(".png", true) -> "image/png"

jetbrains/plugin/src/main/kotlin/ai/kilocode/jetbrains/webview/WebViewManager.kt

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,9 @@ class WebViewManager(var project: Project) : Disposable, ThemeChangeListener {
314314
fun updateWebViewHtml(data: WebviewHtmlUpdateData) {
315315
data.htmlContent = data.htmlContent.replace("/jetbrains/resources/kilocode/", "./")
316316
data.htmlContent = data.htmlContent.replace("<html lang=\"en\">", "<html lang=\"en\" style=\"background: var(--vscode-sideBar-background);\">")
317+
// Replace index.css/index.js with main.css/main.js to match actual build output
318+
data.htmlContent = data.htmlContent.replace("assets/index.css", "assets/main.css")
319+
data.htmlContent = data.htmlContent.replace("assets/index.js", "assets/main.js")
317320
val encodedState = getLatestWebView()?.state.toString().replace("\"", "\\\"")
318321
val mRst = """<script\s+nonce="([A-Za-z0-9]{32})">""".toRegex().find(data.htmlContent)
319322
val str = mRst?.value ?: ""
@@ -378,28 +381,33 @@ class WebViewManager(var project: Project) : Disposable, ThemeChangeListener {
378381
""",
379382
)
380383

381-
logger.info("Received HTML update event: handle=${data.handle}, html length: ${data.htmlContent.length}")
384+
logger.info("=== Received HTML update event ===")
385+
logger.info("Handle: ${data.handle}")
386+
logger.info("HTML length: ${data.htmlContent.length}")
382387

383388
val webView = getLatestWebView()
384389

385390
if (webView != null) {
386391
try {
387392
// If HTTP server is running
388393
if (resourceRootDir != null) {
394+
logger.info("Resource root directory is set: ${resourceRootDir?.pathString}")
395+
389396
// Generate unique file name for WebView
390397
val filename = "index.html"
391398

392399
// Save HTML content to file
393-
saveHtmlToResourceDir(data.htmlContent, filename)
400+
val savedPath = saveHtmlToResourceDir(data.htmlContent, filename)
401+
logger.info("HTML saved to: ${savedPath?.pathString}")
394402

395403
// Use HTTP URL to load WebView content
396404
val url = "http://localhost:12345/$filename"
397-
logger.info("Load WebView HTML content via HTTP: $url")
405+
logger.info("Loading WebView via HTTP URL: $url")
398406

399407
webView.loadUrl(url)
400408
} else {
401409
// Fallback to direct HTML loading
402-
logger.warn("HTTP server not running or resource directory not set, loading HTML content directly")
410+
logger.warn("Resource root directory is NULL - loading HTML content directly")
403411
webView.loadHtml(data.htmlContent)
404412
}
405413

@@ -552,8 +560,12 @@ class WebViewInstance(
552560
fun sendThemeConfigToWebView(themeConfig: JsonObject, bodyThemeClass: String) {
553561
currentThemeConfig = themeConfig
554562
this.bodyThemeClass = bodyThemeClass
555-
if (isDisposed or !isPageLoaded) {
556-
logger.warn("WebView has been disposed or not loaded, cannot send theme config:$isDisposed,$isPageLoaded")
563+
if (isDisposed) {
564+
logger.warn("WebView has been disposed, cannot send theme config")
565+
return
566+
}
567+
if (!isPageLoaded) {
568+
logger.debug("WebView page not yet loaded, theme will be injected after page load")
557569
return
558570
}
559571
injectTheme()
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { Plugin } from "vite"
2+
3+
// Custom plugin to handle CSS per entry point
4+
// This tracks which CSS belongs to which entry by analyzing the chunk graph
5+
6+
export const cssPerEntryPlugin = (): Plugin => ({
7+
name: "css-per-entry",
8+
enforce: "post",
9+
generateBundle(_options, bundle) {
10+
// Map to track which chunks belong to which entry
11+
const entryChunks: Record<string, Set<string>> = {}
12+
const chunkToEntry: Record<string, string> = {}
13+
14+
// First pass: identify all entry points and their chunk graphs
15+
for (const [fileName, chunk] of Object.entries(bundle)) {
16+
if (chunk.type === "chunk" && chunk.isEntry) {
17+
const entryName = chunk.name
18+
entryChunks[entryName] = new Set<string>()
19+
entryChunks[entryName].add(fileName)
20+
chunkToEntry[fileName] = entryName
21+
22+
// Traverse all imports recursively
23+
const traverseImports = (chunkFileName: string, visited = new Set<string>()) => {
24+
if (visited.has(chunkFileName)) return
25+
visited.add(chunkFileName)
26+
27+
const currentChunk = bundle[chunkFileName]
28+
if (currentChunk && currentChunk.type === "chunk") {
29+
entryChunks[entryName].add(chunkFileName)
30+
chunkToEntry[chunkFileName] = entryName
31+
32+
// Follow static imports
33+
currentChunk.imports.forEach((imp) => traverseImports(imp, visited))
34+
35+
// Follow dynamic imports
36+
if (currentChunk.dynamicImports) {
37+
currentChunk.dynamicImports.forEach((imp) => traverseImports(imp, visited))
38+
}
39+
}
40+
}
41+
42+
traverseImports(fileName)
43+
}
44+
}
45+
46+
// Second pass: collect CSS for each entry based on chunk ownership
47+
const entryCSS: Record<string, string[]> = {}
48+
49+
for (const [fileName, asset] of Object.entries(bundle)) {
50+
if (asset.type === "asset" && fileName.endsWith(".css")) {
51+
// Try to find which entry this CSS belongs to by checking chunks
52+
let assignedEntry: string | null = null
53+
54+
// Check all chunks to see which one references this CSS
55+
for (const [chunkFileName, chunk] of Object.entries(bundle)) {
56+
if (chunk.type === "chunk" && chunk.viteMetadata?.importedCss) {
57+
if (chunk.viteMetadata.importedCss.has(fileName)) {
58+
// This chunk imports this CSS, find which entry owns this chunk
59+
assignedEntry = chunkToEntry[chunkFileName]
60+
if (assignedEntry) break
61+
}
62+
}
63+
}
64+
65+
// If we found an entry for this CSS, assign it
66+
if (assignedEntry) {
67+
if (!entryCSS[assignedEntry]) {
68+
entryCSS[assignedEntry] = []
69+
}
70+
entryCSS[assignedEntry].push(fileName)
71+
}
72+
}
73+
}
74+
75+
// Third pass: merge CSS for each entry and delete originals
76+
for (const [entryName, cssFiles] of Object.entries(entryCSS)) {
77+
if (cssFiles.length > 0) {
78+
let mergedCSS = ""
79+
cssFiles.forEach((cssFile) => {
80+
const asset = bundle[cssFile]
81+
if (asset && asset.type === "asset" && typeof asset.source === "string") {
82+
mergedCSS += asset.source + "\n"
83+
delete bundle[cssFile]
84+
}
85+
})
86+
87+
if (mergedCSS) {
88+
this.emitFile({
89+
type: "asset",
90+
fileName: `assets/${entryName}.css`,
91+
source: mergedCSS,
92+
})
93+
}
94+
}
95+
}
96+
},
97+
})

webview-ui/vite.config.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import react from "@vitejs/plugin-react"
77
import tailwindcss from "@tailwindcss/vite"
88

99
import { sourcemapPlugin } from "./src/vite-plugins/sourcemapPlugin"
10+
import { cssPerEntryPlugin } from "./src/kilocode/vite-plugins/cssPerEntryPlugin" // kilocode_change
1011

1112
function getGitSha() {
1213
let gitSha: string | undefined = undefined
@@ -91,7 +92,14 @@ export default defineConfig(({ mode }) => {
9192
define["process.env.PKG_OUTPUT_CHANNEL"] = JSON.stringify("Kilo-Code-Nightly")
9293
}
9394

94-
const plugins: PluginOption[] = [react(), tailwindcss(), persistPortPlugin(), wasmPlugin(), sourcemapPlugin()]
95+
const plugins: PluginOption[] = [
96+
react(),
97+
tailwindcss(),
98+
persistPortPlugin(),
99+
wasmPlugin(),
100+
sourcemapPlugin(),
101+
cssPerEntryPlugin(), // kilocode_change: enable per-entry CSS files
102+
]
95103

96104
return {
97105
plugins,
@@ -110,6 +118,7 @@ export default defineConfig(({ mode }) => {
110118
sourcemap: true,
111119
// Ensure source maps are properly included in the build
112120
minify: mode === "production" ? "esbuild" : false,
121+
cssCodeSplit: true, // kilocode_change: enable CSS code splitting so CSS files are generated
113122
rollupOptions: {
114123
input: {
115124
main: resolve(__dirname, "index.html"),
@@ -138,6 +147,11 @@ export default defineConfig(({ mode }) => {
138147
if (assetInfo.name && assetInfo.name.endsWith(".map")) {
139148
return "assets/[name]"
140149
}
150+
// kilocode_change start - CSS files handled by cssPerEntryPlugin
151+
if (assetInfo.name && assetInfo.name.endsWith(".css")) {
152+
return "assets/[name]-[hash].css"
153+
}
154+
// kilocode_change end
141155
return "assets/[name][extname]"
142156
},
143157
manualChunks: (id, { getModuleInfo }) => {

0 commit comments

Comments
 (0)