Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/world-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@kilocode/cli": minor
"kilo-code": minor
"@kilocode/world": minor
---

Add the built-in `world` tool for browser automation, letting Kilo navigate sites, click, type, and capture screenshots from chat. It runs in a persistent, isolated browser session with per-action permissions, controls for headed browsing and system Chrome, and a bundled daemon. Configure it from the CLI command palette or Settings → Browser in the VS Code extension.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ node_modules
playground
tmp
dist
packages/kilo-world/.world-daemon-*
ts-dist
storybook-static
.turbo
Expand Down
20 changes: 19 additions & 1 deletion bun.lock

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@
"marked": "17.0.1",
"marked-shiki": "1.2.1",
"remend": "1.3.0",
"@playwright/test": "1.59.1",
"@playwright/test": "1.57.0",
"playwright": "1.57.0",
Comment thread
IamCoder18 marked this conversation as resolved.
"semver": "7.7.4",
"typescript": "5.8.2",
"@typescript/native-preview": "7.0.0-dev.20260316.1",
Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/v1/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,35 @@ export const Info = Schema.Struct({
description:
"Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.",
}),
// kilocode_change start
world: Schema.optional(
Schema.Struct({
browser: Schema.optional(
Schema.Struct({
headless: Schema.optional(Schema.Boolean),
anti_detect: Schema.optional(Schema.Boolean),
timeout_ms: Schema.optional(Schema.Number),
viewport: Schema.optional(
Schema.Struct({
width: Schema.Number,
height: Schema.Number,
}),
),
executable_path: Schema.optional(Schema.String).annotate({
description: "Browser executable path. The world tool honors this setting only from global config.",
}),
use_system_chrome: Schema.optional(Schema.Boolean).annotate({
description:
"Use the system-installed Google Chrome instead of the bundled Chromium. Falls back to bundled Chromium when Chrome is not found.",
}),
args: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Additional browser arguments. The world tool honors this setting only from global config.",
}),
}),
),
}),
).annotate({ description: "Browser runtime settings for the world tool" }),
// kilocode_change end
compaction: Schema.optional(
Schema.Struct({
auto: Schema.optional(Schema.Boolean).annotate({
Expand Down Expand Up @@ -300,6 +329,10 @@ export const Info = Schema.Struct({
openTelemetry: Schema.Boolean.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(true))).annotate({
description: "Enable telemetry. Set to false to opt-out.",
}),
world_browser: Schema.optional(Schema.Boolean).annotate({
description:
"Enable the World browser tool. The browser runs headless by default and returns an inline image after every visual action. Disable to hide browser capabilities from the agent.",
}),
// kilocode_change end
primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
description: "Tools that should only be available to primary agents.",
Expand Down
15 changes: 0 additions & 15 deletions packages/kilo-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1074,21 +1074,6 @@
"maximum": 24,
"description": "Font size in pixels for the Kilo Code webview UI."
},
"kilo-code.new.browserAutomation.enabled": {
"type": "boolean",
"default": false,
"description": "Enable browser automation powered by Playwright. When enabled, the AI agent can interact with web pages in a visible Chrome window."
},
"kilo-code.new.browserAutomation.useSystemChrome": {
"type": "boolean",
"default": true,
"description": "Use your system's installed Chrome browser instead of downloading a separate Chromium instance."
},
"kilo-code.new.browserAutomation.headless": {
"type": "boolean",
"default": false,
"description": "Run browser automation in headless mode (no visible window). When disabled, you can watch the agent interact with the browser."
},
"kilo-code.new.attention.enabled": {
"type": "boolean",
"default": false,
Expand Down
2 changes: 2 additions & 0 deletions packages/kilo-vscode/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
copyKiloSandboxWorker,
copySandboxResources,
copyTreeSitterResources,
copyWorldDaemon,
} from "../src/services/cli-backend/cli-resources"
import { ensureFfmpegForTarget } from "./ffmpeg-helper"

Expand Down Expand Up @@ -83,6 +84,7 @@ for (const config of targets) {
await copyTreeSitterResources(sourceBinary, targetBinary)
await copySandboxResources(sourceBinary, targetBinary)
await copyKiloSandboxWorker(sourceBinary, targetBinary)
await copyWorldDaemon(sourceBinary, targetBinary)
Comment thread
IamCoder18 marked this conversation as resolved.

if (config.binary !== "kilo.exe") {
chmodSync(targetBinary, 0o755)
Expand Down
24 changes: 19 additions & 5 deletions packages/kilo-vscode/script/local-bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import {
copyKiloSandboxWorker,
copySandboxResources,
copyTreeSitterResources,
copyWorldDaemon,
hasKiloSandboxWorker,
hasTreeSitterResources,
hasWorldDaemon,
kiloSandboxWorkerForBinary,
sanitizeSandboxResources,
} from "../src/services/cli-backend/cli-resources"
import { WorldDaemon } from "../../kilo-world/script/daemon"
import { currentBwrapTarget, ensureBwrapForTarget } from "./bwrap-helper"
import { currentFfmpegTarget, ensureFfmpegForTarget } from "./ffmpeg-helper"

Expand All @@ -36,6 +39,7 @@ const coreDir = join(packagesDir, "core")
const gatewayDir = join(packagesDir, "kilo-gateway")
const indexingDir = join(packagesDir, "kilo-indexing")
const sandboxDir = join(packagesDir, "kilo-sandbox")
const worldDir = join(packagesDir, "kilo-world")

const targetBinDir = join(kiloVscodeDir, "bin")
const binName = process.platform === "win32" ? "kilo.exe" : "kilo"
Expand All @@ -53,7 +57,8 @@ async function cliSourceHash(): Promise<string | null> {
const gatewayResult = await $`git log -1 --format=%H -- .`.cwd(gatewayDir).quiet()
const indexingResult = await $`git log -1 --format=%H -- .`.cwd(indexingDir).quiet()
const sandboxResult = await $`git log -1 --format=%H -- .`.cwd(sandboxDir).quiet()
return `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${gatewayResult.text().trim()}-${indexingResult.text().trim()}-${sandboxResult.text().trim()}`
const worldResult = await $`git log -1 --format=%H -- .`.cwd(worldDir).quiet()
return `${opencodeResult.text().trim()}-${coreResult.text().trim()}-${gatewayResult.text().trim()}-${indexingResult.text().trim()}-${sandboxResult.text().trim()}-${worldResult.text().trim()}`
} catch {
return null
}
Expand All @@ -66,12 +71,14 @@ async function isDirty(): Promise<boolean> {
const gatewayResult = await $`git status --porcelain -- .`.cwd(gatewayDir).quiet()
const indexingResult = await $`git status --porcelain -- .`.cwd(indexingDir).quiet()
const sandboxResult = await $`git status --porcelain -- .`.cwd(sandboxDir).quiet()
const worldResult = await $`git status --porcelain -- .`.cwd(worldDir).quiet()
return (
opencodeResult.text().trim().length > 0 ||
coreResult.text().trim().length > 0 ||
gatewayResult.text().trim().length > 0 ||
indexingResult.text().trim().length > 0 ||
sandboxResult.text().trim().length > 0
sandboxResult.text().trim().length > 0 ||
worldResult.text().trim().length > 0
)
} catch {
return false
Expand Down Expand Up @@ -109,7 +116,8 @@ async function findKiloBinaryInOpencodeDist(): Promise<string | null> {
const preferred = join(distDir, `@kilocode`, tag, "bin", binName)
try {
statSync(preferred)
if (!hasTreeSitterResources(preferred) || !hasKiloSandboxWorker(preferred)) return null
if (!hasTreeSitterResources(preferred) || !hasKiloSandboxWorker(preferred) || !hasWorldDaemon(preferred))
return null
return preferred
} catch {
// fall through to generic search
Expand All @@ -135,7 +143,7 @@ async function findKiloBinaryInOpencodeDist(): Promise<string | null> {
continue
}
if (e.isFile() && (e.name === "kilo" || e.name === "kilo.exe") && basename(dirname(p)) === "bin") {
if (!hasTreeSitterResources(p) || !hasKiloSandboxWorker(p)) continue
if (!hasTreeSitterResources(p) || !hasKiloSandboxWorker(p) || !hasWorldDaemon(p)) continue
return p
}
}
Expand Down Expand Up @@ -212,6 +220,7 @@ async function writeSourceWrapper() {
)
chmodSync(targetBinPath, 0o755)
await bundleKiloSandboxWorker()
await WorldDaemon.copy(await WorldDaemon.bundle(), targetBinDir)
await ensureLocalHelpers()

const hash = await cliSourceHash()
Expand All @@ -224,7 +233,11 @@ async function writeSourceWrapper() {
async function main() {
const targetFile = Bun.file(targetBinPath)
const exists = await targetFile.exists()
const ready = exists && hasTreeSitterResources(targetBinPath) && hasKiloSandboxWorker(targetBinPath)
const ready =
exists &&
hasTreeSitterResources(targetBinPath) &&
hasKiloSandboxWorker(targetBinPath) &&
hasWorldDaemon(targetBinPath)

const stale = ready && !forceRebuild && (await isStale())
const rebuild = forceRebuild || stale || !ready
Expand Down Expand Up @@ -267,6 +280,7 @@ async function main() {
await copyTreeSitterResources(sourceBinPath, targetBinPath)
await copySandboxResources(sourceBinPath, targetBinPath)
await copyKiloSandboxWorker(sourceBinPath, targetBinPath)
await copyWorldDaemon(sourceBinPath, targetBinPath)
chmodSync(targetBinPath, 0o755)
await ensureLocalHelpers()

Expand Down
32 changes: 20 additions & 12 deletions packages/kilo-vscode/script/watch-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
import { watch, chmodSync } from "node:fs"
import { join, relative } from "node:path"
import { $ } from "bun"
import { copySandboxResources, copyTreeSitterResources } from "../src/services/cli-backend/cli-resources"
import {
copySandboxResources,
copyTreeSitterResources,
copyWorldDaemon,
} from "../src/services/cli-backend/cli-resources"

const kiloVscodeDir = join(import.meta.dir, "..")
const packagesDir = join(kiloVscodeDir, "..")
const opencodeDir = join(packagesDir, "opencode")
const opencodeSrcDir = join(opencodeDir, "src")
const worldSrcDir = join(packagesDir, "kilo-world", "src")
const targetBinDir = join(kiloVscodeDir, "bin")
const targetBinPath = join(targetBinDir, "kilo")

Expand Down Expand Up @@ -60,6 +65,7 @@ async function rebuild() {
await $`cp ${source} ${targetBinPath}`
await copyTreeSitterResources(source, targetBinPath)
await copySandboxResources(source, targetBinPath)
await copyWorldDaemon(source, targetBinPath)
chmodSync(targetBinPath, 0o755)

const elapsed = ((performance.now() - start) / 1000).toFixed(1)
Expand All @@ -81,16 +87,18 @@ log(`Watching ${relative(kiloVscodeDir, opencodeSrcDir)}/ for changes...`)
const debounce = 500
let timer: ReturnType<typeof setTimeout> | null = null

watch(opencodeSrcDir, { recursive: true }, (_event, filename) => {
if (!filename) return
// Skip test files
if (filename.endsWith(".test.ts") || filename.endsWith(".test.tsx")) return

if (timer) clearTimeout(timer)
timer = setTimeout(() => {
log(`Change detected: ${filename}`)
rebuild()
}, debounce)
})
for (const dir of [opencodeSrcDir, worldSrcDir]) {
watch(dir, { recursive: true }, (_event, filename) => {
if (!filename) return
// Skip test files
if (filename.endsWith(".test.ts") || filename.endsWith(".test.tsx")) return

if (timer) clearTimeout(timer)
timer = setTimeout(() => {
log(`Change detected: ${relative(packagesDir, join(dir, filename))}`)
rebuild()
}, debounce)
})
}

log("CLI watcher running. Press Ctrl+C to stop.")
19 changes: 1 addition & 18 deletions packages/kilo-vscode/src/KiloProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1017,7 +1017,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
connection: this.connectionService,
dir: this.getWorkspaceDirectory(this.currentSession?.id),
post: (msg) => this.postMessage(msg),
browserSettings: () => this.sendBrowserSettings(),
exportTranscript: (sessionID) => this.handleExportSessionTranscript(sessionID),
copy: (text) => vscode.env.clipboard.writeText(text),
openSessions: (ids) => this.trackOpenSessions(ids),
Expand Down Expand Up @@ -3971,7 +3970,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper

/**
* Handle a generic setting update from the webview.
* The key uses dot notation relative to `kilo-code.new` (e.g. "browserAutomation.enabled").
* The key uses dot notation relative to `kilo-code.new`.
*/
private async handleUpdateSetting(key: string, value: unknown): Promise<void> {
if (key === "maxCost") {
Expand Down Expand Up @@ -4042,7 +4041,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
// Re-send all settings to the webview so the UI reflects the reset
this.postMessage(buildAutocompleteSettingsMessage())
await this.sendIndexingSettings()
this.sendBrowserSettings()
this.sendNotificationSettings()
this.sendTimelineSetting()
this.postMessage(buildThroughputSettingMessage())
Expand All @@ -4059,21 +4057,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
vscode.window.showInformationMessage("Kilo Code settings have been reset to defaults.")
}

/**
* Read the current browser automation settings and push them to the webview.
*/
private sendBrowserSettings(): void {
const config = vscode.workspace.getConfiguration("kilo-code.new.browserAutomation")
this.postMessage({
type: "browserSettingsLoaded",
settings: {
enabled: config.get<boolean>("enabled", false),
useSystemChrome: config.get<boolean>("useSystemChrome", true),
headless: config.get<boolean>("headless", false),
},
})
}

/**
* Read the current Claude Code compatibility setting and push it to the webview.
*/
Expand Down
Loading
Loading