Skip to content
Draft
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
4 changes: 4 additions & 0 deletions .roo/roomotes.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
version: "1.0"
port: 8443

commands:
- name: Install dependencies
run: pnpm install
timeout: 60

- name: Serve
run: pnpm serve
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions scripts/serve.sh
Original file line number Diff line number Diff line change
@@ -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
117 changes: 116 additions & 1 deletion src/esbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Comment on lines +202 to +212
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual [esbuild-problem-matcher#onStart] and [esbuild-problem-matcher#onEnd] markers here are redundant since the esbuild-problem-matcher plugin (lines 80-94) already emits these markers via esbuild's build.onStart() and build.onEnd() hooks. This causes duplicate markers in the output which could confuse VS Code problem matchers that expect balanced start/end pairs.

Suggested change
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)
rebuildTimeout = setTimeout(async () => {
console.log(`[${name}] File ${eventType}: ${path.relative(srcDir, filePath)}`)
try {
await Promise.all([extensionCtx.rebuild(), workerCtx.rebuild()])
} catch (err) {
console.error(`[${name}] Rebuild failed:`, err.message)
}
}, 200)

Fix it with Roo Code or mention @roomote and request a fix.

}

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()])
Expand Down
5 changes: 5 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
111 changes: 111 additions & 0 deletions webview-ui/watch.mjs
Original file line number Diff line number Diff line change
@@ -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)
})
Loading