Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/cool-cycles-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Fix Jetbrains webview
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,59 @@ class LocalCefResHandle(val resourceBasePath: String, val request: CefRequest?)
private var offset = 0

init {
val requestPath = request?.url?.decodeURLPart()?.replace("http://localhost:", "")?.substringAfter("/")?.substringBefore("?")
logger.info("init LocalCefResHandle,requestPath:$requestPath,resourceBasePath:$resourceBasePath")
val requestPath = request?.url?.let { url ->
val withoutProtocol = url.substringAfter("http://localhost:")
val withoutPort = withoutProtocol.substringAfter("/")
withoutPort.substringBefore("?").decodeURLPart()
}
logger.info("=== LocalCefResHandle INIT ===")
logger.info("Full request URL: ${request?.url}")
logger.info("Decoded request path: $requestPath")
logger.info("Resource base path: $resourceBasePath")

requestPath?.let {
val filePath = if (requestPath.isEmpty()) {
"$resourceBasePath/index.html"
} else {
"$resourceBasePath/$requestPath"
}
file = File(filePath)

logger.info("Constructed file path: $filePath")
var currentFile = File(filePath)

// If file doesn't exist, try alternative paths
if (!currentFile.exists()) {
if (!requestPath.startsWith("webview-ui/build/")) {
val alternativePath = "$resourceBasePath/webview-ui/build/$requestPath"
currentFile = File(alternativePath)
}
}

logger.info("File exists: ${currentFile.exists()}, Is file: ${currentFile.isFile}, Absolute path: ${currentFile.absolutePath}")

if (file!!.exists() && file!!.isFile) {
if (currentFile.exists() && currentFile.isFile) {
try {
fileContent = file!!.readBytes()
fileContent = currentFile.readBytes()
file = currentFile
logger.info("Successfully read file content, size: ${fileContent?.size} bytes")
} catch (e: Exception) {
logger.warn("cannot get fileContent,e:$e")
logger.error("Failed to read file content", e)
file = null
fileContent = null
}
} else {
logger.warn("File not found or not a file: ${currentFile.absolutePath}")
// List directory contents to help debug
val parentDir = currentFile.parentFile
if (parentDir?.exists() == true) {
logger.info("Parent directory exists: ${parentDir.absolutePath}")
logger.info("Parent directory contents: ${parentDir.listFiles()?.joinToString(", ") { it.name }}")
} else {
logger.warn("Parent directory does not exist: ${parentDir?.absolutePath}")
}
file = null
fileContent = null
}
logger.info("init LocalCefResHandle,filePath:$filePath,file:$file,exists:${file?.exists()}")
}
}

Expand All @@ -72,6 +102,9 @@ class LocalCefResHandle(val resourceBasePath: String, val request: CefRequest?)
return when {
filePath.endsWith(".html", true) -> "text/html"
filePath.endsWith(".css", true) -> "text/css"
filePath.endsWith(".js.map", true) -> "application/json"
filePath.endsWith(".css.map", true) -> "application/json"
filePath.endsWith(".map", true) -> "application/json"
filePath.endsWith(".js", true) -> "application/javascript"
filePath.endsWith(".json", true) -> "application/json"
filePath.endsWith(".png", true) -> "image/png"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,9 @@ class WebViewManager(var project: Project) : Disposable, ThemeChangeListener {
fun updateWebViewHtml(data: WebviewHtmlUpdateData) {
data.htmlContent = data.htmlContent.replace("/jetbrains/resources/kilocode/", "./")
data.htmlContent = data.htmlContent.replace("<html lang=\"en\">", "<html lang=\"en\" style=\"background: var(--vscode-sideBar-background);\">")
// Replace index.css/index.js with main.css/main.js to match actual build output
data.htmlContent = data.htmlContent.replace("assets/index.css", "assets/main.css")
data.htmlContent = data.htmlContent.replace("assets/index.js", "assets/main.js")
val encodedState = getLatestWebView()?.state.toString().replace("\"", "\\\"")
val mRst = """<script\s+nonce="([A-Za-z0-9]{32})">""".toRegex().find(data.htmlContent)
val str = mRst?.value ?: ""
Expand Down Expand Up @@ -378,28 +381,33 @@ class WebViewManager(var project: Project) : Disposable, ThemeChangeListener {
""",
)

logger.info("Received HTML update event: handle=${data.handle}, html length: ${data.htmlContent.length}")
logger.info("=== Received HTML update event ===")
logger.info("Handle: ${data.handle}")
logger.info("HTML length: ${data.htmlContent.length}")

val webView = getLatestWebView()

if (webView != null) {
try {
// If HTTP server is running
if (resourceRootDir != null) {
logger.info("Resource root directory is set: ${resourceRootDir?.pathString}")

// Generate unique file name for WebView
val filename = "index.html"

// Save HTML content to file
saveHtmlToResourceDir(data.htmlContent, filename)
val savedPath = saveHtmlToResourceDir(data.htmlContent, filename)
logger.info("HTML saved to: ${savedPath?.pathString}")

// Use HTTP URL to load WebView content
val url = "http://localhost:12345/$filename"
logger.info("Load WebView HTML content via HTTP: $url")
logger.info("Loading WebView via HTTP URL: $url")

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

Expand Down Expand Up @@ -552,8 +560,12 @@ class WebViewInstance(
fun sendThemeConfigToWebView(themeConfig: JsonObject, bodyThemeClass: String) {
currentThemeConfig = themeConfig
this.bodyThemeClass = bodyThemeClass
if (isDisposed or !isPageLoaded) {
logger.warn("WebView has been disposed or not loaded, cannot send theme config:$isDisposed,$isPageLoaded")
if (isDisposed) {
logger.warn("WebView has been disposed, cannot send theme config")
return
}
if (!isPageLoaded) {
logger.debug("WebView page not yet loaded, theme will be injected after page load")
return
}
injectTheme()
Expand Down
99 changes: 99 additions & 0 deletions webview-ui/src/kilocode/vite-plugins/cssPerEntryPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Plugin } from "vite"

// Custom plugin to handle CSS per entry point
// This tracks which CSS belongs to which entry by analyzing the chunk graph

export const cssPerEntryPlugin = (): Plugin => ({
name: "css-per-entry",
enforce: "post",
generateBundle(_options, bundle) {
// Map to track which chunks belong to which entry
const entryChunks: Record<string, Set<string>> = {}
const chunkToEntry: Record<string, string> = {}

// First pass: identify all entry points and their chunk graphs
for (const [fileName, chunk] of Object.entries(bundle)) {
if (chunk.type === "chunk" && chunk.isEntry) {
const entryName = chunk.name
entryChunks[entryName] = new Set<string>()
entryChunks[entryName].add(fileName)
chunkToEntry[fileName] = entryName

// Traverse all imports recursively
const traverseImports = (chunkFileName: string, visited = new Set<string>()) => {
if (visited.has(chunkFileName)) return
visited.add(chunkFileName)

const currentChunk = bundle[chunkFileName]
if (currentChunk && currentChunk.type === "chunk") {
entryChunks[entryName].add(chunkFileName)
chunkToEntry[chunkFileName] = entryName

// Follow static imports
currentChunk.imports.forEach((imp) => traverseImports(imp, visited))

// Follow dynamic imports
if (currentChunk.dynamicImports) {
currentChunk.dynamicImports.forEach((imp) => traverseImports(imp, visited))
}
}
}

traverseImports(fileName)
}
}

// Second pass: collect CSS for each entry based on chunk ownership
const entryCSS: Record<string, string[]> = {}

for (const [fileName, asset] of Object.entries(bundle)) {
if (asset.type === "asset" && fileName.endsWith(".css")) {
// Try to find which entry this CSS belongs to by checking chunks
let assignedEntry: string | null = null

// Check all chunks to see which one references this CSS
for (const [chunkFileName, chunk] of Object.entries(bundle)) {
if (chunk.type === "chunk" && chunk.viteMetadata?.importedCss) {
if (chunk.viteMetadata.importedCss.has(fileName)) {
// This chunk imports this CSS, find which entry owns this chunk
assignedEntry = chunkToEntry[chunkFileName]
if (assignedEntry) break
}
}
}

// If we found an entry for this CSS, assign it
if (assignedEntry) {
if (!entryCSS[assignedEntry]) {
entryCSS[assignedEntry] = []
}
entryCSS[assignedEntry].push(fileName)
}
}
}

// Third pass: merge CSS for each entry and delete originals
for (const [entryName, cssFiles] of Object.entries(entryCSS)) {
if (cssFiles.length > 0) {
let mergedCSS = ""
cssFiles.forEach((cssFile) => {
const asset = bundle[cssFile]
if (asset && asset.type === "asset") {
const source =
typeof asset.source === "string" ? asset.source : new TextDecoder().decode(asset.source)
mergedCSS += source + "\n"
delete bundle[cssFile]
}
})

if (mergedCSS) {
this.emitFile({
type: "asset",
fileName: `assets/${entryName}.css`,
source: mergedCSS,
})
}
}
}
},
})
11 changes: 10 additions & 1 deletion webview-ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import react from "@vitejs/plugin-react"
import tailwindcss from "@tailwindcss/vite"

import { sourcemapPlugin } from "./src/vite-plugins/sourcemapPlugin"
import { cssPerEntryPlugin } from "./src/kilocode/vite-plugins/cssPerEntryPlugin" // kilocode_change

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

const plugins: PluginOption[] = [react(), tailwindcss(), persistPortPlugin(), wasmPlugin(), sourcemapPlugin()]
const plugins: PluginOption[] = [
react(),
tailwindcss(),
persistPortPlugin(),
wasmPlugin(),
sourcemapPlugin(),
cssPerEntryPlugin(), // kilocode_change: enable per-entry CSS files
]

return {
plugins,
Expand All @@ -110,6 +118,7 @@ export default defineConfig(({ mode }) => {
sourcemap: true,
// Ensure source maps are properly included in the build
minify: mode === "production" ? "esbuild" : false,
cssCodeSplit: true, // kilocode_change: enable CSS code splitting so CSS files are generated
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
Expand Down