iteration

This commit is contained in:
Nahiyan Khan
2025-06-22 15:23:37 -04:00
parent 523437af33
commit bfabb969b7
13 changed files with 526 additions and 311 deletions
+3 -3
View File
@@ -428,7 +428,7 @@ export default function ChatInput({
return (
<div
className={`flex flex-col relative h-auto rounded-xl border mx-6 mb-4 transition-colors ${
className={`flex flex-col relative h-auto rounded-xl border mx-6 mb-6 transition-colors ${
isFocused
? 'border-borderProminent hover:border-borderProminent'
: 'border-borderSubtle hover:border-borderStandard'
@@ -464,14 +464,14 @@ export default function ChatInput({
</Button>
{isLoading ? (
<Button type="button" onClick={onStop} shape="round" size="sm" variant="secondary">
<Button type="button" onClick={onStop} shape="round" size="sm" variant="ghost">
<Stop />
</Button>
) : (
<Button
type="submit"
size="sm"
variant="default"
variant="ghost"
shape="round"
// disabled={!displayValue.trim() && pastedImages.length === 0}
>
+149 -19
View File
@@ -50,8 +50,14 @@ import {
Sidebar,
SidebarContent,
} from './ui/sidebar';
import MoreMenu from './more_menu/MoreMenu';
import BottomMenu from './bottom_menu/BottomMenu';
import { SessionInsights } from './sessions/SessionsInsights';
import { useSidebar } from './ui/sidebar';
import { Button } from './ui/button';
import { Gear, Idea } from './icons';
import { Tooltip, TooltipContent, TooltipTrigger } from './ui/Tooltip';
import { Bot, Folder, Save, Send } from 'lucide-react';
import { ChatSmart } from './icons';
// Context for sharing current model info
const CurrentModelContext = createContext<{ model: string; mode: string } | null>(null);
@@ -137,9 +143,10 @@ function ChatContentWithSidebar({
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) {
const safeIsMacOS = (window?.electron?.platform || 'darwin') === 'darwin';
const { open: isSidebarOpen } = useSidebar();
// Calculate padding based on sidebar state and macOS
const headerPadding = safeIsMacOS ? 'pl-24' : 'pl-12';
const headerPadding = !isSidebarOpen ? (safeIsMacOS ? 'pl-20' : 'pl-12') : 'pl-4';
const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
@@ -612,23 +619,136 @@ function ChatContentWithSidebar({
</Sidebar>
<SidebarInset>
<div className="flex flex-col min-w-0 h-[calc(100dvh-40px)] shadow-default bg-background-default mt-2 mr-2 mb-2 rounded-2xl animate-in fade-in slide-in-from-bottom-8 duration-500">
<div className="fixed w-[16rem] left-0 top-0 z-10">
<div
className={`flex w-full py-4 ${headerPadding} pr-5 z-50 transition-[padding] duration-200 ease-in-out`}
>
<SidebarTrigger />
{/* <MoreMenu
setView={setView || (() => {})}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen || (() => {})}
/> */}
<div
className={`h-12 flex items-center justify-between fade-in duration-300 transition-all border-b ${!isSidebarOpen ? 'border-border-default' : 'border-transparent'}`}
>
<div className={`flex items-center ${headerPadding}`}>
<SidebarTrigger className="no-drag" />
</div>
<div className="flex items-center pr-4">
{setIsGoosehintsModalOpen && (
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => setIsGoosehintsModalOpen(true)}
className="px-3"
variant="ghost"
size="sm"
shape="round"
>
<div className="flex gap-2 items-center text-text-default">
<Idea className="w-4 h-4" />
{/* Configure .goosehints */}
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Customize instructions</p>
</TooltipContent>
</Tooltip>
)}
{recipeConfig ? (
<>
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => {
window.electron.createChatWindow(
undefined,
undefined,
undefined,
undefined,
recipeConfig as Recipe,
'recipeEditor'
);
}}
className="px-3"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<Send className="w-4 h-4" />
View recipe
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>View the recipe you're using</p>
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
// onClick={handleSaveRecipeClick}
className="px-3"
variant="ghost"
size="sm"
shape="round"
>
<div className="flex gap-2 items-center text-text-default">
<Save className="w-4 h-4" />
Save recipe
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Save this recipe for reuse</p>
</TooltipContent>
</Tooltip>
</>
) : (
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => {
window.electron.logInfo('Make Agent button clicked');
window.dispatchEvent(new CustomEvent('make-agent-from-chat'));
}}
className="px-3"
variant="ghost"
size="sm"
shape="round"
>
<div className="flex gap-2 items-center text-text-default">
<Bot className="w-4 h-4" />
{/* Make Agent from this session */}
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Make a custom agent you can share or reuse</p>
</TooltipContent>
</Tooltip>
)}
{/* <Button
onClick={async () => {
await remove('GOOSE_PROVIDER', false);
await remove('GOOSE_MODEL', false);
setView?.('welcome');
}}
className="px-3 hover:shadow-default hover:bg-background-default transition-all duration-200 text-red-400 hover:text-red-300"
variant="ghost"
shape="round"
>
<Refresh />
</Button> */}
</div>
</div>
<div
className="flex flex-col min-w-0 flex-1 overflow-y-scroll relative pl-6 pr-4 pb-16 pt-10"
className="flex flex-col min-w-0 flex-1 overflow-y-scroll relative pl-6 pr-4 pb-16 pt-6"
onDrop={handleDrop}
onDragOver={handleDragOver}
>
{/* Session Insights at the top */}
{messages.length === 0 && (
<div className="mb-8 animate-[fadein_500ms_ease-in_forwards]">
<SessionInsights />
</div>
)}
{recipeConfig?.title && messages.length > 0 && (
<AgentHeader
title={recipeConfig.title}
@@ -644,13 +764,9 @@ function ChatContentWithSidebar({
/>
)}
{messages.length === 0 ? (
<Splash
append={append}
activities={
Array.isArray(recipeConfig?.activities) ? recipeConfig!.activities : null
}
title={recipeConfig?.title}
/>
<>
<SearchView>{/* Empty search view when no messages */}</SearchView>
</>
) : (
<>
<SearchView>
@@ -741,6 +857,20 @@ function ChatContentWithSidebar({
</div>
<div className="relative z-10 animate-[fadein_400ms_ease-in_forwards]">
{isLoading && <LoadingGoose />}
{/* Splash component at the bottom when no messages
{messages.length === 0 && (
<div className="mb-4">
<Splash
append={append}
activities={
Array.isArray(recipeConfig?.activities) ? recipeConfig!.activities : null
}
title={recipeConfig?.title}
/>
</div>
)} */}
<ChatInput
handleSubmit={handleSubmit}
isLoading={isLoading}
@@ -1,15 +1,15 @@
import React, { useEffect, useState } from 'react';
import { Folder, FileText, Clock, Save } from 'lucide-react';
import { Folder, FileText, Clock, MessageCircle } from 'lucide-react';
import { SidebarContent, SidebarFooter } from '../ui/sidebar';
import { Button } from '../ui/button';
import { ChatSmart, Idea, Time, Send, Refresh, Gear } from '../icons';
import { Separator } from '../ui/separator';
import { ChatSmart, Time, Gear, LinkedIn, Youtube, Discord } from '../icons';
import { ViewOptions, View } from '../../App';
import { useConfig } from '../ConfigContext';
import { Recipe } from '../../recipe';
import { saveRecipe, generateRecipeFilename } from '../../recipe/recipeStorage';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/Tooltip';
import ThemeSelector from './ThemeSelector';
import GooseLogo from '../GooseLogo';
interface SidebarProps {
onSelectSession: (sessionId: string) => void;
@@ -96,14 +96,64 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView
return (
<>
<SidebarContent>
{/* Action Buttons */}
<div className="px-1 pt-14 py-0 space-y-2 relative">
{/* <SidebarHeader>
<div className="flex items-center gap-2 pt-12 pb-4">
<GooseLogo size="small" />
<span className="text-base">codename goose</span>
</div>
</SidebarHeader> */}
{/* Menu */}
<div className="px-1 py-0 pt-14 space-y-2 relative">
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => {
window.electron.createChatWindow(
undefined,
window.appConfig.get('GOOSE_WORKING_DIR') as string | undefined
);
}}
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<ChatSmart className="w-4 h-4" />
<span className="text-sm">New session</span>
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Start a new session in the current directory</p>
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => {
window.electron.directoryChooser();
}}
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<Folder className="w-4 h-4" />
<span className="text-sm">Open directory</span>
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Start a new session in a different directory</p>
</TooltipContent>
</Tooltip>
{setView && (
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => setView('sessions')}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
@@ -123,7 +173,7 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView
<TooltipTrigger className="w-full">
<Button
onClick={() => setView('schedules')}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
@@ -138,106 +188,17 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView
</Tooltip>
)}
{setIsGoosehintsModalOpen && (
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => setIsGoosehintsModalOpen(true)}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<Idea className="w-4 h-4" />
Configure .goosehints
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Customize instructions</p>
</TooltipContent>
</Tooltip>
)}
{recipeConfig ? (
<>
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => {
window.electron.createChatWindow(
undefined,
undefined,
undefined,
undefined,
recipeConfig as Recipe,
'recipeEditor'
);
}}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<Send className="w-4 h-4" />
View recipe
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>View the recipe you're using</p>
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={handleSaveRecipeClick}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<Save className="w-4 h-4" />
Save recipe
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Save this recipe for reuse</p>
</TooltipContent>
</Tooltip>
</>
) : (
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => {
window.electron.logInfo('Make Agent button clicked');
window.dispatchEvent(new CustomEvent('make-agent-from-chat'));
}}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<Send className="w-4 h-4" />
Make Agent from this session
</div>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Make a custom agent you can share or reuse</p>
</TooltipContent>
</Tooltip>
)}
{setView && (
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => setView('recipes')}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
<FileText className="w-4 h-4" />
Recipe Library
Recipe library
</div>
</Button>
</TooltipTrigger>
@@ -247,12 +208,12 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView
</Tooltip>
)}
{/* {setView && (
{setView && (
<Tooltip delayDuration={500}>
<TooltipTrigger className="w-full">
<Button
onClick={() => setView('settings')}
className="w-full justify-start px-3 rounded-lg h-fit hover:shadow-default hover:bg-background-default transition-all duration-200"
className="w-full justify-start px-3 rounded-lg h-fit hover:bg-neutral-200 transition-all duration-200"
variant="ghost"
>
<div className="flex gap-2 items-center text-text-default">
@@ -265,77 +226,69 @@ const AppSidebar: React.FC<SidebarProps> = ({ setIsGoosehintsModalOpen, setView
<p>View all settings and options</p>
</TooltipContent>
</Tooltip>
)} */}
)}
</div>
{/* Theme Selector */}
<div className="mt-4">
<ThemeSelector />
</div>
<div className="mt-4">{/* <ThemeSelector /> */}</div>
</SidebarContent>
<SidebarFooter>
<div className="flex justify-between items-center w-full">
<Tooltip delayDuration={500}>
<TooltipTrigger>
<Button
onClick={() => {
window.electron.createChatWindow(
undefined,
window.appConfig.get('GOOSE_WORKING_DIR') as string | undefined
);
}}
className="px-3 hover:shadow-default hover:bg-background-default transition-all duration-200"
variant="ghost"
shape="round"
>
<ChatSmart className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Start a new session in the current directory</p>
</TooltipContent>
</Tooltip>
<div className="flex items-center gap-2">
<GooseLogo size="small" />
<span className="text-base">codename goose</span>
</div>
<Tooltip delayDuration={500}>
<TooltipTrigger>
<Button
onClick={() => {
window.electron.directoryChooser();
}}
className="px-3 hover:shadow-default hover:bg-background-default transition-all duration-200"
variant="ghost"
shape="round"
>
<Folder className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="right">
<p>Start a new session in a different directory</p>
</TooltipContent>
</Tooltip>
<div className="pb-4">
<div className="flex gap-2">
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<a
href="https://discord.gg/pvQ8S2e5"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center h-8 w-8 p-0 rounded-full hover:bg-neutral-200 transition-all duration-200"
>
<MessageCircle className="w-4 h-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">
<p>Join our Discord</p>
</TooltipContent>
</Tooltip>
{/* <Button
onClick={async () => {
await remove('GOOSE_PROVIDER', false);
await remove('GOOSE_MODEL', false);
setView?.('welcome');
}}
className="px-3 hover:shadow-default hover:bg-background-default transition-all duration-200 text-red-400 hover:text-red-300"
variant="ghost"
shape="round"
>
<Refresh />
</Button> */}
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<a
href="https://www.linkedin.com/company/block-opensource"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center h-8 w-8 p-0 rounded-full hover:bg-neutral-200 transition-all duration-200"
>
<LinkedIn className="w-4 h-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">
<p>Follow us on LinkedIn</p>
</TooltipContent>
</Tooltip>
<Button
onClick={() => setView?.('settings')}
className="px-3 hover:shadow-default hover:bg-background-default transition-all duration-200"
variant="ghost"
shape="round"
>
<Gear />
</Button>
<Tooltip delayDuration={500}>
<TooltipTrigger asChild>
<a
href="https://www.youtube.com/@blockopensource"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center h-8 w-8 p-0 rounded-full hover:bg-neutral-200 transition-all duration-200"
>
<Youtube className="w-4 h-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">
<p>Watch on YouTube</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</SidebarFooter>
+26 -39
View File
@@ -2,7 +2,6 @@ import { useTextAnimator } from '../hooks/use-text-animator';
import { Card } from './ui/card';
import { useState, useEffect } from 'react';
import { gsap } from 'gsap';
import { SessionInsights } from './sessions/SessionsInsights';
// Register GSAP plugins
gsap.registerPlugin();
@@ -81,9 +80,9 @@ export default function Splash({ append, activities, title }: SplashProps) {
}, []);
return (
<div className="flex flex-col h-full">
<div className="flex flex-col">
{title && (
<div className="flex items-center px-4 py-2">
<div className="flex items-center px-4 py-2 mb-4">
<span className="w-2 h-2 rounded-full bg-blockTeal mr-2" />
<span className="text-sm">
<span className="text-text-muted">Agent</span>{' '}
@@ -91,45 +90,33 @@ export default function Splash({ append, activities, title }: SplashProps) {
</span>
</div>
)}
<div className="flex flex-col flex-1">
<div className="h-full flex flex-col pb-12">
{/* <div className="relative text-textStandard mb-12">
<div className="w-min animate-[flyin_2s_var(--spring-easing)_forwards]">
<GooseLogo />
</div>
</div> */}
<div className="flex flex-col mb-10 mt-5">
<h1 className="text-text-prominent text-4xl font-light">
<span>{greeting.prefix}</span>
</h1>
<div className="text-text-muted text-4xl font-light inline">{greeting.message}</div>
{/* Compact greeting section */}
<div className="flex flex-col px-6 mb-0">
<h1 className="text-text-prominent text-4xl font-light mb-2">
<span>{greeting.prefix}</span>
{/* <span className="text-text-muted">{greeting.message}</span> */}
</h1>
</div>
<div className="flex flex-col">
{messagePill && (
<div className="mb-4 p-3 rounded-lg border animate-[fadein_500ms_ease-in_forwards]">
{messagePill.replace(/^message:/i, '').trim()}
</div>
)}
<div className="flex flex-col">
{messagePill && (
<div className="mb-6 p-4 rounded-lg border animate-[fadein_500ms_ease-in_forwards]">
{messagePill.replace(/^message:/i, '').trim()}
</div>
)}
<div className="flex flex-wrap gap-4 animate-[fadein_500ms_ease-in_forwards]">
{remainingPills.map((content, index) => (
<Card
key={index}
onClick={() => append(content)}
title={content.length > 100 ? content : undefined}
className="cursor-pointer px-6 w-[256px]"
>
{content.length > 100 ? content.slice(0, 100) + '...' : content}
</Card>
))}
</div>
<div className="animate-[fadein_500ms_ease-in_forwards]">
<SessionInsights />
</div>
</div>
<div className="flex flex-wrap gap-3 animate-[fadein_500ms_ease-in_forwards]">
{remainingPills.map((content, index) => (
<Card
key={index}
onClick={() => append(content)}
title={content.length > 100 ? content : undefined}
className="cursor-pointer px-4 py-2 w-[200px]"
>
{content.length > 100 ? content.slice(0, 100) + '...' : content}
</Card>
))}
</div>
</div>
</div>
@@ -9,6 +9,7 @@ import { useConfig } from '../ConfigContext';
import { useModelAndProvider } from '../ModelAndProviderContext';
import { Message } from '../../types/message';
import { ManualSummarizeButton } from '../context_management/ManualSummaryButton';
import { DirSwitcher } from './DirSwitcher';
const TOKEN_LIMIT_DEFAULT = 128000; // fallback for custom models that the backend doesn't know about
const TOKEN_WARNING_THRESHOLD = 0.8; // warning shows at 80% of the token limit
@@ -202,30 +203,36 @@ export default function BottomMenu({
}, [isModelMenuOpen]);
return (
<div className="flex w-full justify-end items-center pb-2 pr-3 transition-colors relative text-xs align-middle animate-in fade-in slide-in-from-bottom-8 duration-500">
{/* Tool and Token count */}
{<BottomMenuAlertPopover alerts={alerts} />}
<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>
<DirSwitcher hasMessages={messages.length > 0} />
</div>
{/* Model Selector Dropdown */}
<ModelsBottomBar dropdownRef={dropdownRef} setView={setView} />
<div className="flex items-center">
{/* Tool and Token count */}
{<BottomMenuAlertPopover alerts={alerts} />}
{/* Separator */}
<div className="w-1 h-1 rounded-full bg-background-accent/50 mx-2" />
{/* Model Selector Dropdown */}
<ModelsBottomBar dropdownRef={dropdownRef} setView={setView} />
{/* Goose Mode Selector Dropdown */}
<BottomMenuModeSelection setView={setView} />
{/* Separator */}
<div className="w-1 h-1 rounded-full bg-background-accent/50 mx-2" />
{/* Summarize Context Button - ADD THIS */}
{messages.length > 0 && (
<>
<div className="w-1 h-1 rounded-full bg-background-accent/50 mx-2" />
<ManualSummarizeButton
messages={messages}
isLoading={isLoading}
setMessages={setMessages}
/>
</>
)}
{/* Goose Mode Selector Dropdown */}
<BottomMenuModeSelection setView={setView} />
{/* Summarize Context Button - ADD THIS */}
{messages.length > 0 && (
<>
<div className="w-1 h-1 rounded-full bg-background-accent/50 mx-2" />
<ManualSummarizeButton
messages={messages}
isLoading={isLoading}
setMessages={setMessages}
/>
</>
)}
</div>
</div>
);
}
@@ -0,0 +1,44 @@
import React, { useState } from 'react';
import { Document } from '../icons';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
interface DirSwitcherProps {
hasMessages?: boolean;
className?: string;
}
export const DirSwitcher: React.FC<DirSwitcherProps> = ({
hasMessages = false,
className = '',
}) => {
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const handleDirectoryChange = async () => {
if (hasMessages) {
window.electron.directoryChooser();
} else {
window.electron.directoryChooser(true);
}
};
return (
<TooltipProvider>
<Tooltip open={isTooltipOpen} onOpenChange={setIsTooltipOpen}>
<TooltipTrigger asChild>
<button
className={`z-[100] hover:cursor-pointer text-text-default/70 hover:text-text-default text-xs flex items-center transition-colors pl-1 [&>svg]:size-4 ${className}`}
onClick={handleDirectoryChange}
>
<Document className="mr-1" />
<div className="max-w-[200px] truncate [direction:rtl]">
{String(window.appConfig.get('GOOSE_WORKING_DIR'))}
</div>
</button>
</TooltipTrigger>
<TooltipContent className="max-w-96 overflow-auto scrollbar-thin" side="top">
{window.appConfig.get('GOOSE_WORKING_DIR') as string}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
@@ -0,0 +1,18 @@
export default function Discord({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 640 512"
aria-hidden="true"
className={className}
>
<path
d="M524.5 69.8a1.5 1.5 0 0 0 -.8-.7A485.1 485.1 0 0 0 404.1 32a1.8 1.8 0 0 0 -1.9 .9 337.5 337.5 0 0 0 -14.9 30.6 447.8 447.8 0 0 0 -134.4 0 309.5 309.5 0 0 0 -15.1-30.6 1.9 1.9 0 0 0 -1.9-.9A483.7 483.7 0 0 0 116.1 69.1a1.7 1.7 0 0 0 -.8 .7C39.1 183.7 18.2 294.7 28.4 404.4a2 2 0 0 0 .8 1.4A487.7 487.7 0 0 0 176 479.9a1.9 1.9 0 0 0 2.1-.7A348.2 348.2 0 0 0 208.1 430.4a1.9 1.9 0 0 0 -1-2.6 321.2 321.2 0 0 1 -45.9-21.9 1.9 1.9 0 0 1 -.2-3.1c3.1-2.3 6.2-4.7 9.1-7.1a1.8 1.8 0 0 1 1.9-.3c96.2 43.9 200.4 43.9 295.5 0a1.8 1.8 0 0 1 1.9 .2c2.9 2.4 6 4.9 9.1 7.2a1.9 1.9 0 0 1 -.2 3.1 301.4 301.4 0 0 1 -45.9 21.8 1.9 1.9 0 0 0 -1 2.6 391.1 391.1 0 0 0 30 48.8 1.9 1.9 0 0 0 2.1 .7A486 486 0 0 0 610.7 405.7a1.9 1.9 0 0 0 .8-1.4C623.7 277.6 590.9 167.5 524.5 69.8zM222.5 337.6c-29 0-52.8-26.6-52.8-59.2S193.1 219.1 222.5 219.1c29.7 0 53.3 26.8 52.8 59.2C275.3 311 251.9 337.6 222.5 337.6zm195.4 0c-29 0-52.8-26.6-52.8-59.2S388.4 219.1 417.9 219.1c29.7 0 53.3 26.8 52.8 59.2C470.7 311 447.5 337.6 417.9 337.6z"
fill="currentColor"
/>
</svg>
);
}
@@ -0,0 +1,18 @@
export default function LinkedIn({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 448 512"
aria-hidden="true"
className={className}
>
<path
d="M416 32H31.9C14.3 32 0 46.5 0 64.3v383.4C0 465.5 14.3 480 31.9 480H416c17.6 0 32-14.5 32-32.3V64.3c0-17.8-14.4-32.3-32-32.3zM135.4 416H69V202.2h66.5V416zm-33.2-243c-21.3 0-38.5-17.3-38.5-38.5S80.9 96 102.2 96c21.2 0 38.5 17.3 38.5 38.5 0 21.3-17.2 38.5-38.5 38.5zm282.1 243h-66.4V312c0-24.8-.5-56.7-34.5-56.7-34.6 0-39.9 27-39.9 54.9V416h-66.4V202.2h63.7v29.2h.9c8.9-16.8 30.6-34.5 62.9-34.5 67.2 0 79.7 44.3 79.7 101.9V416z"
fill="currentColor"
/>
</svg>
);
}
@@ -0,0 +1,18 @@
export default function Youtube({ className = '' }) {
return (
<svg
width="1.5rem"
height="1.5rem"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
aria-hidden="true"
className={className}
>
<path
d="M549.7 124.1c-6.3-23.7-24.8-42.3-48.3-48.6C458.8 64 288 64 288 64S117.2 64 74.6 75.5c-23.5 6.3-42 24.9-48.3 48.6-11.4 42.9-11.4 132.3-11.4 132.3s0 89.4 11.4 132.3c6.3 23.7 24.8 41.5 48.3 47.8C117.2 448 288 448 288 448s170.8 0 213.4-11.5c23.5-6.3 42-24.2 48.3-47.8 11.4-42.9 11.4-132.3 11.4-132.3s0-89.4-11.4-132.3zm-317.5 213.5V175.2l142.7 81.2-142.7 81.2z"
fill="currentColor"
/>
</svg>
);
}
@@ -9,9 +9,11 @@ import ChevronUp from './ChevronUp';
import { ChevronRight } from './ChevronRight';
import Close from './Close';
import Copy from './Copy';
import Discord from './Discord';
import Document from './Document';
import Edit from './Edit';
import Idea from './Idea';
import LinkedIn from './LinkedIn';
import More from './More';
import Refresh from './Refresh';
import SensitiveHidden from './SensitiveHidden';
@@ -20,6 +22,7 @@ import Send from './Send';
import Settings from './Settings';
import Time from './Time';
import { Gear } from './Gear';
import Youtube from './Youtube';
export {
ArrowDown,
@@ -33,10 +36,12 @@ export {
ChevronUp,
Close,
Copy,
Discord,
Document,
Edit,
Idea,
Gear,
LinkedIn,
More,
Refresh,
SensitiveHidden,
@@ -44,4 +49,5 @@ export {
Send,
Settings,
Time,
Youtube,
};
@@ -11,6 +11,36 @@ interface SessionInsights {
totalTokens: number;
}
// Greeting component
function GreetingCard() {
const [selectedGreeting, setSelectedGreeting] = useState<{
prefix: string;
} | null>(null);
// Select a random greeting on component mount
useEffect(() => {
const prefixes = ['Hello.', 'Welcome.', 'Greetings.', 'Welcome back.', 'Hello there.'];
const randomPrefixIndex = Math.floor(Math.random() * prefixes.length);
setSelectedGreeting({
prefix: prefixes[randomPrefixIndex],
});
}, []);
const greeting = selectedGreeting || { prefix: 'Hello.', message: ' How can I help you today?' };
return (
<Card className="col-span-2 border-none animate-in fade-in slide-in-from-bottom-8 duration-500 bg-background-accent">
<CardContent className="flex flex-col justify-end h-full py-4">
<h1 className="text-text-on-accent text-4xl font-light">
<span>{greeting.prefix}</span>
</h1>
{/* <p className="text-text-on-accent font-light text-lg">{greeting.message}</p> */}
</CardContent>
</Card>
);
}
export function SessionInsights() {
const [insights, setInsights] = useState<SessionInsights | null>(null);
const [error, setError] = useState<string | null>(null);
@@ -55,45 +85,49 @@ export function SessionInsights() {
}
return (
<div className="grid grid-cols-4 gap-4">
{/* Total Sessions Card */}
<Card className="w-full animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent className="flex flex-col justify-end h-full">
<div className="flex flex-col justify-end">
<p className="text-4xl font-mono font-light flex items-end" ref={totalSessionsRef}>
{insights?.totalSessions}
</p>
<CardDescription>Total sessions</CardDescription>
</div>
</CardContent>
</Card>
<>
<div className="grid grid-cols-4 gap-4 mb-4 mt-2">
<GreetingCard />
</div>
<div className="grid grid-cols-4 gap-4">
{/* Total Sessions Card */}
<Card className="w-full animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent className="flex flex-col justify-end h-full">
<div className="flex flex-col justify-end">
<p className="text-4xl font-mono font-light flex items-end" ref={totalSessionsRef}>
{insights?.totalSessions}
</p>
<CardDescription>Total sessions</CardDescription>
</div>
</CardContent>
</Card>
{/* Average Duration Card */}
<Card className="w-full animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent className="flex flex-col justify-end h-full">
<div className="flex flex-col justify-end">
<p className="text-4xl font-mono font-light flex items-end">
<span ref={avgDurationRef}>{insights?.avgSessionDuration?.toFixed(1)}m</span>
</p>
<CardDescription>Avg. duration</CardDescription>
</div>
</CardContent>
</Card>
{/* Average Duration Card */}
<Card className="w-full animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent className="flex flex-col justify-end h-full">
<div className="flex flex-col justify-end">
<p className="text-4xl font-mono font-light flex items-end">
<span ref={avgDurationRef}>{insights?.avgSessionDuration?.toFixed(1)}m</span>
</p>
<CardDescription>Avg. duration</CardDescription>
</div>
</CardContent>
</Card>
{/* Total Tokens Card */}
<Card className="w-full col-span-2 animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent className="flex flex-col justify-end h-full">
<div className="flex flex-col justify-end">
<p className="text-4xl font-mono font-light flex items-end" ref={totalTokensRef}>
{insights?.totalTokens ? `${(insights.totalTokens / 1000000).toFixed(2)}M` : ''}
</p>
<CardDescription>Total tokens (millions)</CardDescription>
</div>
</CardContent>
</Card>
{/* Total Tokens Card */}
<Card className="w-full col-span-2 animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent className="flex flex-col justify-end h-full">
<div className="flex flex-col justify-end">
<p className="text-4xl font-mono font-light flex items-end" ref={totalTokensRef}>
{insights?.totalTokens ? `${(insights.totalTokens / 1000000).toFixed(2)}M` : ''}
</p>
<CardDescription>Total tokens (millions)</CardDescription>
</div>
</CardContent>
</Card>
{/* Activity Heatmap Card */}
{/* <Card className="w-full col-span-4">
{/* Activity Heatmap Card */}
{/* <Card className="w-full col-span-4">
<CardContent>
<CardDescription className="mb-4">
<span className="text-lg text-text-default">Activity Heatmap</span>
@@ -102,25 +136,26 @@ export function SessionInsights() {
</CardContent>
</Card> */}
{/* Most Active Directories Card */}
<Card className="w-full col-span-4 animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent>
<CardDescription className="mb-4">
<span className="text-lg text-text-default">Active directories</span>
</CardDescription>
<div className="space-y-2 ">
{insights.mostActiveDirs.map(([dir, count], index) => (
<div key={index} className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-2">
<FolderOpen className="h-4 w-4 text-text-muted" />
<span className="truncate max-w-[200px]">{dir}</span>
{/* Most Active Directories Card */}
<Card className="w-full col-span-4 animate-in fade-in slide-in-from-bottom-8 duration-500">
<CardContent>
<CardDescription className="mb-4">
<span className="text-lg text-text-default">Active directories</span>
</CardDescription>
<div className="space-y-2 ">
{insights.mostActiveDirs.map(([dir, count], index) => (
<div key={index} className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-2">
<FolderOpen className="h-4 w-4 text-text-muted" />
<span className="truncate max-w-[400px] rtl">{dir}</span>
</div>
<span className="text-text-default font-mono font-light">{count} sessions</span>
</div>
<span className="text-text-default font-mono font-light">{count} sessions</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</>
);
}
+11 -12
View File
@@ -11,6 +11,7 @@ import {
Tray,
App,
globalShortcut,
shell,
} from 'electron';
import type { OpenDialogReturnValue } from 'electron';
import { Buffer } from 'node:buffer';
@@ -528,12 +529,18 @@ const createChat = async (
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// Open all links in external browser
if (url.startsWith('http:') || url.startsWith('https:')) {
electron.shell.openExternal(url);
shell.openExternal(url);
return { action: 'deny' };
}
return { action: 'allow' };
});
// Handle new-window events (alternative approach for external links)
mainWindow.webContents.on('new-window', (event, url) => {
event.preventDefault();
shell.openExternal(url);
});
// Load the index.html of the app.
let queryParams = '';
if (query) {
@@ -1756,18 +1763,10 @@ app.whenReady().then(async () => {
return;
}
// On macOS, use the 'open' command with Chrome
if (process.platform === 'darwin') {
spawn('open', ['-a', 'Google Chrome', url]);
} else if (process.platform === 'win32') {
// On Windows, start is built-in command of cmd.exe
spawn('cmd.exe', ['/c', 'start', '', 'chrome', url]);
} else {
// On Linux, use xdg-open with chrome
spawn('xdg-open', [url]);
}
// Use shell.openExternal to open URL in user's default browser
shell.openExternal(url);
} catch (error) {
console.error('Error opening URL in Chrome:', error);
console.error('Error opening URL in browser:', error);
}
});
+1 -1
View File
@@ -12,7 +12,7 @@
--color-black: #000000;
--color-neutral-100: #e8eaf2;
--color-neutral-200: #ccd2dd;
--color-neutral-200: #d4dae2;
--color-neutral-300: #a1a7b0;
--color-neutral-400: #6e747e;
--color-neutral-500: #32353b;