diff --git a/crates/goose-server/src/routes/session.rs b/crates/goose-server/src/routes/session.rs index aeeef3e695..8ed509e46f 100644 --- a/crates/goose-server/src/routes/session.rs +++ b/crates/goose-server/src/routes/session.rs @@ -168,7 +168,7 @@ async fn get_session_insights( // Calculate insights let total_sessions = sessions.len(); - + // Debug: Log if we have very few sessions, which might indicate filtering issues if total_sessions == 0 { info!("Warning: No sessions found with descriptions"); @@ -191,7 +191,10 @@ async fn get_session_insights( total_tokens += tokens as i64; } else if tokens < 0 { // Log negative token values for debugging - info!("Warning: Session {} has negative accumulated_total_tokens: {}", session.id, tokens); + info!( + "Warning: Session {} has negative accumulated_total_tokens: {}", + session.id, tokens + ); } } diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index fbb0b71ebb..6f05bafbc4 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -431,7 +431,8 @@ const RecipeEditorRoute = () => { } if (!config) { - config = window.electron.getConfig().recipeConfig; + const electronConfig = window.electron.getConfig(); + config = electronConfig.recipeConfig; } return ; @@ -767,27 +768,28 @@ export default function App() { if (viewType) { if (viewType === 'recipeEditor' && recipeConfig) { - console.log('Setting view to recipeEditor with config:', recipeConfig); - // Handle recipe editor deep link - window.history.replaceState({ config: recipeConfig }, '', '/recipe-editor'); + // Handle recipe editor deep link - use hash routing + window.location.hash = '#/recipe-editor'; + window.history.replaceState({ config: recipeConfig }, '', '#/recipe-editor'); } else { // Handle other deep links by redirecting to appropriate route const routeMap: Record = { - chat: '/', - pair: '/pair', - settings: '/settings', - sessions: '/sessions', - schedules: '/schedules', - recipes: '/recipes', - permission: '/permission', - ConfigureProviders: '/configure-providers', - sharedSession: '/shared-session', - recipeEditor: '/recipe-editor', - welcome: '/welcome', + chat: '#/', + pair: '#/pair', + settings: '#/settings', + sessions: '#/sessions', + schedules: '#/schedules', + recipes: '#/recipes', + permission: '#/permission', + ConfigureProviders: '#/configure-providers', + sharedSession: '#/shared-session', + recipeEditor: '#/recipe-editor', + welcome: '#/welcome', }; const route = routeMap[viewType]; if (route) { + window.location.hash = route; window.history.replaceState({}, '', route); } } @@ -859,7 +861,12 @@ export default function App() { }); // Check if we have a recipe config from a deeplink - if (recipeConfig && typeof recipeConfig === 'object') { + // But skip navigation if we're ignoring recipe config changes (to prevent conflicts with new window creation) + if ( + recipeConfig && + typeof recipeConfig === 'object' && + !window.sessionStorage.getItem('ignoreRecipeConfigChanges') + ) { console.log( 'Recipe deeplink detected, navigating to pair view with config:', recipeConfig @@ -882,6 +889,10 @@ export default function App() { '', '#/pair' ); + } else if (window.sessionStorage.getItem('ignoreRecipeConfigChanges')) { + console.log( + 'Ignoring recipe config changes to prevent navigation conflicts with new window creation' + ); } else { // Only navigate to chat route if we're not already on a valid route const currentHash = window.location.hash; @@ -941,12 +952,19 @@ export default function App() { // Handle navigation to pair view for recipe deeplinks after router is ready useEffect(() => { const recipeConfig = window.appConfig.get('recipeConfig'); - if (recipeConfig && typeof recipeConfig === 'object' && window.location.hash === '#/') { + if ( + recipeConfig && + typeof recipeConfig === 'object' && + window.location.hash === '#/' && + !window.sessionStorage.getItem('ignoreRecipeConfigChanges') + ) { console.log('Router ready - navigating to pair view for recipe deeplink:', recipeConfig); // Small delay to ensure router is fully initialized setTimeout(() => { window.location.hash = '#/pair'; }, 100); + } else if (window.sessionStorage.getItem('ignoreRecipeConfigChanges')) { + console.log('Router ready - ignoring recipe config navigation due to new window creation'); } }, []); diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index 633eb170b3..ddf518ad42 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -188,10 +188,14 @@ function BaseChatContent({ }); // Use shared recipe manager - const { recipeConfig, initialPrompt, isGeneratingRecipe, handleAutoExecution } = useRecipeManager( - messages, - location.state - ); + const { + recipeConfig, + initialPrompt, + isGeneratingRecipe, + handleAutoExecution, + recipeError, + setRecipeError, + } = useRecipeManager(messages, location.state); // Reset recipe usage tracking when recipe changes useEffect(() => { @@ -487,6 +491,24 @@ function BaseChatContent({ }} summaryContent={summaryContent} /> + + {/* Recipe Error Modal */} + {recipeError && ( +
+
+

Recipe Creation Failed

+

{recipeError}

+
+ +
+
+
+ )} ); } diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index e4670c4b6d..c376a6f749 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1215,6 +1215,7 @@ export default function ChatInput({ setView={setView} alerts={alerts} recipeConfig={recipeConfig} + hasMessages={messages.length > 0} /> diff --git a/ui/desktop/src/components/ProgressiveMessageList.tsx b/ui/desktop/src/components/ProgressiveMessageList.tsx index 9749b715af..1136f05ee6 100644 --- a/ui/desktop/src/components/ProgressiveMessageList.tsx +++ b/ui/desktop/src/components/ProgressiveMessageList.tsx @@ -80,30 +80,18 @@ export default function ProgressiveMessageList({ // Simple progressive loading - start immediately when component mounts if needed useEffect(() => { - console.log( - `ProgressiveMessageList: ${messages.length} messages, threshold: ${showLoadingThreshold}, renderedCount: ${renderedCount}` - ); - if (messages.length <= showLoadingThreshold) { - // Small list - render all immediately - console.log('ProgressiveMessageList: Small list, rendering all immediately'); setRenderedCount(messages.length); setIsLoading(false); return; } - console.log('ProgressiveMessageList: Large list, starting progressive loading'); - // Large list - start progressive loading const loadNextBatch = () => { setRenderedCount((current) => { const nextCount = Math.min(current + batchSize, messages.length); - console.log( - `ProgressiveMessageList: Loading batch, now showing ${nextCount}/${messages.length}` - ); if (nextCount >= messages.length) { - console.log('ProgressiveMessageList: All messages loaded'); setIsLoading(false); // Trigger scroll to bottom window.setTimeout(() => { diff --git a/ui/desktop/src/components/RecipeEditor.tsx b/ui/desktop/src/components/RecipeEditor.tsx index d9518df8ac..c3f1ce6d3d 100644 --- a/ui/desktop/src/components/RecipeEditor.tsx +++ b/ui/desktop/src/components/RecipeEditor.tsx @@ -352,7 +352,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { setErrors({ ...errors, title: undefined }); } }} - className={`w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent ${ + className={`w-full max-w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent overflow-hidden ${ errors.title ? 'border-red-500' : 'border-borderSubtle' }`} placeholder="Agent Recipe Title (required)" @@ -375,7 +375,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { setErrors({ ...errors, description: undefined }); } }} - className={`w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent ${ + className={`w-full max-w-full p-3 border rounded-lg bg-background-default text-textStandard focus:outline-none focus:ring-2 focus:ring-borderProminent overflow-hidden ${ errors.description ? 'border-red-500' : 'border-borderSubtle' }`} placeholder="Description (required)" @@ -423,21 +423,21 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { {/* Deep Link Display */} -
+
{!requiredFieldsAreFilled() ? (
Fill in required fields to generate link
) : ( -
-
+
+
Copy this link to share with friends or paste directly in Chrome to open
)} {requiredFieldsAreFilled() && ( -
validateForm() && handleCopy()} - className={`text-sm truncate dark:text-white font-mono ${!title.trim() || !description.trim() ? 'text-textDisabled' : 'text-textStandard'}`} - > - {deeplink} +
+
validateForm() && handleCopy()} + className={`text-sm dark:text-white font-mono cursor-pointer hover:bg-background-default p-2 rounded transition-colors overflow-x-auto whitespace-nowrap ${!title.trim() || !description.trim() ? 'text-textDisabled' : 'text-textStandard'}`} + style={{ maxWidth: '500px', width: '100%' }} + > + {deeplink} +
)}
@@ -507,17 +510,9 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { onClose={() => setIsScheduleModalOpen(false)} recipe={getCurrentConfig()} onCreateSchedule={(deepLink) => { - // Open the schedules view with the deep link pre-filled - window.electron.createChatWindow( - undefined, - undefined, - undefined, - undefined, - undefined, - 'schedules' - ); - // Store the deep link in localStorage for the schedules view to pick up + // Navigate to the schedules view with the deep link pre-filled localStorage.setItem('pendingScheduleDeepLink', deepLink); + navigate('/schedules'); }} /> diff --git a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx index 68ddd737f0..832b1296d4 100644 --- a/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx +++ b/ui/desktop/src/components/settings/models/bottom_bar/ModelsBottomBar.tsx @@ -25,12 +25,14 @@ interface ModelsBottomBarProps { setView: (view: View) => void; alerts: Alert[]; recipeConfig?: Recipe | null; + hasMessages?: boolean; // Add prop to know if there are messages to create a recipe from } export default function ModelsBottomBar({ dropdownRef, setView, alerts, recipeConfig, + hasMessages = false, }: ModelsBottomBarProps) { const { currentModel, @@ -243,10 +245,18 @@ export default function ModelsBottomBar({ )} - setView('recipeEditor')}> - Create a recipe - - + {/* Only show "Create a recipe from this session" when there are messages to create from */} + {hasMessages && ( + { + // Signal to create an agent from the current chat + window.dispatchEvent(new CustomEvent('make-agent-from-chat')); + }} + > + Create a recipe from this session + + + )} diff --git a/ui/desktop/src/hooks/useRecipeManager.ts b/ui/desktop/src/hooks/useRecipeManager.ts index 6e805604af..fca9210cc3 100644 --- a/ui/desktop/src/hooks/useRecipeManager.ts +++ b/ui/desktop/src/hooks/useRecipeManager.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState, useRef } from 'react'; import { createRecipe, Recipe } from '../recipe'; import { Message, createUserMessage } from '../types/message'; import { updateSystemPromptWithParameters } from '../utils/providerUtils'; @@ -15,10 +15,20 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt const [isParameterModalOpen, setIsParameterModalOpen] = useState(false); const [recipeParameters, setRecipeParameters] = useState | null>(null); const [readyForAutoUserPrompt, setReadyForAutoUserPrompt] = useState(false); + const [recipeError, setRecipeError] = useState(null); // Get chat context to access persisted recipe const chatContext = useChatContext(); + // Use a ref to capture the current messages for the event handler + const messagesRef = useRef(messages); + const isCreatingRecipeRef = useRef(false); + + // Update the ref when messages change + useEffect(() => { + messagesRef.current = messages; + }, [messages]); + // Get recipeConfig from multiple sources with priority: // 1. Chat context (persisted recipe) // 2. Navigation state (from recipes view) @@ -149,13 +159,27 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt // Listen for make-agent-from-chat event useEffect(() => { const handleMakeAgent = async () => { + // Prevent duplicate calls using global flag + if (window.isCreatingRecipe) { + return; + } + + // Prevent duplicate calls using local ref + if (isCreatingRecipeRef.current) { + return; + } + window.electron.logInfo('Making recipe from chat...'); + + // Set both local and global flags + isCreatingRecipeRef.current = true; + window.isCreatingRecipe = true; setIsGeneratingRecipe(true); try { - // Create recipe directly from chat messages + // Create recipe directly from chat messages using the ref to get current messages const createRecipeRequest = { - messages: messages, + messages: messagesRef.current, title: '', description: '', }; @@ -166,16 +190,14 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt throw new Error(`Failed to create recipe: ${response.error}`); } - window.electron.logInfo('Created recipe:'); - window.electron.logInfo(JSON.stringify(response.recipe, null, 2)); + window.electron.logInfo('Created recipe successfully'); - // First, verify the recipe data + // Verify the recipe data if (!response.recipe) { throw new Error('No recipe data received'); } // Create a new window for the recipe editor - console.log('Opening recipe editor with config:', response.recipe); const recipeConfig = { id: response.recipe.title || 'untitled', title: response.recipe.title || 'Untitled Recipe', @@ -184,6 +206,12 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt activities: response.recipe.activities || [], prompt: response.recipe.prompt || '', }; + + // Set a flag to prevent the current window from reacting to recipe config changes + // This prevents navigation conflicts when creating new windows + window.sessionStorage.setItem('ignoreRecipeConfigChanges', 'true'); + + // Create the new window window.electron.createChatWindow( undefined, // query undefined, // dir @@ -194,11 +222,21 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt ); window.electron.logInfo('Opening recipe editor window'); + + // Clear the ignore flag after a short delay + setTimeout(() => { + window.sessionStorage.removeItem('ignoreRecipeConfigChanges'); + }, 1000); } catch (error) { window.electron.logInfo('Failed to create recipe:'); const errorMessage = error instanceof Error ? error.message : String(error); window.electron.logInfo(errorMessage); + + // Set the error state to show in modal + setRecipeError(errorMessage); } finally { + isCreatingRecipeRef.current = false; + window.isCreatingRecipe = false; setIsGeneratingRecipe(false); } }; @@ -208,7 +246,7 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt return () => { window.removeEventListener('make-agent-from-chat', handleMakeAgent); }; - }, [messages]); + }, []); return { recipeConfig, @@ -219,5 +257,7 @@ export const useRecipeManager = (messages: Message[], locationState?: LocationSt recipeParameters, handleParameterSubmit, handleAutoExecution, + recipeError, + setRecipeError, }; }; diff --git a/ui/desktop/src/json.d.ts b/ui/desktop/src/vite-env.d.ts similarity index 81% rename from ui/desktop/src/json.d.ts rename to ui/desktop/src/vite-env.d.ts index 02b4714e65..61bdfbe0a0 100644 --- a/ui/desktop/src/json.d.ts +++ b/ui/desktop/src/vite-env.d.ts @@ -1,3 +1,5 @@ +/// + declare module '*.json' { const value: Record; export default value; @@ -49,3 +51,12 @@ declare namespace React { WebkitAppRegion?: 'drag' | 'no-drag'; } } + +// Extend Window interface to include global recipe creation flag +declare global { + interface Window { + isCreatingRecipe?: boolean; + } +} + +export {};