used recipe id or deeplink to start agent (#5154)

This commit is contained in:
Lifei Zhou
2025-10-16 08:36:40 +11:00
committed by GitHub
parent 04faf59bc2
commit a6d0666ffd
15 changed files with 357 additions and 184 deletions
+20 -8
View File
@@ -177,7 +177,7 @@ export type Envs = {
};
export type ErrorResponse = {
error: string;
message: string;
};
export type ExtendPromptRequest = {
@@ -667,6 +667,10 @@ export type SaveRecipeRequest = {
recipe: Recipe;
};
export type SaveRecipeResponse = {
id: string;
};
export type ScanRecipeRequest = {
recipe: Recipe;
};
@@ -764,6 +768,8 @@ export type SetupResponse = {
export type StartAgentRequest = {
recipe?: Recipe | null;
recipe_deeplink?: string | null;
recipe_id?: string | null;
working_dir: string;
};
@@ -1041,9 +1047,9 @@ export type StartAgentData = {
export type StartAgentErrors = {
/**
* Bad request - invalid working directory
* Bad request
*/
400: unknown;
400: ErrorResponse;
/**
* Unauthorized - invalid secret key
*/
@@ -1051,9 +1057,11 @@ export type StartAgentErrors = {
/**
* Internal server error
*/
500: unknown;
500: ErrorResponse;
};
export type StartAgentError = StartAgentErrors[keyof StartAgentErrors];
export type StartAgentResponses = {
/**
* Agent started successfully
@@ -1873,9 +1881,13 @@ export type SaveRecipeData = {
export type SaveRecipeErrors = {
/**
* Unauthorized - Invalid or missing API key
* Unauthorized
*/
401: unknown;
401: ErrorResponse;
/**
* Not found
*/
404: ErrorResponse;
/**
* Internal server error
*/
@@ -1888,10 +1900,10 @@ export type SaveRecipeResponses = {
/**
* Recipe saved to file successfully
*/
204: void;
204: SaveRecipeResponse;
};
export type SaveRecipeResponse = SaveRecipeResponses[keyof SaveRecipeResponses];
export type SaveRecipeResponse2 = SaveRecipeResponses[keyof SaveRecipeResponses];
export type ScanRecipeData = {
body: ScanRecipeRequest;
@@ -295,7 +295,7 @@ export default function CreateEditRecipeModal({
try {
const recipe = getCurrentRecipe();
await saveRecipe(recipe, recipeId);
let saved_recipe_id = await saveRecipe(recipe, recipeId);
// Close modal first
onClose(true);
@@ -306,9 +306,9 @@ export default function CreateEditRecipeModal({
undefined,
undefined,
undefined,
recipe,
undefined,
recipeId ?? undefined
undefined,
saved_recipe_id
);
toastSuccess({
@@ -183,13 +183,21 @@ export default function CreateRecipeFromSessionModal({
extensions: [], // Will be populated based on current extensions
};
await saveRecipe(recipe, null);
let recipeId = await saveRecipe(recipe, null);
onRecipeCreated?.(recipe);
onClose();
if (runAfterSave) {
window.electron.createChatWindow(undefined, undefined, undefined, undefined, recipe);
window.electron.createChatWindow(
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
recipeId
);
}
} catch (error) {
console.error('Failed to create recipe:', error);
+59 -8
View File
@@ -47,15 +47,23 @@ export function useAgent(): UseAgentReturn {
const [agentState, setAgentState] = useState<AgentState>(AgentState.UNINITIALIZED);
const [sessionId, setSessionId] = useState<string | null>(null);
const initPromiseRef = useRef<Promise<ChatType> | null>(null);
const [recipeFromAppConfig, setRecipeFromAppConfig] = useState<Recipe | null>(
(window.appConfig.get('recipe') as Recipe) || null
const recipeIdFromConfig = useRef<string | null>(
(window.appConfig.get('recipeId') as string | null | undefined) ?? null
);
const recipeDeeplinkFromConfig = useRef<string | null>(
(window.appConfig.get('recipeDeeplink') as string | null | undefined) ?? null
);
const scheduledJobIdFromConfig = useRef<string | null>(
(window.appConfig.get('scheduledJobId') as string | null | undefined) ?? null
);
const { getExtensions, addExtension, read } = useConfig();
const resetChat = useCallback(() => {
setSessionId(null);
setAgentState(AgentState.UNINITIALIZED);
setRecipeFromAppConfig(null);
recipeIdFromConfig.current = null;
recipeDeeplinkFromConfig.current = null;
scheduledJobIdFromConfig.current = null;
}, []);
const agentIsInitialized = agentState === AgentState.INITIALIZED;
@@ -110,7 +118,11 @@ export function useAgent(): UseAgentReturn {
: await startAgent({
body: {
working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string,
recipe: recipeFromAppConfig ?? initContext.recipe,
...buildRecipeInput(
initContext.recipe,
recipeIdFromConfig.current,
recipeDeeplinkFromConfig.current
),
},
throwOnError: true,
});
@@ -121,6 +133,18 @@ export function useAgent(): UseAgentReturn {
}
setSessionId(agentSession.id);
if (!initContext.recipe && agentSession.recipe && scheduledJobIdFromConfig.current) {
agentSession.recipe = {
...agentSession.recipe,
scheduledJobId: scheduledJobIdFromConfig.current,
isScheduledExecution: true,
} as Recipe;
scheduledJobIdFromConfig.current = null;
}
recipeIdFromConfig.current = null;
recipeDeeplinkFromConfig.current = null;
agentWaitingMessage('Agent is loading config');
await initConfig();
@@ -169,10 +193,17 @@ export function useAgent(): UseAgentReturn {
return initChat;
} catch (error) {
if ((error + '').includes('Failed to create provider')) {
if (
(error + '').includes('Failed to create provider') ||
error instanceof NoProviderOrModelError
) {
setAgentState(AgentState.NO_PROVIDER);
} else {
setAgentState(AgentState.ERROR);
throw error;
}
setAgentState(AgentState.ERROR);
if (typeof error === 'object' && error !== null && 'message' in error) {
let error_message = error.message as string;
throw new Error(error_message);
}
throw error;
} finally {
@@ -184,7 +215,7 @@ export function useAgent(): UseAgentReturn {
initPromiseRef.current = initPromise;
return initPromise;
},
[agentIsInitialized, sessionId, read, recipeFromAppConfig, getExtensions, addExtension]
[agentIsInitialized, sessionId, read, getExtensions, addExtension]
);
return {
@@ -220,3 +251,23 @@ const handleConfigRecovery = async () => {
}
}
};
const buildRecipeInput = (
recipeOverride?: Recipe,
recipeId?: string | null,
recipeDeeplink?: string | null
) => {
if (recipeId) {
return { recipe_id: recipeId };
}
if (recipeDeeplink) {
return { recipe_deeplink: recipeDeeplink };
}
if (recipeOverride) {
return { recipe: recipeOverride };
}
return {};
};
-19
View File
@@ -68,13 +68,6 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => {
}
return;
}
// If we have a recipe from app config (deeplink), persist it
// But only if the chat context doesn't explicitly have null (which indicates it was cleared)
const appRecipe = window.appConfig.get('recipe') as Recipe | null;
if (appRecipe && chatContext.chat.recipe === undefined) {
chatContext.setRecipe(appRecipe);
}
}, [chatContext, recipe]);
useEffect(() => {
@@ -87,18 +80,6 @@ export const useRecipeManager = (chat: ChatType, recipe?: Recipe | null) => {
if (finalRecipe) {
hasCheckedRecipeRef.current = true;
// If the recipe comes from session metadata (not from navigation state),
// it means it was already accepted in a previous session, so auto-accept it
const hasMessages = chat.messages.length > 0;
const isFromSessionMetadata = !recipe && finalRecipe && hasMessages;
if (isFromSessionMetadata) {
// Recipe loaded from session metadata should be automatically accepted
setRecipeAccepted(true);
setIsRecipeWarningModalOpen(false);
return;
}
try {
const hasAccepted = await window.electron.hasAcceptedRecipeBefore(finalRecipe);
+6 -55
View File
@@ -50,25 +50,9 @@ import {
import { UPDATES_ENABLED } from './updates';
import { Recipe } from './recipe';
import './utils/recipeHash';
import { decodeRecipe } from './api';
import { Client, createClient, createConfig } from './api/client';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
async function decodeRecipeMain(client: Client, deeplink: string): Promise<Recipe | null> {
try {
return (
await decodeRecipe({
client,
throwOnError: true,
body: { deeplink },
})
).data.recipe;
} catch (e) {
console.error('Failed to decode recipe:', e);
}
return null;
}
// Updater functions (moved here to keep updates.ts minimal for release replacement)
function shouldSetupUpdater(): boolean {
// Setup updater if either the flag is enabled OR dev updates are enabled
@@ -544,12 +528,6 @@ const createChat = async (
}
// Create window config with loading state for recipe deeplinks
let isLoadingRecipe = false;
if (!recipe && recipeDeeplink) {
isLoadingRecipe = true;
console.log('[Main] Creating window with recipe loading state for deeplink:', recipeDeeplink);
}
// Load and manage window state
const mainWindowState = windowStateKeeper({
defaultWidth: 940, // large enough to show the sidebar on launch
@@ -584,8 +562,9 @@ const createChat = async (
REQUEST_DIR: dir,
GOOSE_BASE_URL_SHARE: baseUrlShare,
GOOSE_VERSION: version,
recipe: recipe,
recipeId: recipeId,
recipeDeeplink: recipeDeeplink,
scheduledJobId: scheduledJobId,
}),
],
partition: 'persist:goose', // Add this line to ensure persistence
@@ -697,7 +676,10 @@ const createChat = async (
if (viewType) {
appPath = routeMap[viewType] || '/';
}
if (appPath === '/' && (recipe !== undefined || recipeDeeplink !== undefined)) {
if (
appPath === '/' &&
(recipe !== undefined || recipeDeeplink !== undefined || recipeId !== undefined)
) {
appPath = '/pair';
}
@@ -747,37 +729,6 @@ const createChat = async (
windowMap.set(windowId, mainWindow);
// Handle recipe decoding in the background after window is created
if (isLoadingRecipe && recipeDeeplink) {
console.log('[Main] Starting background recipe decoding for:', recipeDeeplink);
// Decode recipe asynchronously after window is created
decodeRecipeMain(goosedClient, recipeDeeplink)
.then((decodedRecipe) => {
if (decodedRecipe) {
console.log('[Main] Recipe decoded successfully, updating window config');
// Handle scheduled job parameters if present
if (scheduledJobId) {
decodedRecipe.scheduledJobId = scheduledJobId;
decodedRecipe.isScheduledExecution = true;
}
// Send the decoded recipe to the renderer process
mainWindow.webContents.send('recipe-decoded', decodedRecipe);
} else {
console.error('[Main] Failed to decode recipe from deeplink');
// Send error to renderer
mainWindow.webContents.send('recipe-decode-error', 'Failed to decode recipe');
}
})
.catch((error) => {
console.error('[Main] Error decoding recipe:', error);
// Send error to renderer
mainWindow.webContents.send('recipe-decode-error', error.message || 'Unknown error');
});
}
// Handle window closure
mainWindow.on('closed', () => {
windowMap.delete(windowId);
-5
View File
@@ -255,11 +255,6 @@ const appConfigAPI: AppConfigAPI = {
getAll: () => config,
};
// Listen for recipe updates and update config directly
ipcRenderer.on('recipe-decoded', (_, decodedRecipe) => {
config.recipe = decodedRecipe;
});
// Expose the APIs
contextBridge.exposeInMainWorld('electron', electronAPI);
contextBridge.exposeInMainWorld('appConfig', appConfigAPI);
+3 -2
View File
@@ -1,14 +1,15 @@
import { Recipe, saveRecipe as saveRecipeApi, listRecipes, RecipeManifestResponse } from '../api';
export async function saveRecipe(recipe: Recipe, recipeId?: string | null): Promise<void> {
export async function saveRecipe(recipe: Recipe, recipeId?: string | null): Promise<string> {
try {
await saveRecipeApi({
let response = await saveRecipeApi({
body: {
recipe,
id: recipeId,
},
throwOnError: true,
});
return response.data.id;
} catch (error) {
let error_message = 'unknown error';
if (typeof error === 'object' && error !== null && 'message' in error) {
+1 -1
View File
@@ -225,7 +225,7 @@ export const initializeSystem = async (
}
// Get recipe - prefer from options (session metadata) over app config
const recipe = options?.recipe || window.appConfig?.get?.('recipe');
const recipe = options?.recipe;
const recipe_instructions = (recipe as { instructions?: string })?.instructions;
const responseConfig = (recipe as { response?: { json_schema?: unknown } })?.response;
const subRecipes = (recipe as { sub_recipes?: SubRecipe[] })?.sub_recipes;