refactor: revert hotreload

This commit is contained in:
Catriel Müller
2026-02-16 18:25:27 -03:00
parent 4bb728182a
commit 53698da5b7
9 changed files with 32 additions and 335 deletions
+2 -81
View File
@@ -23,13 +23,10 @@ export class KiloProvider implements vscode.WebviewViewProvider {
private unsubscribeEvent: (() => void) | null = null
private unsubscribeState: (() => void) | null = null
private webviewMessageDisposable: vscode.Disposable | null = null
private webviewWatcherDisposable: vscode.Disposable | null = null
private debounceTimer: NodeJS.Timeout | null = null
constructor(
private readonly extensionUri: vscode.Uri,
private readonly connectionService: KiloConnectionService,
private readonly context: vscode.ExtensionContext,
) {}
/**
@@ -118,9 +115,6 @@ export class KiloProvider implements vscode.WebviewViewProvider {
// Handle messages from webview (shared handler)
this.setupWebviewMessageHandler(webviewView.webview)
// Set up hot reload watcher for development mode
this.setupWebviewHotReload(webviewView.webview)
// Initialize connection to CLI backend
this.initializeConnection()
}
@@ -143,9 +137,6 @@ export class KiloProvider implements vscode.WebviewViewProvider {
// Handle messages from webview (shared handler)
this.setupWebviewMessageHandler(panel.webview)
// Set up hot reload watcher for development mode
this.setupWebviewHotReload(panel.webview)
this.initializeConnection()
}
@@ -292,67 +283,6 @@ export class KiloProvider implements vscode.WebviewViewProvider {
})
}
/**
* Set up hot reload watcher for webview in development mode.
* Watches CSS and JS files separately for optimal hot-swapping.
* - CSS: instant hot-swap with no state loss
* - JS: state-preserving reload via getState/setState
*/
private setupWebviewHotReload(webview: vscode.Webview): void {
// Only enable in development mode
if (this.context.extensionMode !== vscode.ExtensionMode.Development) {
return
}
// Clean up existing watcher if any
this.webviewWatcherDisposable?.dispose()
this.debounceTimer && clearTimeout(this.debounceTimer)
// Watch CSS files for instant hot-swap
const cssWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(this.extensionUri, "dist/webview.css"),
)
const reloadCSS = () => {
console.log("[Kilo New] KiloProvider: 🎨 Hot-swapping CSS (dev mode)")
const styleUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "dist", "webview.css")).toString()
const cacheBusted = `${styleUri}?t=${Date.now()}`
this.postMessage({
type: "hotReloadCSS",
href: cacheBusted,
})
}
cssWatcher.onDidChange(reloadCSS)
cssWatcher.onDidCreate(reloadCSS)
// Watch JS files for state-preserving reload
const jsWatcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(this.extensionUri, "dist/webview.js"),
)
const reloadJS = () => {
// Clear any pending reload
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
}
// Debounce JS reloads by 300ms to avoid multiple rapid reloads
this.debounceTimer = setTimeout(() => {
console.log("[Kilo New] KiloProvider: 🔄 Hot reloading JS with state preservation (dev mode)")
this.postMessage({
type: "hotReloadJS",
})
this.debounceTimer = null
}, 300)
}
jsWatcher.onDidChange(reloadJS)
jsWatcher.onDidCreate(reloadJS)
this.webviewWatcherDisposable = vscode.Disposable.from(cssWatcher, jsWatcher)
}
/**
* Initialize connection to the CLI backend server.
* Subscribes to the shared KiloConnectionService.
@@ -1340,7 +1270,6 @@ export class KiloProvider implements vscode.WebviewViewProvider {
const iconsBaseUri = webview.asWebviewUri(vscode.Uri.joinPath(this.extensionUri, "assets", "icons"))
const nonce = getNonce()
const isDev = this.context.extensionMode === vscode.ExtensionMode.Development
// CSP allows:
// - default-src 'none': Block everything by default
@@ -1357,9 +1286,6 @@ export class KiloProvider implements vscode.WebviewViewProvider {
`img-src ${webview.cspSource} data: https:`,
].join("; ")
// No inline hot reload script - everything handled in the main app (index.tsx)
const hotReloadScript = ""
return `<!DOCTYPE html>
<html lang="en" data-theme="kilo-vscode">
<head>
@@ -1367,7 +1293,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Security-Policy" content="${csp}">
<title>Kilo Code</title>
<link id="kilo-stylesheet" rel="stylesheet" href="${styleUri}">
<link rel="stylesheet" href="${styleUri}">
<style>
html, body {
margin: 0;
@@ -1393,7 +1319,7 @@ export class KiloProvider implements vscode.WebviewViewProvider {
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}">window.ICONS_BASE_URI = "${iconsBaseUri}";</script>${hotReloadScript}
<script nonce="${nonce}">window.ICONS_BASE_URI = "${iconsBaseUri}";</script>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`
@@ -1407,11 +1333,6 @@ export class KiloProvider implements vscode.WebviewViewProvider {
this.unsubscribeEvent?.()
this.unsubscribeState?.()
this.webviewMessageDisposable?.dispose()
this.webviewWatcherDisposable?.dispose()
if (this.debounceTimer) {
clearTimeout(this.debounceTimer)
this.debounceTimer = null
}
this.trackedSessionIds.clear()
}
}
+2 -22
View File
@@ -1,6 +1,4 @@
import * as vscode from "vscode"
import * as fs from "fs"
import * as path from "path"
import { KiloProvider } from "./KiloProvider"
import { AgentManagerProvider } from "./AgentManagerProvider"
import { EXTENSION_DISPLAY_NAME } from "./constants"
@@ -11,24 +9,6 @@ import { BrowserAutomationService } from "./services/browser-automation"
export function activate(context: vscode.ExtensionContext) {
console.log("Kilo Code extension is now active")
// kilocode_change - ensure VS Code directories exist to prevent ENOENT errors during webview hot reload
// VS Code 1.109+ checks for these directories during webview initialization
try {
const userConfigPath = path.join(context.globalStorageUri.fsPath, "..", "..", "..", "User")
fs.mkdirSync(path.join(userConfigPath, "prompts"), { recursive: true })
// Also create .github/agents in workspace roots to prevent workspace-specific ENOENT
const workspaceFolders = vscode.workspace.workspaceFolders
if (workspaceFolders) {
for (const folder of workspaceFolders) {
const agentsPath = path.join(folder.uri.fsPath, ".github", "agents")
fs.mkdirSync(agentsPath, { recursive: true })
}
}
} catch {
// Ignore errors (e.g., permissions, already exists)
}
// Create shared connection service (one server for all webviews)
const connectionService = new KiloConnectionService(context)
@@ -44,7 +24,7 @@ export function activate(context: vscode.ExtensionContext) {
})
// Create the provider with shared service
const provider = new KiloProvider(context.extensionUri, connectionService, context)
const provider = new KiloProvider(context.extensionUri, connectionService)
// Register the webview view provider for the sidebar
context.subscriptions.push(vscode.window.registerWebviewViewProvider(KiloProvider.viewType, provider))
@@ -115,7 +95,7 @@ async function openKiloInNewTab(context: vscode.ExtensionContext, connectionServ
dark: vscode.Uri.joinPath(context.extensionUri, "assets", "icons", "kilo-dark.svg"),
}
const tabProvider = new KiloProvider(context.extensionUri, connectionService, context)
const tabProvider = new KiloProvider(context.extensionUri, connectionService)
tabProvider.resolveWebviewPanel(panel)
// Wait for the new panel to become active before locking the editor group.
+26 -52
View File
@@ -1,4 +1,4 @@
import { Component, createSignal, createMemo, createEffect, Switch, Match, onMount, onCleanup } from "solid-js"
import { Component, createSignal, createMemo, Switch, Match, onMount, onCleanup } from "solid-js"
import { ThemeProvider } from "@kilocode/kilo-ui/theme"
import { DialogProvider } from "@kilocode/kilo-ui/context/dialog"
import { MarkedProvider } from "@kilocode/kilo-ui/context/marked"
@@ -6,7 +6,7 @@ import { DataProvider } from "@kilocode/kilo-ui/context/data"
import { Toast } from "@kilocode/kilo-ui/toast"
import Settings from "./components/Settings"
import ProfileView from "./components/ProfileView"
import { VSCodeProvider, useVSCode } from "./context/vscode"
import { VSCodeProvider } from "./context/vscode"
import { ServerProvider, useServer } from "./context/server"
import { ProviderProvider } from "./context/provider"
import { ConfigProvider } from "./context/config"
@@ -15,7 +15,6 @@ import { LanguageProvider } from "./context/language"
import { ChatView } from "./components/chat"
import SessionList from "./components/history/SessionList"
import type { Message as SDKMessage, Part as SDKPart } from "@kilocode/sdk/v2"
import { restoreHotReloadState } from "./utils/hot-reload"
import "./styles/chat.css"
type ViewType = "newTask" | "marketplace" | "history" | "profile" | "settings"
@@ -92,25 +91,6 @@ const AppContent: Component = () => {
const [currentView, setCurrentView] = createSignal<ViewType>("newTask")
const session = useSession()
const server = useServer()
const vscode = useVSCode()
// Restore view from hot reload state
onMount(() => {
const restored = restoreHotReloadState(vscode)
if (restored?.currentView && VALID_VIEWS.has(restored.currentView)) {
console.log("[Kilo HMR] 🔄 Restored view:", restored.currentView)
setCurrentView(restored.currentView as ViewType)
}
})
// Expose state getter for hot reload (called by inline script before reload)
createEffect(() => {
;(window as any).__getHotReloadState = () => ({
currentView: currentView(),
currentSessionID: session.currentSessionID(),
selectedAgent: session.selectedAgent(),
})
})
const handleViewAction = (action: string) => {
switch (action) {
@@ -183,36 +163,30 @@ const AppContent: Component = () => {
// Main App component with context providers
const App: Component = () => {
console.log("[Kilo Debug] App component rendering")
try {
return (
<ThemeProvider defaultTheme="kilo-vscode">
<DialogProvider>
<VSCodeProvider>
<ServerProvider>
<LanguageBridge>
<MarkedProvider>
<ProviderProvider>
<ConfigProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
</ConfigProvider>
</ProviderProvider>
</MarkedProvider>
</LanguageBridge>
</ServerProvider>
</VSCodeProvider>
<Toast.Region />
</DialogProvider>
</ThemeProvider>
)
} catch (error) {
console.error("[Kilo Debug] Error in App component:", error)
throw error
}
return (
<ThemeProvider defaultTheme="kilo-vscode">
<DialogProvider>
<VSCodeProvider>
<ServerProvider>
<LanguageBridge>
<MarkedProvider>
<ProviderProvider>
<ConfigProvider>
<SessionProvider>
<DataBridge>
<AppContent />
</DataBridge>
</SessionProvider>
</ConfigProvider>
</ProviderProvider>
</MarkedProvider>
</LanguageBridge>
</ServerProvider>
</VSCodeProvider>
<Toast.Region />
</DialogProvider>
</ThemeProvider>
)
}
export default App
@@ -3,7 +3,7 @@
* Text input with send/abort buttons and ghost-text autocomplete for the chat interface
*/
import { Component, createSignal, createEffect, onMount, onCleanup, Show } from "solid-js"
import { Component, createSignal, onCleanup, Show } from "solid-js"
import { Button } from "@kilocode/kilo-ui/button"
import { Tooltip } from "@kilocode/kilo-ui/tooltip"
import { useSession } from "../../context/session"
@@ -12,7 +12,6 @@ import { useLanguage } from "../../context/language"
import { useVSCode } from "../../context/vscode"
import { ModelSelector } from "./ModelSelector"
import { ModeSwitcher } from "./ModeSwitcher"
import { restoreHotReloadState } from "../../utils/hot-reload"
const AUTOCOMPLETE_DEBOUNCE_MS = 500
const MIN_TEXT_LENGTH = 3
@@ -33,23 +32,6 @@ export const PromptInput: Component = () => {
const isDisabled = () => !server.isConnected()
const canSend = () => text().trim().length > 0 && !isBusy() && !isDisabled()
// Restore input text from hot reload
onMount(() => {
const restored = restoreHotReloadState(vscode)
if (restored?.inputText) {
console.log("[Kilo HMR] 🔄 Restoring input text")
setText(restored.inputText)
if (textareaRef) {
textareaRef.value = restored.inputText
}
}
})
// Expose text getter for hot reload (called by inline script before reload)
createEffect(() => {
;(window as any).__getPromptInputText = () => text()
})
// Listen for chat completion results from the extension
const unsubscribe = vscode.onMessage((message) => {
if (message.type === "chatCompletionResult") {
@@ -20,7 +20,6 @@ import { createStore, produce } from "solid-js/store"
import { useVSCode } from "./vscode"
import { useServer } from "./server"
import { useProvider } from "./provider"
import { restoreHotReloadState } from "../utils/hot-reload"
import type {
SessionInfo,
Message,
@@ -144,23 +143,6 @@ export const SessionProvider: ParentComponent = (props) => {
modelSelections: {},
})
// Restore state from hot reload
onMount(() => {
const restored = restoreHotReloadState(vscode)
if (restored) {
if (restored.currentSessionID) {
console.log("[Kilo HMR] 🔄 Restoring session:", restored.currentSessionID)
setCurrentSessionID(restored.currentSessionID)
// Request messages for the restored session
vscode.postMessage({ type: "loadMessages", sessionID: restored.currentSessionID })
}
if (restored.selectedAgent) {
console.log("[Kilo HMR] 🔄 Restoring agent:", restored.selectedAgent)
setSelectedAgentName(restored.selectedAgent)
}
}
})
// Keep pending selection in sync with provider default until the user
// explicitly changes it (or a session exists).
createEffect(() => {
@@ -38,51 +38,12 @@ interface VSCodeContextValue {
const VSCodeContext = createContext<VSCodeContextValue>()
export const VSCodeProvider: ParentComponent = (props) => {
console.log("[Kilo Debug] VSCodeProvider initializing")
const api = getVSCodeAPI()
console.log("[Kilo Debug] VSCodeProvider got API")
const handlers = new Set<(message: ExtensionMessage) => void>()
// Listen for messages from the extension
const messageListener = (event: MessageEvent) => {
const message = event.data as ExtensionMessage
// Handle hot reload messages first (dev mode only)
if (message?.type === "hotReloadCSS") {
const link = document.getElementById("kilo-stylesheet") as HTMLLinkElement
if (link) {
console.log("[Kilo HMR] Swapping CSS")
link.href = (message as any).href
}
return
}
if (message?.type === "hotReloadJS") {
console.log("[Kilo HMR] Reloading with state preservation")
// Collect state from the app
const appState = (window as any).__getHotReloadState ? (window as any).__getHotReloadState() : {}
const inputText = (window as any).__getPromptInputText ? (window as any).__getPromptInputText() : ""
// Save current state before reload
const state = (api.getState() || {}) as any
state._hotReload = {
timestamp: Date.now(),
currentView: appState.currentView,
currentSessionID: appState.currentSessionID,
selectedAgent: appState.selectedAgent,
inputText,
}
api.setState(state)
console.log("[Kilo HMR] Saved state:", state._hotReload)
// Reload the webview
location.reload()
return
}
// Forward all other messages to handlers
handlers.forEach((handler) => handler(message))
}
@@ -105,7 +66,6 @@ export const VSCodeProvider: ParentComponent = (props) => {
setState: <T,>(state: T) => api.setState(state),
}
console.log("[Kilo Debug] VSCodeProvider rendering children")
return <VSCodeContext.Provider value={value}>{props.children}</VSCodeContext.Provider>
}
+1 -17
View File
@@ -1,28 +1,12 @@
/* @refresh reload */
console.log("[Kilo Debug] index.tsx loaded")
import "@kilocode/kilo-ui/styles"
console.log("[Kilo Debug] styles imported")
import { render } from "solid-js/web"
console.log("[Kilo Debug] solid-js/web imported")
import App from "./App"
console.log("[Kilo Debug] App imported")
const root = document.getElementById("root")
console.log("[Kilo Debug] root element:", root)
if (!root) {
console.error("[Kilo Debug] Root element not found!")
throw new Error("Root element not found")
}
console.log("[Kilo Debug] About to render App")
try {
render(() => <App />, root)
console.log("[Kilo Debug] App rendered successfully")
} catch (error) {
console.error("[Kilo Debug] Error rendering App:", error)
throw error
}
render(() => <App />, root)
@@ -482,15 +482,6 @@ export interface NotificationSettingsLoadedMessage {
}
}
export interface HotReloadCSSMessage {
type: "hotReloadCSS"
href: string
}
export interface HotReloadJSMessage {
type: "hotReloadJS"
}
export type ExtensionMessage =
| ReadyMessage
| ConnectionStateMessage
@@ -523,8 +514,6 @@ export type ExtensionMessage =
| ConfigLoadedMessage
| ConfigUpdatedMessage
| NotificationSettingsLoadedMessage
| HotReloadCSSMessage
| HotReloadJSMessage
// ============================================
// Messages FROM webview TO extension
@@ -1,75 +0,0 @@
/**
* Hot reload state management
* Saves and restores critical UI state across JS reloads
*/
import type { VSCodeAPI } from "../types/messages"
export interface HotReloadState {
_hotReload?: {
timestamp: number
currentView?: string
currentSessionID?: string
inputText?: string
scrollPosition?: number
selectedProviderID?: string
selectedModelID?: string
selectedAgent?: string
}
}
/**
* Save current state before a hot reload
*/
export function saveHotReloadState(
vscodeApi: VSCodeAPI,
state: {
currentView?: string
currentSessionID?: string
inputText?: string
scrollPosition?: number
selectedProviderID?: string
selectedModelID?: string
selectedAgent?: string
},
): void {
const existing = (vscodeApi.getState() || {}) as HotReloadState
existing._hotReload = {
timestamp: Date.now(),
...state,
}
vscodeApi.setState(existing)
console.log("[Kilo HMR] 💾 Saved state:", existing._hotReload)
}
/**
* Restore state after a hot reload
* Returns the saved state and clears it from storage
*/
export function restoreHotReloadState(vscodeApi: VSCodeAPI): HotReloadState["_hotReload"] | undefined {
const state = (vscodeApi.getState() || {}) as HotReloadState
const hotReloadState = state._hotReload
if (hotReloadState) {
// Check if the reload happened within the last 5 seconds
const age = Date.now() - hotReloadState.timestamp
if (age < 5000) {
console.log("[Kilo HMR] 📦 Restored state:", hotReloadState)
// Clear the hot reload state after restoring
delete state._hotReload
vscodeApi.setState(state)
return hotReloadState
}
console.log("[Kilo HMR] ⏰ Hot reload state expired (age: ${age}ms)")
}
return undefined
}
/**
* Check if this is a hot reload (vs initial load)
*/
export function isHotReload(vscodeApi: VSCodeAPI): boolean {
const state = (vscodeApi.getState() || {}) as HotReloadState
return !!state._hotReload
}