mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
iteration
This commit is contained in:
@@ -816,7 +816,7 @@ function ChatContentWithSidebar({
|
||||
)}
|
||||
</div>
|
||||
<div className="relative z-10 animate-[fadein_400ms_ease-in_forwards]">
|
||||
{isLoading && <LoadingGoose />}
|
||||
{/* {isLoading && <LoadingGoose />} */}
|
||||
|
||||
{/* Splash component at the bottom when no messages
|
||||
{messages.length === 0 && (
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
|
||||
import ThemeSelector from './ThemeSelector';
|
||||
import GooseLogo from '../GooseLogo';
|
||||
import { useChatContext } from '../../contexts/ChatContext';
|
||||
import { fetchSessions, type Session } from '../../sessions';
|
||||
|
||||
interface SidebarProps {
|
||||
onSelectSession: (sessionId: string) => void;
|
||||
@@ -41,12 +40,9 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView,
|
||||
const [saveRecipeName, setSaveRecipeName] = useState('');
|
||||
const [saveGlobal, setSaveGlobal] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [recentSessions, setRecentSessions] = useState<Session[]>([]);
|
||||
const { remove } = useConfig();
|
||||
const { hasActiveSession, resetChat } = useChatContext();
|
||||
|
||||
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Trigger animation after a small delay
|
||||
const timer = setTimeout(() => {
|
||||
@@ -55,74 +51,6 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView,
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// Debounced refresh function
|
||||
const debouncedRefresh = useCallback(() => {
|
||||
console.log('AppSidebar: Debounced refresh triggered');
|
||||
// Clear any existing timeout
|
||||
if (refreshTimeoutRef.current) {
|
||||
clearTimeout(refreshTimeoutRef.current);
|
||||
}
|
||||
|
||||
// Set new timeout - reduced to 200ms for faster response
|
||||
refreshTimeoutRef.current = setTimeout(() => {
|
||||
console.log('AppSidebar: Executing debounced refresh');
|
||||
loadRecentSessions();
|
||||
refreshTimeoutRef.current = null;
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
// Cleanup timeout on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (refreshTimeoutRef.current) {
|
||||
clearTimeout(refreshTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Load recent sessions
|
||||
const loadRecentSessions = async () => {
|
||||
try {
|
||||
const sessions = await fetchSessions();
|
||||
// Take only the last 5 sessions
|
||||
setRecentSessions(sessions.slice(0, 5));
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent sessions:', error);
|
||||
setRecentSessions([]);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('AppSidebar: Initial load');
|
||||
loadRecentSessions();
|
||||
}, []);
|
||||
|
||||
// Add effect to listen for session creation events
|
||||
useEffect(() => {
|
||||
const handleSessionCreated = () => {
|
||||
console.log('AppSidebar: Session created event received');
|
||||
// Immediately refresh to show the new session
|
||||
loadRecentSessions();
|
||||
};
|
||||
|
||||
const handleMessageStreamFinish = () => {
|
||||
console.log('AppSidebar: Message stream finished event received');
|
||||
// Always refresh when message stream finishes
|
||||
debouncedRefresh();
|
||||
};
|
||||
|
||||
// Listen for custom events that indicate a session was created
|
||||
window.addEventListener('session-created', handleSessionCreated);
|
||||
|
||||
// Also listen for message stream finish events
|
||||
window.addEventListener('message-stream-finished', handleMessageStreamFinish);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('session-created', handleSessionCreated);
|
||||
window.removeEventListener('message-stream-finished', handleMessageStreamFinish);
|
||||
};
|
||||
}, [debouncedRefresh]);
|
||||
|
||||
const handleSaveRecipe = async () => {
|
||||
if (!saveRecipeName.trim()) {
|
||||
return;
|
||||
@@ -198,51 +126,63 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView,
|
||||
{/* Menu */}
|
||||
<div className="px-1 py-0 pt-14 space-y-2 relative">
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
// If we're not on the chat page and have an active session, just navigate back to chat
|
||||
if (currentPath !== '/' && hasActiveSession) {
|
||||
navigate('/');
|
||||
} else if (hasActiveSession) {
|
||||
// If we're already on the chat page and have an active session, create a new session
|
||||
resetChat();
|
||||
navigate('/');
|
||||
} else {
|
||||
// Navigate to home if no active session
|
||||
navigate('/');
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.1 }}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
// If we're not on the chat page and have an active session, just navigate back to chat
|
||||
if (currentPath !== '/' && hasActiveSession) {
|
||||
navigate('/');
|
||||
} else if (hasActiveSession) {
|
||||
// If we're already on the chat page and have an active session, create a new session
|
||||
resetChat();
|
||||
navigate('/');
|
||||
} else {
|
||||
// Navigate to home if no active session
|
||||
navigate('/');
|
||||
}
|
||||
}}
|
||||
isActive={isActivePath('/')}
|
||||
tooltip={
|
||||
currentPath !== '/' && hasActiveSession
|
||||
? 'Return to chat'
|
||||
: hasActiveSession
|
||||
? 'Create a new session'
|
||||
: 'Go back to the main chat screen'
|
||||
}
|
||||
}}
|
||||
isActive={isActivePath('/')}
|
||||
tooltip={
|
||||
currentPath !== '/' && hasActiveSession
|
||||
? 'Return to chat'
|
||||
: hasActiveSession
|
||||
? 'Create a new session'
|
||||
: 'Go back to the main chat screen'
|
||||
}
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
<span>Home</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
<span>Home</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</motion.div>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
window.electron.createChatWindow(
|
||||
undefined,
|
||||
window.appConfig.get('GOOSE_WORKING_DIR') as string | undefined
|
||||
);
|
||||
}}
|
||||
tooltip="Start a new session in a new window"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<ChatSmart className="w-4 h-4" />
|
||||
<span>New window</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.15 }}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
window.electron.createChatWindow(
|
||||
undefined,
|
||||
window.appConfig.get('GOOSE_WORKING_DIR') as string | undefined
|
||||
);
|
||||
}}
|
||||
tooltip="Start a new session in a new window"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<ChatSmart className="w-4 h-4" />
|
||||
<span>New window</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</motion.div>
|
||||
|
||||
{/* <SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
@@ -256,147 +196,82 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView,
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem> */}
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate('/sessions')}
|
||||
isActive={isActivePath('/sessions')}
|
||||
tooltip="View and share previous sessions"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<Time className="w-4 h-4" />
|
||||
<span>History</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
{process.env.ALPHA && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.2 }}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate('/schedules')}
|
||||
isActive={isActivePath('/schedules')}
|
||||
tooltip="Manage scheduled runs"
|
||||
onClick={() => navigate('/sessions')}
|
||||
isActive={isActivePath('/sessions')}
|
||||
tooltip="View and share previous sessions"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Scheduler</span>
|
||||
<Time className="w-4 h-4" />
|
||||
<span>History</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</motion.div>
|
||||
|
||||
{process.env.ALPHA && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.25 }}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate('/schedules')}
|
||||
isActive={isActivePath('/schedules')}
|
||||
tooltip="Manage scheduled runs"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Scheduler</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate('/recipes')}
|
||||
isActive={isActivePath('/recipes')}
|
||||
tooltip="Browse your saved recipes"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Recipe library</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.3 }}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate('/recipes')}
|
||||
isActive={isActivePath('/recipes')}
|
||||
tooltip="Browse your saved recipes"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
<span>Recipe library</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</motion.div>
|
||||
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate('/settings')}
|
||||
isActive={isActivePath('/settings')}
|
||||
tooltip="View all settings and options"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<Gear className="w-4 h-4" />
|
||||
<span>Settings</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ type: 'spring', stiffness: 300, damping: 25, delay: 0.35 }}
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => navigate('/settings')}
|
||||
isActive={isActivePath('/settings')}
|
||||
tooltip="View all settings and options"
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<Gear className="w-4 h-4" />
|
||||
<span>Settings</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</motion.div>
|
||||
</SidebarMenu>
|
||||
</div>
|
||||
|
||||
{/* Recent Sessions */}
|
||||
{recentSessions.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel className="text-xs uppercase text-text-muted px-3">
|
||||
Recent
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<AnimatePresence mode="popLayout">
|
||||
{recentSessions.map((session, index) => {
|
||||
const hasDescription =
|
||||
session.metadata.description && session.metadata.description.trim() !== '';
|
||||
const isNewSession = session.id.match(/^\d{8}_\d{6}$/);
|
||||
const messageCount = session.metadata.message_count || 0;
|
||||
// Show loading for new sessions with few messages and no description
|
||||
// Only show loading for sessions created in the last 5 minutes
|
||||
// Backend generates descriptions for sessions with < 4 user messages
|
||||
const sessionDate = new Date(session.modified);
|
||||
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
|
||||
const isRecentSession = sessionDate > fiveMinutesAgo;
|
||||
const shouldShowLoading =
|
||||
!hasDescription && isNewSession && messageCount <= 3 && isRecentSession;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={session.id}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
y: -20,
|
||||
scale: 0.95,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
}}
|
||||
exit={{
|
||||
opacity: 0,
|
||||
y: 20,
|
||||
scale: 0.95,
|
||||
}}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 300,
|
||||
damping: 25,
|
||||
delay: index * 0.05,
|
||||
}}
|
||||
layout
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
onClick={() => {
|
||||
const workingDir = session.metadata.working_dir;
|
||||
if (workingDir) {
|
||||
window.electron.createChatWindow(
|
||||
undefined,
|
||||
workingDir,
|
||||
undefined,
|
||||
session.id
|
||||
);
|
||||
}
|
||||
}}
|
||||
tooltip={`Resume session: ${session.metadata.description || session.id}`}
|
||||
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<span className="truncate">
|
||||
{shouldShowLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
<span className="animate-pulse">Generating description...</span>
|
||||
</div>
|
||||
) : (
|
||||
session.metadata.description || session.id
|
||||
)}
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Theme Selector */}
|
||||
<div className="mt-4 opacity-0">
|
||||
<ThemeSelector />
|
||||
@@ -404,12 +279,12 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView,
|
||||
</SidebarContent>
|
||||
|
||||
<SidebarFooter>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 animate-in fade-in slide-in-from-right-8 duration-500">
|
||||
<GooseLogo size="small" />
|
||||
<span className="text-base">codename goose</span>
|
||||
</div>
|
||||
|
||||
<div className="pb-4">
|
||||
<div className="pb-4 animate-in fade-in slide-in-from-right-8 duration-500">
|
||||
<div className="flex gap-2">
|
||||
<Tooltip delayDuration={500}>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -203,7 +203,7 @@ export default function BottomMenu({
|
||||
}, [isModelMenuOpen]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full justify-between items-center pb-2 pr-3 transition-colors relative text-xs align-middle animate-in fade-in slide-in-from-bottom-8 duration-500">
|
||||
<div className="flex w-full justify-between items-center pb-2 pr-3 transition-colors relative text-xs align-middle animate-in fade-in slide-in-from-right-8 duration-500">
|
||||
<div>
|
||||
<DirSwitcher hasMessages={messages.length > 0} />
|
||||
</div>
|
||||
|
||||
@@ -130,10 +130,10 @@ export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBa
|
||||
{isModelMenuOpen && (
|
||||
<div className="absolute bottom-[24px] right-[-55px] w-[300px] z-50 bg-background-default rounded-lg border">
|
||||
<div className="">
|
||||
<div className="text-sm text-textProminent mt-2 ml-2">Current:</div>
|
||||
{/* <div className="text-sm text-textProminent mt-2 ml-2">Current:</div>
|
||||
<div className="flex items-center justify-between text-sm ml-2">
|
||||
{currentModel} -- {displayProvider}
|
||||
</div>
|
||||
</div> */}
|
||||
<div
|
||||
className="flex items-center justify-between text-textStandard p-2 cursor-pointer transition-colors hover:bg-bgStandard
|
||||
border-t border-borderSubtle mt-2"
|
||||
|
||||
@@ -11,10 +11,10 @@ export default function SuspenseLoader() {
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||
>
|
||||
<div className="flex gap-2 items-center justify-end">
|
||||
{/* <div className="flex gap-2 items-center justify-end">
|
||||
<GooseLogo size="small" />
|
||||
<span className="text-text-muted">Loading...</span>
|
||||
</div>
|
||||
</div> */}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user