+ {/* Sandbox Warning Reset */}
+ {skipSandboxWarning && (
+
+
+
+
+
+
+
Sandbox Warning Disabled
+
+ The sandbox environment warning is hidden on startup
+
+
+
+
+
+ )}
+
{/* Project Delete */}
{project && (
@@ -60,7 +97,7 @@ export function DangerZoneSection({ project, onDeleteClick }: DangerZoneSectionP
)}
{/* Empty state when nothing to show */}
- {!project && (
+ {!skipSandboxWarning && !project && (
No danger zone actions available.
diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts
index 6c0d096d..728293d3 100644
--- a/apps/ui/src/hooks/use-settings-migration.ts
+++ b/apps/ui/src/hooks/use-settings-migration.ts
@@ -479,6 +479,7 @@ function hydrateStoreFromSettings(settings: GlobalSettings): void {
enabledCursorModels: settings.enabledCursorModels ?? current.enabledCursorModels,
cursorDefaultModel: settings.cursorDefaultModel ?? 'auto',
autoLoadClaudeMd: settings.autoLoadClaudeMd ?? false,
+ skipSandboxWarning: settings.skipSandboxWarning ?? false,
keyboardShortcuts: {
...current.keyboardShortcuts,
...(settings.keyboardShortcuts as unknown as Partial
),
@@ -535,6 +536,7 @@ function buildSettingsUpdateFromStore(): Record {
validationModel: state.validationModel,
phaseModels: state.phaseModels,
autoLoadClaudeMd: state.autoLoadClaudeMd,
+ skipSandboxWarning: state.skipSandboxWarning,
keyboardShortcuts: state.keyboardShortcuts,
aiProfiles: state.aiProfiles,
mcpServers: state.mcpServers,
diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts
index 8d4188ff..f01d67cf 100644
--- a/apps/ui/src/lib/http-api-client.ts
+++ b/apps/ui/src/lib/http-api-client.ts
@@ -379,6 +379,32 @@ export const verifySession = async (): Promise => {
}
};
+/**
+ * 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 =
| 'agent:stream'
| 'auto-mode:event'
diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx
index c253ffa2..502aba11 100644
--- a/apps/ui/src/routes/__root.tsx
+++ b/apps/ui/src/routes/__root.tsx
@@ -16,19 +16,28 @@ import {
initApiKey,
isElectronMode,
verifySession,
+ checkSandboxEnvironment,
getServerUrlSync,
checkExternalServerMode,
isExternalServerMode,
} from '@/lib/http-api-client';
import { Toaster } from 'sonner';
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';
const logger = createLogger('RootLayout');
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);
@@ -44,6 +53,12 @@ function RootLayoutContent() {
const isSetupRoute = location.pathname === '/setup';
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('pending');
+
// Hidden streamer panel - opens with "\" key
const handleStreamerPanelShortcut = useCallback((event: KeyboardEvent) => {
const activeElement = document.activeElement;
@@ -90,6 +105,73 @@ function RootLayoutContent() {
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
const authCheckRunning = useRef(false);
@@ -234,12 +316,28 @@ function RootLayoutContent() {
}
}, [deferredTheme]);
+ // Show sandbox rejection screen if user denied the risk warning
+ if (sandboxStatus === 'denied') {
+ return ;
+ }
+
+ // 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)
if (isLoginRoute) {
return (
-
-
-
+ <>
+
+
+
+
+ >
);
}
@@ -275,30 +373,37 @@ function RootLayoutContent() {
}
return (
-
- {/* Full-width titlebar drag region for Electron window dragging */}
- {isElectron() && (
+ <>
+
+ {/* Full-width titlebar drag region for Electron window dragging */}
+ {isElectron() && (
+
+ )}
+
- )}
-
-
-
-
+ className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
+ style={{ marginRight: streamerPanelOpen ? '250px' : '0' }}
+ >
+
+
- {/* Hidden streamer panel - opens with "\" key, pushes content */}
-
+
+
+
-
-
+ >
);
}
diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts
index 03cee293..a3915fd1 100644
--- a/apps/ui/src/store/app-store.ts
+++ b/apps/ui/src/store/app-store.ts
@@ -511,6 +511,7 @@ export interface AppState {
// Claude Agent SDK Settings
autoLoadClaudeMd: boolean; // Auto-load CLAUDE.md files using SDK's settingSources option
+ skipSandboxWarning: boolean; // Skip the sandbox environment warning dialog on startup
// MCP Servers
mcpServers: MCPServerConfig[]; // List of configured MCP servers for agent use
@@ -816,6 +817,7 @@ export interface AppActions {
// Claude Agent SDK Settings actions
setAutoLoadClaudeMd: (enabled: boolean) => Promise
;
+ setSkipSandboxWarning: (skip: boolean) => Promise;
// Prompt Customization actions
setPromptCustomization: (customization: PromptCustomization) => Promise;
@@ -1036,6 +1038,7 @@ const initialState: AppState = {
enabledCursorModels: getAllCursorModelIds(), // All Cursor models enabled by default
cursorDefaultModel: 'auto', // Default to auto selection
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
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
aiProfiles: DEFAULT_AI_PROFILES,
@@ -1734,6 +1737,17 @@ export const useAppStore = create()((set, get) => ({
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
setPromptCustomization: async (customization) => {
set({ promptCustomization: customization });
diff --git a/apps/ui/src/styles/global.css b/apps/ui/src/styles/global.css
index a335ebd0..70d6a0f6 100644
--- a/apps/ui/src/styles/global.css
+++ b/apps/ui/src/styles/global.css
@@ -367,6 +367,17 @@
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 */
button:not(:disabled),
[role='button']:not([aria-disabled='true']),
diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts
index 6cce2b9b..d8b0dab2 100644
--- a/libs/types/src/settings.ts
+++ b/libs/types/src/settings.ts
@@ -486,6 +486,8 @@ export interface GlobalSettings {
// Claude Agent SDK Settings
/** Auto-load CLAUDE.md files using SDK's settingSources option */
autoLoadClaudeMd?: boolean;
+ /** Skip the sandbox environment warning dialog on startup */
+ skipSandboxWarning?: boolean;
// MCP Server Configuration
/** List of configured MCP servers for agent use */