Merge branch 'main' into feat/linux-network-sandbox

This commit is contained in:
Marius
2026-06-25 10:09:43 +02:00
committed by GitHub
15 changed files with 476 additions and 37 deletions
+5
View File
@@ -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.
+12
View File
@@ -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
+55 -3
View File
@@ -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;
}
}
}
+10 -8
View File
@@ -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>
+4
View File
@@ -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/**
+101
View File
@@ -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
}
+12 -3
View File
@@ -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 })
}
})
})
+11 -2
View File
@@ -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>