Add session to agents (#4216)
Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: Jack Amadeo <jackamadeo@squareup.com> Co-authored-by: Jack Amadeo <jackamadeo@block.xyz>
This commit is contained in:
@@ -41,7 +41,7 @@
|
||||
* while remaining flexible enough to support different UI contexts (Hub vs Pair).
|
||||
*/
|
||||
|
||||
import React, { useEffect, useContext, createContext, useRef } from 'react';
|
||||
import React, { createContext, useContext, useEffect, useRef } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { SearchView } from './conversation/SearchView';
|
||||
import { AgentHeader } from './AgentHeader';
|
||||
@@ -63,31 +63,31 @@ import { useFileDrop } from '../hooks/useFileDrop';
|
||||
import { useCostTracking } from '../hooks/useCostTracking';
|
||||
import { Message } from '../types/message';
|
||||
import { ChatState } from '../types/chatState';
|
||||
import { ChatType } from '../types/chat';
|
||||
import { useToolCount } from './alerts/useToolCount';
|
||||
|
||||
// Context for sharing current model info
|
||||
const CurrentModelContext = createContext<{ model: string; mode: string } | null>(null);
|
||||
export const useCurrentModelInfo = () => useContext(CurrentModelContext);
|
||||
|
||||
import { ChatType } from '../types/chat';
|
||||
|
||||
interface BaseChatProps {
|
||||
chat: ChatType;
|
||||
setChat: (chat: ChatType) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
setIsGoosehintsModalOpen?: (isOpen: boolean) => void;
|
||||
enableLocalStorage?: boolean;
|
||||
onMessageStreamFinish?: () => void;
|
||||
onMessageSubmit?: (message: string) => void; // Callback after message is submitted
|
||||
onMessageSubmit?: (message: string) => void;
|
||||
renderHeader?: () => React.ReactNode;
|
||||
renderBeforeMessages?: () => React.ReactNode;
|
||||
renderAfterMessages?: () => React.ReactNode;
|
||||
customChatInputProps?: Record<string, unknown>;
|
||||
customMainLayoutProps?: Record<string, unknown>;
|
||||
contentClassName?: string; // Add custom class for content area
|
||||
disableSearch?: boolean; // Disable search functionality (for Hub)
|
||||
showPopularTopics?: boolean; // Show popular chat topics in empty state (for Pair)
|
||||
suppressEmptyState?: boolean; // Suppress empty state content (for transitions)
|
||||
contentClassName?: string;
|
||||
disableSearch?: boolean;
|
||||
showPopularTopics?: boolean;
|
||||
suppressEmptyState?: boolean;
|
||||
autoSubmit?: boolean;
|
||||
loadingChat: boolean;
|
||||
}
|
||||
|
||||
function BaseChatContent({
|
||||
@@ -95,7 +95,6 @@ function BaseChatContent({
|
||||
setChat,
|
||||
setView,
|
||||
setIsGoosehintsModalOpen,
|
||||
enableLocalStorage = false,
|
||||
onMessageStreamFinish,
|
||||
onMessageSubmit,
|
||||
renderHeader,
|
||||
@@ -108,6 +107,7 @@ function BaseChatContent({
|
||||
showPopularTopics = false,
|
||||
suppressEmptyState = false,
|
||||
autoSubmit = false,
|
||||
loadingChat = false,
|
||||
}: BaseChatProps) {
|
||||
const location = useLocation();
|
||||
const scrollRef = useRef<ScrollAreaHandle>(null);
|
||||
@@ -131,7 +131,6 @@ function BaseChatContent({
|
||||
error,
|
||||
setMessages,
|
||||
input,
|
||||
setInput: _setInput,
|
||||
handleSubmit: engineHandleSubmit,
|
||||
onStopGoose,
|
||||
sessionTokenCount,
|
||||
@@ -158,7 +157,6 @@ function BaseChatContent({
|
||||
setHasStartedUsingRecipe(true);
|
||||
}
|
||||
},
|
||||
enableLocalStorage,
|
||||
});
|
||||
|
||||
// Use shared recipe manager
|
||||
@@ -177,7 +175,7 @@ function BaseChatContent({
|
||||
handleRecipeAccept,
|
||||
handleRecipeCancel,
|
||||
hasSecurityWarnings,
|
||||
} = useRecipeManager(messages, location.state);
|
||||
} = useRecipeManager(chat, location.state?.recipeConfig);
|
||||
|
||||
// Reset recipe usage tracking when recipe changes
|
||||
useEffect(() => {
|
||||
@@ -251,6 +249,8 @@ function BaseChatContent({
|
||||
engineHandleSubmit(combinedTextFromInput);
|
||||
};
|
||||
|
||||
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
|
||||
@@ -324,13 +324,11 @@ function BaseChatContent({
|
||||
{/* Messages or RecipeActivities or Popular Topics */}
|
||||
{
|
||||
// Check if we should show splash instead of messages
|
||||
(() => {
|
||||
// Show splash if we have a recipe and user hasn't started using it yet, and recipe has been accepted
|
||||
const shouldShowSplash =
|
||||
recipeConfig && recipeAccepted && !hasStartedUsingRecipe && !suppressEmptyState;
|
||||
|
||||
return shouldShowSplash;
|
||||
})() ? (
|
||||
// Show splash if we have a recipe and user hasn't started using it yet, and recipe has been accepted
|
||||
loadingChat ? null : recipeConfig &&
|
||||
recipeAccepted &&
|
||||
!hasStartedUsingRecipe &&
|
||||
!suppressEmptyState ? (
|
||||
<>
|
||||
{/* Show RecipeActivities when we have a recipe config and user hasn't started using it */}
|
||||
{recipeConfig ? (
|
||||
@@ -416,7 +414,7 @@ function BaseChatContent({
|
||||
null as Message | null
|
||||
);
|
||||
if (lastUserMessage) {
|
||||
append(lastUserMessage);
|
||||
await append(lastUserMessage);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -426,6 +424,7 @@ function BaseChatContent({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="block h-8" />
|
||||
</>
|
||||
) : showPopularTopics ? (
|
||||
@@ -439,10 +438,16 @@ function BaseChatContent({
|
||||
</ScrollArea>
|
||||
|
||||
{/* Fixed loading indicator at bottom left of chat container */}
|
||||
{(chatState !== ChatState.Idle || isCompacting) && (
|
||||
{(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>
|
||||
@@ -453,6 +458,7 @@ function BaseChatContent({
|
||||
className={`relative z-10 ${disableAnimation ? '' : 'animate-[fadein_400ms_ease-in_forwards]'}`}
|
||||
>
|
||||
<ChatInput
|
||||
sessionId={chat.sessionId}
|
||||
handleSubmit={handleSubmit}
|
||||
chatState={chatState}
|
||||
onStop={onStopGoose}
|
||||
@@ -472,6 +478,7 @@ function BaseChatContent({
|
||||
recipeConfig={recipeConfig}
|
||||
recipeAccepted={recipeAccepted}
|
||||
initialPrompt={initialPrompt}
|
||||
toolCount={toolCount || 0}
|
||||
autoSubmit={autoSubmit}
|
||||
setAncestorMessages={setAncestorMessages}
|
||||
append={append}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { DirSwitcher } from './bottom_menu/DirSwitcher';
|
||||
import ModelsBottomBar from './settings/models/bottom_bar/ModelsBottomBar';
|
||||
import { BottomMenuModeSelection } from './bottom_menu/BottomMenuModeSelection';
|
||||
import { AlertType, useAlerts } from './alerts';
|
||||
import { useToolCount } from './alerts/useToolCount';
|
||||
import { useConfig } from './ConfigContext';
|
||||
import { useModelAndProvider } from './ModelAndProviderContext';
|
||||
import { useWhisper } from '../hooks/useWhisper';
|
||||
@@ -58,6 +57,7 @@ interface ModelLimit {
|
||||
}
|
||||
|
||||
interface ChatInputProps {
|
||||
sessionId: string | null;
|
||||
handleSubmit: (e: React.FormEvent) => void;
|
||||
chatState: ChatState;
|
||||
onStop?: () => void;
|
||||
@@ -83,6 +83,7 @@ interface ChatInputProps {
|
||||
recipeConfig?: Recipe | null;
|
||||
recipeAccepted?: boolean;
|
||||
initialPrompt?: string;
|
||||
toolCount: number;
|
||||
autoSubmit: boolean;
|
||||
setAncestorMessages?: (messages: Message[]) => void;
|
||||
append?: (message: Message) => void;
|
||||
@@ -90,6 +91,7 @@ interface ChatInputProps {
|
||||
}
|
||||
|
||||
export default function ChatInput({
|
||||
sessionId,
|
||||
handleSubmit,
|
||||
chatState = ChatState.Idle,
|
||||
onStop,
|
||||
@@ -109,6 +111,7 @@ export default function ChatInput({
|
||||
recipeConfig,
|
||||
recipeAccepted,
|
||||
initialPrompt,
|
||||
toolCount,
|
||||
autoSubmit = false,
|
||||
append,
|
||||
setAncestorMessages,
|
||||
@@ -133,7 +136,6 @@ export default function ChatInput({
|
||||
const dropdownRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
||||
null
|
||||
) as React.RefObject<HTMLDivElement>;
|
||||
const toolCount = useToolCount();
|
||||
const { isCompacting, handleManualCompaction } = useContextManager();
|
||||
const { getProviders, read } = useConfig();
|
||||
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
|
||||
@@ -296,33 +298,16 @@ export default function ChatInput({
|
||||
setHasUserTyped(false);
|
||||
}, [initialValue]); // Keep only initialValue as a dependency
|
||||
|
||||
// Track if we've already set the recipe prompt to avoid re-setting it
|
||||
const hasSetRecipePromptRef = useRef(false);
|
||||
|
||||
// Handle recipe prompt updates
|
||||
useEffect(() => {
|
||||
// If recipe is accepted and we have an initial prompt, and no messages yet, and we haven't set it before
|
||||
if (
|
||||
recipeAccepted &&
|
||||
initialPrompt &&
|
||||
messages.length === 0 &&
|
||||
!hasSetRecipePromptRef.current
|
||||
) {
|
||||
if (recipeAccepted && initialPrompt && messages.length === 0) {
|
||||
setDisplayValue(initialPrompt);
|
||||
setValue(initialPrompt);
|
||||
hasSetRecipePromptRef.current = true;
|
||||
setTimeout(() => {
|
||||
textAreaRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
// we don't need hasSetRecipePromptRef in the dependency array because it is a ref that persists across renders
|
||||
}, [recipeAccepted, initialPrompt, messages.length]);
|
||||
|
||||
// Reset the recipe prompt flag when the recipe changes or messages are added
|
||||
useEffect(() => {
|
||||
if (messages.length > 0 || !recipeAccepted || !initialPrompt) {
|
||||
hasSetRecipePromptRef.current = false;
|
||||
}
|
||||
}, [recipeAccepted, initialPrompt, messages.length]);
|
||||
|
||||
// Draft functionality - load draft if no initial value or recipe
|
||||
@@ -920,6 +905,14 @@ export default function ChatInput({
|
||||
return true; // Return true if message was queued
|
||||
};
|
||||
|
||||
const canSubmit =
|
||||
!isLoading &&
|
||||
!isCompacting &&
|
||||
agentIsReady &&
|
||||
(displayValue.trim() ||
|
||||
pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) ||
|
||||
allDroppedFiles.some((file) => !file.error && !file.isLoading));
|
||||
|
||||
const performSubmit = useCallback(
|
||||
(text?: string) => {
|
||||
const validPastedImageFilesPaths = pastedImages
|
||||
@@ -1061,13 +1054,6 @@ export default function ChatInput({
|
||||
return;
|
||||
}
|
||||
|
||||
const canSubmit =
|
||||
!isLoading &&
|
||||
!isCompacting &&
|
||||
agentIsReady &&
|
||||
(displayValue.trim() ||
|
||||
pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) ||
|
||||
allDroppedFiles.some((file) => !file.error && !file.isLoading));
|
||||
if (canSubmit) {
|
||||
performSubmit();
|
||||
}
|
||||
@@ -1575,6 +1561,7 @@ export default function ChatInput({
|
||||
<Tooltip>
|
||||
<div>
|
||||
<ModelsBottomBar
|
||||
sessionId={sessionId}
|
||||
dropdownRef={dropdownRef}
|
||||
setView={setView}
|
||||
alerts={alerts}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { cn } from '../utils';
|
||||
interface GooseMessageProps {
|
||||
// messages up to this index are presumed to be "history" from a resumed session, this is used to track older tool confirmation requests
|
||||
// anything before this index should not render any buttons, but anything after should
|
||||
sessionId: string;
|
||||
messageHistoryIndex: number;
|
||||
message: Message;
|
||||
messages: Message[];
|
||||
@@ -40,6 +41,7 @@ interface GooseMessageProps {
|
||||
}
|
||||
|
||||
export default function GooseMessage({
|
||||
sessionId,
|
||||
messageHistoryIndex,
|
||||
message,
|
||||
metadata,
|
||||
@@ -293,6 +295,7 @@ export default function GooseMessage({
|
||||
|
||||
{hasToolConfirmation && (
|
||||
<ToolCallConfirmation
|
||||
sessionId={sessionId}
|
||||
isCancelledMessage={messageIndex == messageHistoryIndex - 1}
|
||||
isClicked={messageIndex < messageHistoryIndex}
|
||||
toolConfirmationId={toolConfirmationContent.id}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { initializeAgent } from '../agent';
|
||||
import { toastError, toastSuccess } from '../toasts';
|
||||
import Model, { getProviderMetadata } from './settings/models/modelInterface';
|
||||
import { ProviderMetadata } from '../api';
|
||||
import { ProviderMetadata, updateAgentProvider } from '../api';
|
||||
import { useConfig } from './ConfigContext';
|
||||
import {
|
||||
getModelDisplayName,
|
||||
@@ -26,7 +25,7 @@ const SWITCH_MODEL_SUCCESS_MSG = 'Successfully switched models';
|
||||
interface ModelAndProviderContextType {
|
||||
currentModel: string | null;
|
||||
currentProvider: string | null;
|
||||
changeModel: (model: Model) => Promise<void>;
|
||||
changeModel: (sessionId: string | null, model: Model) => Promise<void>;
|
||||
getCurrentModelAndProvider: () => Promise<{ model: string; provider: string }>;
|
||||
getFallbackModelAndProvider: () => Promise<{ model: string; provider: string }>;
|
||||
getCurrentModelAndProviderForDisplay: () => Promise<{ model: string; provider: string }>;
|
||||
@@ -44,50 +43,43 @@ const ModelAndProviderContext = createContext<ModelAndProviderContextType | unde
|
||||
export const ModelAndProviderProvider: React.FC<ModelAndProviderProviderProps> = ({ children }) => {
|
||||
const [currentModel, setCurrentModel] = useState<string | null>(null);
|
||||
const [currentProvider, setCurrentProvider] = useState<string | null>(null);
|
||||
const { read, upsert, getProviders, config } = useConfig();
|
||||
const { read, upsert, getProviders } = useConfig();
|
||||
|
||||
const changeModel = useCallback(
|
||||
async (model: Model) => {
|
||||
async (sessionId: string | null, model: Model) => {
|
||||
const modelName = model.name;
|
||||
const providerName = model.provider;
|
||||
try {
|
||||
await initializeAgent({
|
||||
model: model.name,
|
||||
provider: model.provider,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to change model at agent step -- ${modelName} ${providerName}`);
|
||||
toastError({
|
||||
title: CHANGE_MODEL_ERROR_TITLE,
|
||||
msg: SWITCH_MODEL_AGENT_ERROR_MSG,
|
||||
traceback: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
// don't write to config
|
||||
return;
|
||||
}
|
||||
let phase = 'agent';
|
||||
|
||||
try {
|
||||
if (sessionId) {
|
||||
await updateAgentProvider({
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
provider: providerName,
|
||||
model: modelName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
phase = 'config';
|
||||
await upsert('GOOSE_PROVIDER', providerName, false);
|
||||
await upsert('GOOSE_MODEL', modelName, false);
|
||||
|
||||
// Update local state
|
||||
setCurrentProvider(providerName);
|
||||
setCurrentModel(modelName);
|
||||
} catch (error) {
|
||||
console.error(`Failed to change model at config step -- ${modelName} ${providerName}}`);
|
||||
toastError({
|
||||
title: CHANGE_MODEL_ERROR_TITLE,
|
||||
msg: CONFIG_UPDATE_ERROR_MSG,
|
||||
traceback: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
// agent and config will be out of sync at this point
|
||||
// TODO: reset agent to use current config settings
|
||||
} finally {
|
||||
// show toast
|
||||
|
||||
toastSuccess({
|
||||
title: CHANGE_MODEL_TOAST_TITLE,
|
||||
msg: `${SWITCH_MODEL_SUCCESS_MSG} -- using ${model.alias ?? modelName} from ${model.subtext ?? providerName}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to change model at ${phase} step -- ${modelName} ${providerName}`);
|
||||
toastError({
|
||||
title: CHANGE_MODEL_ERROR_TITLE,
|
||||
msg: phase === 'agent' ? SWITCH_MODEL_AGENT_ERROR_MSG : CONFIG_UPDATE_ERROR_MSG,
|
||||
traceback: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
},
|
||||
[upsert]
|
||||
@@ -183,19 +175,6 @@ export const ModelAndProviderProvider: React.FC<ModelAndProviderProviderProps> =
|
||||
refreshCurrentModelAndProvider();
|
||||
}, [refreshCurrentModelAndProvider]);
|
||||
|
||||
// Extract config values for dependency array
|
||||
const configObj = config as Record<string, unknown>;
|
||||
const gooseModel = configObj?.GOOSE_MODEL;
|
||||
const gooseProvider = configObj?.GOOSE_PROVIDER;
|
||||
|
||||
// Listen for config changes and refresh when GOOSE_MODEL or GOOSE_PROVIDER changes
|
||||
useEffect(() => {
|
||||
// Only refresh if the config has loaded and model/provider values exist
|
||||
if (config && Object.keys(config).length > 0 && (gooseModel || gooseProvider)) {
|
||||
refreshCurrentModelAndProvider();
|
||||
}
|
||||
}, [config, gooseModel, gooseProvider, refreshCurrentModelAndProvider]);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
currentModel,
|
||||
|
||||
@@ -143,7 +143,8 @@ describe('OllamaSetup', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when Ollama and model are both available', () => {
|
||||
// TODO: re-enable when we have ollama back in the onboarding
|
||||
describe.skip('when Ollama and model are both available', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(ollamaDetection.checkOllamaStatus).mockResolvedValue({
|
||||
isRunning: true,
|
||||
|
||||
@@ -9,18 +9,18 @@ import {
|
||||
getPreferredModel,
|
||||
type PullProgress,
|
||||
} from '../utils/ollamaDetection';
|
||||
import { initializeSystem } from '../utils/providerUtils';
|
||||
//import { initializeSystem } from '../utils/providerUtils';
|
||||
import { toastService } from '../toasts';
|
||||
import { Ollama } from './icons';
|
||||
|
||||
interface OllamaSetupProps {
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
setIsExtensionsLoading?: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export function OllamaSetup({ onSuccess, onCancel, setIsExtensionsLoading }: OllamaSetupProps) {
|
||||
const { addExtension, getExtensions, upsert } = useConfig();
|
||||
export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
|
||||
//const { addExtension, getExtensions, upsert } = useConfig();
|
||||
const { upsert } = useConfig();
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
const [ollamaDetected, setOllamaDetected] = useState(false);
|
||||
const [isPolling, setIsPolling] = useState(false);
|
||||
@@ -110,13 +110,6 @@ export function OllamaSetup({ onSuccess, onCancel, setIsExtensionsLoading }: Oll
|
||||
await upsert('GOOSE_MODEL', getPreferredModel(), false);
|
||||
await upsert('OLLAMA_HOST', 'localhost', false);
|
||||
|
||||
// Initialize the system with Ollama
|
||||
await initializeSystem('ollama', getPreferredModel(), {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setIsExtensionsLoading,
|
||||
});
|
||||
|
||||
toastService.success({
|
||||
title: 'Success!',
|
||||
msg: `Connected to Ollama with ${getPreferredModel()} model.`,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* - Configurable batch size and delay
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Message } from '../types/message';
|
||||
import GooseMessage from './GooseMessage';
|
||||
import UserMessage from './UserMessage';
|
||||
@@ -22,10 +22,11 @@ import { CompactionMarker } from './context_management/CompactionMarker';
|
||||
import { useContextManager } from './context_management/ContextManager';
|
||||
import { NotificationEvent } from '../hooks/useMessageStream';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
import { ChatType } from '../types/chat';
|
||||
|
||||
interface ProgressiveMessageListProps {
|
||||
messages: Message[];
|
||||
chat?: { id: string; messageHistoryIndex: number }; // Make optional for session history
|
||||
chat?: Pick<ChatType, 'sessionId' | 'messageHistoryIndex'>;
|
||||
toolCallNotifications?: Map<string, NotificationEvent[]>; // Make optional
|
||||
append?: (value: string) => void; // Make optional
|
||||
appendMessage?: (message: Message) => void; // Make optional
|
||||
@@ -152,8 +153,7 @@ export default function ProgressiveMessageList({
|
||||
// Render messages up to the current rendered count
|
||||
const renderMessages = useCallback(() => {
|
||||
const messagesToRender = messages.slice(0, renderedCount);
|
||||
|
||||
const renderedMessages = messagesToRender
|
||||
return messagesToRender
|
||||
.map((message, index) => {
|
||||
// Use custom render function if provided
|
||||
if (renderMessage) {
|
||||
@@ -170,7 +170,7 @@ export default function ProgressiveMessageList({
|
||||
|
||||
const isUser = isUserMessage(message);
|
||||
|
||||
const result = (
|
||||
return (
|
||||
<div
|
||||
key={message.id && `${message.id}-${message.content.length}`}
|
||||
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} ${isUser ? 'user' : 'assistant'}`}
|
||||
@@ -192,6 +192,7 @@ export default function ProgressiveMessageList({
|
||||
<CompactionMarker message={message} />
|
||||
) : (
|
||||
<GooseMessage
|
||||
sessionId={chat.sessionId}
|
||||
messageHistoryIndex={chat.messageHistoryIndex}
|
||||
message={message}
|
||||
messages={messages}
|
||||
@@ -210,12 +211,8 @@ export default function ProgressiveMessageList({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return result;
|
||||
})
|
||||
.filter(Boolean); // Filter out null values
|
||||
|
||||
return renderedMessages;
|
||||
.filter(Boolean);
|
||||
}, [
|
||||
messages,
|
||||
renderedCount,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { SetupModal } from './SetupModal';
|
||||
import { startOpenRouterSetup } from '../utils/openRouterSetup';
|
||||
import { startTetrateSetup } from '../utils/tetrateSetup';
|
||||
import WelcomeGooseLogo from './WelcomeGooseLogo';
|
||||
import { initializeSystem } from '../utils/providerUtils';
|
||||
import { toastService } from '../toasts';
|
||||
import { OllamaSetup } from './OllamaSetup';
|
||||
|
||||
@@ -13,12 +12,12 @@ import { Goose } from './icons/Goose';
|
||||
import { OpenRouter } from './icons';
|
||||
|
||||
interface ProviderGuardProps {
|
||||
didSelectProvider: boolean;
|
||||
children: React.ReactNode;
|
||||
setIsExtensionsLoading?: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export default function ProviderGuard({ children, setIsExtensionsLoading }: ProviderGuardProps) {
|
||||
const { read, getExtensions, addExtension } = useConfig();
|
||||
export default function ProviderGuard({ didSelectProvider, children }: ProviderGuardProps) {
|
||||
const { read } = useConfig();
|
||||
const navigate = useNavigate();
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
const [hasProvider, setHasProvider] = useState(false);
|
||||
@@ -69,13 +68,6 @@ export default function ProviderGuard({ children, setIsExtensionsLoading }: Prov
|
||||
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
|
||||
|
||||
if (provider && model) {
|
||||
// Initialize the system with the new provider/model
|
||||
await initializeSystem(provider as string, model as string, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setIsExtensionsLoading,
|
||||
});
|
||||
|
||||
toastService.configure({ silent: false });
|
||||
toastService.success({
|
||||
title: 'Success!',
|
||||
@@ -136,13 +128,6 @@ export default function ProviderGuard({ children, setIsExtensionsLoading }: Prov
|
||||
const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL;
|
||||
|
||||
if (provider && model) {
|
||||
// Initialize the system with the new provider/model
|
||||
await initializeSystem(provider as string, model as string, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setIsExtensionsLoading,
|
||||
});
|
||||
|
||||
toastService.configure({ silent: false });
|
||||
toastService.success({
|
||||
title: 'Success!',
|
||||
@@ -207,8 +192,11 @@ export default function ProviderGuard({ children, setIsExtensionsLoading }: Prov
|
||||
};
|
||||
|
||||
checkProvider();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [read]);
|
||||
}, [
|
||||
navigate,
|
||||
read,
|
||||
didSelectProvider, // When the user makes a selection, re-trigger this check
|
||||
]);
|
||||
|
||||
if (
|
||||
isChecking &&
|
||||
@@ -270,7 +258,6 @@ export default function ProviderGuard({ children, setIsExtensionsLoading }: Prov
|
||||
setShowOllamaSetup(false);
|
||||
setShowFirstTimeSetup(true);
|
||||
}}
|
||||
setIsExtensionsLoading={setIsExtensionsLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -21,6 +21,7 @@ const toolConfirmationState = new Map<
|
||||
>();
|
||||
|
||||
interface ToolConfirmationProps {
|
||||
sessionId: string;
|
||||
isCancelledMessage: boolean;
|
||||
isClicked: boolean;
|
||||
toolConfirmationId: string;
|
||||
@@ -28,6 +29,7 @@ interface ToolConfirmationProps {
|
||||
}
|
||||
|
||||
export default function ToolConfirmation({
|
||||
sessionId,
|
||||
isCancelledMessage,
|
||||
isClicked,
|
||||
toolConfirmationId,
|
||||
@@ -68,34 +70,37 @@ export default function ToolConfirmation({
|
||||
}
|
||||
}, [isClicked, clicked, status, toolName, toolConfirmationId]);
|
||||
|
||||
const handleButtonClick = async (action: string) => {
|
||||
const newClicked = true;
|
||||
const newStatus = action;
|
||||
let newActionDisplay = '';
|
||||
const handleButtonClick = async (newStatus: string) => {
|
||||
let newActionDisplay;
|
||||
|
||||
if (action === ALWAYS_ALLOW) {
|
||||
if (newStatus === ALWAYS_ALLOW) {
|
||||
newActionDisplay = 'always allowed';
|
||||
} else if (action === ALLOW_ONCE) {
|
||||
} else if (newStatus === ALLOW_ONCE) {
|
||||
newActionDisplay = 'allowed once';
|
||||
} else {
|
||||
newActionDisplay = 'denied';
|
||||
}
|
||||
|
||||
// Update local state
|
||||
setClicked(newClicked);
|
||||
setClicked(true);
|
||||
setStatus(newStatus);
|
||||
setActionDisplay(newActionDisplay);
|
||||
|
||||
// Store in global state for persistence across navigation
|
||||
toolConfirmationState.set(toolConfirmationId, {
|
||||
clicked: newClicked,
|
||||
clicked: true,
|
||||
status: newStatus,
|
||||
actionDisplay: newActionDisplay,
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await confirmPermission({
|
||||
body: { id: toolConfirmationId, action, principal_type: 'Tool' },
|
||||
body: {
|
||||
session_id: sessionId,
|
||||
id: toolConfirmationId,
|
||||
action: newStatus,
|
||||
principal_type: 'Tool',
|
||||
},
|
||||
});
|
||||
if (response.error) {
|
||||
console.error('Failed to confirm permission:', response.error);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getTools } from '../../api';
|
||||
|
||||
const { clearTimeout } = window;
|
||||
|
||||
export const useToolCount = () => {
|
||||
export const useToolCount = (sessionId: string) => {
|
||||
const [toolCount, setToolCount] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -11,7 +11,7 @@ export const useToolCount = () => {
|
||||
|
||||
const fetchTools = async () => {
|
||||
try {
|
||||
const response = await getTools();
|
||||
const response = await getTools({ query: { session_id: sessionId } });
|
||||
if (!response.error && response.data) {
|
||||
setToolCount(response.data.length);
|
||||
} else {
|
||||
@@ -30,7 +30,7 @@ export const useToolCount = () => {
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, []);
|
||||
}, [sessionId]);
|
||||
|
||||
return toolCount;
|
||||
};
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function manageContextFromBackend({
|
||||
}
|
||||
|
||||
// Function to convert API Message to frontend Message
|
||||
// TODO(Douwe): get rid of this and use the API Message format everywhere
|
||||
export function convertApiMessageToFrontendMessage(
|
||||
apiMessage: ApiMessage,
|
||||
display?: boolean,
|
||||
|
||||
@@ -7,44 +7,30 @@
|
||||
* Key Responsibilities:
|
||||
* - Displays SessionInsights to show session statistics and recent chats
|
||||
* - Provides a ChatInput for users to start new conversations
|
||||
* - Creates a new chat session with the submitted message and navigates to Pair
|
||||
* - Navigates to Pair with the submitted message to start a new conversation
|
||||
* - Ensures each submission from Hub always starts a fresh conversation
|
||||
*
|
||||
* Navigation Flow:
|
||||
* Hub (input submission) → Pair (new conversation with the submitted message)
|
||||
*
|
||||
* Unlike the previous implementation that used BaseChat, the Hub now uses only
|
||||
* ChatInput directly, which allows for clean separation between the landing page
|
||||
* and active conversation states. This ensures that every message submitted from
|
||||
* the Hub creates a brand new chat session in the Pair view.
|
||||
*/
|
||||
|
||||
import { SessionInsights } from './sessions/SessionsInsights';
|
||||
import ChatInput from './ChatInput';
|
||||
import { generateSessionId } from '../sessions';
|
||||
import { ChatState } from '../types/chatState';
|
||||
import { ContextManagerProvider } from './context_management/ContextManager';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
|
||||
import { ChatType } from '../types/chat';
|
||||
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
|
||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||
|
||||
export default function Hub({
|
||||
chat: _chat,
|
||||
setChat: _setChat,
|
||||
setPairChat,
|
||||
setView,
|
||||
setIsGoosehintsModalOpen,
|
||||
isExtensionsLoading,
|
||||
resetChat,
|
||||
}: {
|
||||
readyForAutoUserPrompt: boolean;
|
||||
chat: ChatType;
|
||||
setChat: (chat: ChatType) => void;
|
||||
setPairChat: (chat: ChatType) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||
isExtensionsLoading: boolean;
|
||||
resetChat: () => void;
|
||||
}) {
|
||||
// Handle chat input submission - create new chat and navigate to pair
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
@@ -52,29 +38,15 @@ export default function Hub({
|
||||
const combinedTextFromInput = customEvent.detail?.value || '';
|
||||
|
||||
if (combinedTextFromInput.trim()) {
|
||||
// Always create a completely new chat session with a unique ID for the PAIR
|
||||
const newChatId = generateSessionId();
|
||||
const newPairChat = {
|
||||
id: newChatId, // This generates a unique ID each time
|
||||
title: DEFAULT_CHAT_TITLE,
|
||||
messages: [], // Always start with empty messages
|
||||
messageHistoryIndex: 0,
|
||||
recipeConfig: null, // Clear recipe for new chats from Hub
|
||||
recipeParameters: null, // Clear parameters for new chats from Hub
|
||||
};
|
||||
|
||||
// Update the PAIR chat state immediately to prevent flashing
|
||||
setPairChat(newPairChat);
|
||||
|
||||
// Navigate to pair page with the message to be submitted immediately
|
||||
// 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,
|
||||
resetChat: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Prevent default form submission
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
@@ -86,6 +58,7 @@ export default function Hub({
|
||||
</div>
|
||||
|
||||
<ChatInput
|
||||
sessionId={null}
|
||||
handleSubmit={handleSubmit}
|
||||
autoSubmit={false}
|
||||
chatState={ChatState.Idle}
|
||||
@@ -104,6 +77,7 @@ export default function Hub({
|
||||
sessionCosts={undefined}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
isExtensionsLoading={isExtensionsLoading}
|
||||
toolCount={0}
|
||||
/>
|
||||
</div>
|
||||
</ContextManagerProvider>
|
||||
|
||||
@@ -1,145 +1,111 @@
|
||||
/**
|
||||
* Pair Component
|
||||
*
|
||||
* The Pair component represents the active conversation mode in the Goose Desktop application.
|
||||
* This is where users engage in ongoing conversations with the AI assistant after transitioning
|
||||
* from the Hub's initial welcome screen.
|
||||
*
|
||||
* Key Responsibilities:
|
||||
* - Manages active chat sessions with full message history
|
||||
* - Handles transitions from Hub with initial input processing
|
||||
* - Provides the main conversational interface for extended interactions
|
||||
* - Enables local storage persistence for conversation continuity
|
||||
* - Supports all advanced chat features like file attachments, tool usage, etc.
|
||||
*
|
||||
* Navigation Flow:
|
||||
* Hub (initial message) → Pair (active conversation) → Hub (new session)
|
||||
*
|
||||
* The Pair component is essentially a specialized wrapper around BaseChat that:
|
||||
* - Processes initial input from the Hub transition
|
||||
* - Enables conversation persistence
|
||||
* - Provides the full-featured chat experience
|
||||
*
|
||||
* Unlike Hub, Pair assumes an active conversation state and focuses on
|
||||
* maintaining conversation flow rather than onboarding new users.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { View, ViewOptions } from '../utils/navigationUtils';
|
||||
import BaseChat from './BaseChat';
|
||||
import { useRecipeManager } from '../hooks/useRecipeManager';
|
||||
import { useIsMobile } from '../hooks/use-mobile';
|
||||
import { useSidebar } from './ui/sidebar';
|
||||
import { AgentState, InitializationContext } from '../hooks/useAgent';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { cn } from '../utils';
|
||||
|
||||
import { ChatType } from '../types/chat';
|
||||
import { DEFAULT_CHAT_TITLE } from '../contexts/ChatContext';
|
||||
|
||||
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;
|
||||
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,
|
||||
setView,
|
||||
setIsGoosehintsModalOpen,
|
||||
}: {
|
||||
chat: ChatType;
|
||||
setChat: (chat: ChatType) => void;
|
||||
setView: (view: View, viewOptions?: ViewOptions) => void;
|
||||
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
setFatalError,
|
||||
setAgentWaitingMessage,
|
||||
agentState,
|
||||
loadCurrentChat,
|
||||
resumeSessionId,
|
||||
initialMessage,
|
||||
}: PairProps & PairRouteState) {
|
||||
const isMobile = useIsMobile();
|
||||
const { state: sidebarState } = useSidebar();
|
||||
const [hasProcessedInitialInput, setHasProcessedInitialInput] = useState(false);
|
||||
const [shouldAutoSubmit, setShouldAutoSubmit] = useState(false);
|
||||
const [initialMessage, setInitialMessage] = useState<string | null>(null);
|
||||
const [messageToSubmit, setMessageToSubmit] = useState<string | null>(null);
|
||||
const [isTransitioningFromHub, setIsTransitioningFromHub] = useState(false);
|
||||
const [loadingChat, setLoadingChat] = useState(false);
|
||||
|
||||
// Get recipe configuration and parameter handling
|
||||
const { initialPrompt: recipeInitialPrompt } = useRecipeManager(chat.messages, location.state);
|
||||
|
||||
// Handle recipe loading from recipes view - reset chat if needed
|
||||
useEffect(() => {
|
||||
if (location.state?.resetChat && location.state?.recipeConfig) {
|
||||
// Reset the chat to start fresh with the recipe
|
||||
const newChat = {
|
||||
id: chat.id, // Keep the same ID to maintain the session
|
||||
title: location.state.recipeConfig.title || 'Recipe Chat',
|
||||
messages: [], // Clear messages to start fresh
|
||||
messageHistoryIndex: 0,
|
||||
recipeConfig: location.state.recipeConfig, // Set the recipe config in chat state
|
||||
recipeParameters: null, // Clear parameters for new recipe
|
||||
};
|
||||
setChat(newChat);
|
||||
const initializeFromState = async () => {
|
||||
setLoadingChat(true);
|
||||
try {
|
||||
const chat = await loadCurrentChat({
|
||||
resumeSessionId: resumeSessionId,
|
||||
setAgentWaitingMessage,
|
||||
});
|
||||
setChat(chat);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
setFatalError(`Agent init failure: ${error instanceof Error ? error.message : '' + error}`);
|
||||
} finally {
|
||||
setLoadingChat(false);
|
||||
}
|
||||
};
|
||||
initializeFromState();
|
||||
}, [
|
||||
agentState,
|
||||
setChat,
|
||||
setFatalError,
|
||||
setAgentWaitingMessage,
|
||||
loadCurrentChat,
|
||||
resumeSessionId,
|
||||
]);
|
||||
|
||||
// Clear the location state to prevent re-processing
|
||||
window.history.replaceState({}, '', '/pair');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.state, chat.id]);
|
||||
|
||||
// Handle initial message from hub page
|
||||
// Followed by sending the initialMessage if we have one. This will happen
|
||||
// only once, unless we reset the chat in step one.
|
||||
useEffect(() => {
|
||||
const messageFromHub = location.state?.initialMessage;
|
||||
const resetChat = location.state?.resetChat;
|
||||
|
||||
// If we have a resetChat flag from Hub, clear any existing recipe config
|
||||
// This scenario occurs when a user navigates from Hub to start a new chat,
|
||||
// ensuring any previous recipe configuration is cleared for a fresh start
|
||||
if (resetChat) {
|
||||
const newChat: ChatType = {
|
||||
...chat,
|
||||
recipeConfig: null,
|
||||
recipeParameters: null,
|
||||
title: DEFAULT_CHAT_TITLE,
|
||||
messages: [], // Clear messages for fresh start
|
||||
messageHistoryIndex: 0,
|
||||
};
|
||||
setChat(newChat);
|
||||
if (agentState !== AgentState.INITIALIZED || !initialMessage || hasProcessedInitialInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset processing state when we have a new message from hub
|
||||
if (messageFromHub) {
|
||||
// Set transitioning state to prevent showing popular topics
|
||||
setIsTransitioningFromHub(true);
|
||||
setIsTransitioningFromHub(true);
|
||||
setHasProcessedInitialInput(true);
|
||||
setMessageToSubmit(initialMessage);
|
||||
setShouldAutoSubmit(true);
|
||||
}, [agentState, initialMessage, hasProcessedInitialInput]);
|
||||
|
||||
// If this is a different message than what we processed before, reset the flag
|
||||
if (messageFromHub !== initialMessage) {
|
||||
setHasProcessedInitialInput(false);
|
||||
}
|
||||
|
||||
if (!hasProcessedInitialInput) {
|
||||
setHasProcessedInitialInput(true);
|
||||
setInitialMessage(messageFromHub);
|
||||
setShouldAutoSubmit(true);
|
||||
|
||||
// Clear the location state to prevent re-processing
|
||||
window.history.replaceState({}, '', '/pair');
|
||||
}
|
||||
useEffect(() => {
|
||||
if (agentState === AgentState.NO_PROVIDER) {
|
||||
setView('welcome');
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [location.state, hasProcessedInitialInput, initialMessage]);
|
||||
}, [agentState, setView]);
|
||||
|
||||
const { initialPrompt: recipeInitialPrompt } = useRecipeManager(chat, chat.recipeConfig || null);
|
||||
|
||||
// Custom message submit handler
|
||||
const handleMessageSubmit = (message: string) => {
|
||||
// This is called after a message is submitted
|
||||
// Clean up any auto submit state:
|
||||
setShouldAutoSubmit(false);
|
||||
setIsTransitioningFromHub(false); // Clear transitioning state once message is submitted
|
||||
setIsTransitioningFromHub(false);
|
||||
setMessageToSubmit(null);
|
||||
console.log('Message submitted:', message);
|
||||
};
|
||||
|
||||
// Custom message stream finish handler to handle recipe auto-execution
|
||||
const handleMessageStreamFinish = () => {
|
||||
// This will be called with the proper append function from BaseChat
|
||||
// For now, we'll handle auto-execution in the BaseChat component
|
||||
};
|
||||
const recipePrompt =
|
||||
agentState === 'initialized' && chat.messages.length === 0 && recipeInitialPrompt;
|
||||
|
||||
// Determine the initial value for the chat input
|
||||
// Priority: Hub message > Recipe prompt > empty
|
||||
const initialValue = initialMessage || recipeInitialPrompt || undefined;
|
||||
const initialValue = messageToSubmit || recipePrompt || undefined;
|
||||
|
||||
// Custom chat input props for Pair-specific behavior
|
||||
const customChatInputProps = {
|
||||
// Pass initial message from Hub or recipe prompt
|
||||
initialValue,
|
||||
@@ -148,13 +114,12 @@ export default function Pair({
|
||||
return (
|
||||
<BaseChat
|
||||
chat={chat}
|
||||
loadingChat={loadingChat}
|
||||
autoSubmit={shouldAutoSubmit}
|
||||
setChat={setChat}
|
||||
setView={setView}
|
||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||
enableLocalStorage={true} // Enable local storage for Pair mode
|
||||
onMessageSubmit={handleMessageSubmit}
|
||||
onMessageStreamFinish={handleMessageStreamFinish}
|
||||
customChatInputProps={customChatInputProps}
|
||||
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
|
||||
|
||||
@@ -459,7 +459,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
setSelectedSessionDetails(null);
|
||||
setSessionDetailsError(null);
|
||||
}}
|
||||
onRetry={() => loadAndShowSessionDetails(selectedSessionDetails.session_id)}
|
||||
onRetry={() => loadAndShowSessionDetails(selectedSessionDetails?.sessionId)}
|
||||
showActionButtons={true}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
LoaderCircle,
|
||||
AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { type SessionDetails } from '../../sessions';
|
||||
import { resumeSession, type SessionDetails } from '../../sessions';
|
||||
import { Button } from '../ui/button';
|
||||
import { toast } from 'react-toastify';
|
||||
import { MainPanelLayout } from '../Layout/MainPanelLayout';
|
||||
@@ -38,10 +38,7 @@ const isUserMessage = (message: Message): boolean => {
|
||||
if (message.role === 'assistant') {
|
||||
return false;
|
||||
}
|
||||
if (message.content.every((c) => c.type === 'toolConfirmationRequest')) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return !message.content.every((c) => c.type === 'toolConfirmationRequest');
|
||||
};
|
||||
|
||||
const filterMessagesForDisplay = (messages: Message[]): Message[] => {
|
||||
@@ -112,7 +109,7 @@ const SessionMessages: React.FC<{
|
||||
<ProgressiveMessageList
|
||||
messages={filteredMessages}
|
||||
chat={{
|
||||
id: 'session-preview',
|
||||
sessionId: 'session-preview',
|
||||
messageHistoryIndex: filteredMessages.length,
|
||||
}}
|
||||
toolCallNotifications={new Map()}
|
||||
@@ -189,7 +186,7 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
session.metadata.working_dir,
|
||||
session.messages,
|
||||
session.metadata.description || 'Shared Session',
|
||||
session.metadata.total_tokens
|
||||
session.metadata.total_tokens || 0
|
||||
);
|
||||
|
||||
const shareableLink = `goose://sessions/${shareToken}`;
|
||||
@@ -219,31 +216,10 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
};
|
||||
|
||||
const handleLaunchInNewWindow = () => {
|
||||
if (session) {
|
||||
console.log('Launching session in new window:', session.session_id);
|
||||
console.log('Session details:', session);
|
||||
|
||||
// Get the working directory from the session metadata
|
||||
const workingDir = session.metadata?.working_dir;
|
||||
|
||||
if (workingDir) {
|
||||
console.log(
|
||||
`Opening new window with session ID: ${session.session_id}, in working dir: ${workingDir}`
|
||||
);
|
||||
|
||||
// Create a new chat window with the working directory and session ID
|
||||
window.electron.createChatWindow(
|
||||
undefined, // query
|
||||
workingDir, // dir
|
||||
undefined, // version
|
||||
session.session_id // resumeSessionId
|
||||
);
|
||||
|
||||
console.log('createChatWindow called successfully');
|
||||
} else {
|
||||
console.error('No working directory found in session metadata');
|
||||
toast.error('Could not launch session: Missing working directory');
|
||||
}
|
||||
try {
|
||||
resumeSession(session);
|
||||
} catch (error) {
|
||||
toast.error(`Could not launch session: ${error instanceof Error ? error.message : error}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -312,7 +288,7 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
||||
{session.metadata.total_tokens !== null && (
|
||||
<span className="flex items-center">
|
||||
<Target className="w-4 h-4 mr-1" />
|
||||
{session.metadata.total_tokens.toLocaleString()}
|
||||
{(session.metadata.total_tokens || 0).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -413,7 +413,7 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
|
||||
<div className="flex items-center">
|
||||
<Target className="w-3 h-3 mr-1" />
|
||||
<span className="font-mono">
|
||||
{session.metadata.total_tokens.toLocaleString()}
|
||||
{(session.metadata.total_tokens || 0).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import { Card, CardContent, CardDescription } from '../ui/card';
|
||||
import { getApiUrl } from '../../config';
|
||||
import { Greeting } from '../common/Greeting';
|
||||
import { fetchSessions, fetchSessionDetails, type Session } from '../../sessions';
|
||||
import { fetchSessions, type Session, resumeSession } from '../../sessions';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button } from '../ui/button';
|
||||
import { ChatSmart } from '../icons/';
|
||||
@@ -104,21 +104,13 @@ export function SessionInsights() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleSessionClick = async (sessionId: string) => {
|
||||
const handleSessionClick = async (session: Session) => {
|
||||
try {
|
||||
// Fetch the session details
|
||||
const sessionDetails = await fetchSessionDetails(sessionId);
|
||||
|
||||
// Navigate to pair view with the resumed session
|
||||
navigate('/pair', {
|
||||
state: { resumedSession: sessionDetails },
|
||||
replace: true,
|
||||
});
|
||||
resumeSession(session);
|
||||
} catch (error) {
|
||||
console.error('Failed to load session:', error);
|
||||
// Fallback to the sessions view if loading fails
|
||||
console.error('Failed to start session:', error);
|
||||
navigate('/sessions', {
|
||||
state: { selectedSessionId: sessionId },
|
||||
state: { selectedSessionId: session.id },
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
@@ -358,13 +350,13 @@ export function SessionInsights() {
|
||||
<div
|
||||
key={session.id}
|
||||
className="flex items-center justify-between text-sm py-1 px-2 rounded-md hover:bg-background-muted cursor-pointer transition-colors session-item"
|
||||
onClick={() => handleSessionClick(session.id)}
|
||||
onClick={() => handleSessionClick(session)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{ animationDelay: `${index * 0.1}s` }}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
await handleSessionClick(session.id);
|
||||
await handleSessionClick(session);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -68,7 +68,7 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
||||
|
||||
const handleRetryLoadSession = () => {
|
||||
if (selectedSession) {
|
||||
loadSessionDetails(selectedSession.session_id);
|
||||
loadSessionDetails(selectedSession.sessionId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
||||
<SessionHistoryView
|
||||
session={
|
||||
selectedSession || {
|
||||
session_id: initialSessionId || '',
|
||||
sessionId: initialSessionId || '',
|
||||
messages: [],
|
||||
metadata: {
|
||||
description: 'Loading...',
|
||||
@@ -97,7 +97,7 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
||||
<SessionListView
|
||||
setView={setView}
|
||||
onSelectSession={handleSelectSession}
|
||||
selectedSessionId={selectedSession?.session_id ?? null}
|
||||
selectedSessionId={selectedSession?.sessionId ?? null}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Sliders, ChefHat, Bot, Eye, Save } from 'lucide-react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useModelAndProvider } from '../../../ModelAndProviderContext';
|
||||
import { AddModelModal } from '../subcomponents/AddModelModal';
|
||||
import { SwitchModelModal } from '../subcomponents/SwitchModelModal';
|
||||
import { LeadWorkerSettings } from '../subcomponents/LeadWorkerSettings';
|
||||
import { View } from '../../../../utils/navigationUtils';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import { toastSuccess, toastError } from '../../../../toasts';
|
||||
import ViewRecipeModal from '../../../ViewRecipeModal';
|
||||
|
||||
interface ModelsBottomBarProps {
|
||||
sessionId: string | null;
|
||||
dropdownRef: React.RefObject<HTMLDivElement>;
|
||||
setView: (view: View) => void;
|
||||
alerts: Alert[];
|
||||
@@ -30,6 +31,7 @@ interface ModelsBottomBarProps {
|
||||
}
|
||||
|
||||
export default function ModelsBottomBar({
|
||||
sessionId,
|
||||
dropdownRef,
|
||||
setView,
|
||||
alerts,
|
||||
@@ -284,7 +286,11 @@ export default function ModelsBottomBar({
|
||||
</DropdownMenu>
|
||||
|
||||
{isAddModelModalOpen ? (
|
||||
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
|
||||
<SwitchModelModal
|
||||
sessionId={sessionId}
|
||||
setView={setView}
|
||||
onClose={() => setIsAddModelModalOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isLeadWorkerModalOpen ? (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '../../../ui/button';
|
||||
import { AddModelModal } from './AddModelModal';
|
||||
import { SwitchModelModal } from './SwitchModelModal';
|
||||
import type { View } from '../../../../utils/navigationUtils';
|
||||
import { shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
|
||||
@@ -23,7 +23,11 @@ export default function ModelSettingsButtons({ setView }: ConfigureModelButtonsP
|
||||
Switch models
|
||||
</Button>
|
||||
{isAddModelModalOpen ? (
|
||||
<AddModelModal setView={setView} onClose={() => setIsAddModelModalOpen(false)} />
|
||||
<SwitchModelModal
|
||||
sessionId={null}
|
||||
setView={setView}
|
||||
onClose={() => setIsAddModelModalOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{!hasPredefinedModels && (
|
||||
<Button
|
||||
|
||||
+4
-3
@@ -19,11 +19,12 @@ import type { View } from '../../../../utils/navigationUtils';
|
||||
import Model, { getProviderMetadata } from '../modelInterface';
|
||||
import { getPredefinedModelsFromEnv, shouldShowPredefinedModels } from '../predefinedModelsUtils';
|
||||
|
||||
type AddModelModalProps = {
|
||||
type SwitchModelModalProps = {
|
||||
sessionId: string | null;
|
||||
onClose: () => void;
|
||||
setView: (view: View) => void;
|
||||
};
|
||||
export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
||||
export const SwitchModelModal = ({ sessionId, onClose, setView }: SwitchModelModalProps) => {
|
||||
const { getProviders, getProviderModels, read } = useConfig();
|
||||
const { changeModel } = useModelAndProvider();
|
||||
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
|
||||
@@ -92,7 +93,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
|
||||
modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model;
|
||||
}
|
||||
|
||||
await changeModel(modelObj);
|
||||
await changeModel(sessionId, modelObj);
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
@@ -40,7 +40,10 @@ export default function PermissionModal({ extensionName, onClose }: PermissionMo
|
||||
useEffect(() => {
|
||||
const fetchTools = async () => {
|
||||
try {
|
||||
const response = await getTools({ query: { extension_name: extensionName } });
|
||||
const response = await getTools({
|
||||
// TODO(Douwe): pass session ID or maybe? do we configure the tools for the agent or globally?
|
||||
query: { extension_name: extensionName, session_id: '' },
|
||||
});
|
||||
if (response.error) {
|
||||
console.error('Failed to get tools');
|
||||
} else {
|
||||
|
||||
@@ -3,22 +3,16 @@ import { ScrollArea } from '../../ui/scroll-area';
|
||||
import BackButton from '../../ui/BackButton';
|
||||
import ProviderGrid from './ProviderGrid';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import { ProviderDetails } from '../../../api/types.gen';
|
||||
import { initializeSystem } from '../../../utils/providerUtils';
|
||||
import { ProviderDetails } from '../../../api';
|
||||
import { toastService } from '../../../toasts';
|
||||
|
||||
interface ProviderSettingsProps {
|
||||
onClose: () => void;
|
||||
isOnboarding: boolean;
|
||||
setIsExtensionsLoading?: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export default function ProviderSettings({
|
||||
onClose,
|
||||
isOnboarding,
|
||||
setIsExtensionsLoading,
|
||||
}: ProviderSettingsProps) {
|
||||
const { getProviders, upsert, getExtensions, addExtension } = useConfig();
|
||||
export default function ProviderSettings({ onClose, isOnboarding }: ProviderSettingsProps) {
|
||||
const { getProviders, upsert } = useConfig();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [providers, setProviders] = useState<ProviderDetails[]>([]);
|
||||
const initialLoadDone = useRef(false);
|
||||
@@ -72,13 +66,6 @@ export default function ProviderSettings({
|
||||
console.log('Setting GOOSE_MODEL to', model)
|
||||
);
|
||||
|
||||
// initialize agent
|
||||
await initializeSystem(provider.name, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
setIsExtensionsLoading,
|
||||
});
|
||||
|
||||
toastService.configure({ silent: false });
|
||||
toastService.success({
|
||||
title: 'Success!',
|
||||
@@ -98,7 +85,7 @@ export default function ProviderSettings({
|
||||
});
|
||||
}
|
||||
},
|
||||
[onClose, upsert, getExtensions, addExtension, setIsExtensionsLoading]
|
||||
[onClose, upsert]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user