diff --git a/.roo/roomotes.yml b/.roo/roomotes.yml index 0ea30b93af7..a7b733798d9 100644 --- a/.roo/roomotes.yml +++ b/.roo/roomotes.yml @@ -1,6 +1,10 @@ version: "1.0" +port: 8443 commands: - name: Install dependencies run: pnpm install timeout: 60 + + - name: Serve + run: pnpm serve \ No newline at end of file diff --git a/package.json b/package.json index ed26ce17348..5f7f426ad2a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,8 @@ "changeset:version": "cp CHANGELOG.md src/CHANGELOG.md && changeset version && cp -vf src/CHANGELOG.md .", "knip": "knip --include files", "evals": "dotenvx run -f packages/evals/.env.development packages/evals/.env.local -- docker compose -f packages/evals/docker-compose.yml --profile server --profile runner up --build --scale runner=0", - "npm:publish:types": "pnpm --filter @roo-code/types npm:publish" + "npm:publish:types": "pnpm --filter @roo-code/types npm:publish", + "serve": "bash scripts/serve.sh" }, "devDependencies": { "@changesets/cli": "^2.27.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 177d0b3e5ab..c3b7742f94e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1338,6 +1338,9 @@ importers: '@vitest/ui': specifier: ^3.2.3 version: 3.2.4(vitest@3.2.4) + chokidar: + specifier: ^4.0.1 + version: 4.0.3 identity-obj-proxy: specifier: ^3.0.0 version: 3.0.0 diff --git a/scripts/serve.sh b/scripts/serve.sh new file mode 100755 index 00000000000..baa2075ae9a --- /dev/null +++ b/scripts/serve.sh @@ -0,0 +1,94 @@ +#!/bin/bash +set -e + +PORT=${PORT:-8443} + +# Install code-server if missing +if ! command -v code-server &> /dev/null; then + echo "Installing code-server..." + curl -fsSL https://code-server.dev/install.sh | sh +fi + +# Set up extension symlink for live development +EXT_DIR="$HOME/.local/share/code-server/extensions" +mkdir -p "$EXT_DIR" +ln -sfn "$(pwd)/src" "$EXT_DIR/roo-cline" + +echo "==============================================" +echo "Setting up environment variables for watchers" +echo "==============================================" + +# Enable polling for file watchers (helps with symlinks and various environments) +# Chokidar (used by Vite and now esbuild) +export CHOKIDAR_USEPOLLING=true +export CHOKIDAR_INTERVAL=1000 +echo "CHOKIDAR_USEPOLLING=$CHOKIDAR_USEPOLLING" +echo "CHOKIDAR_INTERVAL=$CHOKIDAR_INTERVAL" + +# Watchpack (used by webpack) +export WATCHPACK_POLLING=true + +# TypeScript watch mode - use polling instead of fs events +export TSC_WATCHFILE=UseFsEventsWithFallbackDynamicPolling +export TSC_WATCHDIRECTORY=UseFsEventsWithFallbackDynamicPolling + +# Disable atomic writes so file watchers detect changes properly +export DISABLE_ATOMICWRITES=true + +# Set development environment (from .vscode/launch.json) +export NODE_ENV=development +export VSCODE_DEBUG_MODE=true + +# Trap to clean up all background processes on exit +cleanup() { + echo "Stopping all processes..." + jobs -p | xargs -r kill 2>/dev/null +} +trap cleanup EXIT INT TERM + +# Build all workspace packages first +echo "" +echo "==============================================" +echo "Building workspace packages..." +echo "==============================================" +pnpm build + +# Start code-server in background FIRST +echo "" +echo "==============================================" +echo "Starting code-server on port $PORT" +echo "Extension files are at: $(pwd)/src" +echo "Symlinked to: $EXT_DIR/roo-cline" +echo "==============================================" +code-server --auth none --bind-addr 0.0.0.0:${PORT} . & +CODE_SERVER_PID=$! + +# Give code-server a moment to start +sleep 2 + +# Start watchers with explicit env vars using env command +echo "" +echo "==============================================" +echo "Starting file watchers..." +echo "==============================================" + +# Run webview watcher (custom chokidar-based script) +env CHOKIDAR_USEPOLLING=true CHOKIDAR_INTERVAL=1000 pnpm --filter @roo-code/vscode-webview dev:watch & + +# Run bundle watcher (custom chokidar-based script) +env CHOKIDAR_USEPOLLING=true CHOKIDAR_INTERVAL=1000 pnpm --filter roo-cline watch:bundle & + +# Run tsc watcher +env TSC_WATCHFILE=UseFsEventsWithFallbackDynamicPolling pnpm --filter roo-cline watch:tsc & + +echo "" +echo "==============================================" +echo "All processes started!" +echo "code-server running at http://localhost:${PORT}" +echo "Watchers are running - file changes should trigger rebuilds" +echo "Press Ctrl+C to stop all processes" +echo "==============================================" +echo "" + +# Wait for all background processes +wait diff --git a/src/esbuild.mjs b/src/esbuild.mjs index aabacfcee99..3c22cc8adf2 100644 --- a/src/esbuild.mjs +++ b/src/esbuild.mjs @@ -4,6 +4,8 @@ import * as path from "path" import { fileURLToPath } from "url" import process from "node:process" import * as console from "node:console" +import { setTimeout, clearTimeout } from "node:timers" +import chokidar from "chokidar" import { copyPaths, copyWasms, copyLocales, setupLocaleWatcher } from "@roo-code/build" @@ -121,9 +123,122 @@ async function main() { ]) if (watch) { - await Promise.all([extensionCtx.watch(), workerCtx.watch()]) + // Use chokidar for file watching with polling support + // This is more reliable than esbuild's native watcher in environments like code-server + const usePolling = process.env.CHOKIDAR_USEPOLLING === "true" + const pollInterval = parseInt(process.env.CHOKIDAR_INTERVAL || "1000", 10) + + console.log(`[${name}] ========================================`) + console.log(`[${name}] Starting watch mode`) + console.log(`[${name}] CHOKIDAR_USEPOLLING: ${process.env.CHOKIDAR_USEPOLLING}`) + console.log(`[${name}] Polling enabled: ${usePolling}`) + console.log(`[${name}] Poll interval: ${pollInterval}ms`) + console.log(`[${name}] Watching directory: ${srcDir}`) + console.log(`[${name}] CWD: ${process.cwd()}`) + console.log(`[${name}] ========================================`) + + // Initial build + await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()]) copyLocales(srcDir, distDir) setupLocaleWatcher(srcDir, distDir) + + // Set up chokidar watcher - watch the srcDir directly + console.log(`[${name}] Setting up chokidar watcher...`) + console.log(`[${name}] srcDir:`, srcDir) + + // List files to verify they exist + const extensionTs = path.join(srcDir, "extension.ts") + console.log(`[${name}] extension.ts exists:`, fs.existsSync(extensionTs)) + + const watcher = chokidar.watch(srcDir, { + ignored: (filePath) => { + // Ignore node_modules, dist, and test files + const relativePath = path.relative(srcDir, filePath) + return relativePath.includes("node_modules") || + relativePath.includes("dist") || + relativePath.endsWith(".spec.ts") || + relativePath.endsWith(".test.ts") + }, + persistent: true, + usePolling, + interval: pollInterval, + ignoreInitial: false, // Count files during initial scan + depth: 10, + }) + + console.log(`[${name}] Watcher created, waiting for ready event...`) + + let rebuildTimeout = null + let fileCount = 0 + let isReady = false + + const triggerRebuild = (eventType, filePath) => { + if (!isReady) return + + // Ignore directories that are written to during build + const ignoredPaths = [ + "/dist/", "\\dist\\", "/dist", "\\dist", + "/node_modules/", "\\node_modules\\", + "/assets/", "\\assets\\", + "/webview-ui/", "\\webview-ui\\", + ] + for (const ignored of ignoredPaths) { + if (filePath.includes(ignored)) { + return + } + } + + // Only rebuild for .ts, .tsx source files (not .json since those can be copied) + const shouldRebuild = (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) && + !filePath.endsWith(".d.ts") && !filePath.endsWith(".spec.ts") && !filePath.endsWith(".test.ts") + if (!shouldRebuild) { + return + } + + // Debounce rebuilds + if (rebuildTimeout) { + clearTimeout(rebuildTimeout) + } + rebuildTimeout = setTimeout(async () => { + console.log(`[${name}] File ${eventType}: ${path.relative(srcDir, filePath)}`) + console.log(`[esbuild-problem-matcher#onStart]`) + try { + await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()]) + console.log(`[esbuild-problem-matcher#onEnd]`) + } catch (err) { + console.error(`[${name}] Rebuild failed:`, err.message) + console.log(`[esbuild-problem-matcher#onEnd]`) + } + }, 200) + } + + watcher.on("change", (p) => triggerRebuild("changed", p)) + watcher.on("add", (p) => { + if (!isReady && (p.endsWith(".ts") || p.endsWith(".tsx") || p.endsWith(".json"))) { + fileCount++ + } + triggerRebuild("added", p) + }) + watcher.on("unlink", (p) => triggerRebuild("deleted", p)) + watcher.on("error", (err) => console.error(`[${name}] Watcher error:`, err)) + watcher.on("ready", () => { + isReady = true + console.log(`[${name}] ========================================`) + console.log(`[${name}] Watcher ready!`) + console.log(`[${name}] Watching ${fileCount} files`) + console.log(`[${name}] Listening for changes...`) + console.log(`[${name}] ========================================`) + }) + + // Also add a raw event listener to see ALL events + watcher.on("raw", (event, rawPath, details) => { + if (process.env.DEBUG_WATCHER === "true") { + console.log(`[${name}] Raw event:`, event, rawPath) + } + }) + + // Keep the process running + await new Promise(() => {}) } else { await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()]) await Promise.all([extensionCtx.dispose(), workerCtx.dispose()]) diff --git a/src/extension.ts b/src/extension.ts index c12f223f954..7c3654b7721 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -347,6 +347,11 @@ export async function activate(context: vscode.ExtensionContext) { // Watch the core files and automatically reload the extension host. if (process.env.NODE_ENV === "development") { const watchPaths = [ + // Watch compiled output - triggers reload when esbuild finishes a rebuild + { path: path.join(context.extensionPath, "dist"), pattern: "extension.js" }, + // Also watch webview build output + { path: path.join(context.extensionPath, "webview-ui/build"), pattern: "**/*" }, + // Watch source files for changes that might not trigger a rebuild { path: context.extensionPath, pattern: "**/*.ts" }, { path: path.join(context.extensionPath, "../packages/types"), pattern: "**/*.ts" }, { path: path.join(context.extensionPath, "../packages/telemetry"), pattern: "**/*.ts" }, diff --git a/webview-ui/package.json b/webview-ui/package.json index a316861389b..50bb466151c 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -9,6 +9,7 @@ "test": "vitest run", "format": "prettier --write src", "dev": "vite", + "dev:watch": "node watch.mjs", "build": "tsc -b && vite build", "build:nightly": "tsc -b && vite build --mode nightly", "preview": "vite preview", @@ -86,6 +87,7 @@ "devDependencies": { "@roo-code/config-eslint": "workspace:^", "@roo-code/config-typescript": "workspace:^", + "chokidar": "^4.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.6.1", diff --git a/webview-ui/watch.mjs b/webview-ui/watch.mjs new file mode 100644 index 00000000000..5a457f4b919 --- /dev/null +++ b/webview-ui/watch.mjs @@ -0,0 +1,111 @@ +import chokidar from "chokidar" +import { exec } from "child_process" +import path from "path" +import { fileURLToPath } from "url" + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const usePolling = process.env.CHOKIDAR_USEPOLLING === "true" +const pollInterval = parseInt(process.env.CHOKIDAR_INTERVAL || "1000", 10) + +console.log(`[webview] ========================================`) +console.log(`[webview] Starting watch mode`) +console.log(`[webview] Polling: ${usePolling}, Interval: ${pollInterval}ms`) +console.log(`[webview] Watching: ${path.join(__dirname, "src")}`) +console.log(`[webview] ========================================`) + +let buildInProgress = false +let pendingBuild = false + +const runBuild = () => { + if (buildInProgress) { + pendingBuild = true + return + } + + buildInProgress = true + console.log(`[webview] Building...`) + + exec("pnpm vite build", { cwd: __dirname }, (error, stdout, stderr) => { + buildInProgress = false + + if (error) { + console.error(`[webview] Build failed:`, error.message) + if (stderr) console.error(stderr) + } else { + console.log(`[webview] Build complete`) + } + + if (pendingBuild) { + pendingBuild = false + runBuild() + } + }) +} + +const srcDir = path.join(__dirname, "src") +console.log(`[webview] srcDir: ${srcDir}`) + +const watcher = chokidar.watch(srcDir, { + ignored: (filePath) => { + const relativePath = path.relative(srcDir, filePath) + return relativePath.includes("node_modules") || + relativePath.endsWith(".spec.ts") || + relativePath.endsWith(".spec.tsx") || + relativePath.endsWith(".test.ts") || + relativePath.endsWith(".test.tsx") + }, + persistent: true, + usePolling, + interval: pollInterval, + ignoreInitial: false, // Count files during initial scan + depth: 10, +}) + +let fileCount = 0 +let isReady = false + +let debounceTimeout = null +watcher.on("change", (filePath) => { + if (!isReady) return + if (debounceTimeout) clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => { + console.log(`[webview] File changed: ${filePath}`) + runBuild() + }, 200) +}) + +watcher.on("add", (filePath) => { + if (!isReady) { + fileCount++ + return + } + if (debounceTimeout) clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => { + console.log(`[webview] File added: ${filePath}`) + runBuild() + }, 200) +}) + +watcher.on("unlink", (filePath) => { + if (!isReady) return + if (debounceTimeout) clearTimeout(debounceTimeout) + debounceTimeout = setTimeout(() => { + console.log(`[webview] File deleted: ${filePath}`) + runBuild() + }, 200) +}) + +watcher.on("ready", () => { + isReady = true + console.log(`[webview] ========================================`) + console.log(`[webview] Watcher ready!`) + console.log(`[webview] Watching ${fileCount} files`) + console.log(`[webview] Listening for changes...`) + console.log(`[webview] ========================================`) +}) + +watcher.on("error", (error) => { + console.error(`[webview] Watcher error:`, error) +})