diff --git a/.changeset/agent-manager-sandbox-default.md b/.changeset/agent-manager-sandbox-default.md new file mode 100644 index 00000000000..f93953ab147 --- /dev/null +++ b/.changeset/agent-manager-sandbox-default.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Remember the Agent Manager new-worktree sandbox toggle for future sessions. diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index d8f663c3139..ec08b55391e 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -1167,7 +1167,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await this.fetchAndSendSandboxStatus(message.sessionID) break case "requestSandboxDefault": - await this.fetchAndSendSandboxDefault(message.contextDirectory) + await this.fetchAndSendSandboxDefault(message.contextDirectory, message.requestID) break case "setSandboxDefault": await this.handleSetSandboxDefault(message.enabled, message.requestID, message.contextDirectory) diff --git a/packages/kilo-vscode/src/agent-manager/types.ts b/packages/kilo-vscode/src/agent-manager/types.ts index 3fda920167d..0c8aeebff75 100644 --- a/packages/kilo-vscode/src/agent-manager/types.ts +++ b/packages/kilo-vscode/src/agent-manager/types.ts @@ -643,6 +643,7 @@ interface SendCommandIn { interface RequestSandboxDefaultIn { type: "requestSandboxDefault" + requestID?: string agentManagerContext?: string contextDirectory?: string } diff --git a/packages/kilo-vscode/tests/unit/new-worktree-dialog-sandbox.test.ts b/packages/kilo-vscode/tests/unit/new-worktree-dialog-sandbox.test.ts new file mode 100644 index 00000000000..83dd1ef91f5 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/new-worktree-dialog-sandbox.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" + +const path = join(__dirname, "..", "..", "webview-ui", "agent-manager", "NewWorktreeDialog.tsx") +const providerPath = join(__dirname, "..", "..", "src", "KiloProvider.ts") +const src = readFileSync(path, "utf8") +const provider = readFileSync(providerPath, "utf8") + +describe("NewWorktreeDialog sandbox toggle", () => { + it("uses the persisted default and only sends explicit modal overrides", () => { + expect(src).toContain('vscode.postMessage({ type: "requestSandboxDefault", requestID: sandboxRequestID })') + expect(src).toContain('if (message.type !== "sandboxDefaultStatus") return') + expect(src).toContain("if (message.requestID !== sandboxRequestID) return") + expect(src).toContain("setSandbox(message.enabled)") + expect(src).toContain("setSandboxOverride(next === sandboxDefault() ? undefined : next)") + expect(src).toContain( + 'vscode.postMessage({ type: "setSandboxDefault", enabled: next, requestID: sandboxRequestID })', + ) + expect(src).toContain("sandbox: sandboxVisible() ? sandboxOverride() : undefined") + expect(src).toContain("const sandboxVisible = () => features().sandboxControls") + expect(provider).toContain("await this.fetchAndSendSandboxDefault(message.contextDirectory, message.requestID)") + expect(src).not.toContain("createSignal(config().experimental?.sandbox === true)") + expect(src).not.toContain("visible as isSandboxVisible") + }) +}) diff --git a/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts index 136dc3da293..2815cf0b6c4 100644 --- a/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts +++ b/packages/kilo-vscode/tests/unit/sandbox-bootstrap.test.ts @@ -123,10 +123,15 @@ describe("Agent Manager sandbox startup", () => { expect(provider).toContain("wt.result.path, wt.result.branch, session.id") }) - 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") + test("uses the persisted sandbox default for UI and only sends explicit overrides", () => { + expect(dialog).toContain("const sandboxVisible = () => features().sandboxControls") + expect(dialog).toContain('vscode.postMessage({ type: "requestSandboxDefault", requestID: sandboxRequestID })') + expect(dialog).toContain( + 'vscode.postMessage({ type: "setSandboxDefault", enabled: next, requestID: sandboxRequestID })', + ) + expect(dialog).toContain("sandbox: sandboxVisible() ? sandboxOverride() : undefined") expect(dialog).toContain("") + expect(dialog).not.toContain("visible as isSandboxVisible") }) test("places the sandbox toggle with prompt actions instead of model selectors", () => { diff --git a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx index 43277a79496..f3b39c2cdb9 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/NewWorktreeDialog.tsx @@ -20,7 +20,6 @@ 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, SandboxTooltipContent } from "../src/components/shared/SandboxButton" import { @@ -104,8 +103,14 @@ 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 sandboxVisible = () => isSandboxVisible(features(), config()) + const [sandbox, setSandbox] = createSignal() + const [sandboxDefault, setSandboxDefault] = createSignal() + const [sandboxOverride, setSandboxOverride] = createSignal() + const [sandboxAvailable, setSandboxAvailable] = createSignal(true) + const [sandboxReason, setSandboxReason] = createSignal() + const [sandboxRevision, setSandboxRevision] = createSignal(-1) + const sandboxRequestID = crypto.randomUUID() + const sandboxVisible = () => features().sandboxControls const speech = useSpeechToText(vscode, server, { t }) const canUseSpeech = () => canUseSpeechToText(config(), provider.authStates()) const speechModel = () => selectedSpeechToTextModel(config()) @@ -146,6 +151,45 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran if (!stored || !list.includes(stored)) setVariant(list[0]) }) + createEffect(() => { + if (!sandboxVisible()) return + if (server.connectionState() !== "connected") { + setSandbox(undefined) + setSandboxDefault(undefined) + setSandboxOverride(undefined) + return + } + vscode.postMessage({ type: "requestSandboxDefault", requestID: sandboxRequestID }) + }) + + const unsubSandbox = vscode.onMessage((message) => { + if (message.type !== "sandboxDefaultStatus") return + if (message.requestID !== sandboxRequestID) return + if (message.revision < sandboxRevision()) return + + setSandboxRevision(message.revision) + setSandboxDefault(message.desired) + setSandboxAvailable(message.available) + setSandboxReason(message.reason) + + const override = sandboxOverride() + if (override === undefined) { + setSandbox(message.enabled) + return + } + if (override === message.desired) setSandboxOverride(undefined) + }) + onCleanup(unsubSandbox) + + const toggleSandbox = () => { + const current = sandbox() + if (current === undefined || !sandboxAvailable()) return + const next = !current + setSandbox(next) + setSandboxOverride(next === sandboxDefault() ? undefined : next) + vscode.postMessage({ type: "setSandboxDefault", enabled: next, requestID: sandboxRequestID }) + } + const imageAttach = useImageAttachments() imageAttach.setFilePathDropHandler((paths) => { const cwd = server.workspaceDirectory() @@ -250,7 +294,7 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran baseBranch: advanced ? (baseBranch() ?? undefined) : undefined, branchName: customBranch, modelAllocations: allocations, - sandbox: sandboxVisible() ? sandbox() : undefined, + sandbox: sandboxVisible() ? sandboxOverride() : undefined, files: imgFiles, }) @@ -467,20 +511,20 @@ export const NewWorktreeDialog: Component<{ onClose: () => void; defaultBaseBran
} tooltipClass="prompt-sandbox-tooltip-content" - onToggle={click( - "sandbox_toggle", - "configure_worktree_dialog", - () => setSandbox(!sandbox()), - () => ({ enabled: !sandbox() }), - )} + onToggle={click("sandbox_toggle", "configure_worktree_dialog", toggleSandbox, () => ({ + enabled: !(sandbox() ?? false), + }))} /> 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 b77530566df..e4175d022f6 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 @@ -954,6 +954,7 @@ export interface RequestSandboxStatusMessage { export interface RequestSandboxDefaultMessage { type: "requestSandboxDefault" + requestID?: string agentManagerContext?: string contextDirectory?: string }