Skip to content
Closed
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
2 changes: 1 addition & 1 deletion ui/desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ vi.mock('./contexts/ChatContext', () => ({
title: 'Test Chat',
messages: [],
messageHistoryIndex: 0,
recipeConfig: null,
recipe: null,
},
setChat: vi.fn(),
setPairChat: vi.fn(), // Keep this from HEAD
Expand Down
21 changes: 15 additions & 6 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,18 +297,27 @@ export function AppInner() {
title: 'Pair Chat',
messages: [],
messageHistoryIndex: 0,
recipeConfig: null,
recipe: null,
});

const { addExtension } = useConfig();
const { agentState, loadCurrentChat, resetChat } = useAgent();
const resetChatForNewConversation = useCallback(() => {
const { agentState, loadCurrentChat, resetForNewConversation } = useAgent();
const resetChatForNewConversation = useCallback(async () => {
setSearchParams((prev) => {
prev.delete('resumeSessionId');
return prev;
});
resetChat();
}, [setSearchParams, resetChat]);
try {
const newChat = await resetForNewConversation({
setAgentWaitingMessage,
setIsExtensionsLoading,
});
setChat(newChat);
} catch (error) {
console.error('Failed to reset for new conversation:', error);
throw error;
}
}, [setSearchParams, resetForNewConversation, setAgentWaitingMessage, setChat]);

useEffect(() => {
console.log('Sending reactReady signal to Electron');
Expand Down Expand Up @@ -343,7 +352,7 @@ export function AppInner() {
}
})();
}
}, [resetChat, loadCurrentChat, setAgentWaitingMessage, navigate, loadingHub, setChat]);
}, [loadCurrentChat, setAgentWaitingMessage, navigate, loadingHub, setChat]);

useEffect(() => {
const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => {
Expand Down
131 changes: 85 additions & 46 deletions ui/desktop/src/components/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ import { RecipeWarningModal } from './ui/RecipeWarningModal';
import ParameterInputModal from './ParameterInputModal';
import CreateRecipeFromSessionModal from './recipes/CreateRecipeFromSessionModal';
import { useChatEngine } from '../hooks/useChatEngine';
import { useRecipeManager } from '../hooks/useRecipeManager';
import { useRecipeState } from '../hooks/useRecipeState';
import { useRecipeUI } from '../hooks/useRecipeUI';
import { Recipe } from '../recipe';
import { useRecipeCreationModal } from '../hooks/useRecipeCreationModal';
import { useFileDrop } from '../hooks/useFileDrop';
import { useCostTracking } from '../hooks/useCostTracking';
import { Message } from '../types/message';
Expand Down Expand Up @@ -148,37 +151,68 @@ function BaseChatContent({
},
onMessageSent: () => {
// Mark that user has started using the recipe
if (recipeConfig) {
if (recipe) {
setHasStartedUsingRecipe(true);
}
},
});

// Use shared recipe manager
const {
recipeConfig,
recipeParameters,
filteredParameters,
initialPrompt,
isParameterModalOpen,
setIsParameterModalOpen,
handleParameterSubmit,
handleAutoExecution,
isRecipeWarningModalOpen,
const recipeState = useRecipeState(location.state?.recipe || chat.recipe);
const recipe = recipeState.recipe;
const recipeParameters = chat.recipeParameters;
const filteredParameters = recipeState.filteredParameters;
const hasAllRequiredParameters = recipeState.hasAllRequiredParameters(recipeParameters || null);
const initialPrompt = recipeState.getInitialPrompt(recipeParameters || null);
const recipeAccepted = recipeState.recipeAccepted;
const hasSecurityWarnings = recipeState.hasSecurityWarnings;

const recipeUI = useRecipeUI(
chat,
recipeAccepted,
handleRecipeAccept,
handleRecipeCancel,
recipeState.requiresParameters,
hasAllRequiredParameters,
hasSecurityWarnings,
isCreateRecipeModalOpen,
setIsCreateRecipeModalOpen,
handleRecipeCreated,
handleStartRecipe,
} = useRecipeManager(chat, location.state?.recipeConfig);
recipe
);

// Use recipe creation modal hook for UI-specific functionality
const { isCreateRecipeModalOpen, setIsCreateRecipeModalOpen } = useRecipeCreationModal(
chat.sessionId
);

// Custom handlers that need BaseChat-specific logic
const handleRecipeAccept = async () => {
await recipeState.acceptRecipe();
recipeUI.setIsRecipeWarningModalOpen(false);
};

const handleRecipeCancel = () => {
recipeUI.handleRecipeCancel(() => {
window.electron.closeWindow();
});
};

const handleParameterSubmit = async (inputValues: Record<string, string>) => {
setChat({
...chat,
recipeParameters: inputValues,
});
await recipeUI.handleParameterSubmit(inputValues);
};

const handleStartRecipe = (recipe: Recipe) => {
setChat({
...chat,
messages: [],
recipe: recipe,
recipeParameters: null,
});
};

// Reset recipe usage tracking when recipe changes
useEffect(() => {
const previousTitle = currentRecipeTitle;
const newTitle = recipeConfig?.title || null;
const newTitle = recipe?.title || null;
const hasRecipeChanged = newTitle !== currentRecipeTitle;

if (hasRecipeChanged) {
Expand All @@ -196,16 +230,16 @@ function BaseChatContent({
setHasStartedUsingRecipe(true);
}
}
}, [recipeConfig?.title, currentRecipeTitle, messages.length]);
}, [recipe?.title, currentRecipeTitle, messages.length]);

// Handle recipe auto-execution
useEffect(() => {
const isProcessingResponse =
chatState !== ChatState.Idle && chatState !== ChatState.WaitingForUserInput;
handleAutoExecution(append, isProcessingResponse, () => {
recipeUI.handleAutoExecution(append, isProcessingResponse, () => {
setHasStartedUsingRecipe(true);
});
}, [handleAutoExecution, append, chatState]);
}, [recipeUI, append, chatState]);

// Use shared file drop
const { droppedFiles, setDroppedFiles, handleDrop, handleDragOver } = useFileDrop();
Expand Down Expand Up @@ -250,7 +284,7 @@ function BaseChatContent({
const combinedTextFromInput = customEvent.detail?.value || '';

// Mark that user has started using the recipe when they submit a message
if (recipeConfig && combinedTextFromInput.trim()) {
if (recipe && combinedTextFromInput.trim()) {
setHasStartedUsingRecipe(true);
}

Expand All @@ -267,7 +301,7 @@ function BaseChatContent({
// Wrapper for append that tracks recipe usage
const appendWithTracking = (text: string | Message) => {
// Mark that user has started using the recipe when they use append
if (recipeConfig) {
if (recipe) {
setHasStartedUsingRecipe(true);
}
append(text);
Expand Down Expand Up @@ -311,14 +345,12 @@ function BaseChatContent({
paddingY={0}
>
{/* Recipe agent header - sticky at top of chat container */}
{recipeConfig?.title && (
{recipe?.title && (
<div className="sticky top-0 z-10 bg-background-default px-0 -mx-6 mb-6 pt-6">
<AgentHeader
title={recipeConfig.title}
title={recipe.title}
profileInfo={
recipeConfig.profile
? `${recipeConfig.profile} - ${recipeConfig.mcps || 12} MCPs`
: undefined
recipe.profile ? `${recipe.profile} - ${recipe.mcps || 12} MCPs` : undefined
}
onChangeProfile={() => {
console.log('Change profile clicked');
Expand All @@ -332,14 +364,12 @@ function BaseChatContent({
{renderBeforeMessages && renderBeforeMessages()}

{/* Recipe Activities - always show when recipe is active and accepted */}
{recipeConfig && recipeAccepted && !suppressEmptyState && (
{recipe && recipeAccepted && !suppressEmptyState && (
<div className={hasStartedUsingRecipe ? 'mb-6' : ''}>
<RecipeActivities
append={(text: string) => appendWithTracking(text)}
activities={
Array.isArray(recipeConfig.activities) ? recipeConfig.activities : null
}
title={recipeConfig.title}
activities={Array.isArray(recipe.activities) ? recipe.activities : null}
title={recipe.title}
parameterValues={recipeParameters || {}}
/>
</div>
Expand Down Expand Up @@ -431,7 +461,7 @@ function BaseChatContent({

<div className="block h-8" />
</>
) : !recipeConfig && showPopularTopics ? (
) : !recipe && showPopularTopics ? (
/* Show PopularChatTopics when no messages, no recipe, and showPopularTopics is true (Pair view) */
<PopularChatTopics append={(text: string) => append(text)} />
) : null /* Show nothing when messages.length === 0 && suppressEmptyState === true */
Expand Down Expand Up @@ -479,7 +509,7 @@ function BaseChatContent({
disableAnimation={disableAnimation}
sessionCosts={sessionCosts}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
recipeConfig={recipeConfig}
recipe={recipe}
recipeAccepted={recipeAccepted}
initialPrompt={initialPrompt}
toolCount={toolCount || 0}
Expand All @@ -492,23 +522,23 @@ function BaseChatContent({

{/* Recipe Warning Modal */}
<RecipeWarningModal
isOpen={isRecipeWarningModalOpen}
isOpen={recipeUI.isRecipeWarningModalOpen}
onConfirm={handleRecipeAccept}
onCancel={handleRecipeCancel}
onCancel={() => handleRecipeCancel()}
recipeDetails={{
title: recipeConfig?.title,
description: recipeConfig?.description,
instructions: recipeConfig?.instructions || undefined,
title: recipe?.title,
description: recipe?.description,
instructions: recipe?.instructions || undefined,
}}
hasSecurityWarnings={hasSecurityWarnings}
/>

{/* Recipe Parameter Modal */}
{isParameterModalOpen && filteredParameters.length > 0 && (
{recipeUI.isParameterModalOpen && filteredParameters.length > 0 && (
<ParameterInputModal
parameters={filteredParameters}
onSubmit={handleParameterSubmit}
onClose={() => setIsParameterModalOpen(false)}
onClose={() => recipeUI.setIsParameterModalOpen(false)}
/>
)}

Expand All @@ -517,7 +547,16 @@ function BaseChatContent({
isOpen={isCreateRecipeModalOpen}
onClose={() => setIsCreateRecipeModalOpen(false)}
sessionId={chat.sessionId}
onRecipeCreated={handleRecipeCreated}
onRecipeCreated={(recipe) =>
recipeUI.handleRecipeCreated(recipe, (recipe) => {
import('../toasts').then(({ toastSuccess }) => {
toastSuccess({
title: 'Recipe created successfully!',
msg: `"${recipe.title}" has been saved and is ready to use.`,
});
});
})
}
onStartRecipe={handleStartRecipe}
/>
</div>
Expand Down
10 changes: 5 additions & 5 deletions ui/desktop/src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ interface ChatInputProps {
};
setIsGoosehintsModalOpen?: (isOpen: boolean) => void;
disableAnimation?: boolean;
recipeConfig?: Recipe | null;
recipe?: Recipe | null;
recipeAccepted?: boolean;
initialPrompt?: string;
toolCount: number;
Expand All @@ -108,7 +108,7 @@ export default function ChatInput({
disableAnimation = false,
sessionCosts,
setIsGoosehintsModalOpen,
recipeConfig,
recipe,
recipeAccepted,
initialPrompt,
toolCount,
Expand Down Expand Up @@ -318,7 +318,7 @@ export default function ChatInput({

useEffect(() => {
// Only load draft once and if conditions are met
if (!initialValue && !recipeConfig && !draftLoadedRef.current && chatContext) {
if (!initialValue && !recipe && !draftLoadedRef.current && chatContext) {
const draftText = chatContext.draft || '';

if (draftText) {
Expand All @@ -329,7 +329,7 @@ export default function ChatInput({
// Always mark as loaded after checking, regardless of whether we found a draft
draftLoadedRef.current = true;
}
}, [chatContext, initialValue, recipeConfig]);
}, [chatContext, initialValue, recipe]);

// Save draft when user types (debounced)
const debouncedSaveDraft = useMemo(
Expand Down Expand Up @@ -1624,7 +1624,7 @@ export default function ChatInput({
dropdownRef={dropdownRef}
setView={setView}
alerts={alerts}
recipeConfig={recipeConfig}
recipe={recipe}
hasMessages={messages.length > 0}
/>
</div>
Expand Down
28 changes: 12 additions & 16 deletions ui/desktop/src/components/pair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,23 +73,19 @@ export default function Pair({
prevRecipeRef.current = recipe;

try {
// Load a fresh chat session with forced reset
// Load a fresh chat session with recipe reset behavior
const newChat = await loadCurrentChat({
resumeSessionId: undefined,
recipeConfig: recipe,
recipe: recipe,
setAgentWaitingMessage,
forceReset: true,
resetOptions: {
resetSession: true,
clearMessages: true,
clearRecipeParameters: true,
},
});

// Set the chat with the recipe and ensure messages are cleared
const chatWithRecipe = {
...newChat,
recipeConfig: recipe,
recipeParameters: null,
messages: [],
};

setChat(chatWithRecipe);
setChat(newChat);
setSearchParams((prev) => {
prev.set('resumeSessionId', newChat.sessionId);
return prev;
Expand Down Expand Up @@ -119,13 +115,13 @@ export default function Pair({
// BUT only if we're resuming the same session (not starting a new chat)
let finalChat = loadedChat;
if (
!loadedChat.recipeConfig &&
currentChat?.recipeConfig &&
!loadedChat.recipe &&
currentChat?.recipe &&
resumeSessionId === currentChat.sessionId
) {
finalChat = {
...loadedChat,
recipeConfig: currentChat.recipeConfig,
recipe: currentChat.recipe,
recipeParameters: currentChat.recipeParameters || null,
};
}
Expand Down Expand Up @@ -174,7 +170,7 @@ export default function Pair({

const { initialPrompt: recipeInitialPrompt } = useRecipeManager(
chat,
recipe || chat.recipeConfig || null
recipe || chat.recipe || null
);

const handleMessageSubmit = () => {
Expand Down
Loading