UI Recipe/Custom Agents (#2119)
Co-authored-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
+163
-53
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user