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:
2025-12-19 13:30:58 +01:00
parent 17e3d80e86
commit 165a7729a1
2 changed files with 163 additions and 6 deletions

View File

@@ -44,6 +44,8 @@ export function Sidebar({ open, onToggle }) {
stopClaudeSession,
clearMessages,
updateSessionConfig,
settings,
updateSettings,
} = useSessionManager();
const { user, authEnabled, logout, isAdmin } = useAuth();
@@ -102,6 +104,22 @@ export function Sidebar({ open, onToggle }) {
updateSessionConfig(focusedSessionId, { resumeOnStart: !resumeSession });
}, [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
const browsePath = useCallback(async (path) => {
if (!currentHost) return;
@@ -320,7 +338,7 @@ export function Sidebar({ open, onToggle }) {
)}
{/* Resume toggle */}
<div className="pt-2">
<div className="pt-2 space-y-3">
<label className="flex items-center gap-3 cursor-pointer group">
<div
onClick={handleToggleResume}
@@ -340,6 +358,49 @@ export function Sidebar({ open, onToggle }) {
Resume previous session
</span>
</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>

View File

@@ -8,8 +8,28 @@ function getWsUrl() {
}
const SESSIONS_STORAGE_KEY = 'claude-webui-sessions';
const SETTINGS_STORAGE_KEY = 'claude-webui-settings';
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
function generateSessionId() {
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)
const [tabOrder, setTabOrder] = useState([]);
// Global settings (autoConnect, etc.)
const [settings, setSettings] = useState(() => loadSettings());
// WebSocket refs keyed by session ID
const wsRefs = useRef({});
@@ -74,9 +97,14 @@ export function SessionProvider({ children }) {
const sessionsRef = useRef(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)
const initialLoadDone = useRef(false);
const sessionsToConnect = useRef([]);
const sessionsToAutoStart = useRef(new Set()); // Sessions that should auto-start after connecting
// Load sessions from localStorage on mount
useEffect(() => {
@@ -690,16 +718,73 @@ export function SessionProvider({ children }) {
};
}, [updateSession, handleWsMessage]);
// Auto-connect restored sessions
// Auto-connect restored sessions (only if autoConnect is enabled)
useEffect(() => {
if (sessionsToConnect.current.length > 0) {
if (sessionsToConnect.current.length > 0 && settings.autoConnect) {
const toConnect = [...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 => {
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
const disconnectSession = useCallback((sessionId) => {
@@ -955,6 +1040,15 @@ export function SessionProvider({ children }) {
updateSession(sessionId, config);
}, [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
const focusedSession = useMemo(() => {
return focusedSessionId ? sessions[focusedSessionId] : null;
@@ -969,6 +1063,7 @@ export function SessionProvider({ children }) {
splitSessions,
focusedSessionId,
focusedSession,
settings,
// Session management
createSession,
@@ -976,6 +1071,7 @@ export function SessionProvider({ children }) {
removeSession,
renameSession,
updateSessionConfig,
updateSettings,
// Focus & view
setFocusedSessionId,
@@ -1001,8 +1097,8 @@ export function SessionProvider({ children }) {
changePermissionMode,
respondToPermission,
}), [
sessions, sessionMessages, tabOrder, splitSessions, focusedSessionId, focusedSession,
createSession, closeSession, removeSession, renameSession, updateSessionConfig,
sessions, sessionMessages, tabOrder, splitSessions, focusedSessionId, focusedSession, settings,
createSession, closeSession, removeSession, renameSession, updateSessionConfig, updateSettings,
setFocusedSessionId, markAsRead, reorderTabs, addToSplit, removeFromSplit, clearSplit,
connectSession, disconnectSession, startClaudeSession, stopClaudeSession,
sendMessage, stopGeneration, clearMessages, setCompacting, changePermissionMode, respondToPermission,