Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 12 additions & 84 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ const PairRouteWrapper = ({
const setView = useMemo(() => createNavigationHandler(navigate), [navigate]);
const routeState =
(location.state as PairRouteState) || (window.history.state as PairRouteState) || {};
const [resumeSessionId] = useState(routeState.resumeSessionId);
const [initialMessage] = useState(routeState.initialMessage);

return (
<Pair
Expand All @@ -93,7 +95,8 @@ const PairRouteWrapper = ({
setFatalError={setFatalError}
setAgentWaitingMessage={setAgentWaitingMessage}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
routeState={routeState}
resumeSessionId={resumeSessionId}
initialMessage={initialMessage}
/>
);
};
Expand Down Expand Up @@ -121,23 +124,8 @@ const SchedulesRoute = () => {
return <SchedulesView onClose={() => navigate('/')} />;
};

const RecipesRoute = ({ resetChat }: { resetChat: () => void }) => {
const navigate = useNavigate();

return (
<RecipesView
onLoadRecipe={(recipe) => {
// Navigate to pair view with the recipe configuration in state
resetChat();
const stateData: PairRouteState = {
recipeConfig: recipe,
};
navigate('/pair', {
state: stateData,
});
}}
/>
);
const RecipesRoute = () => {
return <RecipesView />;
};

const RecipeEditorRoute = () => {
Expand Down Expand Up @@ -378,11 +366,14 @@ export default function App() {

const stateData: PairRouteState = {
resumeSessionId: resumeSessionId || undefined,
recipeConfig: recipeFromAppConfig || undefined,
};
(async () => {
try {
await loadCurrentChat({ setAgentWaitingMessage, ...stateData });
await loadCurrentChat({
setAgentWaitingMessage,
recipeConfig: recipeFromAppConfig || undefined,
...stateData,
});
} catch (e) {
if (e instanceof NoProviderOrModelError) {
// the onboarding flow will trigger
Expand Down Expand Up @@ -474,69 +465,6 @@ export default function App() {
};
}, []);

// Handle recipe decode events from main process
useEffect(() => {
const handleLoadRecipeDeeplink = (_event: IpcRendererEvent, ...args: unknown[]) => {
const recipeDeeplink = args[0] as string;
const scheduledJobId = args[1] as string | undefined;

// Store the deeplink info in app config for processing
const config = window.electron.getConfig();
config.recipeDeeplink = recipeDeeplink;
if (scheduledJobId) {
config.scheduledJobId = scheduledJobId;
}

// Navigate to pair view to handle the recipe loading
if (window.location.hash !== '#/pair') {
window.location.hash = '#/pair';
}
};

const handleRecipeDecoded = (_event: IpcRendererEvent, ...args: unknown[]) => {
const decodedRecipe = args[0] as Recipe;

setChat((prevChat) => ({
...prevChat,
recipeConfig: decodedRecipe,
title: decodedRecipe.title || 'Recipe Chat',
messages: [], // Start fresh for recipe
messageHistoryIndex: 0,
}));

const stateData: PairRouteState = {
recipeConfig: decodedRecipe,
};

resetChat();

// Navigate to pair view if not already there
if (window.location.hash !== '#/pair') {
window.location.hash = '#/pair';
}
window.history.replaceState(stateData, '', '#/pair');
};

const handleRecipeDecodeError = (_event: IpcRendererEvent, ...args: unknown[]) => {
const errorMessage = args[0] as string;
console.error('[App] Recipe decode error:', errorMessage);

// Show error to user - you could add a toast notification here
// For now, just log the error and navigate to recipes page
window.location.hash = '#/recipes';
};

window.electron.on('load-recipe-deeplink', handleLoadRecipeDeeplink);
window.electron.on('recipe-decoded', handleRecipeDecoded);
window.electron.on('recipe-decode-error', handleRecipeDecodeError);

return () => {
window.electron.off('load-recipe-deeplink', handleLoadRecipeDeeplink);
window.electron.off('recipe-decoded', handleRecipeDecoded);
window.electron.off('recipe-decode-error', handleRecipeDecodeError);
};
}, [setChat, resetChat]);

useEffect(() => {
console.log('Setting up keyboard shortcuts');
const handleKeyDown = (event: KeyboardEvent) => {
Expand Down Expand Up @@ -735,7 +663,7 @@ export default function App() {
<Route path="extensions" element={<ExtensionsRoute />} />
<Route path="sessions" element={<SessionsRoute />} />
<Route path="schedules" element={<SchedulesRoute />} />
<Route path="recipes" element={<RecipesRoute resetChat={resetChat} />} />
<Route path="recipes" element={<RecipesRoute />} />
<Route path="recipe-editor" element={<RecipeEditorRoute />} />
<Route
path="shared-session"
Expand Down
16 changes: 10 additions & 6 deletions ui/desktop/src/components/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ interface BaseChatProps {
showPopularTopics?: boolean;
suppressEmptyState?: boolean;
autoSubmit?: boolean;
recipeResetOverride: boolean;
loadingChat: boolean;
}

Expand All @@ -107,7 +106,6 @@ function BaseChatContent({
disableSearch = false,
showPopularTopics = false,
suppressEmptyState = false,
recipeResetOverride,
autoSubmit = false,
loadingChat = false,
}: BaseChatProps) {
Expand Down Expand Up @@ -303,7 +301,7 @@ function BaseChatContent({
paddingY={0}
>
{/* Recipe agent header - sticky at top of chat container */}
{recipeConfig?.title && !recipeResetOverride && (
{recipeConfig?.title && (
<div className="sticky top-0 z-10 bg-background-default px-0 -mx-6 mb-6 pt-6">
<AgentHeader
title={recipeConfig.title}
Expand Down Expand Up @@ -443,7 +441,13 @@ function BaseChatContent({
{(chatState !== ChatState.Idle || loadingChat || isCompacting) && (
<div className="absolute bottom-1 left-4 z-20 pointer-events-none">
<LoadingGoose
message={isCompacting ? 'goose is compacting the conversation...' : undefined}
message={
loadingChat
? 'loading conversation...'
: isCompacting
? 'goose is compacting the conversation...'
: undefined
}
chatState={chatState}
/>
</div>
Expand Down Expand Up @@ -497,7 +501,7 @@ function BaseChatContent({
/>

{/* Recipe Parameter Modal */}
{isParameterModalOpen && recipeConfig?.parameters && !recipeResetOverride && (
{isParameterModalOpen && recipeConfig?.parameters && (
<ParameterInputModal
parameters={recipeConfig.parameters}
onSubmit={handleParameterSubmit}
Expand All @@ -506,7 +510,7 @@ function BaseChatContent({
)}

{/* Recipe Error Modal */}
{recipeError && !recipeResetOverride && (
{recipeError && (
<div className="fixed inset-0 z-[300] flex items-center justify-center bg-black/50">
<div className="bg-background-default border border-borderSubtle rounded-lg p-6 w-96 max-w-[90vw]">
<h3 className="text-lg font-medium text-textProminent mb-4">Recipe Creation Failed</h3>
Expand Down
7 changes: 1 addition & 6 deletions ui/desktop/src/components/RecipesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@ import { toastSuccess, toastError } from '../toasts';
import { useEscapeKey } from '../hooks/useEscapeKey';
import { deleteRecipe, RecipeManifestResponse } from '../api';

interface RecipesViewProps {
onLoadRecipe?: (recipe: Recipe) => void;
}

// @ts-expect-error until we make onLoadRecipe work for loading recipes in the same window
export default function RecipesView({ _onLoadRecipe }: RecipesViewProps = {}) {
export default function RecipesView() {
const [savedRecipes, setSavedRecipes] = useState<RecipeManifestResponse[]>([]);
const [loading, setLoading] = useState(true);
const [showSkeleton, setShowSkeleton] = useState(true);
Expand Down
53 changes: 20 additions & 33 deletions ui/desktop/src/components/pair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@ import 'react-toastify/dist/ReactToastify.css';
import { cn } from '../utils';

import { ChatType } from '../types/chat';
import { Recipe } from '../recipe';

export interface PairRouteState {
resumeSessionId?: string;
recipeConfig?: Recipe;
initialMessage?: string;
}

interface PairProps {
chat: ChatType;
setChat: (chat: ChatType) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
setFatalError: (value: ((prevState: string | null) => string | null) | string | null) => void;
setAgentWaitingMessage: (msg: string | null) => void;
agentState: AgentState;
loadCurrentChat: (context: InitializationContext) => Promise<ChatType>;
}

export default function Pair({
chat,
setChat,
Expand All @@ -26,38 +35,25 @@ export default function Pair({
setAgentWaitingMessage,
agentState,
loadCurrentChat,
routeState,
}: {
chat: ChatType;
setChat: (chat: ChatType) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
setFatalError: (value: ((prevState: string | null) => string | null) | string | null) => void;
setAgentWaitingMessage: (msg: string | null) => void;
agentState: AgentState;
loadCurrentChat: (context: InitializationContext) => Promise<ChatType>;
routeState: PairRouteState;
}) {
resumeSessionId,
initialMessage,
}: PairProps & PairRouteState) {
const isMobile = useIsMobile();
const { state: sidebarState } = useSidebar();
const [hasProcessedInitialInput, setHasProcessedInitialInput] = useState(false);
const [shouldAutoSubmit, setShouldAutoSubmit] = useState(false);
const [messageToSubmit, setMessageToSubmit] = useState<string | null>(null);
const [isTransitioningFromHub, setIsTransitioningFromHub] = useState(false);
const [recipeResetOverride, setRecipeResetOverride] = useState(false);
const [loadingChat, setLoadingChat] = useState(false);

useEffect(() => {
const initializeFromState = async () => {
setLoadingChat(true);
try {
const chat = await loadCurrentChat({
resumeSessionId: routeState.resumeSessionId,
resumeSessionId: resumeSessionId,
setAgentWaitingMessage,
});
if (!chat.recipeConfig && agentState === 'initialized') {
setRecipeResetOverride(true);
}
setChat(chat);
} catch (error) {
console.log(error);
Expand All @@ -73,26 +69,21 @@ export default function Pair({
setFatalError,
setAgentWaitingMessage,
loadCurrentChat,
routeState.resumeSessionId,
routeState.recipeConfig,
resumeSessionId,
]);

// Followed by sending the initialMessage if we have one. This will happen
// only once, unless we reset the chat in step one.
useEffect(() => {
if (
agentState !== AgentState.INITIALIZED ||
!routeState.initialMessage ||
hasProcessedInitialInput
) {
if (agentState !== AgentState.INITIALIZED || !initialMessage || hasProcessedInitialInput) {
return;
}

setIsTransitioningFromHub(true);
setHasProcessedInitialInput(true);
setMessageToSubmit(routeState.initialMessage);
setMessageToSubmit(initialMessage);
setShouldAutoSubmit(true);
}, [agentState, routeState.initialMessage, hasProcessedInitialInput]);
}, [agentState, initialMessage, hasProcessedInitialInput]);

useEffect(() => {
if (agentState === AgentState.NO_PROVIDER) {
Expand All @@ -111,10 +102,7 @@ export default function Pair({
};

const recipePrompt =
agentState === 'initialized' &&
!recipeResetOverride &&
chat.messages.length === 0 &&
recipeInitialPrompt;
agentState === 'initialized' && chat.messages.length === 0 && recipeInitialPrompt;

const initialValue = messageToSubmit || recipePrompt || undefined;

Expand All @@ -136,7 +124,6 @@ export default function Pair({
contentClassName={cn('pr-1 pb-10', (isMobile || sidebarState === 'collapsed') && 'pt-11')} // Use dynamic content class with mobile margin and sidebar state
showPopularTopics={!isTransitioningFromHub} // Don't show popular topics while transitioning from Hub
suppressEmptyState={isTransitioningFromHub} // Suppress all empty state content while transitioning from Hub
recipeResetOverride={recipeResetOverride}
/>
);
}
5 changes: 0 additions & 5 deletions ui/desktop/src/hooks/useAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,10 @@ export function useAgent(): UseAgentReturn {
}

agentWaitingMessage('Extensions are loading');
console.log(`calling initializeSystem with sessionId: ${agentSessionInfo.session_id}`);
await initializeSystem(agentSessionInfo.session_id, provider as string, model as string, {
getExtensions,
addExtension,
});
console.log('init system done!!');

if (COST_TRACKING_ENABLED) {
try {
Expand Down Expand Up @@ -194,7 +192,6 @@ const handleConfigRecovery = async () => {
const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3;

if (shouldMigrateExtensions) {
console.log('Performing extension migration...');
try {
await backupConfig({ throwOnError: true });
await initConfig();
Expand All @@ -203,12 +200,10 @@ const handleConfigRecovery = async () => {
}
}

console.log('Attempting config recovery...');
try {
await validateConfig({ throwOnError: true });
await readAllConfig({ throwOnError: true });
} catch {
console.log('Config validation failed, attempting recovery...');
try {
await recoverConfig({ throwOnError: true });
await readAllConfig({ throwOnError: true });
Expand Down
2 changes: 0 additions & 2 deletions ui/desktop/src/hooks/useRecipeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ export const useRecipeManager = (chat: ChatType, recipeConfig?: Recipe | null) =
const hasParameters = !!recipeParameters;
const hasMessages = messages.length > 0;
useEffect(() => {
// If we have parameters and they haven't been set yet, open the modal.
console.log('should open modal', requiresParameters, hasParameters, recipeAccepted);
if (requiresParameters && recipeAccepted) {
if (!hasParameters && !hasMessages) {
setIsParameterModalOpen(true);
Expand Down