UI Recipe/Custom Agents (#2119)

Co-authored-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
Zaki Ali
2025-04-11 13:28:29 -07:00
committed by GitHub
parent b030f845ce
commit 454e4a47f4
21 changed files with 1321 additions and 246 deletions
+163 -53
View File
@@ -48,9 +48,34 @@ if (!gotTheLock) {
if (process.platform === 'win32') {
const protocolUrl = commandLine.find((arg) => arg.startsWith('goose://'));
if (protocolUrl) {
const parsedUrl = new URL(protocolUrl);
// If it's a bot/recipe URL, handle it directly by creating a new window
if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
app.whenReady().then(() => {
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
let recipeConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
});
return; // Skip the rest of the handler
}
// For non-bot URLs, continue with normal handling
handleProtocolUrl(protocolUrl);
}
// Only focus existing windows for non-bot/recipe URLs
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
const mainWindow = existingWindows[0];
@@ -61,7 +86,6 @@ if (!gotTheLock) {
}
}
});
if (process.platform === 'win32') {
const protocolUrl = process.argv.find((arg) => arg.startsWith('goose://'));
if (protocolUrl) {
@@ -79,12 +103,17 @@ async function handleProtocolUrl(url: string) {
if (!url) return;
pendingDeepLink = url;
const parsedUrl = new URL(url);
const parsedUrl = new URL(url);
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname !== 'bot') {
if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
// For bot/recipe URLs, skip existing window processing
// and let processProtocolUrl handle it entirely
processProtocolUrl(parsedUrl, null);
} else {
// For other URL types, reuse existing window if available
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0];
@@ -95,16 +124,16 @@ async function handleProtocolUrl(url: string) {
} else {
firstOpenWindow = await createChat(app, undefined, openDir);
}
}
if (firstOpenWindow) {
const webContents = firstOpenWindow.webContents;
if (webContents.isLoadingMainFrame()) {
webContents.once('did-finish-load', () => {
if (firstOpenWindow) {
const webContents = firstOpenWindow.webContents;
if (webContents.isLoadingMainFrame()) {
webContents.once('did-finish-load', () => {
processProtocolUrl(parsedUrl, firstOpenWindow);
});
} else {
processProtocolUrl(parsedUrl, firstOpenWindow);
});
} else {
processProtocolUrl(parsedUrl, firstOpenWindow);
}
}
}
}
@@ -117,55 +146,61 @@ function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) {
window.webContents.send('add-extension', pendingDeepLink);
} else if (parsedUrl.hostname === 'sessions') {
window.webContents.send('open-shared-session', pendingDeepLink);
} else if (parsedUrl.hostname === 'bot') {
let botConfig = null;
} else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
let recipeConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
createChat(app, undefined, openDir, undefined, undefined, botConfig);
// Create a new window and ignore the passed-in window
createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
}
pendingDeepLink = null;
}
app.on('open-url', async (event, url) => {
if (process.platform !== 'win32') {
pendingDeepLink = url;
const parsedUrl = new URL(url);
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname !== 'bot') {
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0];
if (firstOpenWindow.isMinimized()) firstOpenWindow.restore();
firstOpenWindow.focus();
} else {
firstOpenWindow = await createChat(app, undefined, openDir);
// Handle bot/recipe URLs by directly creating a new window
if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
let recipeConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
// Create a new window directly
await createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
return; // Skip the rest of the handler
}
// For non-bot URLs, continue with normal handling
pendingDeepLink = url;
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0];
if (firstOpenWindow.isMinimized()) firstOpenWindow.restore();
firstOpenWindow.focus();
} else {
firstOpenWindow = await createChat(app, undefined, openDir);
}
if (parsedUrl.hostname === 'extension') {
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
} else if (parsedUrl.hostname === 'sessions') {
firstOpenWindow.webContents.send('open-shared-session', pendingDeepLink);
} else if (parsedUrl.hostname === 'bot') {
let botConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
firstOpenWindow = await createChat(app, undefined, openDir, undefined, undefined, botConfig);
}
}
});
@@ -221,6 +256,7 @@ const getVersion = () => {
};
let [provider, model] = getGooseProvider();
console.log('[main] Got provider and model:', { provider, model });
let sharingUrl = getSharingUrl();
@@ -235,11 +271,13 @@ let appConfig = {
secretKey: generateSecretKey(),
};
console.log('[main] Created appConfig:', appConfig);
// Track windows by ID
let windowCounter = 0;
const windowMap = new Map<number, BrowserWindow>();
interface BotConfig {
interface RecipeConfig {
id: string;
name: string;
description: string;
@@ -253,12 +291,42 @@ const createChat = async (
dir?: string,
version?: string,
resumeSessionId?: string,
botConfig?: BotConfig
recipeConfig?: RecipeConfig, // Bot configuration
viewType?: string // View type
) => {
// Apply current environment settings before creating chat
updateEnvironmentVariables(envToggles);
// Initialize variables for process and configuration
let port = 0;
let working_dir = '';
let goosedProcess = null;
const [port, working_dir, goosedProcess] = await startGoosed(app, dir);
if (viewType === 'recipeEditor') {
// For recipeEditor, get the port from existing windows' config
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
// Get the config from localStorage through an existing window
try {
const result = await existingWindows[0].webContents.executeJavaScript(
`localStorage.getItem('gooseConfig')`
);
if (result) {
const config = JSON.parse(result);
port = config.GOOSE_PORT;
working_dir = config.GOOSE_WORKING_DIR;
}
} catch (e) {
console.error('Failed to get config from localStorage:', e);
}
}
if (port === 0) {
console.error('No existing Goose process found for recipeEditor');
throw new Error('Cannot create recipeEditor window: No existing Goose process found');
}
} else {
// Apply current environment settings before creating chat
updateEnvironmentVariables(envToggles);
// Start new Goosed process for regular windows
[port, working_dir, goosedProcess] = await startGoosed(app, dir);
}
const mainWindow = new BrowserWindow({
titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default',
@@ -279,17 +347,37 @@ const createChat = async (
JSON.stringify({
...appConfig,
GOOSE_PORT: port,
GOOSE_WORKING_DIR: working_dir,
GOOSE_WORKGIN_DIR: working_dir,
REQUEST_DIR: dir,
GOOSE_BASE_URL_SHARE: sharingUrl,
GOOSE_VERSION: gooseVersion,
botConfig: botConfig,
recipeConfig: recipeConfig,
}),
],
partition: 'persist:goose', // Add this line to ensure persistence
},
});
// Store config in localStorage for future windows
const windowConfig = {
...appConfig,
GOOSE_PORT: port,
GOOSE_WORKING_DIR: working_dir,
REQUEST_DIR: dir,
GOOSE_BASE_URL_SHARE: sharingUrl,
recipeConfig: recipeConfig,
};
// We need to wait for the window to load before we can access localStorage
mainWindow.webContents.on('did-finish-load', () => {
const configStr = JSON.stringify(windowConfig).replace(/'/g, "\\'");
mainWindow.webContents.executeJavaScript(`
localStorage.setItem('gooseConfig', '${configStr}')
`);
});
console.log('[main] Creating window with config:', windowConfig);
// Handle new window creation for links
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// Open all links in external browser
@@ -313,6 +401,13 @@ const createChat = async (
: `?resumeSessionId=${encodeURIComponent(resumeSessionId)}`;
}
// Add view type to query params if provided
if (viewType) {
queryParams = queryParams
? `${queryParams}&view=${encodeURIComponent(viewType)}`
: `?view=${encodeURIComponent(viewType)}`;
}
const primaryDisplay = electron.screen.getPrimaryDisplay();
const { width } = primaryDisplay.workAreaSize;
@@ -351,9 +446,12 @@ const createChat = async (
});
windowMap.set(windowId, mainWindow);
// Handle window closure
mainWindow.on('closed', () => {
windowMap.delete(windowId);
goosedProcess.kill();
if (goosedProcess) {
goosedProcess.kill();
}
});
return mainWindow;
};
@@ -477,6 +575,10 @@ ipcMain.on('react-ready', () => {
} else {
console.log('No pending deep link to process');
}
// We don't need to handle pending deep links here anymore
// since we're handling them in the window creation flow
console.log('[main] React ready - window is prepared for deep links');
});
// Handle directory chooser
@@ -675,10 +777,10 @@ app.whenReady().then(async () => {
// Extract the bot config from the URL
const configParam = new URL(sqlBotUrl).searchParams.get('config');
let botConfig = null;
let recipeConfig = null;
if (configParam) {
try {
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
@@ -688,7 +790,7 @@ app.whenReady().then(async () => {
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
createChat(app, undefined, openDir, undefined, undefined, botConfig);
createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
},
})
);
@@ -704,13 +806,21 @@ app.whenReady().then(async () => {
}
});
ipcMain.on('create-chat-window', (_event, query, dir, version, resumeSessionId, botConfig) => {
if (!dir?.trim()) {
const recentDirs = loadRecentDirs();
dir = recentDirs.length > 0 ? recentDirs[0] : null;
ipcMain.on(
'create-chat-window',
(_, query, dir, version, resumeSessionId, recipeConfig, viewType) => {
if (!dir?.trim()) {
const recentDirs = loadRecentDirs();
dir = recentDirs.length > 0 ? recentDirs[0] : null;
}
// Log the recipeConfig for debugging
console.log('Creating chat window with recipeConfig:', recipeConfig);
// Pass recipeConfig as part of viewOptions when viewType is recipeEditor
createChat(app, query, dir, version, resumeSessionId, recipeConfig, viewType);
}
createChat(app, query, dir, version, resumeSessionId, botConfig);
});
);
ipcMain.on('notify', (_event, data) => {
console.log('NOTIFY', data);