feat: enhance sandbox risk dialog and settings management

- Updated the SandboxRiskDialog to include a checkbox for users to opt-out of future warnings, passing the state to the onConfirm callback.
- Modified SettingsView to manage the skipSandboxWarning state, allowing users to reset the warning preference.
- Enhanced DangerZoneSection to display a message when the sandbox warning is disabled and provide an option to reset this setting.
- Updated RootLayoutContent to respect the user's choice regarding the sandbox warning, auto-confirming if the user opts to skip it.
- Added skipSandboxWarning state management to the app store for persistent user preferences.
This commit is contained in:
webdevcody
2026-01-01 16:49:35 -05:00
parent 59d47928a7
commit fb46c0c9ea
5 changed files with 151 additions and 48 deletions

View File

@@ -16,10 +16,12 @@ import {
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
interface SandboxRiskDialogProps {
open: boolean;
onConfirm: () => void;
onConfirm: (skipInFuture: boolean) => void;
onDeny: () => void;
}
@@ -27,6 +29,13 @@ const DOCKER_COMMAND = 'npm run dev:docker';
export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialogProps) {
const [copied, setCopied] = useState(false);
const [skipInFuture, setSkipInFuture] = useState(false);
const handleConfirm = () => {
onConfirm(skipInFuture);
// Reset checkbox state after confirmation
setSkipInFuture(false);
};
const handleCopy = async () => {
try {
@@ -93,18 +102,34 @@ export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialog
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-2 pt-4">
<Button variant="outline" onClick={onDeny} className="px-4" data-testid="sandbox-deny">
Deny &amp; Exit
</Button>
<Button
variant="destructive"
onClick={onConfirm}
className="px-4"
data-testid="sandbox-confirm"
>
<ShieldAlert className="w-4 h-4 mr-2" />I Accept the Risks
</Button>
<DialogFooter className="flex-col gap-4 sm:flex-col pt-4">
<div className="flex items-center space-x-2 self-start">
<Checkbox
id="skip-sandbox-warning"
checked={skipInFuture}
onCheckedChange={(checked) => setSkipInFuture(checked === true)}
data-testid="sandbox-skip-checkbox"
/>
<Label
htmlFor="skip-sandbox-warning"
className="text-sm text-muted-foreground cursor-pointer"
>
Do not show this warning again
</Label>
</div>
<div className="flex gap-2 sm:gap-2 w-full sm:justify-end">
<Button variant="outline" onClick={onDeny} className="px-4" data-testid="sandbox-deny">
Deny &amp; Exit
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
className="px-4"
data-testid="sandbox-confirm"
>
<ShieldAlert className="w-4 h-4 mr-2" />I Accept the Risks
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -55,6 +55,8 @@ export function SettingsView() {
setAutoLoadClaudeMd,
enableSandboxMode,
setEnableSandboxMode,
skipSandboxWarning,
setSkipSandboxWarning,
promptCustomization,
setPromptCustomization,
} = useAppStore();
@@ -184,6 +186,8 @@ export function SettingsView() {
<DangerZoneSection
project={settingsProject}
onDeleteClick={() => setShowDeleteDialog(true)}
skipSandboxWarning={skipSandboxWarning}
onResetSandboxWarning={() => setSkipSandboxWarning(false)}
/>
);
default:

View File

@@ -1,16 +1,21 @@
import { Button } from '@/components/ui/button';
import { Trash2, Folder, AlertTriangle } from 'lucide-react';
import { Trash2, Folder, AlertTriangle, Shield, RotateCcw } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { Project } from '../shared/types';
interface DangerZoneSectionProps {
project: Project | null;
onDeleteClick: () => void;
skipSandboxWarning: boolean;
onResetSandboxWarning: () => void;
}
export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionProps) {
if (!project) return null;
export function DangerZoneSection({
project,
onDeleteClick,
skipSandboxWarning,
onResetSandboxWarning,
}: DangerZoneSectionProps) {
return (
<div
className={cn(
@@ -28,35 +33,75 @@ export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionP
<h2 className="text-lg font-semibold text-foreground tracking-tight">Danger Zone</h2>
</div>
<p className="text-sm text-muted-foreground/80 ml-12">
Permanently remove this project from Automaker.
Destructive actions and reset options.
</p>
</div>
<div className="p-6">
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">{project.name}</p>
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">{project.path}</p>
<div className="p-6 space-y-4">
{/* Sandbox Warning Reset */}
{skipSandboxWarning && (
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-destructive/15 to-destructive/10 border border-destructive/20 flex items-center justify-center shrink-0">
<Shield className="w-5 h-5 text-destructive" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground">Sandbox Warning Disabled</p>
<p className="text-xs text-muted-foreground/70 mt-0.5">
The sandbox environment warning is hidden on startup
</p>
</div>
</div>
<Button
variant="outline"
onClick={onResetSandboxWarning}
data-testid="reset-sandbox-warning-button"
className={cn(
'shrink-0 gap-2',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<RotateCcw className="w-4 h-4" />
Reset
</Button>
</div>
<Button
variant="destructive"
onClick={onDeleteClick}
data-testid="delete-project-button"
className={cn(
'shrink-0',
'shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<Trash2 className="w-4 h-4 mr-2" />
Delete Project
</Button>
</div>
)}
{/* Project Delete */}
{project && (
<div className="flex items-center justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
<div className="flex items-center gap-3.5 min-w-0">
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/15 to-brand-600/10 border border-brand-500/20 flex items-center justify-center shrink-0">
<Folder className="w-5 h-5 text-brand-500" />
</div>
<div className="min-w-0">
<p className="font-medium text-foreground truncate">{project.name}</p>
<p className="text-xs text-muted-foreground/70 truncate mt-0.5">{project.path}</p>
</div>
</div>
<Button
variant="destructive"
onClick={onDeleteClick}
data-testid="delete-project-button"
className={cn(
'shrink-0',
'shadow-md shadow-destructive/20 hover:shadow-lg hover:shadow-destructive/25',
'transition-all duration-200 ease-out',
'hover:scale-[1.02] active:scale-[0.98]'
)}
>
<Trash2 className="w-4 h-4 mr-2" />
Delete Project
</Button>
</div>
)}
{/* Empty state when nothing to show */}
{!skipSandboxWarning && !project && (
<p className="text-sm text-muted-foreground/60 text-center py-4">
No danger zone actions available.
</p>
)}
</div>
</div>
);

View File

@@ -25,7 +25,13 @@ import { SandboxRejectionScreen } from '@/components/dialogs/sandbox-rejection-s
function RootLayoutContent() {
const location = useLocation();
const { setIpcConnected, currentProject, getEffectiveTheme } = useAppStore();
const {
setIpcConnected,
currentProject,
getEffectiveTheme,
skipSandboxWarning,
setSkipSandboxWarning,
} = useAppStore();
const { setupComplete } = useSetupStore();
const navigate = useNavigate();
const [isMounted, setIsMounted] = useState(false);
@@ -106,6 +112,9 @@ function RootLayoutContent() {
if (result.isContainerized) {
// Running in a container, no warning needed
setSandboxStatus('containerized');
} else if (skipSandboxWarning) {
// User opted to skip the warning, auto-confirm
setSandboxStatus('confirmed');
} else {
// Not containerized, show warning dialog
setSandboxStatus('needs-confirmation');
@@ -113,17 +122,27 @@ function RootLayoutContent() {
} catch (error) {
console.error('[Sandbox] Failed to check environment:', error);
// On error, assume not containerized and show warning
setSandboxStatus('needs-confirmation');
if (skipSandboxWarning) {
setSandboxStatus('confirmed');
} else {
setSandboxStatus('needs-confirmation');
}
}
};
checkSandbox();
}, [sandboxStatus]);
}, [sandboxStatus, skipSandboxWarning]);
// Handle sandbox risk confirmation
const handleSandboxConfirm = useCallback(() => {
setSandboxStatus('confirmed');
}, []);
const handleSandboxConfirm = useCallback(
(skipInFuture: boolean) => {
if (skipInFuture) {
setSkipSandboxWarning(true);
}
setSandboxStatus('confirmed');
},
[setSkipSandboxWarning]
);
// Handle sandbox risk denial
const handleSandboxDeny = useCallback(async () => {

View File

@@ -487,6 +487,7 @@ export interface AppState {
// Claude Agent SDK Settings
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
enableSandboxMode: boolean; // Enable sandbox mode for bash commands (may cause issues on some systems)
skipSandboxWarning: boolean; // Skip the sandbox environment warning dialog on startup
// MCP Servers
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
@@ -775,6 +776,7 @@ export interface AppActions {
// Claude Agent SDK Settings actions
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
setEnableSandboxMode: (enabled: boolean) => Promise<void>;
setSkipSandboxWarning: (skip: boolean) => Promise<void>;
setMcpAutoApproveTools: (enabled: boolean) => Promise<void>;
setMcpUnrestrictedTools: (enabled: boolean) => Promise<void>;
@@ -976,6 +978,7 @@ const initialState: AppState = {
validationModel: 'opus', // Default to opus for GitHub issue validation
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
enableSandboxMode: false, // Default to disabled (can be enabled for additional security)
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
mcpServers: [], // No MCP servers configured by default
mcpAutoApproveTools: true, // Default to enabled - bypass permission prompts for MCP tools
mcpUnrestrictedTools: true, // Default to enabled - don't filter allowedTools when MCP enabled
@@ -1623,6 +1626,12 @@ export const useAppStore = create<AppState & AppActions>()(
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
setSkipSandboxWarning: async (skip) => {
set({ skipSandboxWarning: skip });
// Sync to server settings file
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
await syncSettingsToServer();
},
setMcpAutoApproveTools: async (enabled) => {
set({ mcpAutoApproveTools: enabled });
// Sync to server settings file
@@ -2921,6 +2930,7 @@ export const useAppStore = create<AppState & AppActions>()(
validationModel: state.validationModel,
autoLoadClaudeMd: state.autoLoadClaudeMd,
enableSandboxMode: state.enableSandboxMode,
skipSandboxWarning: state.skipSandboxWarning,
// MCP settings
mcpServers: state.mcpServers,
mcpAutoApproveTools: state.mcpAutoApproveTools,