mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: add sandbox risk confirmation and rejection screens
- Introduced `SandboxRiskDialog` to prompt users about risks when running outside a containerized environment. - Added `SandboxRejectionScreen` for users who deny the sandbox risk confirmation, providing options to reload or restart the app. - Updated settings view and danger zone section to manage sandbox warning preferences. - Implemented a new API endpoint to check if the application is running in a containerized environment. - Enhanced state management to handle sandbox warning settings across the application.
This commit is contained in:
@@ -3,4 +3,6 @@ export { DeleteAllArchivedSessionsDialog } from './delete-all-archived-sessions-
|
|||||||
export { DeleteSessionDialog } from './delete-session-dialog';
|
export { DeleteSessionDialog } from './delete-session-dialog';
|
||||||
export { FileBrowserDialog } from './file-browser-dialog';
|
export { FileBrowserDialog } from './file-browser-dialog';
|
||||||
export { NewProjectModal } from './new-project-modal';
|
export { NewProjectModal } from './new-project-modal';
|
||||||
|
export { SandboxRejectionScreen } from './sandbox-rejection-screen';
|
||||||
|
export { SandboxRiskDialog } from './sandbox-risk-dialog';
|
||||||
export { WorkspacePickerModal } from './workspace-picker-modal';
|
export { WorkspacePickerModal } from './workspace-picker-modal';
|
||||||
|
|||||||
53
apps/ui/src/components/dialogs/sandbox-rejection-screen.tsx
Normal file
53
apps/ui/src/components/dialogs/sandbox-rejection-screen.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Sandbox Rejection Screen
|
||||||
|
*
|
||||||
|
* Shown in web mode when user denies the sandbox risk confirmation.
|
||||||
|
* Prompts them to either restart the app in a container or reload to try again.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ShieldX, RefreshCw } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
export function SandboxRejectionScreen() {
|
||||||
|
const handleReload = () => {
|
||||||
|
// Clear the rejection state and reload
|
||||||
|
sessionStorage.removeItem('automaker-sandbox-denied');
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="max-w-md w-full text-center space-y-6">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="rounded-full bg-destructive/10 p-4">
|
||||||
|
<ShieldX className="w-12 h-12 text-destructive" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h1 className="text-2xl font-semibold">Access Denied</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
You declined to accept the risks of running Automaker outside a sandbox environment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
For safer operation, consider running Automaker in Docker. See the README for
|
||||||
|
instructions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleReload}
|
||||||
|
className="gap-2"
|
||||||
|
data-testid="sandbox-retry"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-4 h-4" />
|
||||||
|
Reload & Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx
Normal file
108
apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Sandbox Risk Confirmation Dialog
|
||||||
|
*
|
||||||
|
* Shows when the app is running outside a containerized environment.
|
||||||
|
* Users must acknowledge the risks before proceeding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ShieldAlert } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
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: (skipInFuture: boolean) => void;
|
||||||
|
onDeny: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SandboxRiskDialog({ open, onConfirm, onDeny }: SandboxRiskDialogProps) {
|
||||||
|
const [skipInFuture, setSkipInFuture] = useState(false);
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
onConfirm(skipInFuture);
|
||||||
|
// Reset checkbox state after confirmation
|
||||||
|
setSkipInFuture(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={() => {}}>
|
||||||
|
<DialogContent
|
||||||
|
className="bg-popover border-border max-w-lg"
|
||||||
|
onPointerDownOutside={(e) => e.preventDefault()}
|
||||||
|
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||||
|
showCloseButton={false}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||||||
|
<ShieldAlert className="w-6 h-6" />
|
||||||
|
Sandbox Environment Not Detected
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription asChild>
|
||||||
|
<div className="space-y-4 pt-2">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
<strong>Warning:</strong> This application is running outside of a containerized
|
||||||
|
sandbox environment. AI agents will have direct access to your filesystem and can
|
||||||
|
execute commands on your system.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-destructive/10 border border-destructive/20 rounded-lg p-4 space-y-2">
|
||||||
|
<p className="text-sm font-medium text-destructive">Potential Risks:</p>
|
||||||
|
<ul className="text-sm text-muted-foreground list-disc list-inside space-y-1">
|
||||||
|
<li>Agents can read, modify, or delete files on your system</li>
|
||||||
|
<li>Agents can execute arbitrary commands and install software</li>
|
||||||
|
<li>Agents can access environment variables and credentials</li>
|
||||||
|
<li>Unintended side effects from agent actions may affect your system</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
For safer operation, consider running Automaker in Docker. See the README for
|
||||||
|
instructions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<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 & 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -52,6 +52,8 @@ export function SettingsView() {
|
|||||||
setAutoLoadClaudeMd,
|
setAutoLoadClaudeMd,
|
||||||
promptCustomization,
|
promptCustomization,
|
||||||
setPromptCustomization,
|
setPromptCustomization,
|
||||||
|
skipSandboxWarning,
|
||||||
|
setSkipSandboxWarning,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Convert electron Project to settings-view Project type
|
// Convert electron Project to settings-view Project type
|
||||||
@@ -149,6 +151,8 @@ export function SettingsView() {
|
|||||||
<DangerZoneSection
|
<DangerZoneSection
|
||||||
project={settingsProject}
|
project={settingsProject}
|
||||||
onDeleteClick={() => setShowDeleteDialog(true)}
|
onDeleteClick={() => setShowDeleteDialog(true)}
|
||||||
|
skipSandboxWarning={skipSandboxWarning}
|
||||||
|
onResetSandboxWarning={() => setSkipSandboxWarning(false)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { Button } from '@/components/ui/button';
|
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 { cn } from '@/lib/utils';
|
||||||
import type { Project } from '../shared/types';
|
import type { Project } from '../shared/types';
|
||||||
|
|
||||||
interface DangerZoneSectionProps {
|
interface DangerZoneSectionProps {
|
||||||
project: Project | null;
|
project: Project | null;
|
||||||
onDeleteClick: () => void;
|
onDeleteClick: () => void;
|
||||||
|
skipSandboxWarning: boolean;
|
||||||
|
onResetSandboxWarning: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionProps) {
|
export function DangerZoneSection({
|
||||||
|
project,
|
||||||
|
onDeleteClick,
|
||||||
|
skipSandboxWarning,
|
||||||
|
onResetSandboxWarning,
|
||||||
|
}: DangerZoneSectionProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -30,6 +37,36 @@ export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionP
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Project Delete */}
|
{/* Project Delete */}
|
||||||
{project && (
|
{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 justify-between gap-4 p-4 rounded-xl bg-destructive/5 border border-destructive/10">
|
||||||
@@ -60,7 +97,7 @@ export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionP
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Empty state when nothing to show */}
|
{/* Empty state when nothing to show */}
|
||||||
{!project && (
|
{!skipSandboxWarning && !project && (
|
||||||
<p className="text-sm text-muted-foreground/60 text-center py-4">
|
<p className="text-sm text-muted-foreground/60 text-center py-4">
|
||||||
No danger zone actions available.
|
No danger zone actions available.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -479,6 +479,7 @@ function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
|||||||
enabledCursorModels: settings.enabledCursorModels ?? current.enabledCursorModels,
|
enabledCursorModels: settings.enabledCursorModels ?? current.enabledCursorModels,
|
||||||
cursorDefaultModel: settings.cursorDefaultModel ?? 'auto',
|
cursorDefaultModel: settings.cursorDefaultModel ?? 'auto',
|
||||||
autoLoadClaudeMd: settings.autoLoadClaudeMd ?? false,
|
autoLoadClaudeMd: settings.autoLoadClaudeMd ?? false,
|
||||||
|
skipSandboxWarning: settings.skipSandboxWarning ?? false,
|
||||||
keyboardShortcuts: {
|
keyboardShortcuts: {
|
||||||
...current.keyboardShortcuts,
|
...current.keyboardShortcuts,
|
||||||
...(settings.keyboardShortcuts as unknown as Partial<typeof current.keyboardShortcuts>),
|
...(settings.keyboardShortcuts as unknown as Partial<typeof current.keyboardShortcuts>),
|
||||||
@@ -535,6 +536,7 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
|
|||||||
validationModel: state.validationModel,
|
validationModel: state.validationModel,
|
||||||
phaseModels: state.phaseModels,
|
phaseModels: state.phaseModels,
|
||||||
autoLoadClaudeMd: state.autoLoadClaudeMd,
|
autoLoadClaudeMd: state.autoLoadClaudeMd,
|
||||||
|
skipSandboxWarning: state.skipSandboxWarning,
|
||||||
keyboardShortcuts: state.keyboardShortcuts,
|
keyboardShortcuts: state.keyboardShortcuts,
|
||||||
aiProfiles: state.aiProfiles,
|
aiProfiles: state.aiProfiles,
|
||||||
mcpServers: state.mcpServers,
|
mcpServers: state.mcpServers,
|
||||||
|
|||||||
@@ -379,6 +379,32 @@ export const verifySession = async (): Promise<boolean> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the server is running in a containerized (sandbox) environment.
|
||||||
|
* This endpoint is unauthenticated so it can be checked before login.
|
||||||
|
*/
|
||||||
|
export const checkSandboxEnvironment = async (): Promise<{
|
||||||
|
isContainerized: boolean;
|
||||||
|
error?: string;
|
||||||
|
}> => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getServerUrl()}/api/health/environment`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.warn('Failed to check sandbox environment');
|
||||||
|
return { isContainerized: false, error: 'Failed to check environment' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return { isContainerized: data.isContainerized ?? false };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Sandbox environment check failed:', error);
|
||||||
|
return { isContainerized: false, error: 'Network error' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
type EventType =
|
type EventType =
|
||||||
| 'agent:stream'
|
| 'agent:stream'
|
||||||
| 'auto-mode:event'
|
| 'auto-mode:event'
|
||||||
|
|||||||
@@ -16,19 +16,28 @@ import {
|
|||||||
initApiKey,
|
initApiKey,
|
||||||
isElectronMode,
|
isElectronMode,
|
||||||
verifySession,
|
verifySession,
|
||||||
|
checkSandboxEnvironment,
|
||||||
getServerUrlSync,
|
getServerUrlSync,
|
||||||
checkExternalServerMode,
|
checkExternalServerMode,
|
||||||
isExternalServerMode,
|
isExternalServerMode,
|
||||||
} from '@/lib/http-api-client';
|
} from '@/lib/http-api-client';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
||||||
|
import { SandboxRiskDialog } from '@/components/dialogs/sandbox-risk-dialog';
|
||||||
|
import { SandboxRejectionScreen } from '@/components/dialogs/sandbox-rejection-screen';
|
||||||
import { LoadingState } from '@/components/ui/loading-state';
|
import { LoadingState } from '@/components/ui/loading-state';
|
||||||
|
|
||||||
const logger = createLogger('RootLayout');
|
const logger = createLogger('RootLayout');
|
||||||
|
|
||||||
function RootLayoutContent() {
|
function RootLayoutContent() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { setIpcConnected, currentProject, getEffectiveTheme } = useAppStore();
|
const {
|
||||||
|
setIpcConnected,
|
||||||
|
currentProject,
|
||||||
|
getEffectiveTheme,
|
||||||
|
skipSandboxWarning,
|
||||||
|
setSkipSandboxWarning,
|
||||||
|
} = useAppStore();
|
||||||
const { setupComplete } = useSetupStore();
|
const { setupComplete } = useSetupStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
@@ -44,6 +53,12 @@ function RootLayoutContent() {
|
|||||||
const isSetupRoute = location.pathname === '/setup';
|
const isSetupRoute = location.pathname === '/setup';
|
||||||
const isLoginRoute = location.pathname === '/login';
|
const isLoginRoute = location.pathname === '/login';
|
||||||
|
|
||||||
|
// Sandbox environment check state
|
||||||
|
type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed';
|
||||||
|
// Always start from pending on a fresh page load so the user sees the prompt
|
||||||
|
// each time the app is launched/refreshed (unless running in a container).
|
||||||
|
const [sandboxStatus, setSandboxStatus] = useState<SandboxStatus>('pending');
|
||||||
|
|
||||||
// Hidden streamer panel - opens with "\" key
|
// Hidden streamer panel - opens with "\" key
|
||||||
const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => {
|
const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => {
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
@@ -90,6 +105,73 @@ function RootLayoutContent() {
|
|||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check sandbox environment on mount
|
||||||
|
useEffect(() => {
|
||||||
|
// Skip if already decided
|
||||||
|
if (sandboxStatus !== 'pending') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkSandbox = async () => {
|
||||||
|
try {
|
||||||
|
const result = await checkSandboxEnvironment();
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to check environment:', error);
|
||||||
|
// On error, assume not containerized and show warning
|
||||||
|
if (skipSandboxWarning) {
|
||||||
|
setSandboxStatus('confirmed');
|
||||||
|
} else {
|
||||||
|
setSandboxStatus('needs-confirmation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkSandbox();
|
||||||
|
}, [sandboxStatus, skipSandboxWarning]);
|
||||||
|
|
||||||
|
// Handle sandbox risk confirmation
|
||||||
|
const handleSandboxConfirm = useCallback(
|
||||||
|
(skipInFuture: boolean) => {
|
||||||
|
if (skipInFuture) {
|
||||||
|
setSkipSandboxWarning(true);
|
||||||
|
}
|
||||||
|
setSandboxStatus('confirmed');
|
||||||
|
},
|
||||||
|
[setSkipSandboxWarning]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle sandbox risk denial
|
||||||
|
const handleSandboxDeny = useCallback(async () => {
|
||||||
|
if (isElectron()) {
|
||||||
|
// In Electron mode, quit the application
|
||||||
|
// Use window.electronAPI directly since getElectronAPI() returns the HTTP client
|
||||||
|
try {
|
||||||
|
const electronAPI = window.electronAPI;
|
||||||
|
if (electronAPI?.quit) {
|
||||||
|
await electronAPI.quit();
|
||||||
|
} else {
|
||||||
|
logger.error('quit() not available on electronAPI');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to quit app:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In web mode, show rejection screen
|
||||||
|
setSandboxStatus('denied');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Ref to prevent concurrent auth checks from running
|
// Ref to prevent concurrent auth checks from running
|
||||||
const authCheckRunning = useRef(false);
|
const authCheckRunning = useRef(false);
|
||||||
|
|
||||||
@@ -234,12 +316,28 @@ function RootLayoutContent() {
|
|||||||
}
|
}
|
||||||
}, [deferredTheme]);
|
}, [deferredTheme]);
|
||||||
|
|
||||||
|
// Show sandbox rejection screen if user denied the risk warning
|
||||||
|
if (sandboxStatus === 'denied') {
|
||||||
|
return <SandboxRejectionScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show sandbox risk dialog if not containerized and user hasn't confirmed
|
||||||
|
// The dialog is rendered as an overlay while the main content is blocked
|
||||||
|
const showSandboxDialog = sandboxStatus === 'needs-confirmation';
|
||||||
|
|
||||||
// Show login page (full screen, no sidebar)
|
// Show login page (full screen, no sidebar)
|
||||||
if (isLoginRoute) {
|
if (isLoginRoute) {
|
||||||
return (
|
return (
|
||||||
<main className="h-screen overflow-hidden" data-testid="app-container">
|
<>
|
||||||
<Outlet />
|
<main className="h-screen overflow-hidden" data-testid="app-container">
|
||||||
</main>
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
<SandboxRiskDialog
|
||||||
|
open={showSandboxDialog}
|
||||||
|
onConfirm={handleSandboxConfirm}
|
||||||
|
onDeny={handleSandboxDeny}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,30 +373,37 @@ function RootLayoutContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex h-screen overflow-hidden" data-testid="app-container">
|
<>
|
||||||
{/* Full-width titlebar drag region for Electron window dragging */}
|
<main className="flex h-screen overflow-hidden" data-testid="app-container">
|
||||||
{isElectron() && (
|
{/* Full-width titlebar drag region for Electron window dragging */}
|
||||||
|
{isElectron() && (
|
||||||
|
<div
|
||||||
|
className={`fixed top-0 left-0 right-0 h-6 titlebar-drag-region z-40 pointer-events-none ${isMac ? 'pl-20' : ''}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Sidebar />
|
||||||
<div
|
<div
|
||||||
className={`fixed top-0 left-0 right-0 h-6 titlebar-drag-region z-40 pointer-events-none ${isMac ? 'pl-20' : ''}`}
|
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
||||||
aria-hidden="true"
|
style={{ marginRight: streamerPanelOpen ? '250px' : '0' }}
|
||||||
/>
|
>
|
||||||
)}
|
<Outlet />
|
||||||
<Sidebar />
|
</div>
|
||||||
<div
|
|
||||||
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
|
||||||
style={{ marginRight: streamerPanelOpen ? '250px' : '0' }}
|
|
||||||
>
|
|
||||||
<Outlet />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hidden streamer panel - opens with "\" key, pushes content */}
|
{/* Hidden streamer panel - opens with "\" key, pushes content */}
|
||||||
<div
|
<div
|
||||||
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
|
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
|
||||||
streamerPanelOpen ? 'translate-x-0' : 'translate-x-full'
|
streamerPanelOpen ? 'translate-x-0' : 'translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
|
/>
|
||||||
|
<Toaster richColors position="bottom-right" />
|
||||||
|
</main>
|
||||||
|
<SandboxRiskDialog
|
||||||
|
open={showSandboxDialog}
|
||||||
|
onConfirm={handleSandboxConfirm}
|
||||||
|
onDeny={handleSandboxDeny}
|
||||||
/>
|
/>
|
||||||
<Toaster richColors position="bottom-right" />
|
</>
|
||||||
</main>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -511,6 +511,7 @@ export interface AppState {
|
|||||||
|
|
||||||
// Claude Agent SDK Settings
|
// Claude Agent SDK Settings
|
||||||
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
|
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
|
||||||
|
skipSandboxWarning: boolean; // Skip the sandbox environment warning dialog on startup
|
||||||
|
|
||||||
// MCP Servers
|
// MCP Servers
|
||||||
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
|
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
|
||||||
@@ -816,6 +817,7 @@ export interface AppActions {
|
|||||||
|
|
||||||
// Claude Agent SDK Settings actions
|
// Claude Agent SDK Settings actions
|
||||||
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
|
setAutoLoadClaudeMd: (enabled: boolean) => Promise<void>;
|
||||||
|
setSkipSandboxWarning: (skip: boolean) => Promise<void>;
|
||||||
|
|
||||||
// Prompt Customization actions
|
// Prompt Customization actions
|
||||||
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
|
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
|
||||||
@@ -1036,6 +1038,7 @@ const initialState: AppState = {
|
|||||||
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
|
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
|
||||||
cursorDefaultModel: 'auto', // Default to auto selection
|
cursorDefaultModel: 'auto', // Default to auto selection
|
||||||
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
autoLoadClaudeMd: false, // Default to disabled (user must opt-in)
|
||||||
|
skipSandboxWarning: false, // Default to disabled (show sandbox warning dialog)
|
||||||
mcpServers: [], // No MCP servers configured by default
|
mcpServers: [], // No MCP servers configured by default
|
||||||
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
|
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
|
||||||
aiProfiles: DEFAULT_AI_PROFILES,
|
aiProfiles: DEFAULT_AI_PROFILES,
|
||||||
@@ -1734,6 +1737,17 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
set({ autoLoadClaudeMd: previous });
|
set({ autoLoadClaudeMd: previous });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setSkipSandboxWarning: async (skip) => {
|
||||||
|
const previous = get().skipSandboxWarning;
|
||||||
|
set({ skipSandboxWarning: skip });
|
||||||
|
// Sync to server settings file
|
||||||
|
const { syncSettingsToServer } = await import('@/hooks/use-settings-migration');
|
||||||
|
const ok = await syncSettingsToServer();
|
||||||
|
if (!ok) {
|
||||||
|
logger.error('Failed to sync skipSandboxWarning setting to server - reverting');
|
||||||
|
set({ skipSandboxWarning: previous });
|
||||||
|
}
|
||||||
|
},
|
||||||
// Prompt Customization actions
|
// Prompt Customization actions
|
||||||
setPromptCustomization: async (customization) => {
|
setPromptCustomization: async (customization) => {
|
||||||
set({ promptCustomization: customization });
|
set({ promptCustomization: customization });
|
||||||
|
|||||||
@@ -367,6 +367,17 @@
|
|||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Text selection styling for readability */
|
||||||
|
::selection {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
/* Ensure all clickable elements show pointer cursor */
|
/* Ensure all clickable elements show pointer cursor */
|
||||||
button:not(:disabled),
|
button:not(:disabled),
|
||||||
[role='button']:not([aria-disabled='true']),
|
[role='button']:not([aria-disabled='true']),
|
||||||
|
|||||||
@@ -486,6 +486,8 @@ export interface GlobalSettings {
|
|||||||
// Claude Agent SDK Settings
|
// Claude Agent SDK Settings
|
||||||
/** Auto-load CLAUDE.md files using SDK's settingSources option */
|
/** Auto-load CLAUDE.md files using SDK's settingSources option */
|
||||||
autoLoadClaudeMd?: boolean;
|
autoLoadClaudeMd?: boolean;
|
||||||
|
/** Skip the sandbox environment warning dialog on startup */
|
||||||
|
skipSandboxWarning?: boolean;
|
||||||
|
|
||||||
// MCP Server Configuration
|
// MCP Server Configuration
|
||||||
/** List of configured MCP servers for agent use */
|
/** List of configured MCP servers for agent use */
|
||||||
|
|||||||
Reference in New Issue
Block a user