feat: Add auto-connect and auto-start toggles for sessions
- Add global settings system (autoConnect, autoStart) with localStorage persistence - Auto-connect: Automatically establish WebSocket connections on page load - Auto-start: Automatically start Claude sessions after connecting (requires auto-connect) - Add two new toggles in Sidebar under Working Directory section - Auto-start toggle is disabled/grayed out when auto-connect is off - Disabling auto-connect also disables auto-start automatically 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,6 +44,8 @@ export function Sidebar({ open, onToggle }) {
|
|||||||
stopClaudeSession,
|
stopClaudeSession,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
updateSessionConfig,
|
updateSessionConfig,
|
||||||
|
settings,
|
||||||
|
updateSettings,
|
||||||
} = useSessionManager();
|
} = useSessionManager();
|
||||||
|
|
||||||
const { user, authEnabled, logout, isAdmin } = useAuth();
|
const { user, authEnabled, logout, isAdmin } = useAuth();
|
||||||
@@ -102,6 +104,22 @@ export function Sidebar({ open, onToggle }) {
|
|||||||
updateSessionConfig(focusedSessionId, { resumeOnStart: !resumeSession });
|
updateSessionConfig(focusedSessionId, { resumeOnStart: !resumeSession });
|
||||||
}, [focusedSessionId, resumeSession, updateSessionConfig]);
|
}, [focusedSessionId, resumeSession, updateSessionConfig]);
|
||||||
|
|
||||||
|
// Handle autoConnect toggle
|
||||||
|
const handleToggleAutoConnect = useCallback(() => {
|
||||||
|
const newAutoConnect = !settings?.autoConnect;
|
||||||
|
// If disabling autoConnect, also disable autoStart
|
||||||
|
if (!newAutoConnect) {
|
||||||
|
updateSettings({ autoConnect: false, autoStart: false });
|
||||||
|
} else {
|
||||||
|
updateSettings({ autoConnect: true });
|
||||||
|
}
|
||||||
|
}, [settings?.autoConnect, updateSettings]);
|
||||||
|
|
||||||
|
// Handle autoStart toggle
|
||||||
|
const handleToggleAutoStart = useCallback(() => {
|
||||||
|
updateSettings({ autoStart: !settings?.autoStart });
|
||||||
|
}, [settings?.autoStart, updateSettings]);
|
||||||
|
|
||||||
// Browse directories on host
|
// Browse directories on host
|
||||||
const browsePath = useCallback(async (path) => {
|
const browsePath = useCallback(async (path) => {
|
||||||
if (!currentHost) return;
|
if (!currentHost) return;
|
||||||
@@ -320,7 +338,7 @@ export function Sidebar({ open, onToggle }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Resume toggle */}
|
{/* Resume toggle */}
|
||||||
<div className="pt-2">
|
<div className="pt-2 space-y-3">
|
||||||
<label className="flex items-center gap-3 cursor-pointer group">
|
<label className="flex items-center gap-3 cursor-pointer group">
|
||||||
<div
|
<div
|
||||||
onClick={handleToggleResume}
|
onClick={handleToggleResume}
|
||||||
@@ -340,6 +358,49 @@ export function Sidebar({ open, onToggle }) {
|
|||||||
Resume previous session
|
Resume previous session
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{/* Auto-connect toggle */}
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer group">
|
||||||
|
<div
|
||||||
|
onClick={handleToggleAutoConnect}
|
||||||
|
className={`
|
||||||
|
relative w-10 h-5 rounded-full transition-colors
|
||||||
|
${settings?.autoConnect ? 'bg-orange-600' : 'bg-dark-700'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform
|
||||||
|
${settings?.autoConnect ? 'translate-x-5' : 'translate-x-0.5'}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-dark-300 group-hover:text-dark-200">
|
||||||
|
Auto-connect on load
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Auto-start toggle (only works if auto-connect is enabled) */}
|
||||||
|
<label className={`flex items-center gap-3 cursor-pointer group ${!settings?.autoConnect ? 'opacity-50' : ''}`}>
|
||||||
|
<div
|
||||||
|
onClick={() => settings?.autoConnect && handleToggleAutoStart()}
|
||||||
|
className={`
|
||||||
|
relative w-10 h-5 rounded-full transition-colors
|
||||||
|
${settings?.autoStart && settings?.autoConnect ? 'bg-orange-600' : 'bg-dark-700'}
|
||||||
|
${!settings?.autoConnect ? 'cursor-not-allowed' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform
|
||||||
|
${settings?.autoStart && settings?.autoConnect ? 'translate-x-5' : 'translate-x-0.5'}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-dark-300 group-hover:text-dark-200">
|
||||||
|
Auto-start sessions
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,28 @@ function getWsUrl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SESSIONS_STORAGE_KEY = 'claude-webui-sessions';
|
const SESSIONS_STORAGE_KEY = 'claude-webui-sessions';
|
||||||
|
const SETTINGS_STORAGE_KEY = 'claude-webui-settings';
|
||||||
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
|
||||||
|
// Load global settings from localStorage
|
||||||
|
function loadSettings() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(SETTINGS_STORAGE_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : { autoConnect: true, autoStart: false };
|
||||||
|
} catch {
|
||||||
|
return { autoConnect: true, autoStart: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save global settings to localStorage
|
||||||
|
function saveSettings(settings) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(settings));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to save settings:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate unique session ID
|
// Generate unique session ID
|
||||||
function generateSessionId() {
|
function generateSessionId() {
|
||||||
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
@@ -64,6 +84,9 @@ export function SessionProvider({ children }) {
|
|||||||
// Tab order (array of session IDs)
|
// Tab order (array of session IDs)
|
||||||
const [tabOrder, setTabOrder] = useState([]);
|
const [tabOrder, setTabOrder] = useState([]);
|
||||||
|
|
||||||
|
// Global settings (autoConnect, etc.)
|
||||||
|
const [settings, setSettings] = useState(() => loadSettings());
|
||||||
|
|
||||||
// WebSocket refs keyed by session ID
|
// WebSocket refs keyed by session ID
|
||||||
const wsRefs = useRef({});
|
const wsRefs = useRef({});
|
||||||
|
|
||||||
@@ -74,9 +97,14 @@ export function SessionProvider({ children }) {
|
|||||||
const sessionsRef = useRef(sessions);
|
const sessionsRef = useRef(sessions);
|
||||||
sessionsRef.current = sessions;
|
sessionsRef.current = sessions;
|
||||||
|
|
||||||
|
// Ref to current settings (for stable callbacks)
|
||||||
|
const settingsRef = useRef(settings);
|
||||||
|
settingsRef.current = settings;
|
||||||
|
|
||||||
// Track if initial load is done (for auto-connecting restored sessions)
|
// Track if initial load is done (for auto-connecting restored sessions)
|
||||||
const initialLoadDone = useRef(false);
|
const initialLoadDone = useRef(false);
|
||||||
const sessionsToConnect = useRef([]);
|
const sessionsToConnect = useRef([]);
|
||||||
|
const sessionsToAutoStart = useRef(new Set()); // Sessions that should auto-start after connecting
|
||||||
|
|
||||||
// Load sessions from localStorage on mount
|
// Load sessions from localStorage on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -690,16 +718,73 @@ export function SessionProvider({ children }) {
|
|||||||
};
|
};
|
||||||
}, [updateSession, handleWsMessage]);
|
}, [updateSession, handleWsMessage]);
|
||||||
|
|
||||||
// Auto-connect restored sessions
|
// Auto-connect restored sessions (only if autoConnect is enabled)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionsToConnect.current.length > 0) {
|
if (sessionsToConnect.current.length > 0 && settings.autoConnect) {
|
||||||
const toConnect = [...sessionsToConnect.current];
|
const toConnect = [...sessionsToConnect.current];
|
||||||
sessionsToConnect.current = [];
|
sessionsToConnect.current = [];
|
||||||
|
|
||||||
|
// Mark sessions for auto-start if autoStart is also enabled
|
||||||
|
if (settings.autoStart) {
|
||||||
|
toConnect.forEach(sessionId => {
|
||||||
|
sessionsToAutoStart.current.add(sessionId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toConnect.forEach(sessionId => {
|
toConnect.forEach(sessionId => {
|
||||||
connectSession(sessionId);
|
connectSession(sessionId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [connectSession]);
|
}, [connectSession, settings.autoConnect, settings.autoStart]);
|
||||||
|
|
||||||
|
// Auto-start sessions after they connect (if marked for auto-start)
|
||||||
|
useEffect(() => {
|
||||||
|
if (sessionsToAutoStart.current.size === 0) return;
|
||||||
|
|
||||||
|
// Check each session marked for auto-start
|
||||||
|
sessionsToAutoStart.current.forEach(sessionId => {
|
||||||
|
const session = sessions[sessionId];
|
||||||
|
// If session is connected but not yet active, start it
|
||||||
|
if (session?.connected && !session?.active) {
|
||||||
|
console.log(`[${sessionId}] Auto-starting session`);
|
||||||
|
sessionsToAutoStart.current.delete(sessionId);
|
||||||
|
|
||||||
|
// Use the websocket directly to start the session
|
||||||
|
const ws = wsRefs.current[sessionId];
|
||||||
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
|
// Load history first if resumeOnStart is enabled
|
||||||
|
const startSession = async () => {
|
||||||
|
if (session.resumeOnStart) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/history/${encodeURIComponent(session.project)}?host=${session.host}`,
|
||||||
|
{ credentials: 'include' }
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.messages && Array.isArray(data.messages)) {
|
||||||
|
setSessionMessages(prev => ({
|
||||||
|
...prev,
|
||||||
|
[sessionId]: data.messages,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load history:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'start_session',
|
||||||
|
project: session.project,
|
||||||
|
resume: session.resumeOnStart,
|
||||||
|
host: session.host,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
startSession();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [sessions]);
|
||||||
|
|
||||||
// Disconnect WebSocket for a session
|
// Disconnect WebSocket for a session
|
||||||
const disconnectSession = useCallback((sessionId) => {
|
const disconnectSession = useCallback((sessionId) => {
|
||||||
@@ -955,6 +1040,15 @@ export function SessionProvider({ children }) {
|
|||||||
updateSession(sessionId, config);
|
updateSession(sessionId, config);
|
||||||
}, [updateSession]);
|
}, [updateSession]);
|
||||||
|
|
||||||
|
// Update global settings (autoConnect, etc.)
|
||||||
|
const updateSettings = useCallback((newSettings) => {
|
||||||
|
setSettings(prev => {
|
||||||
|
const updated = { ...prev, ...newSettings };
|
||||||
|
saveSettings(updated);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Memoize focused session to prevent unnecessary re-renders
|
// Memoize focused session to prevent unnecessary re-renders
|
||||||
const focusedSession = useMemo(() => {
|
const focusedSession = useMemo(() => {
|
||||||
return focusedSessionId ? sessions[focusedSessionId] : null;
|
return focusedSessionId ? sessions[focusedSessionId] : null;
|
||||||
@@ -969,6 +1063,7 @@ export function SessionProvider({ children }) {
|
|||||||
splitSessions,
|
splitSessions,
|
||||||
focusedSessionId,
|
focusedSessionId,
|
||||||
focusedSession,
|
focusedSession,
|
||||||
|
settings,
|
||||||
|
|
||||||
// Session management
|
// Session management
|
||||||
createSession,
|
createSession,
|
||||||
@@ -976,6 +1071,7 @@ export function SessionProvider({ children }) {
|
|||||||
removeSession,
|
removeSession,
|
||||||
renameSession,
|
renameSession,
|
||||||
updateSessionConfig,
|
updateSessionConfig,
|
||||||
|
updateSettings,
|
||||||
|
|
||||||
// Focus & view
|
// Focus & view
|
||||||
setFocusedSessionId,
|
setFocusedSessionId,
|
||||||
@@ -1001,8 +1097,8 @@ export function SessionProvider({ children }) {
|
|||||||
changePermissionMode,
|
changePermissionMode,
|
||||||
respondToPermission,
|
respondToPermission,
|
||||||
}), [
|
}), [
|
||||||
sessions, sessionMessages, tabOrder, splitSessions, focusedSessionId, focusedSession,
|
sessions, sessionMessages, tabOrder, splitSessions, focusedSessionId, focusedSession, settings,
|
||||||
createSession, closeSession, removeSession, renameSession, updateSessionConfig,
|
createSession, closeSession, removeSession, renameSession, updateSessionConfig, updateSettings,
|
||||||
setFocusedSessionId, markAsRead, reorderTabs, addToSplit, removeFromSplit, clearSplit,
|
setFocusedSessionId, markAsRead, reorderTabs, addToSplit, removeFromSplit, clearSplit,
|
||||||
connectSession, disconnectSession, startClaudeSession, stopClaudeSession,
|
connectSession, disconnectSession, startClaudeSession, stopClaudeSession,
|
||||||
sendMessage, stopGeneration, clearMessages, setCompacting, changePermissionMode, respondToPermission,
|
sendMessage, stopGeneration, clearMessages, setCompacting, changePermissionMode, respondToPermission,
|
||||||
|
|||||||
Reference in New Issue
Block a user