mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-19 10:02:04 +08:00
Merge branch 'main' into feat/linux-network-sandbox
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Animate tool-call and reasoning details when expanding and collapsing.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": patch
|
||||
---
|
||||
|
||||
Stop shipping the local-only `.cli-version` build marker in packaged VSIX installs, which previously made production installs detect as local builds and inject a dev-only bwrap fallback.
|
||||
@@ -62,6 +62,18 @@ Pay special attention to:
|
||||
|
||||
When in doubt, add a finding. A human will verify it. Compiling code is not proof the chain is intact.
|
||||
|
||||
## CONFIG_REGRESSION.md
|
||||
|
||||
Check whether this PR introduces or re-introduces fallback logic for `opencode` config files, or accidentally breaks code that now correctly expects only `.kilo`-based configuration.
|
||||
|
||||
Kilo removed fallback support for `opencode` config directories. Look for:
|
||||
|
||||
- Any new or restored code that reads from `opencode` config paths.
|
||||
- Upstream additions to config discovery, loading, or path resolution that add `opencode` fallback candidates we stripped.
|
||||
- Changes that break `.kilo`-only config lookup by removing or reordering it in a multi-path search.
|
||||
|
||||
When in doubt, add a finding. A human should verify config path changes manually.
|
||||
|
||||
## TESTS.md
|
||||
|
||||
Check whether this PR removed any Kilo-specific tests.
|
||||
|
||||
@@ -30,7 +30,7 @@ Every installation has a scope:
|
||||
|
||||
Project files can be committed to version control and shared with teammates. Global files remain on your machine and do not travel with a repository.
|
||||
|
||||
When the same configuration exists at both scopes, project configuration takes precedence over global configuration. Learn more about [Kilo's configuration files and precedence](/docs/getting-started/settings#configuration-files).
|
||||
When the same configuration exists at both scopes, project configuration takes precedence over global configuration. Learn more about [Kilo's configuration files and precedence](/docs/getting-started/settings#config-file-precedence).
|
||||
|
||||
## Files changed by installation
|
||||
|
||||
|
||||
@@ -129,19 +129,62 @@ html[data-theme="kilo-vscode"] [data-component="tool-trigger"] {
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tool-card-down {
|
||||
from {
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tool-card-up {
|
||||
from {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Card styling for tool calls in the VS Code sidebar theme */
|
||||
html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-type="tool"] {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
overflow: visible;
|
||||
|
||||
/* Header trigger — add bottom border when the collapsible is open */
|
||||
[data-component="collapsible"].tool-collapsible {
|
||||
gap: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&:has([data-slot="collapsible-trigger"][aria-expanded="true"]) {
|
||||
gap: 6px;
|
||||
> [data-slot="collapsible-content"] {
|
||||
display: grid;
|
||||
overflow: visible;
|
||||
clip-path: inset(0 -6px);
|
||||
|
||||
> * {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> [data-slot="collapsible-content"][data-expanded] {
|
||||
animation: tool-card-down 220ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
> [data-slot="collapsible-content"][data-closed] {
|
||||
animation: tool-card-up 160ms cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
|
||||
[data-slot="basic-tool-details"]::before {
|
||||
display: block;
|
||||
height: 6px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
[data-slot="collapsible-trigger"] {
|
||||
@@ -409,3 +452,12 @@ html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-ty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html[data-theme="kilo-vscode"] [data-component="tool-part-wrapper"][data-part-type="tool"] {
|
||||
[data-component="collapsible"].tool-collapsible > [data-slot="collapsible-content"][data-expanded],
|
||||
[data-component="collapsible"].tool-collapsible > [data-slot="collapsible-content"][data-closed] {
|
||||
animation-duration: 0.01ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,14 +20,16 @@ export function initialOpen(props: OpenProps) {
|
||||
export function BasicTool(props: BasicToolProps) {
|
||||
const key = () => toolOpenKey(props)
|
||||
const initial = () => initialOpen(props)
|
||||
const change = (open: boolean) => {
|
||||
writeToolOpen(key(), open)
|
||||
props.onOpenChange?.(open)
|
||||
}
|
||||
if (!("children" in props)) {
|
||||
return <Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change} />
|
||||
}
|
||||
return (
|
||||
<Base
|
||||
{...props}
|
||||
defaultOpen={initial()}
|
||||
onOpenChange={(open) => {
|
||||
writeToolOpen(key(), open)
|
||||
props.onOpenChange?.(open)
|
||||
}}
|
||||
/>
|
||||
<Base {...props} defaultOpen={initial()} retainDetails={props.defer} onOpenChange={change}>
|
||||
<div data-slot="basic-tool-details">{props.children}</div>
|
||||
</Base>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -477,6 +477,30 @@ html[data-theme="kilo-vscode"] [data-component="todos"] {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@keyframes reasoning-card-down {
|
||||
from {
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes reasoning-card-up {
|
||||
from {
|
||||
grid-template-rows: 1fr;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
to {
|
||||
grid-template-rows: 0fr;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reasoning block: visually distinct from normal agent output.
|
||||
Reasoning stays open unless the user explicitly collapses it. */
|
||||
[data-component="reasoning-part"] {
|
||||
@@ -550,22 +574,21 @@ html[data-theme="kilo-vscode"] [data-component="todos"] {
|
||||
}
|
||||
}
|
||||
|
||||
/* Collapse/expand animation via Kobalte's CSS variable.
|
||||
Uses grid trick: the outer wrapper transitions grid-template-rows
|
||||
between 0fr (collapsed) and 1fr (expanded) so the content smoothly
|
||||
collapses without needing to know the pixel height up front. */
|
||||
[data-slot="collapsible-content"] {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
transition: grid-template-rows 250ms ease-out;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&[data-expanded] {
|
||||
grid-template-rows: 1fr;
|
||||
animation: reasoning-card-down 220ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
&[data-closed] {
|
||||
animation: reasoning-card-up 160ms cubic-bezier(0.77, 0, 0.175, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,10 +621,6 @@ html[data-theme="kilo-vscode"] [data-component="reasoning-part"] {
|
||||
[data-component="collapsible"].tool-collapsible {
|
||||
gap: 0;
|
||||
border-radius: 0;
|
||||
|
||||
&:has([data-slot="collapsible-trigger"][aria-expanded="true"]) {
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="collapsible-trigger"] {
|
||||
@@ -667,6 +686,12 @@ html[data-theme="kilo-vscode"] [data-component="reasoning-part"] {
|
||||
width: calc(100% + 12px);
|
||||
}
|
||||
|
||||
[data-slot="reasoning-details"]::before {
|
||||
display: block;
|
||||
height: 6px;
|
||||
content: "";
|
||||
}
|
||||
|
||||
[data-slot="reasoning-content"] {
|
||||
padding: 8px 12px 10px;
|
||||
border: 0;
|
||||
@@ -712,3 +737,10 @@ html[data-theme="kilo-vscode"] [data-component="reasoning-part"] {
|
||||
line-height: var(--line-height-large);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[data-component="reasoning-part"] [data-slot="collapsible-content"][data-expanded],
|
||||
[data-component="reasoning-part"] [data-slot="collapsible-content"][data-closed] {
|
||||
animation-duration: 0.01ms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1591,8 +1591,10 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
|
||||
<Collapsible.Arrow />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<div data-slot="reasoning-content" ref={ref} onScroll={onScroll} onWheel={onWheel}>
|
||||
<Markdown text={view().body} cacheKey={id} streaming={!done()} />
|
||||
<div data-slot="reasoning-details">
|
||||
<div data-slot="reasoning-content" ref={ref} onScroll={onScroll} onWheel={onWheel}>
|
||||
<Markdown text={view().body} cacheKey={id} streaming={!done()} />
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
|
||||
@@ -25,6 +25,10 @@ AGENTS.md
|
||||
|
||||
# Include bin/ directory for CLI binary (production)
|
||||
!bin/**
|
||||
# .cli-version is a local-source build marker written by local-bin.ts. It must
|
||||
# not ship in the VSIX, otherwise production installs are detected as local
|
||||
# builds and may inject a dev-only bwrap fallback (see ServerManager.localCli).
|
||||
bin/.cli-version
|
||||
|
||||
# Include WAV assets used by extension-host notification playback
|
||||
!audio-wav/**
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { randomUUID } from "node:crypto"
|
||||
import {
|
||||
chmodSync,
|
||||
constants,
|
||||
copyFileSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
realpathSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
writeFileSync,
|
||||
} from "node:fs"
|
||||
import { dirname } from "node:path"
|
||||
import { localBwrapDigest, localBwrapPath, validLocalBwrap } from "../src/services/cli-backend/cli-resources"
|
||||
|
||||
export function currentBwrapTarget(): string {
|
||||
const os = process.platform === "win32" ? "win32" : process.platform
|
||||
return `${os}-${process.arch}`
|
||||
}
|
||||
|
||||
function arch(target: string): "x64" | "arm64" {
|
||||
if (target === "linux-x64") return "x64"
|
||||
if (target === "linux-arm64") return "arm64"
|
||||
throw new Error(`No Bubblewrap helper configured for target ${target}`)
|
||||
}
|
||||
|
||||
function source() {
|
||||
const configured = process.env.KILO_BWRAP_PATH
|
||||
if (configured) return realpathSync(configured)
|
||||
|
||||
const found = Bun.which("bwrap")
|
||||
if (!found) return
|
||||
const target = realpathSync(found)
|
||||
const entry = statSync(target)
|
||||
const uid = process.getuid?.()
|
||||
const groups = process.getgroups?.() ?? []
|
||||
const writable =
|
||||
(entry.mode & 0o002) !== 0 ||
|
||||
(uid !== undefined && entry.uid === uid && (entry.mode & 0o200) !== 0) ||
|
||||
(groups.includes(entry.gid) && (entry.mode & 0o020) !== 0)
|
||||
if (writable) {
|
||||
throw new Error(`Refusing writable Bubblewrap executable at ${target}; set KILO_BWRAP_PATH to trust it explicitly`)
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
function secure(dir: string) {
|
||||
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
||||
const entry = lstatSync(dir)
|
||||
if (
|
||||
entry.isSymbolicLink() ||
|
||||
!entry.isDirectory() ||
|
||||
entry.uid !== process.getuid?.() ||
|
||||
(entry.mode & 0o077) !== 0
|
||||
) {
|
||||
throw new Error(`Bubblewrap cache directory is not private: ${dir}`)
|
||||
}
|
||||
}
|
||||
|
||||
function stage(source: string, dest: string, digest: string) {
|
||||
const root = dirname(dirname(dest))
|
||||
const dir = dirname(dest)
|
||||
secure(root)
|
||||
secure(dir)
|
||||
|
||||
const token = `${process.pid}-${randomUUID()}`
|
||||
const executable = `${dest}.${token}.tmp`
|
||||
const checksum = `${executable}.sha256`
|
||||
try {
|
||||
copyFileSync(source, executable, constants.COPYFILE_EXCL)
|
||||
chmodSync(executable, 0o755)
|
||||
writeFileSync(checksum, `${digest}\n`, { flag: "wx", mode: 0o600 })
|
||||
renameSync(executable, dest)
|
||||
renameSync(checksum, `${dest}.sha256`)
|
||||
} finally {
|
||||
rmSync(executable, { force: true })
|
||||
rmSync(checksum, { force: true })
|
||||
}
|
||||
|
||||
if (!validLocalBwrap(dest)) throw new Error(`Could not validate staged Bubblewrap executable at ${dest}`)
|
||||
}
|
||||
|
||||
export async function ensureBwrapForTarget(target: string, root?: string): Promise<string | undefined> {
|
||||
const dest = localBwrapPath(target, root)
|
||||
if (!dest) return
|
||||
|
||||
const executable = source()
|
||||
if (executable) {
|
||||
const digest = localBwrapDigest(executable)
|
||||
if (validLocalBwrap(dest) && localBwrapDigest(dest) === digest) return dest
|
||||
stage(executable, dest, digest)
|
||||
return dest
|
||||
}
|
||||
if (validLocalBwrap(dest)) return dest
|
||||
|
||||
const { buildBubblewrap } = await import("../../opencode/script/kilocode/bubblewrap")
|
||||
const built = await buildBubblewrap(arch(target))
|
||||
stage(built.executable, dest, built.digest)
|
||||
return dest
|
||||
}
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
hasKiloSandboxWorker,
|
||||
hasTreeSitterResources,
|
||||
kiloSandboxWorkerForBinary,
|
||||
sanitizeSandboxResources,
|
||||
} from "../src/services/cli-backend/cli-resources"
|
||||
import { currentBwrapTarget, ensureBwrapForTarget } from "./bwrap-helper"
|
||||
import { currentFfmpegTarget, ensureFfmpegForTarget } from "./ffmpeg-helper"
|
||||
|
||||
const forceRebuild = process.argv.includes("--force")
|
||||
@@ -183,6 +185,13 @@ async function bundleKiloSandboxWorker() {
|
||||
await Bun.write(kiloSandboxWorkerForBinary(targetBinPath), result.outputs[0])
|
||||
}
|
||||
|
||||
async function ensureLocalHelpers() {
|
||||
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
|
||||
if (process.env.KILO_SKIP_BUNDLED_BWRAP === "1") return
|
||||
if (await sanitizeSandboxResources(targetBinDir, true)) return
|
||||
await ensureBwrapForTarget(currentBwrapTarget())
|
||||
}
|
||||
|
||||
async function writeSourceWrapper() {
|
||||
if (process.platform === "win32") {
|
||||
throw new Error("Compiled CLI build failed and source wrapper fallback is not supported on Windows.")
|
||||
@@ -202,7 +211,7 @@ async function writeSourceWrapper() {
|
||||
)
|
||||
chmodSync(targetBinPath, 0o755)
|
||||
await bundleKiloSandboxWorker()
|
||||
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
|
||||
await ensureLocalHelpers()
|
||||
|
||||
const hash = await cliSourceHash()
|
||||
if (hash) await Bun.write(versionFile, hash + "\n")
|
||||
@@ -224,7 +233,7 @@ async function main() {
|
||||
log(
|
||||
`CLI binary already present at ${relative(kiloVscodeDir, targetBinPath)} (${Math.round(st.size / 1024 / 1024)}MB). Use --force to rebuild.`,
|
||||
)
|
||||
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
|
||||
await ensureLocalHelpers()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -257,7 +266,7 @@ async function main() {
|
||||
await copySandboxResources(sourceBinPath, targetBinPath)
|
||||
await copyKiloSandboxWorker(sourceBinPath, targetBinPath)
|
||||
chmodSync(targetBinPath, 0o755)
|
||||
await ensureFfmpegForTarget(currentFfmpegTarget(), targetBinDir)
|
||||
await ensureLocalHelpers()
|
||||
|
||||
const hash = await cliSourceHash()
|
||||
if (hash) await Bun.write(versionFile, hash + "\n")
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import * as fs from "fs"
|
||||
import * as crypto from "crypto"
|
||||
import * as os from "os"
|
||||
import * as path from "path"
|
||||
|
||||
const dir = "tree-sitter"
|
||||
const runtime = "tree-sitter.wasm"
|
||||
const kiloSandboxWorker = "kilo-sandbox-mutation-worker.js"
|
||||
const bwrap = "bwrap"
|
||||
const bwrapLicense = path.join("licenses", "bubblewrap")
|
||||
const bwrapLicenseFiles = ["NOTICE", "COPYING", "MUSL-COPYRIGHT", "build.ts"]
|
||||
|
||||
function paths(file: string) {
|
||||
if (/^[a-z]:[\\/]/i.test(file) || file.includes("\\")) return path.win32
|
||||
@@ -51,17 +56,17 @@ export async function copyTreeSitterResources(source: string, target: string): P
|
||||
export async function copySandboxResources(source: string, target: string): Promise<void> {
|
||||
const from = path.dirname(source)
|
||||
const to = path.dirname(target)
|
||||
const helper = path.join(to, "bwrap")
|
||||
const destination = path.join(to, "licenses", "bubblewrap")
|
||||
const helper = path.join(to, bwrap)
|
||||
const destination = path.join(to, bwrapLicense)
|
||||
await fs.promises.rm(helper, { force: true })
|
||||
await fs.promises.rm(destination, { recursive: true, force: true })
|
||||
|
||||
const bwrap = path.join(from, "bwrap")
|
||||
if (!fs.existsSync(bwrap)) return
|
||||
await fs.promises.copyFile(bwrap, helper)
|
||||
const executable = path.join(from, bwrap)
|
||||
if (!fs.existsSync(executable)) return
|
||||
await fs.promises.copyFile(executable, helper)
|
||||
await fs.promises.chmod(helper, 0o755)
|
||||
|
||||
const licenses = path.join(from, "licenses", "bubblewrap")
|
||||
const licenses = path.join(from, bwrapLicense)
|
||||
if (!fs.existsSync(licenses)) return
|
||||
await fs.promises.cp(licenses, destination, { recursive: true })
|
||||
}
|
||||
@@ -72,3 +77,78 @@ export async function copyKiloSandboxWorker(source: string, target: string): Pro
|
||||
if (!fs.existsSync(from)) throw new Error(`Kilo sandbox mutation worker not found at ${from}`)
|
||||
await fs.promises.copyFile(from, to)
|
||||
}
|
||||
|
||||
function cacheRoot() {
|
||||
const root = process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache")
|
||||
return path.join(root, "kilo-vscode", "bwrap")
|
||||
}
|
||||
|
||||
function bwrapLicenseDir(bin: string) {
|
||||
return path.join(bin, bwrapLicense)
|
||||
}
|
||||
|
||||
function hasBwrapSource(dir: string) {
|
||||
try {
|
||||
return fs.readdirSync(dir).some((file) => /^bubblewrap-[a-f0-9]+\.tar\.gz$/.test(file))
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function hasProductionBwrap(bin: string) {
|
||||
const executable = path.join(bin, bwrap)
|
||||
const dir = bwrapLicenseDir(bin)
|
||||
try {
|
||||
const entry = fs.statSync(executable)
|
||||
if (!entry.isFile()) return false
|
||||
if (!bwrapLicenseFiles.every((file) => fs.existsSync(path.join(dir, file)))) return false
|
||||
return hasBwrapSource(dir)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function localBwrapPath(target: string, root = cacheRoot()): string | undefined {
|
||||
if (target !== "linux-x64" && target !== "linux-arm64") return undefined
|
||||
return path.join(root, target, bwrap)
|
||||
}
|
||||
|
||||
export function localBwrapDigest(file: string): string {
|
||||
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex")
|
||||
}
|
||||
|
||||
export function validLocalBwrap(file: string): boolean {
|
||||
try {
|
||||
const entry = fs.lstatSync(file)
|
||||
if (entry.isSymbolicLink() || !entry.isFile() || (entry.mode & 0o6000) !== 0) return false
|
||||
if ((entry.mode & 0o022) !== 0 || (entry.mode & 0o111) === 0) return false
|
||||
const digest = fs.readFileSync(`${file}.sha256`, "utf8").trim()
|
||||
if (!/^[a-f0-9]{64}$/.test(digest)) return false
|
||||
return localBwrapDigest(file) === digest
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveLocalBwrapEnv(
|
||||
extension: string,
|
||||
local: boolean,
|
||||
target = `${process.platform === "win32" ? "win32" : process.platform}-${process.arch}`,
|
||||
root?: string,
|
||||
): Record<string, string> {
|
||||
if (hasProductionBwrap(path.join(extension, "bin"))) return {}
|
||||
if (!local) return {}
|
||||
|
||||
const executable = localBwrapPath(target, root)
|
||||
if (!executable || !validLocalBwrap(executable)) return {}
|
||||
return { KILO_BWRAP_PATH: executable }
|
||||
}
|
||||
|
||||
export async function sanitizeSandboxResources(bin: string, local: boolean): Promise<boolean> {
|
||||
if (hasProductionBwrap(bin)) return true
|
||||
if (!local) return false
|
||||
|
||||
await fs.promises.rm(path.join(bin, bwrap), { force: true })
|
||||
await fs.promises.rm(bwrapLicenseDir(bin), { recursive: true, force: true })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as crypto from "crypto"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import * as vscode from "vscode"
|
||||
import { resolveTreeSitterEnv } from "./cli-resources"
|
||||
import { resolveLocalBwrapEnv, resolveTreeSitterEnv } from "./cli-resources"
|
||||
import { t } from "./i18n"
|
||||
import { parseServerPort } from "./server-utils"
|
||||
|
||||
@@ -94,6 +94,10 @@ export class ServerManager {
|
||||
const spawnCwd = resolveServerCwd(folders, this.context.globalStorageUri.fsPath)
|
||||
fs.mkdirSync(spawnCwd, { recursive: true })
|
||||
const indexingEnv = resolveIndexingEnv(folders)
|
||||
const localCli =
|
||||
this.context.extensionMode === vscode.ExtensionMode.Development ||
|
||||
fs.existsSync(path.join(this.context.extensionPath, "bin", ".cli-version"))
|
||||
const bwrapEnv = process.env.KILO_BWRAP_PATH ? {} : resolveLocalBwrapEnv(this.context.extensionPath, localCli)
|
||||
// TLS / corporate-proxy support:
|
||||
// - Default NODE_USE_SYSTEM_CA=1 so the bundled Bun CLI trusts the OS
|
||||
// trust store (Windows cert store, macOS keychain, Linux /etc/ssl).
|
||||
@@ -141,6 +145,7 @@ export class ServerManager {
|
||||
KILOCODE_EDITOR_NAME: `${vscode.env.appName} ${vscode.version}`,
|
||||
...(!claudeCompat && { KILO_DISABLE_CLAUDE_CODE: "true" }),
|
||||
...resolveTreeSitterEnv(this.context.extensionPath),
|
||||
...bwrapEnv,
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: true,
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test"
|
||||
import * as fs from "node:fs/promises"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
import { ensureBwrapForTarget } from "../../script/bwrap-helper"
|
||||
import {
|
||||
localBwrapPath,
|
||||
resolveLocalBwrapEnv,
|
||||
sanitizeSandboxResources,
|
||||
validLocalBwrap,
|
||||
} from "../../src/services/cli-backend/cli-resources"
|
||||
|
||||
const configured = process.env.KILO_BWRAP_PATH
|
||||
|
||||
afterEach(() => {
|
||||
if (configured === undefined) {
|
||||
delete process.env.KILO_BWRAP_PATH
|
||||
return
|
||||
}
|
||||
process.env.KILO_BWRAP_PATH = configured
|
||||
})
|
||||
|
||||
describe("local Bubblewrap helper", () => {
|
||||
it("copies the configured helper to a cache outside the extension", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-"))
|
||||
try {
|
||||
const source = path.join(root, "source", "bwrap")
|
||||
const extension = path.join(root, "workspace", "packages", "kilo-vscode")
|
||||
const cache = path.join(root, "cache")
|
||||
await fs.mkdir(path.dirname(source), { recursive: true })
|
||||
await fs.writeFile(source, "bubblewrap")
|
||||
process.env.KILO_BWRAP_PATH = source
|
||||
|
||||
const dest = await ensureBwrapForTarget("linux-x64", cache)
|
||||
|
||||
expect(dest?.startsWith(extension)).toBe(false)
|
||||
expect(await fs.readFile(dest!, "utf8")).toBe("bubblewrap")
|
||||
expect((await fs.stat(dest!)).mode & 0o111).not.toBe(0)
|
||||
expect(resolveLocalBwrapEnv(extension, true, "linux-x64", cache)).toEqual({ KILO_BWRAP_PATH: dest })
|
||||
expect(resolveLocalBwrapEnv(extension, false, "linux-x64", cache)).toEqual({})
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("prefers a complete production helper bundled beside the CLI", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-"))
|
||||
try {
|
||||
const extension = path.join(root, "extension")
|
||||
const bin = path.join(extension, "bin")
|
||||
const licenses = path.join(bin, "licenses", "bubblewrap")
|
||||
await fs.mkdir(licenses, { recursive: true })
|
||||
await fs.writeFile(path.join(bin, "bwrap"), "bundled")
|
||||
for (const file of ["NOTICE", "COPYING", "MUSL-COPYRIGHT", "build.ts"]) {
|
||||
await fs.writeFile(path.join(licenses, file), file)
|
||||
}
|
||||
await fs.writeFile(path.join(licenses, "bubblewrap-deadbeef.tar.gz"), "source")
|
||||
|
||||
expect(resolveLocalBwrapEnv(extension, true, "linux-x64", path.join(root, "cache"))).toEqual({})
|
||||
expect(await sanitizeSandboxResources(bin, true)).toBe(true)
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("removes an incomplete helper before local packaging", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-"))
|
||||
try {
|
||||
const bin = path.join(root, "extension", "bin")
|
||||
const helper = path.join(bin, "bwrap")
|
||||
await fs.mkdir(bin, { recursive: true })
|
||||
await fs.writeFile(helper, "stale")
|
||||
|
||||
expect(await sanitizeSandboxResources(bin, true)).toBe(false)
|
||||
expect(
|
||||
await fs.stat(helper).then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
).toBe(false)
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("rejects a symlinked or public cache", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-"))
|
||||
try {
|
||||
const source = path.join(root, "source")
|
||||
const cache = path.join(root, "cache")
|
||||
const dest = localBwrapPath("linux-x64", cache)!
|
||||
await fs.writeFile(source, "bubblewrap")
|
||||
await fs.mkdir(path.dirname(dest), { recursive: true })
|
||||
await fs.symlink(source, dest)
|
||||
await fs.writeFile(`${dest}.sha256`, "0".repeat(64))
|
||||
expect(validLocalBwrap(dest)).toBe(false)
|
||||
|
||||
await fs.rm(cache, { recursive: true, force: true })
|
||||
await fs.mkdir(cache, { recursive: true, mode: 0o777 })
|
||||
await fs.chmod(cache, 0o777)
|
||||
process.env.KILO_BWRAP_PATH = source
|
||||
await expect(ensureBwrapForTarget("linux-x64", cache)).rejects.toThrow("cache directory is not private")
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it("does not stage Bubblewrap for unsupported operating systems", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "kilo-vscode-bwrap-"))
|
||||
try {
|
||||
process.env.KILO_BWRAP_PATH = path.join(root, "missing")
|
||||
|
||||
const dest = await ensureBwrapForTarget("darwin-arm64", path.join(root, "cache"))
|
||||
|
||||
expect(dest).toBeUndefined()
|
||||
expect(resolveLocalBwrapEnv(path.join(root, "extension"), true, "darwin-arm64", root)).toEqual({})
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -33,6 +33,7 @@ export interface BasicToolProps {
|
||||
onOpenChange?: (open: boolean) => void
|
||||
forceOpen?: boolean
|
||||
defer?: boolean
|
||||
retainDetails?: boolean // kilocode_change
|
||||
hasDetails?: boolean // kilocode_change
|
||||
locked?: boolean
|
||||
animated?: boolean
|
||||
@@ -133,7 +134,7 @@ export function BasicTool(props: BasicToolProps) {
|
||||
if (!props.defer) return
|
||||
if (!value) {
|
||||
cancel()
|
||||
setState("ready", false)
|
||||
if (!props.retainDetails) setState("ready", false) // kilocode_change
|
||||
return
|
||||
}
|
||||
|
||||
@@ -183,6 +184,14 @@ export function BasicTool(props: BasicToolProps) {
|
||||
props.onOpenChange?.(value) // kilocode_change
|
||||
}
|
||||
|
||||
// kilocode_change start
|
||||
const end = (event: AnimationEvent) => {
|
||||
if (event.target !== event.currentTarget) return
|
||||
if (!props.retainDetails || open()) return
|
||||
setState("ready", false)
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
const trigger = () => (
|
||||
<div
|
||||
data-component="tool-trigger"
|
||||
@@ -307,7 +316,7 @@ export function BasicTool(props: BasicToolProps) {
|
||||
</Show>
|
||||
{/* kilocode_change start */}
|
||||
<Show when={!props.animated && (hasChildren() || hasDetails()) && !props.hideDetails}>
|
||||
<Collapsible.Content>
|
||||
<Collapsible.Content onAnimationEnd={end}>
|
||||
<Show when={!props.defer || ready()}>{props.children}</Show>
|
||||
</Collapsible.Content>
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user