fix: sandbox enable toggle and Linux status detection - #293
Conversation
Implements the missing sandbox toggle handler and UI switch (all platforms). Also fixes sandboxReady/sandboxAvailable being hardcoded false on native Linux hosts.
There was a problem hiding this comment.
Review mode: initial
Findings
-
[Minor] Success message timeout race condition
When the user toggles sandbox quickly within 3 seconds, the previoussetTimeout(() => setSuccess(null), 3000)may fire after the new success message is set, prematurely clearing it. This happens because the timeout is not cleared before setting a new one.
Suggested fix: Store the timeout ID in a ref and clear it in the cleanup logic ofhandleToggleSandbox.const successTimeoutRef = useRef<NodeJS.Timeout>(); // In handleToggleSandbox, before setting new timeout: if (successTimeoutRef.current) clearTimeout(successTimeoutRef.current); successTimeoutRef.current = setTimeout(() => setSuccess(null), 3000);
-
[Nit] Missing guard for non-Electron environments
handleToggleSandboxassumeswindow.electronAPI.config.saveexists. If the component were ever rendered outside Electron (e.g., in a browser), clicking the toggle would throw. Consider adding a guard similar to the initialization effect.
Suggested fix: Add early return if!isElectron.if (!isElectron) return;
Questions
- The PR description mentions fixing
sandboxReady/sandboxAvailablebeing hardcoded tofalseon native Linux, but no such change appears in the diff. Was this fix already in the base branch, or is it intended for a separate PR?
Summary
- The toggle implementation is generally correct: optimistic UI disabled during saving, error handling, and accessibility attributes are properly applied.
- The 500ms delay for status reload is reasonable but may need adjustment if the backend takes longer in practice.
- The only significant issue is the success message race condition; the other items are minor or questions.
- Testing: Manual verification on Windows and Linux was described. Automated tests are not present in the diff; consider adding unit tests for
handleToggleSandbox(e.g., mockingwindow.electronAPI).
Testing
- Not run (automation)
Open Cowork Bot
Adds isElectron guard and clears prior timeout before setting a new success timeout. Also includes the Linux sandboxReady/sandboxAvailable fix missing from the prior push.
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
-
[Major] Inverted toggle button label
The button label uses{sandboxEnabled ? t('common.enable') : t('common.disable')}. When sandbox is enabled, it shows "enable" (verb), which suggests a click would enable it again, but the actual action toggles off. It should show the opposite action:{sandboxEnabled ? t('common.disable') : t('common.enable')}.
src/renderer/components/settings/SettingsSandbox.tsx:364
Suggested fix:{sandboxEnabled ? t('common.disable') : t('common.enable')}
-
[Minor] Uncleanup setTimeout for status reload
InhandleToggleSandbox, asetTimeoutwith 500ms delay callsloadStatus()but the timeout ID is never captured or cleared. If the component unmounts before the timeout fires (e.g., navigating away from settings), the callback will attempt to update state on an unmounted component, causing a React warning. Store the timeout in a ref and clear it on cleanup usinguseEffector before subsequent toggles.
src/renderer/components/settings/SettingsSandbox.tsx:126
Suggested fix:// Add a ref next to successTimeoutRef const reloadTimeoutRef = useRef<ReturnType<typeof setTimeout>>(); // In handleToggleSandbox after success if (reloadTimeoutRef.current) clearTimeout(reloadTimeoutRef.current); reloadTimeoutRef.current = setTimeout(async () => { await loadStatus(); }, 500); // In a useEffect cleanup for unmount useEffect(() => { return () => { if (reloadTimeoutRef.current) clearTimeout(reloadTimeoutRef.current); }; }, []);
-
[Minor] Linux sandbox status assumed always true even when backend fails
SettingsandboxAvailableandsandboxReadytotrueunconditionally for Linux ignores the possibility that the backend itself is broken or not initialized (e.g.,statusis null). Previouslyfalsewas too restrictive;trueis overly optimistic. Consider checking ifstatusexists and if the backend reported an error, or treat as ready only whenstatus?.initializedis true.
src/renderer/components/settings/SettingsSandbox.tsx:314,320
Suggested fix:: status?.initialized === true; // Linux: ready only if backend reports initialized
Previous feedback resolved
- All prior findings have been addressed: success message timeout race condition is fixed with ref and cleanup, and guard for non-Electron environments is added.
Testing
- Not run (automation)
Open Cowork Bot
Removes redundant enable/disable label (no precedent in SettingsLogs.tsx). Clears prior reload timeout before setting a new one, and on unmount. Linux ready/available now checks backend status.initialized instead of a hardcoded true.
There was a problem hiding this comment.
Review mode: follow-up after new commits
Findings
All previously flagged issues from the prior review have been satisfactorily addressed:
- The inverted toggle button text has been resolved (the text label displaying the action was removed; the switch alone with
aria-labelis sufficient). - The
reloadTimeoutRefis now captured, cleared before reuse, and cleaned up on unmount via a dedicateduseEffect. - The Linux sandbox availability and readiness now check
status?.initialized === trueinstead of being hardcoded totrue, aligning with the backend adapter's actual initialization state.
No new issues introduced.
Residual Risks
- The
loadStatus()call inside the 500ms timeout does not protect against setting state after component unmount if the async operation completes after cleanup. However, the timeout is cleared on unmount, so this scenario is unlikely in practice and matches existing patterns in the codebase. - No automated tests were added for the toggle behavior or Linux status detection. Consider adding unit or integration tests for the sandbox settings component.
Testing
- Not run (automation)
Open Cowork Bot
Four stacked issues were together causing bash tool calls to always fail under Windows/WSL2 sandbox mode with "Working directory does not exist: /workspace / Cannot execute bash commands." even when Settings correctly reported WSL2 as ready. 1. WSLBridge.testDistro() made a single WSL echo probe with no retry. WSL2's lightweight VM can be cold/dormant on first use after boot or idle, causing a transient false negative that got cached as "WSL unavailable" for the rest of the app session. Now retries once after a short delay before giving up. 2. SandboxAdapter.initializeWSL() ran its own independent WSL status check instead of using the shared SandboxBootstrap check that drives the startup popup. Two concurrent checks at cold start could disagree, leaving the adapter stuck on a stale result even after the popup reported success. Now awaits the shared bootstrap instead of racing it. 3. The Settings sandbox.getStatus IPC handler never triggered adapter initialization, so checking Settings before starting any chat session always showed a hardcoded placeholder (Native/none) regardless of real WSL2 status. Now triggers on-demand initialization. 4. Root cause of the actual execution failure: @mariozechner/pi-coding-agent's createAgentSession() only reads tool names off the tools option to build an allow-list, then discards the actual Tool objects and rebuilds default (non-WSL) bash operations internally via createAllTools(cwd, ...). This silently dropped our WSL-routed bash operations (which cd into the sandbox path inside the WSL distro) on every single turn, regardless of session cache state, and fell back to the SDK's plain existsSync-based local bash operations, which then always failed because the sandbox path is a WSL-side path that does not exist on the Windows host. Fixed via a patch-package patch that additionally threads the real tool objects through as AgentSession's baseToolsOverride. Verified via production logs: after this fix, the WslSandboxBash executing-in-distro log line now actually fires, and bash commands return real output from the synced sandbox filesystem instead of the generic error. Separate from OpenCoworkAI#293, which fixes the missing sandbox enable/disable toggle and Linux status detection - that PR addresses whether sandbox mode can be turned on at all; this one addresses whether commands actually execute correctly once it is.
Summary
Two related but separate fixes to the Sandbox settings panel (
SettingsSandbox.tsx):Sandbox enable toggle was missing entirely. The panel displayed status correctly (WSL2/Lima detection, Node/Python/pip checks all worked), but there was no way to actually turn Sandbox Mode on or off —
handleToggleSandboxexisted only as a commented-out// TODO: Re-enable when sandbox debugging is completestub, with no UI control wired to it at all. Backend support (config.saveIPC handler →configStore.update→syncConfigAfterMutation→sessionManager.reloadSandbox()) was already fully implemented and required no changes. This affects all platforms (Windows/WSL2, macOS/Lima, Linux), since it's the same shared component.sandboxReady/sandboxAvailablewere hardcoded tofalseon native Linux hosts. Separately, while testing the toggle fix by running the dev build directly on Linux (as opposed to Windows using WSL2 as a backend — a different code path), the status always read "Sandbox enabled but not fully configured" even though Linux native mode has no further setup step (per the existingsandbox.linuxNativecopy: "Linux runs commands natively without additional sandboxing"). Fixed by treating Linux as always available/ready, matching intended behavior — this does not affect the Windows/WSL2 or macOS/Lima detection logic, which were already correct.Related
Testing
tsc --noEmitandeslintpass with no new errors.