From 9e04e74ebd7be6dce7ed15d7394c9b9eb2772e64 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 25 Jun 2026 13:01:31 +0200 Subject: [PATCH 1/4] feat(agent-manager): add sandbox toggle to new worktree modal Let users choose whether a worktree session starts in the sandbox from the Agent Manager New Worktree modal. The lock button mirrors the sidebar prompt sandbox toggle and is gated by the same features().sandboxControls flag. The CLI session.create endpoint exposes no sandbox parameter, so after each worktree session is created the provider reconciles its sandbox override to the user's choice via sandbox.status then sandbox.toggle (toggling only when the current state differs, so it is safe regardless of the global default). Sandbox setup is best-effort: failures are logged and the worktree stays usable with manual toggle available from the prompt. Visibility is forward-compatible with the sandbox experiment exposure work: the button is hidden until sandboxControls is available, so it requires no change to appear once that lands. --- .changeset/agent-manager-worktree-sandbox.md | 5 ++ .../src/agent-manager/AgentManagerProvider.ts | 17 +++++++ .../src/agent-manager/sandbox-bootstrap.ts | 46 +++++++++++++++++++ .../kilo-vscode/src/agent-manager/types.ts | 2 + .../agent-manager/NewWorktreeDialog.tsx | 26 ++++++++++- .../src/types/messages/webview-messages.ts | 3 ++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 .changeset/agent-manager-worktree-sandbox.md create mode 100644 packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts diff --git a/.changeset/agent-manager-worktree-sandbox.md b/.changeset/agent-manager-worktree-sandbox.md new file mode 100644 index 0000000000..75c4a0cab1 --- /dev/null +++ b/.changeset/agent-manager-worktree-sandbox.md @@ -0,0 +1,5 @@ +--- +"kilo-code": minor +--- + +Add a sandbox toggle to the Agent Manager New Worktree modal so each worktree session can start sandboxed diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 3e2b59e592..1c7cf9d265 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -39,6 +39,7 @@ import { startSession } from "./mcp-warmup" import { readTerminalFont, watchTerminalFont } from "./terminal-font" import { buildKeybindingMap } from "./format-keybinding" import { resolveVersionModels, buildInitialMessages, type CreatedVersion } from "./multi-version" +import { ensureSandbox } from "./sandbox-bootstrap" import { Semaphore } from "./semaphore" import { PLATFORM } from "./constants" import type { AgentManagerOutMessage, AgentManagerInMessage } from "./types" @@ -1290,6 +1291,22 @@ export class AgentManagerProvider implements Disposable { this.registerWorktreeSession(session.id, wt.result.path) this.notifyWorktreeReady(session.id, wt.result, wt.worktree.id) + // Reconcile the session sandbox to the user's choice before the initial + // prompt runs, so tool execution is confined when sandbox is requested. + if (msg.sandbox !== undefined) { + try { + await ensureSandbox( + this.connectionService.getClient(), + session.id, + wt.result.path, + msg.sandbox, + (m) => this.log(m), + ) + } catch (err) { + this.log(`Sandbox setup skipped for ${session.id}:`, err) + } + } + // Set the per-version model immediately so the UI selector reflects // the correct model as soon as the worktree appears, before Phase 2. // Uses a dedicated message type to avoid clearing the busy state. diff --git a/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts b/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts new file mode 100644 index 0000000000..3505d22120 --- /dev/null +++ b/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts @@ -0,0 +1,46 @@ +// Ensure a CLI session's sandbox override matches the desired state. +// +// `session.create` exposes no sandbox parameter, so sandbox state is reconciled +// after the session exists via `sandbox.toggle` (which flips state). This checks +// the current status first and toggles only when the state differs, so it is +// safe regardless of the global `experimental.sandbox` default. +// +// Pure runtime helper — no vscode imports; takes the SDK client directly so it +// can be unit-tested in isolation. Failures are logged and swallowed: sandbox is +// a best-effort safety enhancement, and a worktree stays usable if the sandbox +// backend is unavailable (the user can toggle it manually from the prompt). + +import type { KiloClient } from "@kilocode/sdk/v2/client" + +export async function ensureSandbox( + client: KiloClient, + sessionId: string, + directory: string, + desired: boolean, + log: (msg: string) => void, +): Promise { + const sandbox = client.sandbox + let current: boolean + try { + const { data } = await sandbox.status({ sessionID: sessionId, directory }, { throwOnError: true }) + if (!data.available) { + log(`Sandbox unavailable for ${sessionId}: ${data.reason ?? "unknown"}`) + return + } + current = data.enabled + } catch (err) { + log(`Sandbox status check failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`) + return + } + if (current === desired) return + try { + const { data } = await sandbox.toggle({ sessionID: sessionId, directory }, { throwOnError: true }) + if (!data.available) { + log(`Sandbox toggle unavailable for ${sessionId}: ${data.reason ?? "unknown"}`) + return + } + log(`Sandbox ${data.enabled ? "enabled" : "disabled"} for ${sessionId}`) + } catch (err) { + log(`Sandbox toggle failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`) + } +} diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index e48b9fa7a4..b4c7ec2b9e 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -439,6 +439,8 @@ interface CreateMultiVersionIn { baseBranch?: string branchName?: string modelAllocations?: Array<{ providerID: string; modelID: string; count: number }> + /** When set, reconcile each created session's sandbox override to this state. */ + sandbox?: boolean } interface RenameWorktreeIn { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 25679aad3c..2e1e184532 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -71,7 +71,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const server = useServer() const session = useSession() const provider = useProvider() - const { config } = useConfig() + const { config, features } = useConfig() const metrics = tracker(vscode) const track = (button: string, properties?: Record) => metrics.track(button, "configure_worktree_dialog", properties) @@ -102,6 +102,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const [compareOpen, setCompareOpen] = createSignal(false) const [highlightedIndex, setHighlightedIndex] = createSignal(0) const [variant, setVariant] = createSignal(session.currentVariant()) + const [sandbox, setSandbox] = createSignal(config().experimental?.sandbox === true) const speech = useSpeechToText(vscode, server, { t }) const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) @@ -246,6 +247,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran baseBranch: advanced ? (baseBranch() ?? undefined) : undefined, branchName: customBranch, modelAllocations: allocations, + sandbox: features().sandboxControls ? sandbox() : undefined, files: imgFiles, }) @@ -458,6 +460,28 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran + + + + +
diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 781df064c8..89e83f152a 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -681,6 +681,9 @@ export interface CreateMultiVersionRequest { // Overrides `versions`, `providerID`, and `modelID`. variant?: string modelAllocations?: ModelAllocation[] + // When set, start each created worktree session with the sandbox override + // reconciled to this state. Only sent when sandbox controls are available. + sandbox?: boolean } // Persist tab order for a context (worktree ID or "local") From 3a072e9b2b1ba5f4da6fc1587383411af05d3805 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 25 Jun 2026 13:08:41 +0200 Subject: [PATCH 2/4] refactor(vscode): share one SandboxButton between prompt and worktree modal Extract the sandbox lock toggle into a shared SandboxButtonBase component so the chat prompt and the Agent Manager New Worktree modal render the exact same control instead of duplicating the markup. The base accepts enabled/availability/ reason/disabled/onToggle props; the prompt wires live session sandbox state and the modal wires a local preference, but both surface identical visuals (lock icon, prompt-status-button active styling, tooltip, aria). --- .../agent-manager/NewWorktreeDialog.tsx | 30 ++++------ .../src/components/chat/PromptInput.tsx | 39 +++---------- .../src/components/shared/SandboxButton.tsx | 56 +++++++++++++++++++ 3 files changed, 74 insertions(+), 51 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/components/shared/SandboxButton.tsx diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 2e1e184532..4fef0bfd3e 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -21,6 +21,7 @@ import { ModeSwitcherBase } from "../src/components/shared/ModeSwitcher" import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton" import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" import { ThinkingSelectorBase } from "../src/components/shared/ThinkingSelector" +import { SandboxButtonBase } from "../src/components/shared/SandboxButton" import { MultiModelSelector, type ModelAllocations, @@ -461,26 +462,15 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran - - - + setSandbox(!sandbox()), + () => ({ enabled: !sandbox() }), + )} + />
diff --git a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx index 51daa8f977..b668a94cd8 100644 --- a/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx @@ -20,6 +20,7 @@ import { useConfig } from "../../context/config" import { useProvider } from "../../context/provider" import { ModelSelector } from "../shared/ModelSelector" import { ModeSwitcher } from "../shared/ModeSwitcher" +import { SandboxButtonBase } from "../shared/SandboxButton" import { SpeechToTextButton } from "../speech-to-text/SpeechToTextButton" import { canUseSpeechToText, selectedSpeechToTextModel } from "../speech-to-text/availability" import { ThinkingSelector } from "../shared/ThinkingSelector" @@ -1215,37 +1216,13 @@ export const PromptInput: Component = (props) => { - - - + + + ) +} From 26ea495c337de88856e6cbac120a03b12a4f5792 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 25 Jun 2026 14:02:03 +0200 Subject: [PATCH 3/4] fix(agent-manager): fail closed when sandbox setup fails Require the sandbox experiment for the New Worktree control and omit the request field when the experiment is unavailable. Verify the selected sandbox state and routed directory before exposing each new session. If reconciliation fails, report the setup error, delete the fresh session and worktree, and skip the initial prompt so execution cannot continue unrestricted. Add SDK-backed reconciliation tests and update the shared prompt button contract test. --- .../src/agent-manager/AgentManagerProvider.ts | 57 ++++++-- .../src/agent-manager/sandbox-bootstrap.ts | 79 +++++------ .../prompt-input-connection-guard.test.ts | 7 +- .../tests/unit/sandbox-bootstrap.test.ts | 125 ++++++++++++++++++ .../agent-manager/NewWorktreeDialog.tsx | 6 +- .../src/components/shared/SandboxButton.tsx | 11 +- 6 files changed, 220 insertions(+), 65 deletions(-) create mode 100644 packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts diff --git a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts index 1c7cf9d265..10274a0d15 100644 --- a/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts +++ b/packages/kilo-vscode/src/agent-manager/AgentManagerProvider.ts @@ -851,6 +851,28 @@ export class AgentManagerProvider implements Disposable { } } + /** Remove a worktree whose session could not be safely initialized. */ + private async discardWorktree(id: string, dir: string, sessionId?: string): Promise { + this.getStateManager()?.removeWorktree(id) + this.pushState() + + if (sessionId) { + try { + await this.connectionService + .getClient() + .session.delete({ sessionID: sessionId, directory: dir }, { throwOnError: true }) + } catch (err) { + this.log(`Failed to delete session ${sessionId} after worktree setup failed:`, err) + } + } + + try { + await this.getWorktreeManager()?.removeWorktree(dir) + } catch (err) { + this.log(`Failed to remove worktree ${id} after setup failed:`, err) + } + } + /** Send worktreeSetup.ready + sessionMeta + pushState after worktree creation. */ private notifyWorktreeReady(sessionId: string, result: CreateWorktreeResult, worktreeId?: string): void { this.pushState() @@ -1288,25 +1310,34 @@ export class AgentManagerProvider implements Disposable { const state = this.getStateManager()! state.addSession(session.id, wt.worktree.id) - this.registerWorktreeSession(session.id, wt.result.path) - this.notifyWorktreeReady(session.id, wt.result, wt.worktree.id) - // Reconcile the session sandbox to the user's choice before the initial - // prompt runs, so tool execution is confined when sandbox is requested. + // Sandbox must match the user's choice before this session is exposed or + // receives its initial prompt. A failed reconciliation aborts this version. if (msg.sandbox !== undefined) { try { - await ensureSandbox( - this.connectionService.getClient(), - session.id, - wt.result.path, - msg.sandbox, - (m) => this.log(m), - ) - } catch (err) { - this.log(`Sandbox setup skipped for ${session.id}:`, err) + await ensureSandbox(this.connectionService.getClient(), session.id, wt.result.path, msg.sandbox) + } catch (error) { + const err = getErrorMessage(error) + this.log(`Failed to configure sandbox for ${session.id}: ${err}`) + this.postToWebview({ + type: "agentManager.worktreeSetup", + status: "error", + message: `Failed to configure sandbox: ${err}`, + worktreeId: wt.worktree.id, + }) + this.host.capture("Agent Manager Session Error", { + source: PLATFORM, + error: err, + context: "configureSandbox", + }) + await this.discardWorktree(wt.worktree.id, wt.result.path, session.id) + continue } } + this.registerWorktreeSession(session.id, wt.result.path) + this.notifyWorktreeReady(session.id, wt.result, wt.worktree.id) + // Set the per-version model immediately so the UI selector reflects // the correct model as soon as the worktree appears, before Phase 2. // Uses a dedicated message type to avoid clearing the busy state. diff --git a/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts b/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts index 3505d22120..13e81e6ebc 100644 --- a/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts +++ b/packages/kilo-vscode/src/agent-manager/sandbox-bootstrap.ts @@ -1,46 +1,39 @@ -// Ensure a CLI session's sandbox override matches the desired state. -// -// `session.create` exposes no sandbox parameter, so sandbox state is reconciled -// after the session exists via `sandbox.toggle` (which flips state). This checks -// the current status first and toggles only when the state differs, so it is -// safe regardless of the global `experimental.sandbox` default. -// -// Pure runtime helper — no vscode imports; takes the SDK client directly so it -// can be unit-tested in isolation. Failures are logged and swallowed: sandbox is -// a best-effort safety enhancement, and a worktree stays usable if the sandbox -// backend is unavailable (the user can toggle it manually from the prompt). - import type { KiloClient } from "@kilocode/sdk/v2/client" +import { sameDirectory } from "../kilo-provider-utils" -export async function ensureSandbox( - client: KiloClient, - sessionId: string, - directory: string, - desired: boolean, - log: (msg: string) => void, -): Promise { - const sandbox = client.sandbox - let current: boolean - try { - const { data } = await sandbox.status({ sessionID: sessionId, directory }, { throwOnError: true }) - if (!data.available) { - log(`Sandbox unavailable for ${sessionId}: ${data.reason ?? "unknown"}`) - return - } - current = data.enabled - } catch (err) { - log(`Sandbox status check failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`) - return - } - if (current === desired) return - try { - const { data } = await sandbox.toggle({ sessionID: sessionId, directory }, { throwOnError: true }) - if (!data.available) { - log(`Sandbox toggle unavailable for ${sessionId}: ${data.reason ?? "unknown"}`) - return - } - log(`Sandbox ${data.enabled ? "enabled" : "disabled"} for ${sessionId}`) - } catch (err) { - log(`Sandbox toggle failed for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`) - } +type State = { + directory: string + enabled: boolean + available: boolean + reason?: string + version: number +} + +function unavailable(state: State) { + return new Error(state.reason ?? "Sandbox backend is unavailable") +} + +function routed(state: State, dir: string) { + if (!sameDirectory(state.directory, dir)) throw new Error("Sandbox status resolved a different directory") +} + +function confirm(state: State, dir: string, desired: boolean) { + routed(state, dir) + if (desired && !state.available) throw unavailable(state) + if (state.enabled !== desired) { + throw new Error(`Sandbox remained ${state.enabled ? "enabled" : "disabled"} after reconciliation`) + } + return state +} + +/** Ensure a new session uses the selected sandbox state before its first prompt. */ +export async function ensureSandbox(client: KiloClient, sid: string, dir: string, desired: boolean): Promise { + const sandbox = client.sandbox + const { data: current } = await sandbox.status({ sessionID: sid, directory: dir }, { throwOnError: true }) + routed(current, dir) + if (current.enabled === desired) return confirm(current, dir, desired) + if (!current.available) throw unavailable(current) + + const { data: next } = await sandbox.toggle({ sessionID: sid, directory: dir }, { throwOnError: true }) + return confirm(next, dir, desired) } diff --git a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts index 4fab1cf5a3..951e1748a0 100644 --- a/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts +++ b/packages/kilo-vscode/tests/unit/prompt-input-connection-guard.test.ts @@ -3,7 +3,9 @@ import { readFileSync } from "node:fs" import { join } from "node:path" const path = join(__dirname, "..", "..", "webview-ui", "src", "components", "chat", "PromptInput.tsx") +const buttonPath = join(__dirname, "..", "..", "webview-ui", "src", "components", "shared", "SandboxButton.tsx") const src = readFileSync(path, "utf8") +const button = readFileSync(buttonPath, "utf8") describe("PromptInput connection guard", () => { it("rechecks the connection after resolving async attachments and before clearing the draft", () => { @@ -62,8 +64,11 @@ describe("PromptInput sandbox toggle", () => { expect(src).toContain("const target = untrack(sandboxTarget)") expect(src).toContain("if (target && target !== sessionID) clearSandboxRequest()") expect(src).toContain("sandbox()?.enabled ?? (!sandboxID() && config().experimental?.sandbox === true)") - expect(src).toContain("aria-pressed={sandboxEnabled()}") + expect(src).toContain(" { + const request = input instanceof Request ? input : new Request(input, init) + calls.push(`${request.method} ${new URL(request.url).pathname}`) + const state = states.shift() + if (!state) return Response.json({ message: "Unexpected request" }, { status: 500 }) + return Response.json(state) + }, + { preconnect: globalThis.fetch.preconnect }, + ) satisfies typeof globalThis.fetch + + return { + calls, + client: createKiloClient({ baseUrl: "http://localhost", fetch }), + } +} + +function state(enabled: boolean, available = true, directory = "/repo"): State { + return { directory, enabled, available, version: 1 } +} + +describe("ensureSandbox", () => { + test("does not toggle when the effective state already matches", async () => { + const ctx = setup([state(true)]) + + const result = await ensureSandbox(ctx.client, "session-1", "/repo", true) + + expect(result.enabled).toBe(true) + expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"]) + }) + + test("toggles and verifies the selected state", async () => { + const ctx = setup([state(false), state(true)]) + + const result = await ensureSandbox(ctx.client, "session-1", "/repo", true) + + expect(result.enabled).toBe(true) + expect(ctx.calls).toEqual(["GET /session/session-1/sandbox", "POST /session/session-1/sandbox/toggle"]) + }) + + test("rejects unavailable sandboxing when sandbox was requested", async () => { + const unavailable = { ...state(false, false), reason: "Sandbox backend unavailable" } + const ctx = setup([unavailable]) + + expect(ensureSandbox(ctx.client, "session-1", "/repo", true)).rejects.toThrow("Sandbox backend unavailable") + expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"]) + }) + + test("allows an effectively disabled sandbox when the backend is unavailable", async () => { + const ctx = setup([state(false, false)]) + + const result = await ensureSandbox(ctx.client, "session-1", "/repo", false) + + expect(result.enabled).toBe(false) + expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"]) + }) + + test("rejects a toggle that does not reach the selected state", async () => { + const ctx = setup([state(false), state(false)]) + + expect(ensureSandbox(ctx.client, "session-1", "/repo", true)).rejects.toThrow( + "Sandbox remained disabled after reconciliation", + ) + }) + + test("rejects status returned for a different directory without toggling", async () => { + const ctx = setup([state(false, true, "/other")]) + + expect(ensureSandbox(ctx.client, "session-1", "/repo", true)).rejects.toThrow( + "Sandbox status resolved a different directory", + ) + expect(ctx.calls).toEqual(["GET /session/session-1/sandbox"]) + }) +}) + +describe("Agent Manager sandbox startup", () => { + const provider = readFileSync(join(__dirname, "..", "..", "src", "agent-manager", "AgentManagerProvider.ts"), "utf8") + const dialog = readFileSync( + join(__dirname, "..", "..", "webview-ui", "agent-manager", "NewWorktreeDialog.tsx"), + "utf8", + ) + + test("reconciles before exposing or prompting the session", () => { + const start = provider.indexOf("private async onCreateMultiVersion") + const end = provider.indexOf("\n private ", start + 1) + const body = provider.slice(start, end) + const ensure = body.indexOf("await ensureSandbox") + const discard = body.indexOf("await this.discardWorktree", ensure) + const skip = body.indexOf("continue", discard) + const register = body.indexOf("this.registerWorktreeSession", ensure) + const ready = body.indexOf("this.notifyWorktreeReady", register) + const created = body.indexOf("created.push", ready) + const prompt = body.indexOf('type: "agentManager.sendInitialMessage"', created) + + expect(ensure).toBeGreaterThan(-1) + expect(discard).toBeGreaterThan(ensure) + expect(skip).toBeGreaterThan(discard) + expect(register).toBeGreaterThan(skip) + expect(ready).toBeGreaterThan(register) + expect(created).toBeGreaterThan(ready) + expect(prompt).toBeGreaterThan(created) + }) + + test("uses the experiment-aware visibility condition for UI and payload", () => { + expect(dialog).toContain("const sandboxVisible = () => isSandboxVisible(features(), config())") + expect(dialog).toContain("sandbox: sandboxVisible() ? sandbox() : undefined") + expect(dialog).toContain("") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 4fef0bfd3e..83e6fcb017 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -20,6 +20,7 @@ import { ModelSelectorBase } from "../src/components/shared/ModelSelector" import { ModeSwitcherBase } from "../src/components/shared/ModeSwitcher" import { SpeechToTextButton } from "../src/components/speech-to-text/SpeechToTextButton" import { canUseSpeechToText, selectedSpeechToTextModel } from "../src/components/speech-to-text/availability" +import { visible as isSandboxVisible } from "../src/components/settings/sandboxing" import { ThinkingSelectorBase } from "../src/components/shared/ThinkingSelector" import { SandboxButtonBase } from "../src/components/shared/SandboxButton" import { @@ -104,6 +105,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran const [highlightedIndex, setHighlightedIndex] = createSignal(0) const [variant, setVariant] = createSignal(session.currentVariant()) const [sandbox, setSandbox] = createSignal(config().experimental?.sandbox === true) + const sandboxVisible = () => isSandboxVisible(features(), config()) const speech = useSpeechToText(vscode, server, { t }) const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) @@ -248,7 +250,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran baseBranch: advanced ? (baseBranch() ?? undefined) : undefined, branchName: customBranch, modelAllocations: allocations, - sandbox: features().sandboxControls ? sandbox() : undefined, + sandbox: sandboxVisible() ? sandbox() : undefined, files: imgFiles, }) @@ -461,7 +463,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran - + = (props) => { size="small" onClick={props.onToggle} disabled={props.disabled || unavailable()} - aria-label={props.enabled ? language.t("prompt.action.sandbox.disable") : language.t("prompt.action.sandbox.enable")} + aria-label={ + props.enabled ? language.t("prompt.action.sandbox.disable") : language.t("prompt.action.sandbox.enable") + } aria-pressed={props.enabled} class={`prompt-status-button ${props.enabled ? "prompt-status-button--active" : ""}`} > From 8ffec05df5436eb61a13e2d8556fabd1b331afc8 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 25 Jun 2026 16:01:09 +0200 Subject: [PATCH 4/4] fix(agent-manager): align sandbox with prompt actions Place the New Worktree sandbox toggle in the right-aligned action group beside speech-to-text instead of grouping it with mode, model, and thinking selectors. Add a structural regression assertion for the action placement. --- .../tests/unit/sandbox-bootstrap.test.ts | 13 +++++++++++++ .../webview-ui/agent-manager/NewWorktreeDialog.tsx | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts index 6c1012d5f6..136dc3da29 100644 --- a/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts +++ b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts @@ -128,4 +128,17 @@ describe("Agent Manager sandbox startup", () => { expect(dialog).toContain("sandbox: sandboxVisible() ? sandbox() : undefined") expect(dialog).toContain("") }) + + test("places the sandbox toggle with prompt actions instead of model selectors", () => { + const selectors = dialog.indexOf('
') + const actions = dialog.indexOf('
', selectors) + const sandbox = dialog.indexOf(" void; defaultBaseBran +
+
void; defaultBaseBran )} /> -
-