diff --git a/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx b/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx index 905d82a1..94940257 100644 --- a/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx +++ b/apps/ui/src/components/dialogs/sandbox-risk-dialog.tsx @@ -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 - - - + +
+ setSkipInFuture(checked === true)} + data-testid="sandbox-skip-checkbox" + /> + +
+
+ + +
diff --git a/apps/ui/src/components/views/login-view.tsx b/apps/ui/src/components/views/login-view.tsx index a30ca4ec..c619f1f2 100644 --- a/apps/ui/src/components/views/login-view.tsx +++ b/apps/ui/src/components/views/login-view.tsx @@ -11,9 +11,13 @@ import { login } from '@/lib/http-api-client'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { KeyRound, AlertCircle, Loader2 } from 'lucide-react'; +import { useAuthStore } from '@/store/auth-store'; +import { useSetupStore } from '@/store/setup-store'; export function LoginView() { const navigate = useNavigate(); + const setAuthState = useAuthStore((s) => s.setAuthState); + const setupComplete = useSetupStore((s) => s.setupComplete); const [apiKey, setApiKey] = useState(''); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -26,8 +30,11 @@ export function LoginView() { try { const result = await login(apiKey.trim()); if (result.success) { - // Redirect to home/board on success - navigate({ to: '/' }); + // Mark as authenticated for this session (cookie-based auth) + setAuthState({ isAuthenticated: true, authChecked: true }); + + // After auth, determine if setup is needed or go to app + navigate({ to: setupComplete ? '/' : '/setup' }); } else { setError(result.error || 'Invalid API key'); } @@ -73,7 +80,7 @@ export function LoginView() { {error && (
- + {error}
)} diff --git a/apps/ui/src/components/views/settings-view.tsx b/apps/ui/src/components/views/settings-view.tsx index eb8e6804..f46cb966 100644 --- a/apps/ui/src/components/views/settings-view.tsx +++ b/apps/ui/src/components/views/settings-view.tsx @@ -50,6 +50,8 @@ export function SettingsView() { setAutoLoadClaudeMd, enableSandboxMode, setEnableSandboxMode, + skipSandboxWarning, + setSkipSandboxWarning, promptCustomization, setPromptCustomization, } = useAppStore(); @@ -147,6 +149,8 @@ export function SettingsView() { setShowDeleteDialog(true)} + skipSandboxWarning={skipSandboxWarning} + onResetSandboxWarning={() => setSkipSandboxWarning(false)} /> ); default: diff --git a/apps/ui/src/components/views/settings-view/danger-zone/danger-zone-section.tsx b/apps/ui/src/components/views/settings-view/danger-zone/danger-zone-section.tsx index 80732bdb..0a1d6ed9 100644 --- a/apps/ui/src/components/views/settings-view/danger-zone/danger-zone-section.tsx +++ b/apps/ui/src/components/views/settings-view/danger-zone/danger-zone-section.tsx @@ -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 (
Danger Zone

- Permanently remove this project from Automaker. + Destructive actions and reset options.

-
-
-
-
- -
-
-

{project.name}

-

{project.path}

+
+ {/* Sandbox Warning Reset */} + {skipSandboxWarning && ( +
+
+
+ +
+
+

Sandbox Warning Disabled

+

+ The sandbox environment warning is hidden on startup +

+
+
- -
+ )} + + {/* Project Delete */} + {project && ( +
+
+
+ +
+
+

{project.name}

+

{project.path}

+
+
+ +
+ )} + + {/* Empty state when nothing to show */} + {!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 502f2e96..5f60fc95 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -227,6 +227,7 @@ export async function syncSettingsToServer(): Promise { phaseModels: state.phaseModels, autoLoadClaudeMd: state.autoLoadClaudeMd, enableSandboxMode: state.enableSandboxMode, + 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 20bc38d3..bc4f457f 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -40,9 +40,12 @@ let cachedServerUrl: string | null = null; * Must be called early in Electron mode before making API requests. */ export const initServerUrl = async (): Promise => { - if (typeof window !== 'undefined' && window.electronAPI?.getServerUrl) { + // window.electronAPI is typed as ElectronAPI, but some Electron-only helpers + // (like getServerUrl) are not part of the shared interface. Narrow via `any`. + const electron = typeof window !== 'undefined' ? (window.electronAPI as any) : null; + if (electron?.getServerUrl) { try { - cachedServerUrl = await window.electronAPI.getServerUrl(); + cachedServerUrl = await electron.getServerUrl(); console.log('[HTTP Client] Server URL from Electron:', cachedServerUrl); } catch (error) { console.warn('[HTTP Client] Failed to get server URL from Electron:', error); @@ -109,7 +112,13 @@ export const clearSessionToken = (): void => { * Check if we're running in Electron mode */ export const isElectronMode = (): boolean => { - return typeof window !== 'undefined' && !!window.electronAPI?.getApiKey; + if (typeof window === 'undefined') return false; + + // Prefer a stable runtime marker from preload. + // In some dev/electron setups, method availability can be temporarily undefined + // during early startup, but `isElectron` remains reliable. + const api = window.electronAPI as any; + return api?.isElectron === true || !!api?.getApiKey; }; /** @@ -307,7 +316,9 @@ export const verifySession = async (): Promise => { // Try to clear the cookie via logout (fire and forget) fetch(`${getServerUrl()}/api/auth/logout`, { method: 'POST', + headers: { 'Content-Type': 'application/json' }, credentials: 'include', + body: '{}', }).catch(() => {}); return false; } @@ -356,7 +367,8 @@ type EventType = | 'auto-mode:event' | 'suggestions:event' | 'spec-regeneration:event' - | 'issue-validation:event'; + | 'issue-validation:event' + | 'backlog-plan:event'; type EventCallback = (payload: unknown) => void; @@ -378,17 +390,20 @@ export class HttpApiClient implements ElectronAPI { constructor() { this.serverUrl = getServerUrl(); - // Wait for API key initialization before connecting WebSocket - // This prevents 401 errors on startup in Electron mode - waitForApiKeyInit() - .then(() => { - this.connectWebSocket(); - }) - .catch((error) => { - console.error('[HttpApiClient] API key initialization failed:', error); - // Still attempt WebSocket connection - it may work with cookie auth - this.connectWebSocket(); - }); + // Electron mode: connect WebSocket immediately once API key is ready. + // Web mode: defer WebSocket connection until a consumer subscribes to events, + // to avoid noisy 401s on first-load/login/setup routes. + if (isElectronMode()) { + waitForApiKeyInit() + .then(() => { + this.connectWebSocket(); + }) + .catch((error) => { + console.error('[HttpApiClient] API key initialization failed:', error); + // Still attempt WebSocket connection - it may work with cookie auth + this.connectWebSocket(); + }); + } } /** @@ -436,9 +451,24 @@ export class HttpApiClient implements ElectronAPI { this.isConnecting = true; - // In Electron mode, use API key directly - const apiKey = getApiKey(); - if (apiKey) { + // Electron mode must authenticate with the injected API key. + // If the key isn't ready yet, do NOT fall back to /api/auth/token (web-mode flow). + if (isElectronMode()) { + const apiKey = getApiKey(); + if (!apiKey) { + console.warn( + '[HttpApiClient] Electron mode: API key not ready, delaying WebSocket connect' + ); + this.isConnecting = false; + if (!this.reconnectTimer) { + this.reconnectTimer = setTimeout(() => { + this.reconnectTimer = null; + this.connectWebSocket(); + }, 250); + } + return; + } + const wsUrl = this.serverUrl.replace(/^http/, 'ws') + '/api/events'; this.establishWebSocket(`${wsUrl}?apiKey=${encodeURIComponent(apiKey)}`); return; diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx index 3608334d..d486ce59 100644 --- a/apps/ui/src/routes/__root.tsx +++ b/apps/ui/src/routes/__root.tsx @@ -1,5 +1,5 @@ import { createRootRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router'; -import { useEffect, useState, useCallback, useDeferredValue } from 'react'; +import { useEffect, useState, useCallback, useDeferredValue, useRef } from 'react'; import { Sidebar } from '@/components/layout/sidebar'; import { FileBrowserProvider, @@ -8,6 +8,7 @@ import { } from '@/contexts/file-browser-context'; import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; +import { useAuthStore } from '@/store/auth-store'; import { getElectronAPI, isElectron } from '@/lib/electron'; import { isMac } from '@/lib/utils'; import { @@ -15,19 +16,22 @@ import { isElectronMode, verifySession, checkSandboxEnvironment, + getServerUrlSync, } 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'; -// Session storage key for sandbox risk acknowledgment -const SANDBOX_RISK_ACKNOWLEDGED_KEY = 'automaker-sandbox-risk-acknowledged'; -const SANDBOX_DENIED_KEY = 'automaker-sandbox-denied'; - 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); @@ -35,23 +39,18 @@ function RootLayoutContent() { const [setupHydrated, setSetupHydrated] = useState( () => useSetupStore.persist?.hasHydrated?.() ?? false ); - const [authChecked, setAuthChecked] = useState(false); - const [isAuthenticated, setIsAuthenticated] = useState(false); + const authChecked = useAuthStore((s) => s.authChecked); + const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const { openFileBrowser } = useFileBrowser(); + const isSetupRoute = location.pathname === '/setup'; + const isLoginRoute = location.pathname === '/login'; + // Sandbox environment check state type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed'; - const [sandboxStatus, setSandboxStatus] = useState(() => { - // Check if user previously denied in this session - if (sessionStorage.getItem(SANDBOX_DENIED_KEY)) { - return 'denied'; - } - // Check if user previously acknowledged in this session - if (sessionStorage.getItem(SANDBOX_RISK_ACKNOWLEDGED_KEY)) { - return 'confirmed'; - } - return 'pending'; - }); + // 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) => { @@ -113,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'); @@ -120,23 +122,30 @@ 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(() => { - sessionStorage.setItem(SANDBOX_RISK_ACKNOWLEDGED_KEY, 'true'); - setSandboxStatus('confirmed'); - }, []); + const handleSandboxConfirm = useCallback( + (skipInFuture: boolean) => { + if (skipInFuture) { + setSkipSandboxWarning(true); + } + setSandboxStatus('confirmed'); + }, + [setSkipSandboxWarning] + ); // Handle sandbox risk denial const handleSandboxDeny = useCallback(async () => { - sessionStorage.setItem(SANDBOX_DENIED_KEY, 'true'); - if (isElectron()) { // In Electron mode, quit the application // Use window.electronAPI directly since getElectronAPI() returns the HTTP client @@ -156,19 +165,28 @@ function RootLayoutContent() { } }, []); + // Ref to prevent concurrent auth checks from running + const authCheckRunning = useRef(false); + // Initialize authentication // - Electron mode: Uses API key from IPC (header-based auth) // - Web mode: Uses HTTP-only session cookie useEffect(() => { + // Prevent concurrent auth checks + if (authCheckRunning.current) { + return; + } + const initAuth = async () => { + authCheckRunning.current = true; + try { // Initialize API key for Electron mode await initApiKey(); // In Electron mode, we're always authenticated via header if (isElectronMode()) { - setIsAuthenticated(true); - setAuthChecked(true); + useAuthStore.getState().setAuthState({ isAuthenticated: true, authChecked: true }); return; } @@ -177,31 +195,23 @@ function RootLayoutContent() { const isValid = await verifySession(); if (isValid) { - setIsAuthenticated(true); - setAuthChecked(true); + useAuthStore.getState().setAuthState({ isAuthenticated: true, authChecked: true }); return; } - // Session is invalid or expired - redirect to login - console.log('Session invalid or expired - redirecting to login'); - setIsAuthenticated(false); - setAuthChecked(true); - - if (location.pathname !== '/login') { - navigate({ to: '/login' }); - } + // Session is invalid or expired - treat as not authenticated + useAuthStore.getState().setAuthState({ isAuthenticated: false, authChecked: true }); } catch (error) { console.error('Failed to initialize auth:', error); - setAuthChecked(true); - // On error, redirect to login to be safe - if (location.pathname !== '/login') { - navigate({ to: '/login' }); - } + // On error, treat as not authenticated + useAuthStore.getState().setAuthState({ isAuthenticated: false, authChecked: true }); + } finally { + authCheckRunning.current = false; } }; initAuth(); - }, [location.pathname, navigate]); + }, []); // Runs once per load; auth state drives routing rules // Wait for setup store hydration before enforcing routing rules useEffect(() => { @@ -221,16 +231,34 @@ function RootLayoutContent() { }; }, []); - // Redirect first-run users (or anyone who reopened the wizard) to /setup + // Routing rules (web mode): + // - If not authenticated: force /login (even /setup is protected) + // - If authenticated but setup incomplete: force /setup useEffect(() => { if (!setupHydrated) return; + // Wait for auth check to complete before enforcing any redirects + if (!isElectronMode() && !authChecked) return; + + // Unauthenticated -> force /login + if (!isElectronMode() && !isAuthenticated) { + if (location.pathname !== '/login') { + navigate({ to: '/login' }); + } + return; + } + + // Authenticated -> determine whether setup is required if (!setupComplete && location.pathname !== '/setup') { navigate({ to: '/setup' }); - } else if (setupComplete && location.pathname === '/setup') { + return; + } + + // Setup complete but user is still on /setup -> go to app + if (setupComplete && location.pathname === '/setup') { navigate({ to: '/' }); } - }, [setupComplete, setupHydrated, location.pathname, navigate]); + }, [authChecked, isAuthenticated, setupComplete, setupHydrated, location.pathname, navigate]); useEffect(() => { setGlobalFileBrowser(openFileBrowser); @@ -240,9 +268,19 @@ function RootLayoutContent() { useEffect(() => { const testConnection = async () => { try { - const api = getElectronAPI(); - const result = await api.ping(); - setIpcConnected(result === 'pong'); + if (isElectron()) { + const api = getElectronAPI(); + const result = await api.ping(); + setIpcConnected(result === 'pong'); + return; + } + + // Web mode: check backend availability without instantiating the full HTTP client + const response = await fetch(`${getServerUrlSync()}/api/health`, { + method: 'GET', + signal: AbortSignal.timeout(2000), + }); + setIpcConnected(response.ok); } catch (error) { console.error('IPC connection failed:', error); setIpcConnected(false); @@ -280,10 +318,6 @@ function RootLayoutContent() { } }, [deferredTheme]); - // Login and setup views are full-screen without sidebar - const isSetupRoute = location.pathname === '/setup'; - const isLoginRoute = location.pathname === '/login'; - // Show rejection screen if user denied sandbox risk (web mode only) if (sandboxStatus === 'denied' && !isElectron()) { return ; @@ -323,10 +357,16 @@ function RootLayoutContent() { } // Redirect to login if not authenticated (web mode) + // Show loading state while navigation to login is in progress if (!isElectronMode() && !isAuthenticated) { - return null; // Will redirect via useEffect + return ( +
+
Redirecting to login...
+
+ ); } + // Show setup page (full screen, no sidebar) - authenticated only if (isSetupRoute) { return (
diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index ddcf9b86..b717b3fb 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -499,6 +499,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 @@ -798,6 +799,7 @@ export interface AppActions { // Claude Agent SDK Settings actions setAutoLoadClaudeMd: (enabled: boolean) => Promise; setEnableSandboxMode: (enabled: boolean) => Promise; + setSkipSandboxWarning: (skip: boolean) => Promise; setMcpAutoApproveTools: (enabled: boolean) => Promise; setMcpUnrestrictedTools: (enabled: boolean) => Promise; @@ -1014,6 +1016,7 @@ const initialState: AppState = { cursorDefaultModel: 'auto', // Default to auto selection 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 @@ -1709,6 +1712,12 @@ export const useAppStore = create()( 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 @@ -3010,6 +3019,7 @@ export const useAppStore = create()( cursorDefaultModel: state.cursorDefaultModel, autoLoadClaudeMd: state.autoLoadClaudeMd, enableSandboxMode: state.enableSandboxMode, + skipSandboxWarning: state.skipSandboxWarning, // MCP settings mcpServers: state.mcpServers, mcpAutoApproveTools: state.mcpAutoApproveTools, diff --git a/apps/ui/src/store/auth-store.ts b/apps/ui/src/store/auth-store.ts new file mode 100644 index 00000000..7c594d0d --- /dev/null +++ b/apps/ui/src/store/auth-store.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand'; + +interface AuthState { + /** Whether we've attempted to determine auth status for this page load */ + authChecked: boolean; + /** Whether the user is currently authenticated (web mode: valid session cookie) */ + isAuthenticated: boolean; +} + +interface AuthActions { + setAuthState: (state: Partial) => void; + resetAuth: () => void; +} + +const initialState: AuthState = { + authChecked: false, + isAuthenticated: false, +}; + +/** + * Web authentication state. + * + * Intentionally NOT persisted: source of truth is the server session cookie. + */ +export const useAuthStore = create((set) => ({ + ...initialState, + setAuthState: (state) => set(state), + resetAuth: () => set(initialState), +})); diff --git a/init.mjs b/init.mjs index 9947f574..49d47fa6 100644 --- a/init.mjs +++ b/init.mjs @@ -268,6 +268,20 @@ function runNpm(args, options = {}) { return crossSpawn('npm', args, spawnOptions); } +/** + * Run an npm command and wait for completion + */ +function runNpmAndWait(args, options = {}) { + const child = runNpm(args, options); + return new Promise((resolve, reject) => { + child.on('close', (code) => { + if (code === 0) resolve(); + else reject(new Error(`npm ${args.join(' ')} failed with code ${code}`)); + }); + child.on('error', (err) => reject(err)); + }); +} + /** * Run npx command using cross-spawn for Windows compatibility */ @@ -525,6 +539,10 @@ async function main() { console.log(''); log('Launching Web Application...', 'blue'); + // Build shared packages once (dev:server and dev:web both do this at the root level) + log('Building shared packages...', 'blue'); + await runNpmAndWait(['run', 'build:packages'], { stdio: 'inherit' }); + // Start the backend server log(`Starting backend server on port ${serverPort}...`, 'blue'); @@ -535,7 +553,7 @@ async function main() { // Start server in background, showing output in console AND logging to file const logStream = fs.createWriteStream(path.join(__dirname, 'logs', 'server.log')); - serverProcess = runNpm(['run', 'dev:server'], { + serverProcess = runNpm(['run', '_dev:server'], { stdio: ['ignore', 'pipe', 'pipe'], env: { PORT: String(serverPort), @@ -582,7 +600,7 @@ async function main() { console.log(''); // Start web app - webProcess = runNpm(['run', 'dev:web'], { + webProcess = runNpm(['run', '_dev:web'], { stdio: 'inherit', env: { TEST_PORT: String(webPort), diff --git a/libs/types/src/settings.ts b/libs/types/src/settings.ts index bddebec1..c0b5640f 100644 --- a/libs/types/src/settings.ts +++ b/libs/types/src/settings.ts @@ -437,6 +437,8 @@ export interface GlobalSettings { autoLoadClaudeMd?: boolean; /** Enable sandbox mode for bash commands (default: false, enable for additional security) */ enableSandboxMode?: boolean; + /** Skip showing the sandbox risk warning dialog */ + skipSandboxWarning?: boolean; // MCP Server Configuration /** List of configured MCP servers for agent use */ @@ -635,6 +637,7 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { lastSelectedSessionByProject: {}, autoLoadClaudeMd: false, enableSandboxMode: false, + skipSandboxWarning: false, mcpServers: [], // Default to true for autonomous workflow. Security is enforced when adding servers // via the security warning dialog that explains the risks.