This commit is contained in:
Bruno Bergher 2026-01-16 15:50:00 +00:00
parent d0e4b887e4
commit 9e91335bf6
8 changed files with 335 additions and 40 deletions

View file

@ -6,41 +6,5 @@ commands:
run: pnpm install
timeout: 60
- name: Build & watch (Terminal 1)
run: |
# Build all workspace packages first (handles @roo-code/build and other deps)
pnpm build || exit $?
# Enable polling for file watchers (helps with symlinks and macOS issues)
export CHOKIDAR_USEPOLLING=true
export WATCHPACK_POLLING=true
# Start watchers (same as .vscode/tasks.json "watch" task, using direct pnpm filters)
pnpm --filter @roo-code/vscode-webview dev &
pnpm --filter roo-cline watch:bundle &
pnpm --filter roo-cline watch:tsc &
# Wait for all background jobs
wait
timeout: 0
- name: Start code-server (Terminal 2)
run: |
PORT=${PORT:-8443}
# Install code-server if missing
if ! command -v code-server &> /dev/null; then
curl -fsSL https://code-server.dev/install.sh | sh
fi
# Symlink extension for live development
EXT_DIR="$HOME/.local/share/code-server/extensions"
mkdir -p "$EXT_DIR"
ln -sfn "$(pwd)/src" "$EXT_DIR/roo-cline"
# Disable atomic writes so file watchers detect changes properly
export DISABLE_ATOMICWRITES=true
# Launch code-server
code-server --auth none --bind-addr 0.0.0.0:${PORT} .
timeout: 0
- name: Serve
run: pnpm serve

View file

@ -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",

3
pnpm-lock.yaml generated
View file

@ -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

94
scripts/serve.sh Executable file
View file

@ -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

View file

@ -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()])

View file

@ -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" },

View file

@ -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",

111
webview-ui/watch.mjs Normal file
View file

@ -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)
})