diff --git a/Justfile b/Justfile index ccb6bbbd1edf..6f0ac07a6f72 100644 --- a/Justfile +++ b/Justfile @@ -170,13 +170,14 @@ run-ui-only: @echo "Running UI..." cd ui/desktop && npm install && npm run start-gui -debug-ui: - @echo "🚀 Starting goose frontend in external backend mode" - cd ui/desktop && \ - export GOOSE_EXTERNAL_BACKEND=true && \ - export GOOSE_EXTERNAL_PORT=3000 && \ - npm install && \ - npm run start-gui +debug-ui *alpha: + @echo "🚀 Starting goose frontend in external backend mode{{ if alpha == "alpha" { " with alpha features enabled" } else { "" } }}" + cd ui/desktop && \ + export GOOSE_EXTERNAL_BACKEND=true && \ + export GOOSE_EXTERNAL_PORT=3000 && \ + {{ if alpha == "alpha" { "export ALPHA=true &&" } else { "" } }} \ + npm install && \ + npm run {{ if alpha == "alpha" { "start-alpha-gui" } else { "start-gui" } }} # Run UI with main process debugging enabled # To debug main process: diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 497030432033..dd10704c5214 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -14,7 +14,7 @@ use goose::config::PermissionManager; use goose::config::Config; use goose::model::ModelConfig; use goose::prompt_template::render_global_file; -use goose::providers::create; +use goose::providers::{create, create_with_named_model}; use goose::recipe::Recipe; use goose::recipe_deeplink; use goose::session::{Session, SessionManager}; @@ -28,7 +28,7 @@ use std::collections::HashMap; use std::path::PathBuf; use std::sync::atomic::Ordering; use std::sync::Arc; -use tracing::error; +use tracing::{error, warn}; #[derive(Deserialize, utoipa::ToSchema)] pub struct UpdateFromSessionRequest { @@ -67,6 +67,7 @@ pub struct StartAgentRequest { #[derive(Deserialize, utoipa::ToSchema)] pub struct ResumeAgentRequest { session_id: String, + load_model_and_extensions: bool, } #[utoipa::path( @@ -172,6 +173,7 @@ async fn start_agent( ) )] async fn resume_agent( + State(state): State>, Json(payload): Json, ) -> Result, ErrorResponse> { let session = SessionManager::get_session(&payload.session_id, true) @@ -184,6 +186,74 @@ async fn resume_agent( } })?; + if payload.load_model_and_extensions { + let agent = state + .get_agent_for_route(payload.session_id) + .await + .map_err(|code| ErrorResponse { + message: "Failed to get agent for route".into(), + status: code, + })?; + + let config = Config::global(); + + let provider_result = async { + let provider_name: String = + config + .get_param("GOOSE_PROVIDER") + .map_err(|_| ErrorResponse { + message: "Could not configure agent: missing provider".into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let model: String = config.get_param("GOOSE_MODEL").map_err(|_| ErrorResponse { + message: "Could not configure agent: missing model".into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + let provider = create_with_named_model(&provider_name, &model) + .await + .map_err(|_| ErrorResponse { + message: "Could not configure agent: missing model".into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + })?; + + agent + .update_provider(provider) + .await + .map_err(|e| ErrorResponse { + message: format!("Could not configure agent: {}", e), + status: StatusCode::INTERNAL_SERVER_ERROR, + }) + }; + + let extensions_result = async { + let enabled_configs = goose::config::get_enabled_extensions(); + let agent_clone = agent.clone(); + + let extension_futures = enabled_configs + .into_iter() + .map(|config| { + let config_clone = config.clone(); + let agent_ref = agent_clone.clone(); + + async move { + if let Err(e) = agent_ref.add_extension(config_clone.clone()).await { + warn!("Failed to load extension {}: {}", config_clone.name(), e); + } + Ok::<_, ErrorResponse>(()) + } + }) + .collect::>(); + + futures::future::join_all(extension_futures).await; + Ok::<(), ErrorResponse>(()) // Fixed type annotation + }; + + let (provider_result, _) = tokio::join!(provider_result, extensions_result); + provider_result?; + } + Ok(Json(session)) } diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index 669508ff6f67..e6a2c8de9a63 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -63,9 +63,7 @@ pub struct SessionUpdateBuilder { #[derive(Serialize, ToSchema, Debug)] #[serde(rename_all = "camelCase")] pub struct SessionInsights { - /// Total number of sessions total_sessions: usize, - /// Total tokens used across all sessions total_tokens: i64, } @@ -785,7 +783,9 @@ impl SessionStorage { .await?; let mut messages = Vec::new(); - for (role_str, content_json, created_timestamp, metadata_json) in rows { + for (idx, (role_str, content_json, created_timestamp, metadata_json)) in + rows.into_iter().enumerate() + { let role = match role_str.as_str() { "user" => Role::User, "assistant" => Role::Assistant, @@ -799,6 +799,8 @@ impl SessionStorage { let mut message = Message::new(role, created_timestamp, content); message.metadata = metadata; + // TODO(Douwe): make id required + message = message.with_id(format!("msg_{}_{}", session_id, idx)); messages.push(message); } diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 50c80c25f270..22c72d2c712b 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3794,9 +3794,13 @@ "ResumeAgentRequest": { "type": "object", "required": [ - "session_id" + "session_id", + "load_model_and_extensions" ], "properties": { + "load_model_and_extensions": { + "type": "boolean" + }, "session_id": { "type": "string" } @@ -4122,13 +4126,11 @@ "properties": { "totalSessions": { "type": "integer", - "description": "Total number of sessions", "minimum": 0 }, "totalTokens": { "type": "integer", - "format": "int64", - "description": "Total tokens used across all sessions" + "format": "int64" } } }, diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 403a21af8bb1..fb766f00478f 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -89,18 +89,32 @@ const PairRouteWrapper = ({ const setView = useNavigation(); const routeState = (location.state as PairRouteState) || (window.history.state as PairRouteState) || {}; - const [searchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); const [initialMessage] = useState(routeState.initialMessage); const resumeSessionId = searchParams.get('resumeSessionId') ?? undefined; + // Determine which session ID to use: + // 1. From route state (when navigating from Hub with a new session) + // 2. From URL params (when resuming a session) + // 3. From the existing chat state (when navigating to Pair directly) + const sessionId = routeState.resumeSessionId || resumeSessionId || chat.sessionId; + + // Update URL with session ID if it's not already there (new chat from pair) + useEffect(() => { + if (process.env.ALPHA && sessionId && sessionId !== resumeSessionId) { + setSearchParams((prev) => { + prev.set('resumeSessionId', sessionId); + return prev; + }); + } + }, [sessionId, resumeSessionId, setSearchParams]); + return process.env.ALPHA ? ( ) : ( diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 77b2f0f1330c..542e03cba321 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -605,6 +605,7 @@ export type Response = { }; export type ResumeAgentRequest = { + load_model_and_extensions: boolean; session_id: string; }; @@ -707,13 +708,7 @@ export type SessionDisplayInfo = { }; export type SessionInsights = { - /** - * Total number of sessions - */ totalSessions: number; - /** - * Total tokens used across all sessions - */ totalTokens: number; }; diff --git a/ui/desktop/src/components/AgentHeader.tsx b/ui/desktop/src/components/AgentHeader.tsx deleted file mode 100644 index af7326ca7aef..000000000000 --- a/ui/desktop/src/components/AgentHeader.tsx +++ /dev/null @@ -1,37 +0,0 @@ -interface AgentHeaderProps { - title: string; - profileInfo?: string; - onChangeProfile?: () => void; - showBorder?: boolean; -} - -export function AgentHeader({ - title, - profileInfo, - onChangeProfile, - showBorder = false, -}: AgentHeaderProps) { - return ( -
-
- - - Agent{' '} - {title} - -
- {profileInfo && ( -
- {profileInfo} - {onChangeProfile && ( - - )} -
- )} -
- ); -} diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index b02adaa7dfb8..a5b336a73815 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -44,7 +44,7 @@ import React, { createContext, useContext, useEffect, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import { SearchView } from './conversation/SearchView'; -import { AgentHeader } from './AgentHeader'; +import { RecipeHeader } from './RecipeHeader'; import LoadingGoose from './LoadingGoose'; import RecipeActivities from './recipes/RecipeActivities'; import PopularChatTopics from './PopularChatTopics'; @@ -315,16 +315,7 @@ function BaseChatContent({ {/* Recipe agent header - sticky at top of chat container */} {recipe?.title && (
- { - console.log('Change profile clicked'); - }} - showBorder={true} - /> +
)} diff --git a/ui/desktop/src/components/BaseChat2.tsx b/ui/desktop/src/components/BaseChat2.tsx index 714b0100df7e..e1924fedbbd7 100644 --- a/ui/desktop/src/components/BaseChat2.tsx +++ b/ui/desktop/src/components/BaseChat2.tsx @@ -1,172 +1,121 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { SearchView } from './conversation/SearchView'; import LoadingGoose from './LoadingGoose'; import PopularChatTopics from './PopularChatTopics'; import ProgressiveMessageList from './ProgressiveMessageList'; -import { View, ViewOptions } from '../utils/navigationUtils'; import { ContextManagerProvider } from './context_management/ContextManager'; import { MainPanelLayout } from './Layout/MainPanelLayout'; import ChatInput from './ChatInput'; import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area'; import { useFileDrop } from '../hooks/useFileDrop'; -import { Message, Session } from '../api'; +import { Message } from '../api'; import { ChatState } from '../types/chatState'; import { ChatType } from '../types/chat'; import { useIsMobile } from '../hooks/use-mobile'; import { useSidebar } from './ui/sidebar'; import { cn } from '../utils'; import { useChatStream } from '../hooks/useChatStream'; -import { loadSession } from '../utils/sessionCache'; +import { useNavigation } from '../hooks/useNavigation'; +import { RecipeHeader } from './RecipeHeader'; +import { RecipeWarningModal } from './ui/RecipeWarningModal'; +import { scanRecipe } from '../recipe'; +import { useCostTracking } from '../hooks/useCostTracking'; +import RecipeActivities from './recipes/RecipeActivities'; +import { useToolCount } from './alerts/useToolCount'; interface BaseChatProps { - chat: ChatType; setChat: (chat: ChatType) => void; - setView: (view: View, viewOptions?: ViewOptions) => void; setIsGoosehintsModalOpen?: (isOpen: boolean) => void; - onMessageStreamFinish?: () => void; onMessageSubmit?: (message: string) => void; renderHeader?: () => React.ReactNode; - renderBeforeMessages?: () => React.ReactNode; - renderAfterMessages?: () => React.ReactNode; customChatInputProps?: Record; customMainLayoutProps?: Record; - contentClassName?: string; - disableSearch?: boolean; - showPopularTopics?: boolean; - suppressEmptyState?: boolean; - autoSubmit?: boolean; - resumeSessionId?: string; // Optional session ID to resume on mount + suppressEmptyState: boolean; + sessionId: string; + initialMessage?: string; } function BaseChatContent({ - chat, - setChat, - setView, setIsGoosehintsModalOpen, renderHeader, - renderBeforeMessages, - renderAfterMessages, customChatInputProps = {}, customMainLayoutProps = {}, - disableSearch = false, - resumeSessionId, + sessionId, + initialMessage, }: BaseChatProps) { const location = useLocation(); const scrollRef = useRef(null); const disableAnimation = location.state?.disableAnimation || false; - // const [hasStartedUsingRecipe, setHasStartedUsingRecipe] = React.useState(false); - // const [currentRecipeTitle, setCurrentRecipeTitle] = React.useState(null); - // const { isCompacting, handleManualCompaction } = useContextManager(); + const [hasStartedUsingRecipe, setHasStartedUsingRecipe] = React.useState(false); + const [hasAcceptedRecipe, setHasAcceptedRecipe] = useState(); + const [hasRecipeSecurityWarnings, setHasRecipeSecurityWarnings] = useState(false); + const isMobile = useIsMobile(); const { state: sidebarState } = useSidebar(); + const setView = useNavigation(); const contentClassName = cn('pr-1 pb-10', (isMobile || sidebarState === 'collapsed') && 'pt-11'); // Use shared file drop const { droppedFiles, setDroppedFiles, handleDrop, handleDragOver } = useFileDrop(); - // Use shared cost tracking - // const { sessionCosts } = useCostTracking({ - // sessionInputTokens, - // sessionOutputTokens, - // localInputTokens, - // localOutputTokens, - // session: sessionMetadata, - // }); - - // Session loading state - const [sessionLoadError, setSessionLoadError] = useState(null); - const hasLoadedSessionRef = useRef(false); - - const [messages, setMessages] = useState(chat.messages || []); - - // Load session on mount if resumeSessionId is provided - useEffect(() => { - const needsLoad = resumeSessionId && !hasLoadedSessionRef.current; - - if (needsLoad) { - hasLoadedSessionRef.current = true; - setSessionLoadError(null); - - // Set chat to empty session to indicate loading state - // todo: set to null instead and handle that in other places - const emptyChat: ChatType = { - sessionId: resumeSessionId, - title: 'Loading...', - messageHistoryIndex: 0, - messages: [], - recipe: null, - recipeParameterValues: null, - }; - setChat(emptyChat); - - loadSession(resumeSessionId) - .then((session: Session) => { - const conversation = session.conversation || []; - const loadedChat: ChatType = { - sessionId: session.id, - title: session.description || 'Untitled Chat', - messageHistoryIndex: 0, - messages: conversation, - recipe: null, - recipeParameterValues: null, - }; - - setChat(loadedChat); - }) - .catch((error: Error) => { - const errorMessage = error.message || 'Failed to load session'; - setSessionLoadError(errorMessage); - }); - } - }, [resumeSessionId, setChat]); - - // Update messages when chat changes (e.g., when resuming a session) - useEffect(() => { - if (chat.messages) { - setMessages(chat.messages); - } - }, [chat.messages, chat.sessionId]); + const onStreamFinish = useCallback(() => {}, []); - const { chatState, handleSubmit, stopStreaming } = useChatStream({ - sessionId: chat.sessionId || '', - messages, - setMessages, - onStreamFinish: () => {}, - }); + const { session, messages, chatState, handleSubmit, stopStreaming, sessionLoadError } = + useChatStream({ + sessionId, + onStreamFinish, + initialMessage, + }); const handleFormSubmit = (e: React.FormEvent) => { const customEvent = e as unknown as CustomEvent; const textValue = customEvent.detail?.value || ''; - // if (recipe && textValue.trim()) { - // setHasStartedUsingRecipe(true); - // } - // - // if (onMessageSubmit && textValue.trim()) { - // onMessageSubmit(textValue); - // } - + if (recipe && textValue.trim()) { + setHasStartedUsingRecipe(true); + } handleSubmit(textValue); }; - // TODO(Douwe): send this to the chatbox instead, possibly autosubmit? or backend - const append = (_txt: string) => {}; + const { sessionCosts } = useCostTracking({ + sessionInputTokens: session?.accumulated_input_tokens || 0, + sessionOutputTokens: session?.accumulated_output_tokens || 0, + localInputTokens: 0, + localOutputTokens: 0, + session, + }); + + const recipe = session?.recipe; useEffect(() => { - window.electron.logInfo( - 'Initial messages when resuming session: ' + JSON.stringify(messages, null, 2) - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (!recipe) return; + + (async () => { + const accepted = await window.electron.hasAcceptedRecipeBefore(recipe); + setHasAcceptedRecipe(accepted); + + if (!accepted) { + const scanResult = await scanRecipe(recipe); + setHasRecipeSecurityWarnings(scanResult.has_security_warnings); + } + })(); + }, [recipe]); + + const handleRecipeAccept = async (accept: boolean) => { + if (recipe && accept) { + await window.electron.recordRecipeHash(recipe); + setHasAcceptedRecipe(true); + } else { + setView('chat'); + } + }; // Track if this is the initial render for session resuming const initialRenderRef = useRef(true); - const recipe = chat?.recipe; - // Auto-scroll when messages are loaded (for session resuming) const handleRenderingComplete = React.useCallback(() => { // Only force scroll on the very first render @@ -182,16 +131,7 @@ function BaseChatContent({ } }, [messages.length]); - //const toolCount = useToolCount(chat.sessionId); - - // 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 (recipe) { - // setHasStartedUsingRecipe(true); - // } - // append(text); - // }; + const toolCount = useToolCount(sessionId); // Listen for global scroll-to-bottom requests (e.g., from MCP UI prompt actions) useEffect(() => { @@ -226,11 +166,29 @@ function BaseChatContent({ ); - const showPopularTopics = messages.length === 0; + const showPopularTopics = + messages.length === 0 && !initialMessage && chatState === ChatState.Idle; // TODO(Douwe): get this from the backend const isCompacting = false; + const chat: ChatType = { + messageHistoryIndex: 0, + messages, + recipe, + sessionId, + title: session?.description || 'No Session', + }; + const initialPrompt = messages.length == 0 && recipe?.prompt ? recipe.prompt : ''; + + // Map chatState to LoadingGoose message + const getLoadingMessage = (): string | undefined => { + if (isCompacting) return 'goose is compacting the conversation...'; + if (messages.length === 0 && chatState === ChatState.Thinking) { + return 'loading conversation...'; + } + return undefined; + }; return (

Warning: BaseChat2!

@@ -254,36 +212,22 @@ function BaseChatContent({ paddingX={6} paddingY={0} > - {/*/!* Recipe agent header - sticky at top of chat container *!/*/} - {/*{recipe?.title && (*/} - {/*
*/} - {/* {*/} - {/* console.log('Change profile clicked');*/} - {/* }}*/} - {/* showBorder={true}*/} - {/* />*/} - {/*
*/} - {/*)}*/} - - {/* Custom content before messages */} - {renderBeforeMessages && renderBeforeMessages()} - - {/*/!* Recipe Activities - always show when recipe is active and accepted *!/*/} - {/*{recipe && recipeAccepted && !suppressEmptyState && (*/} - {/*
*/} - {/* appendWithTracking(text)}*/} - {/* activities={Array.isArray(recipe.activities) ? recipe.activities : null}*/} - {/* title={recipe.title}*/} - {/* parameterValues={recipeParameters || {}}*/} - {/* />*/} - {/*
*/} - {/*)}*/} + {recipe?.title && ( +
+ +
+ )} + + {recipe && ( +
+ handleSubmit(text)} + activities={Array.isArray(recipe.activities) ? recipe.activities : null} + title={recipe.title} + //parameterValues={recipeParameters || {}} + /> +
+ )} {sessionLoadError && (
@@ -293,96 +237,30 @@ function BaseChatContent({
)} {/* Messages or Popular Topics */} - { - messages.length > 0 || recipe ? ( - <> - {disableSearch ? ( - renderProgressiveMessageList(chat) - ) : ( - // Render messages with SearchView wrapper when search is enabled - {renderProgressiveMessageList(chat)} - )} - - {/*{error && (*/} - {/* <>*/} - {/*
*/} - {/*
*/} - {/* {error.message || 'Honk! Goose experienced an error while responding'}*/} - {/*
*/} - - {/* /!* Action buttons for all errors including token limit errors *!/*/} - {/*
*/} - {/* {*/} - {/* clearError();*/} - - {/* await handleManualCompaction(*/} - {/* messages,*/} - {/* setMessages,*/} - {/* append,*/} - {/* chat.sessionId*/} - {/* );*/} - {/* }}*/} - {/* >*/} - {/* Summarize Conversation*/} - {/*
*/} - {/* {*/} - {/* // Find the last user message*/} - {/* const lastUserMessage = messages.reduceRight(*/} - {/* (found, m) => found || (m.role === 'user' ? m : null),*/} - {/* null as Message | null*/} - {/* );*/} - {/* if (lastUserMessage) {*/} - {/* await append(lastUserMessage);*/} - {/* }*/} - {/* }}*/} - {/* >*/} - {/* Retry Last Message*/} - {/*
*/} - {/* */} - {/* */} - {/* */} - {/*)}*/} - -
- - ) : !recipe && showPopularTopics ? ( - /* Show PopularChatTopics when no messages, no recipe, and showPopularTopics is true (Pair view) */ - append(text)} /> - ) : null /* Show nothing when messages.length === 0 && suppressEmptyState === true */ - } - - {/* Custom content after messages */} - {renderAfterMessages && renderAfterMessages()} + {messages.length > 0 || recipe ? ( + <> + {renderProgressiveMessageList(chat)} + +
+ + ) : !recipe && showPopularTopics ? ( + handleSubmit(text)} /> + ) : null} - {/* Fixed loading indicator at bottom left of chat container */} - {(messages.length === 0 || isCompacting) && !sessionLoadError && ( + {(chatState !== ChatState.Idle || isCompacting) && !sessionLoadError && (
- +
)}
@@ -391,48 +269,46 @@ function BaseChatContent({ className={`relative z-10 ${disableAnimation ? '' : 'animate-[fadein_400ms_ease-in_forwards]'}`} > setDroppedFiles([])} // Clear dropped files after processing messages={messages} setMessages={(_m) => {}} disableAnimation={disableAnimation} - //sessionCosts={sessionCosts} + sessionCosts={sessionCosts} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} recipe={recipe} - //recipeAccepted={recipeAccepted} + recipeAccepted={hasAcceptedRecipe} initialPrompt={initialPrompt} - //toolCount={toolCount || 0} - toolCount={0} - //autoSubmit={autoSubmit} + toolCount={toolCount || 0} autoSubmit={false} - //append={append} {...customChatInputProps} />
- {/*/!* Recipe Warning Modal *!/*/} - {/**/} + {recipe && ( + handleRecipeAccept(true)} + onCancel={() => handleRecipeAccept(false)} + recipeDetails={{ + title: recipe.title, + description: recipe.description, + instructions: recipe.instructions || undefined, + }} + hasSecurityWarnings={hasRecipeSecurityWarnings} + /> + )} {/*/!* Recipe Parameter Modal *!/*/} {/*{isParameterModalOpen && filteredParameters.length > 0 && (*/} diff --git a/ui/desktop/src/components/Pair2.tsx b/ui/desktop/src/components/Pair2.tsx index fbec745714f8..6d46ce3a7d65 100644 --- a/ui/desktop/src/components/Pair2.tsx +++ b/ui/desktop/src/components/Pair2.tsx @@ -1,35 +1,28 @@ -import { View, ViewOptions } from '../utils/navigationUtils'; import 'react-toastify/dist/ReactToastify.css'; import { ChatType } from '../types/chat'; import BaseChat2 from './BaseChat2'; -export interface PairRouteState { - resumeSessionId?: string; - initialMessage?: string; -} - interface PairProps { - chat: ChatType; setChat: (chat: ChatType) => void; - setView: (view: View, viewOptions?: ViewOptions) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void; + sessionId: string; + initialMessage?: string; } export default function Pair({ - chat, setChat, - setView, setIsGoosehintsModalOpen, - resumeSessionId, -}: PairProps & PairRouteState) { + sessionId, + initialMessage, +}: PairProps) { return ( ); } diff --git a/ui/desktop/src/components/RecipeHeader.tsx b/ui/desktop/src/components/RecipeHeader.tsx new file mode 100644 index 000000000000..def9d951d07e --- /dev/null +++ b/ui/desktop/src/components/RecipeHeader.tsx @@ -0,0 +1,17 @@ +interface RecipeHeaderProps { + title: string; +} + +export function RecipeHeader({ title }: RecipeHeaderProps) { + return ( +
+
+ + + Recipe{' '} + {title} + +
+
+ ); +} diff --git a/ui/desktop/src/components/alerts/useToolCount.ts b/ui/desktop/src/components/alerts/useToolCount.ts index b1a931f1bf82..8cba1c4da66e 100644 --- a/ui/desktop/src/components/alerts/useToolCount.ts +++ b/ui/desktop/src/components/alerts/useToolCount.ts @@ -1,35 +1,22 @@ import { useState, useEffect } from 'react'; import { getTools } from '../../api'; -const { clearTimeout } = window; - +// TODO(Douwe): return this as part of the start agent request export const useToolCount = (sessionId: string) => { const [toolCount, setToolCount] = useState(null); useEffect(() => { - let timeoutId: ReturnType; - const fetchTools = async () => { try { const response = await getTools({ query: { session_id: sessionId } }); - if (!response.error && response.data) { - setToolCount(response.data.length); - } else { - setToolCount(0); - } + setToolCount(response.error || !response.data ? 0 : response.data.length); } catch (err) { console.error('Error fetching tools:', err); setToolCount(0); } }; - // Add initial 1s delay before first fetch - timeoutId = setTimeout(fetchTools, 1000); - - // Cleanup timeouts on unmount - return () => { - clearTimeout(timeoutId); - }; + fetchTools(); }, [sessionId]); return toolCount; diff --git a/ui/desktop/src/components/hub.tsx b/ui/desktop/src/components/hub.tsx index de9876b7b8e8..8cc252badf4d 100644 --- a/ui/desktop/src/components/hub.tsx +++ b/ui/desktop/src/components/hub.tsx @@ -20,6 +20,7 @@ import { ChatState } from '../types/chatState'; import { ContextManagerProvider } from './context_management/ContextManager'; import 'react-toastify/dist/ReactToastify.css'; import { View, ViewOptions } from '../utils/navigationUtils'; +import { startAgent } from '../api'; export default function Hub({ setView, @@ -32,22 +33,35 @@ export default function Hub({ isExtensionsLoading: boolean; resetChat: () => void; }) { - // Handle chat input submission - create new chat and navigate to pair - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { const customEvent = e as unknown as CustomEvent; const combinedTextFromInput = customEvent.detail?.value || ''; if (combinedTextFromInput.trim()) { - // Navigate to pair page with the message to be submitted - // Pair will handle creating the new chat session - resetChat(); - setView('pair', { - disableAnimation: true, - initialMessage: combinedTextFromInput, - }); + if (process.env.ALPHA) { + const newAgent = await startAgent({ + body: { + working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string, + }, + throwOnError: true, + }); + const session = newAgent.data; + setView('pair', { + disableAnimation: true, + initialMessage: combinedTextFromInput, + resumeSessionId: session.id, + }); + } else { + // Navigate to pair page with the message to be submitted + // Pair will handle creating the new chat session + resetChat(); + setView('pair', { + disableAnimation: true, + initialMessage: combinedTextFromInput, + }); + } + e.preventDefault(); } - - e.preventDefault(); }; return ( diff --git a/ui/desktop/src/components/recipes/RecipesView.tsx b/ui/desktop/src/components/recipes/RecipesView.tsx index a508b39a8fc0..a0183c9c4aeb 100644 --- a/ui/desktop/src/components/recipes/RecipesView.tsx +++ b/ui/desktop/src/components/recipes/RecipesView.tsx @@ -8,7 +8,7 @@ import { Skeleton } from '../ui/skeleton'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { toastSuccess } from '../../toasts'; import { useEscapeKey } from '../../hooks/useEscapeKey'; -import { deleteRecipe, RecipeManifestResponse } from '../../api'; +import { deleteRecipe, RecipeManifestResponse, startAgent } from '../../api'; import ImportRecipeForm, { ImportRecipeButton } from './ImportRecipeForm'; import CreateEditRecipeModal from './CreateEditRecipeModal'; import { generateDeepLink, Recipe } from '../../recipe'; @@ -70,30 +70,50 @@ export default function RecipesView() { } }; - const handleLoadRecipe = async (recipe: Recipe, recipeId: string) => { - try { - // onLoadRecipe is not working for loading recipes. It looks correct - // but the instructions are not flowing through to the server. - // Needs a fix but commenting out to get prod back up and running. - // - // if (onLoadRecipe) { - // // Use the callback to navigate within the same window - // onLoadRecipe(savedRecipe.recipe); - // } else { - // Fallback to creating a new window (for backwards compatibility) - window.electron.createChatWindow( - undefined, // query - undefined, // dir - undefined, // version - undefined, // resumeSessionId - recipe, // recipe config - undefined, // view type, - recipeId // recipe id - ); - // } - } catch (err) { - console.error('Failed to load recipe:', err); - setError(err instanceof Error ? err.message : 'Failed to load recipe'); + const handleStartRecipeChat = async (recipe: Recipe, recipeId: string) => { + if (process.env.ALPHA) { + try { + const newAgent = await startAgent({ + body: { + working_dir: window.appConfig.get('GOOSE_WORKING_DIR') as string, + recipe, + }, + throwOnError: true, + }); + const session = newAgent.data; + setView('pair', { + disableAnimation: true, + resumeSessionId: session.id, + }); + } catch (error) { + console.error('Failed to load recipe:', error); + setError(error instanceof Error ? error.message : 'Failed to load recipe'); + } + } else { + try { + // onLoadRecipe is not working for loading recipes. It looks correct + // but the instructions are not flowing through to the server. + // Needs a fix but commenting out to get prod back up and running. + // + // if (onLoadRecipe) { + // // Use the callback to navigate within the same window + // onLoadRecipe(savedRecipe.recipe); + // } else { + // Fallback to creating a new window (for backwards compatibility) + window.electron.createChatWindow( + undefined, // query + undefined, // dir + undefined, // version + undefined, // resumeSessionId + recipe, // recipe config + undefined, // view type, + recipeId // recipe id + ); + // } + } catch (err) { + console.error('Failed to load recipe:', err); + setError(err instanceof Error ? err.message : 'Failed to load recipe'); + } } }; @@ -193,7 +213,7 @@ export default function RecipesView() {