mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-20 23:54:23 +01:00
Add basic cron scheduler to goose-server (#2621)
This commit is contained in:
@@ -8,7 +8,6 @@ import { ToastContainer } from 'react-toastify';
|
||||
import { toastService } from './toasts';
|
||||
import { extractExtensionName } from './components/settings/extensions/utils';
|
||||
import { GoosehintsModal } from './components/GoosehintsModal';
|
||||
import { SessionDetails } from './sessions';
|
||||
|
||||
import ChatView from './components/ChatView';
|
||||
import SuspenseLoader from './suspense-loader';
|
||||
@@ -18,6 +17,7 @@ import MoreModelsView from './components/settings/models/MoreModelsView';
|
||||
import ConfigureProvidersView from './components/settings/providers/ConfigureProvidersView';
|
||||
import SessionsView from './components/sessions/SessionsView';
|
||||
import SharedSessionView from './components/sessions/SharedSessionView';
|
||||
import SchedulesView from './components/schedule/SchedulesView';
|
||||
import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage';
|
||||
import RecipeEditor from './components/RecipeEditor';
|
||||
import { useChat } from './hooks/useChat';
|
||||
@@ -28,7 +28,8 @@ import { addExtensionFromDeepLink as addExtensionFromDeepLinkV2 } from './compon
|
||||
import { backupConfig, initConfig, readAllConfig } from './api/sdk.gen';
|
||||
import PermissionSettingsView from './components/settings_v2/permission/PermissionSetting';
|
||||
|
||||
// Views and their options
|
||||
import { type SessionDetails } from './sessions';
|
||||
|
||||
export type View =
|
||||
| 'welcome'
|
||||
| 'chat'
|
||||
@@ -39,6 +40,7 @@ export type View =
|
||||
| 'ConfigureProviders'
|
||||
| 'settingsV2'
|
||||
| 'sessions'
|
||||
| 'schedules'
|
||||
| 'sharedSession'
|
||||
| 'loading'
|
||||
| 'recipeEditor'
|
||||
@@ -47,8 +49,7 @@ export type View =
|
||||
export type ViewOptions =
|
||||
| SettingsViewOptions
|
||||
| { resumedSession?: SessionDetails }
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
| Record<string, any>;
|
||||
| Record<string, unknown>;
|
||||
|
||||
export type ViewConfig = {
|
||||
view: View;
|
||||
@@ -69,7 +70,6 @@ const getInitialView = (): ViewConfig => {
|
||||
};
|
||||
}
|
||||
|
||||
// Any other URL-specified view
|
||||
if (viewFromUrl) {
|
||||
return {
|
||||
view: viewFromUrl as View,
|
||||
@@ -77,7 +77,6 @@ const getInitialView = (): ViewConfig => {
|
||||
};
|
||||
}
|
||||
|
||||
// Default case
|
||||
return {
|
||||
view: 'loading',
|
||||
viewOptions: {},
|
||||
@@ -93,10 +92,10 @@ export default function App() {
|
||||
const [extensionConfirmLabel, setExtensionConfirmLabel] = useState<string>('');
|
||||
const [extensionConfirmTitle, setExtensionConfirmTitle] = useState<string>('');
|
||||
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>(getInitialView());
|
||||
|
||||
const { getExtensions, addExtension, read } = useConfig();
|
||||
const initAttemptedRef = useRef(false);
|
||||
|
||||
// Utility function to extract the command from the link
|
||||
function extractCommand(link: string): string {
|
||||
const url = new URL(link);
|
||||
const cmd = url.searchParams.get('cmd') || 'Unknown Command';
|
||||
@@ -104,7 +103,6 @@ export default function App() {
|
||||
return `${cmd} ${args.join(' ')}`.trim();
|
||||
}
|
||||
|
||||
// Utility function to extract the remote url from the link
|
||||
function extractRemoteUrl(link: string): string {
|
||||
const url = new URL(link);
|
||||
return url.searchParams.get('url');
|
||||
@@ -116,7 +114,6 @@ export default function App() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Guard against multiple initialization attempts
|
||||
if (initAttemptedRef.current) {
|
||||
console.log('Initialization already attempted, skipping...');
|
||||
return;
|
||||
@@ -129,7 +126,6 @@ export default function App() {
|
||||
const viewType = urlParams.get('view');
|
||||
const recipeConfig = window.appConfig.get('recipeConfig');
|
||||
|
||||
// If we have a specific view type in the URL, use that and skip provider detection
|
||||
if (viewType) {
|
||||
if (viewType === 'recipeEditor' && recipeConfig) {
|
||||
console.log('Setting view to recipeEditor with config:', recipeConfig);
|
||||
@@ -142,39 +138,31 @@ export default function App() {
|
||||
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
// checks if there is a config, and if not creates it
|
||||
await initConfig();
|
||||
|
||||
// now try to read config, if we fail and are migrating backup, then re-init config
|
||||
try {
|
||||
await readAllConfig({ throwOnError: true });
|
||||
} catch (error) {
|
||||
// NOTE: we do this check here and in providerUtils.ts, be sure to clean up both in the future
|
||||
const configVersion = localStorage.getItem('configVersion');
|
||||
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;
|
||||
if (shouldMigrateExtensions) {
|
||||
await backupConfig({ throwOnError: true });
|
||||
await initConfig();
|
||||
} else {
|
||||
// if we've migrated throw this back up
|
||||
throw new Error('Unable to read config file, it may be malformed');
|
||||
}
|
||||
}
|
||||
|
||||
// note: if in a non recipe session, recipeConfig is undefined, otherwise null if error
|
||||
if (recipeConfig === null) {
|
||||
setFatalError('Cannot read recipe config. Please check the deeplink and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = window.electron.getConfig();
|
||||
|
||||
const provider = (await read('GOOSE_PROVIDER', false)) ?? config.GOOSE_DEFAULT_PROVIDER;
|
||||
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
|
||||
|
||||
if (provider && model) {
|
||||
setView('chat');
|
||||
|
||||
try {
|
||||
await initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
@@ -182,13 +170,9 @@ export default function App() {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in initialization:', error);
|
||||
|
||||
// propagate the error upward so the global ErrorUI shows in cases
|
||||
// where going through welcome/onboarding wouldn't address the issue
|
||||
if (error instanceof MalformedConfigError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
setView('welcome');
|
||||
}
|
||||
} else {
|
||||
@@ -201,8 +185,6 @@ export default function App() {
|
||||
);
|
||||
setView('welcome');
|
||||
}
|
||||
|
||||
// Reset toast service after initialization
|
||||
toastService.configure({ silent: false });
|
||||
};
|
||||
|
||||
@@ -215,8 +197,7 @@ export default function App() {
|
||||
setFatalError(`${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
})();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); // Empty dependency array since we only want this to run once
|
||||
}, [read, getExtensions, addExtension]);
|
||||
|
||||
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
|
||||
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
||||
@@ -236,32 +217,26 @@ export default function App() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Handle shared session deep links
|
||||
useEffect(() => {
|
||||
const handleOpenSharedSession = async (_event: IpcRendererEvent, link: string) => {
|
||||
window.electron.logInfo(`Opening shared session from deep link ${link}`);
|
||||
setIsLoadingSharedSession(true);
|
||||
setSharedSessionError(null);
|
||||
|
||||
try {
|
||||
await openSharedSessionFromDeepLink(link, setView);
|
||||
// No need to handle errors here as openSharedSessionFromDeepLink now handles them internally
|
||||
} catch (error) {
|
||||
// This should not happen, but just in case
|
||||
console.error('Unexpected error opening shared session:', error);
|
||||
setView('sessions'); // Fallback to sessions view
|
||||
setView('sessions');
|
||||
} finally {
|
||||
setIsLoadingSharedSession(false);
|
||||
}
|
||||
};
|
||||
|
||||
window.electron.on('open-shared-session', handleOpenSharedSession);
|
||||
return () => {
|
||||
window.electron.off('open-shared-session', handleOpenSharedSession);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcut handler
|
||||
useEffect(() => {
|
||||
console.log('Setting up keyboard shortcuts');
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -277,7 +252,6 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
@@ -288,17 +262,15 @@ export default function App() {
|
||||
console.log('Setting up fatal error handler');
|
||||
const handleFatalError = (_event: IpcRendererEvent, errorMessage: string) => {
|
||||
console.error('Encountered a fatal error: ', errorMessage);
|
||||
// Log additional context that might help diagnose the issue
|
||||
console.error('Current view:', view);
|
||||
console.error('Is loading session:', isLoadingSession);
|
||||
setFatalError(errorMessage);
|
||||
};
|
||||
|
||||
window.electron.on('fatal-error', handleFatalError);
|
||||
return () => {
|
||||
window.electron.off('fatal-error', handleFatalError);
|
||||
};
|
||||
}, [view, isLoadingSession]); // Add dependencies to provide context in error logs
|
||||
}, [view, isLoadingSession]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Setting up view change handler');
|
||||
@@ -306,14 +278,10 @@ export default function App() {
|
||||
console.log(`Received view change request to: ${newView}`);
|
||||
setView(newView);
|
||||
};
|
||||
|
||||
// Get initial view and config
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewFromUrl = urlParams.get('view');
|
||||
if (viewFromUrl) {
|
||||
// Get the config from the electron window config
|
||||
const windowConfig = window.electron.getConfig();
|
||||
|
||||
if (viewFromUrl === 'recipeEditor') {
|
||||
const initialViewOptions = {
|
||||
recipeConfig: windowConfig?.recipeConfig,
|
||||
@@ -324,12 +292,10 @@ export default function App() {
|
||||
setView(viewFromUrl);
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.on('set-view', handleSetView);
|
||||
return () => window.electron.off('set-view', handleSetView);
|
||||
}, []);
|
||||
|
||||
// Add cleanup for session states when view changes
|
||||
useEffect(() => {
|
||||
console.log(`View changed to: ${view}`);
|
||||
if (view !== 'chat' && view !== 'recipeEditor') {
|
||||
@@ -338,10 +304,7 @@ export default function App() {
|
||||
}
|
||||
}, [view]);
|
||||
|
||||
// Configuration for extension security
|
||||
const config = window.electron.getConfig();
|
||||
// If GOOSE_ALLOWLIST_WARNING is true, use warning-only mode (STRICT_ALLOWLIST=false)
|
||||
// If GOOSE_ALLOWLIST_WARNING is not set or false, use strict blocking mode (STRICT_ALLOWLIST=true)
|
||||
const STRICT_ALLOWLIST = config.GOOSE_ALLOWLIST_WARNING === true ? false : true;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -354,35 +317,24 @@ export default function App() {
|
||||
const extName = extractExtensionName(link);
|
||||
window.electron.logInfo(`Adding extension from deep link ${link}`);
|
||||
setPendingLink(link);
|
||||
|
||||
// Default values for confirmation dialog
|
||||
let warningMessage = '';
|
||||
let label = 'OK';
|
||||
let title = 'Confirm Extension Installation';
|
||||
let isBlocked = false;
|
||||
let useDetailedMessage = false;
|
||||
|
||||
// For SSE extensions (with remoteUrl), always use detailed message
|
||||
if (remoteUrl) {
|
||||
useDetailedMessage = true;
|
||||
} else {
|
||||
// For command-based extensions, check against allowlist
|
||||
try {
|
||||
const allowedCommands = await window.electron.getAllowedExtensions();
|
||||
|
||||
// Only check and show warning if we have a non-empty allowlist
|
||||
if (allowedCommands && allowedCommands.length > 0) {
|
||||
const isCommandAllowed = allowedCommands.some((allowedCmd) =>
|
||||
command.startsWith(allowedCmd)
|
||||
);
|
||||
|
||||
if (!isCommandAllowed) {
|
||||
// Not in allowlist - use detailed message and show warning/block
|
||||
useDetailedMessage = true;
|
||||
title = '⛔️ Untrusted Extension ⛔️';
|
||||
|
||||
if (STRICT_ALLOWLIST) {
|
||||
// Block installation completely unless override is active
|
||||
isBlocked = true;
|
||||
label = 'Extension Blocked';
|
||||
warningMessage =
|
||||
@@ -390,7 +342,6 @@ export default function App() {
|
||||
'Installation is blocked by your administrator. ' +
|
||||
'Please contact your administrator if you need this extension.';
|
||||
} else {
|
||||
// Allow override (either because STRICT_ALLOWLIST is false or secret key combo was used)
|
||||
label = 'Override and install';
|
||||
warningMessage =
|
||||
'\n\n⚠️ WARNING: This extension command is not in the allowed list. ' +
|
||||
@@ -398,51 +349,38 @@ export default function App() {
|
||||
'Please contact an admin if you are unsure or want to allow this extension.';
|
||||
}
|
||||
}
|
||||
// If in allowlist, use simple message (useDetailedMessage remains false)
|
||||
}
|
||||
// If no allowlist, use simple message (useDetailedMessage remains false)
|
||||
} catch (error) {
|
||||
console.error('Error checking allowlist:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the appropriate message based on the extension type and allowlist status
|
||||
if (useDetailedMessage) {
|
||||
// Detailed message for SSE extensions or non-allowlisted command extensions
|
||||
const detailedMessage = remoteUrl
|
||||
? `You are about to install the ${extName} extension which connects to:\n\n${remoteUrl}\n\nThis extension will be able to access your conversations and provide additional functionality.`
|
||||
: `You are about to install the ${extName} extension which runs the command:\n\n${command}\n\nThis extension will be able to access your conversations and provide additional functionality.`;
|
||||
|
||||
setModalMessage(`${detailedMessage}${warningMessage}`);
|
||||
} else {
|
||||
// Simple message for allowlisted command extensions or when no allowlist exists
|
||||
const messageDetails = `Command: ${command}`;
|
||||
setModalMessage(
|
||||
`Are you sure you want to install the ${extName} extension?\n\n${messageDetails}`
|
||||
);
|
||||
}
|
||||
|
||||
setExtensionConfirmLabel(label);
|
||||
setExtensionConfirmTitle(title);
|
||||
|
||||
// If blocked, disable the confirmation button functionality by setting a special flag
|
||||
if (isBlocked) {
|
||||
setPendingLink(null); // Clear the pending link so confirmation does nothing
|
||||
setPendingLink(null);
|
||||
}
|
||||
|
||||
setModalVisible(true);
|
||||
} catch (error) {
|
||||
console.error('Error handling add-extension event:', error);
|
||||
}
|
||||
};
|
||||
|
||||
window.electron.on('add-extension', handleAddExtension);
|
||||
return () => {
|
||||
window.electron.off('add-extension', handleAddExtension);
|
||||
};
|
||||
}, [STRICT_ALLOWLIST]);
|
||||
|
||||
// Focus the first found input field
|
||||
useEffect(() => {
|
||||
const handleFocusInput = (_event: IpcRendererEvent) => {
|
||||
const inputField = document.querySelector('input[type="text"], textarea') as HTMLInputElement;
|
||||
@@ -456,28 +394,24 @@ export default function App() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// TODO: modify
|
||||
const handleConfirm = async () => {
|
||||
if (pendingLink) {
|
||||
console.log(`Confirming installation of extension from: ${pendingLink}`);
|
||||
setModalVisible(false); // Dismiss modal immediately
|
||||
setModalVisible(false);
|
||||
try {
|
||||
await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView);
|
||||
console.log('Extension installation successful');
|
||||
} catch (error) {
|
||||
console.error('Failed to add extension:', error);
|
||||
// Consider showing a user-visible error notification here
|
||||
} finally {
|
||||
setPendingLink(null);
|
||||
}
|
||||
} else {
|
||||
// This case happens when pendingLink was cleared due to blocking
|
||||
console.log('Extension installation blocked by allowlist restrictions');
|
||||
setModalVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: modify
|
||||
const handleCancel = () => {
|
||||
console.log('Cancelled extension installation.');
|
||||
setModalVisible(false);
|
||||
@@ -566,6 +500,7 @@ export default function App() {
|
||||
/>
|
||||
)}
|
||||
{view === 'sessions' && <SessionsView setView={setView} />}
|
||||
{view === 'schedules' && <SchedulesView onClose={() => setView('chat')} />}
|
||||
{view === 'sharedSession' && (
|
||||
<SharedSessionView
|
||||
session={viewOptions?.sessionDetails}
|
||||
|
||||
Reference in New Issue
Block a user