remove and cleanup unused code (#4074)

This commit is contained in:
Zane
2025-08-14 16:27:53 -07:00
committed by GitHub
parent c498fa018d
commit f609f27db0
72 changed files with 1 additions and 4792 deletions
-63
View File
@@ -44,7 +44,6 @@ import { COST_TRACKING_ENABLED } from './updates';
import { type SessionDetails } from './sessions';
import ExtensionsView, { ExtensionsViewOptions } from './components/extensions/ExtensionsView';
// import ProjectsContainer from './components/projects/ProjectsContainer';
import { Recipe } from './recipe';
import RecipesView from './components/RecipesView';
import RecipeEditor from './components/RecipeEditor';
@@ -67,7 +66,6 @@ export type View =
| 'recipeEditor'
| 'recipes'
| 'permission';
// | 'projects';
export type ViewOptions = {
// Settings view options
@@ -651,57 +649,6 @@ const ExtensionsRoute = () => {
);
};
// const ProjectsRoute = () => {
// const navigate = useNavigate();
//
// const setView = (view: View, viewOptions?: ViewOptions) => {
// // Convert view to route navigation
// switch (view) {
// case 'chat':
// navigate('/');
// break;
// case 'pair':
// navigate('/pair', { state: viewOptions });
// break;
// case 'settings':
// navigate('/settings', { state: viewOptions });
// break;
// case 'sessions':
// navigate('/sessions');
// break;
// case 'schedules':
// navigate('/schedules');
// break;
// case 'recipes':
// navigate('/recipes');
// break;
// case 'permission':
// navigate('/permission', { state: viewOptions });
// break;
// case 'ConfigureProviders':
// navigate('/configure-providers');
// break;
// case 'sharedSession':
// navigate('/shared-session', { state: viewOptions });
// break;
// case 'recipeEditor':
// navigate('/recipe-editor', { state: viewOptions });
// break;
// case 'welcome':
// navigate('/welcome');
// break;
// default:
// navigate('/');
// }
// };
//
// return (
// <React.Suspense fallback={<div>Loading projects...</div>}>
// <ProjectsContainer setView={setView} />
// </React.Suspense>
// );
// };
export default function App() {
const [fatalError, setFatalError] = useState<string | null>(null);
const [modalVisible, setModalVisible] = useState(false);
@@ -1539,16 +1486,6 @@ export default function App() {
</ProviderGuard>
}
/>
{/*<Route*/}
{/* path="projects"*/}
{/* element={*/}
{/* <ProviderGuard>*/}
{/* <ChatProvider chat={chat} setChat={setChat}>*/}
{/* <ProjectsRoute />*/}
{/* </ChatProvider>*/}
{/* </ProviderGuard> */}
{/* }*/}
{/*/>*/}
</Route>
</Routes>
</div>
-4
View File
@@ -669,10 +669,6 @@ export type SessionMetadata = {
* The number of output tokens used in the session. Retrieved from the provider's last usage.
*/
output_tokens?: number | null;
/**
* ID of the project this session belongs to, if any
*/
project_id?: string | null;
/**
* ID of the schedule that triggered this session, if any
*/
@@ -1,91 +0,0 @@
import React from 'react';
import { Card } from './ui/card';
import { Bird } from './ui/icons';
import { ChevronDown } from './icons';
interface ApiKeyWarningProps {
className?: string;
}
interface CollapsibleProps {
title: string;
children: React.ReactNode;
defaultOpen?: boolean;
}
function Collapsible({ title, children, defaultOpen = false }: CollapsibleProps) {
const [isOpen, setIsOpen] = React.useState(defaultOpen);
return (
<div className="border rounded-lg mb-2">
<button
className="w-full px-4 py-2 text-left flex justify-between items-center hover:bg-gray-50"
onClick={() => setIsOpen(!isOpen)}
>
<span className="font-medium">{title}</span>
<ChevronDown
className={`w-5 h-5 transition-transform ${isOpen ? 'transform rotate-180' : ''}`}
/>
</button>
{isOpen && <div className="px-4 py-2 border-t">{children}</div>}
</div>
);
}
const OPENAI_CONFIG = `export GOOSE_PROVIDER__TYPE=openai
export GOOSE_PROVIDER__HOST=https://api.openai.com
export GOOSE_PROVIDER__MODEL=gpt-4o
export GOOSE_PROVIDER__API_KEY=your_api_key_here`;
const ANTHROPIC_CONFIG = `export GOOSE_PROVIDER__TYPE=anthropic
export GOOSE_PROVIDER__HOST=https://api.anthropic.com
export GOOSE_PROVIDER__MODEL=claude-3-5-sonnet-latest
export GOOSE_PROVIDER__API_KEY=your_api_key_here`;
const DATABRICKS_CONFIG = `export GOOSE_PROVIDER__TYPE=databricks
export GOOSE_PROVIDER__HOST=your_databricks_host
export GOOSE_PROVIDER__MODEL=your_databricks_model`;
const OPENROUTER_CONFIG = `export GOOSE_PROVIDER__TYPE=openrouter
export GOOSE_PROVIDER__HOST=https://openrouter.ai
export GOOSE_PROVIDER__MODEL=anthropic/claude-3.5-sonnet
export GOOSE_PROVIDER__API_KEY=your_api_key_here`;
export function ApiKeyWarning({ className }: ApiKeyWarningProps) {
return (
<Card
className={`flex flex-col items-center p-8 space-y-6 bg-card-gradient w-full h-full ${className}`}
>
<div className="w-16 h-16">
<Bird />
</div>
<div className="text-center space-y-4 max-w-2xl w-full">
<h2 className="text-2xl font-semibold text-gray-800">Credentials Required</h2>
<p className="text-gray-600 mb-4">
To use Goose, you need to set environment variables for one of the following providers:
</p>
<div className="text-left">
<Collapsible title="OpenAI Configuration" defaultOpen={true}>
<pre className="bg-gray-50 p-4 rounded-md text-sm">{OPENAI_CONFIG}</pre>
</Collapsible>
<Collapsible title="Anthropic (Claude) Configuration">
<pre className="bg-gray-50 p-4 rounded-md text-sm">{ANTHROPIC_CONFIG}</pre>
</Collapsible>
<Collapsible title="Databricks Configuration">
<pre className="bg-gray-50 p-4 rounded-md text-sm">{DATABRICKS_CONFIG}</pre>
</Collapsible>
<Collapsible title="OpenRouter Configuration">
<pre className="bg-gray-50 p-4 rounded-md text-sm">{OPENROUTER_CONFIG}</pre>
</Collapsible>
</div>
<p className="text-gray-600 mt-4">
After setting these variables, restart Goose for the changes to take effect.
</p>
</div>
</Card>
);
}
@@ -1,16 +0,0 @@
export function LoadingPlaceholder() {
return (
<div className="space-y-2">
{[...Array(2)].map((_, index) => (
<div
key={index}
className="h-4 bg-gradient-to-r from-[#ffffff]/30 via-[#7A7EFB]/40 to-[#ffffff]/30
animate-shimmer-pulse bg-[length:200%_100%] rounded-full"
style={{
width: `${Math.floor(Math.random() * (100 - 70 + 1) + 70)}%`,
}}
/>
))}
</div>
);
}
-83
View File
@@ -1,83 +0,0 @@
import React, { useEffect, useRef } from 'react';
import { Card } from './ui/card';
interface ModalProps {
children: React.ReactNode;
footer?: React.ReactNode; // Optional footer
onClose: () => void; // Function to call when modal should close
preventBackdropClose?: boolean; // Optional prop to prevent closing on backdrop click
}
/**
* A reusable modal component that renders content with a semi-transparent backdrop and blur effect.
* Closes when clicking outside the modal or pressing Esc key.
*/
export default function Modal({
children,
footer,
onClose,
preventBackdropClose = false,
}: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
// Handle click outside the modal content
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (preventBackdropClose) return;
// Check if the click was on the backdrop and not on the modal content
// Also check if the click target is not part of a Select menu
if (
modalRef.current &&
!modalRef.current.contains(e.target as Node) &&
!(e.target as HTMLElement).closest('.select__menu') &&
window.getSelection()?.toString().length === 0 // Ensure no text is selected
) {
onClose();
}
};
// Handle Esc key press
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
// Don't close if a select menu is open
const selectMenu = document.querySelector('.select__menu');
if (!selectMenu) {
onClose();
}
}
};
// Add event listener for Escape key
document.addEventListener('keydown', handleEscKey);
// Add overflow-hidden to body to prevent scrolling background
document.body.style.overflow = 'hidden';
// Clean up
return () => {
document.removeEventListener('keydown', handleEscKey);
// Restore body scrolling when modal closes
document.body.style.overflow = '';
};
}, [onClose]);
return (
<div
className="fixed inset-0 bg-black/30 dark:bg-white/20 transition-colors animate-[fadein_200ms_ease-in_forwards] flex items-center justify-center p-4 z-[9999]"
onClick={handleBackdropClick}
style={{ isolation: 'isolate' }} /* Creates a new stacking context */
>
<Card
ref={modalRef}
className="relative w-[500px] max-w-full bg-background-default rounded-xl my-10 max-h-[90vh] flex flex-col shadow-xl z-[10000]"
>
<div className="p-8 max-h-[calc(90vh-180px)] overflow-y-auto">{children}</div>
{footer && (
<div className="border-t border-borderSubtle bg-background-default w-full rounded-b-xl overflow-hidden">
{footer}
</div>
)}
</Card>
</div>
);
}
-84
View File
@@ -1,84 +0,0 @@
import MarkdownContent from './MarkdownContent';
function truncateText(text: string, maxLength: number = 100): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
interface SplashPillProps {
content: string;
append: (text: string) => void;
className?: string;
}
function SplashPill({ content, append, className = '' }: SplashPillProps) {
const displayText = truncateText(content);
return (
<div
className={`px-4 py-2 text-sm text-center text-textStandard cursor-pointer border border-borderSubtle hover:bg-bgSubtle rounded-full transition-all duration-150 ${className}`}
onClick={async () => {
// Always use the full text (longForm or original content) when clicked
await append(content);
}}
title={content.length > 100 ? content : undefined} // Show full text on hover if truncated
>
<div className="whitespace-normal">{displayText}</div>
</div>
);
}
interface ContextBlockProps {
content: string;
}
function ContextBlock({ content }: ContextBlockProps) {
// Remove the "message:" prefix and trim whitespace
const displayText = content.replace(/^message:/i, '').trim();
return (
<div className="mb-6 p-4 bg-bgSubtle rounded-lg border border-borderStandard animate-[fadein_500ms_ease-in_forwards]">
<MarkdownContent content={displayText} />
</div>
);
}
interface SplashPillsProps {
append: (text: string) => void;
activities: string[] | null;
}
export default function SplashPills({ append, activities = null }: SplashPillsProps) {
// If custom activities are provided, use those instead of the default ones
const defaultPills = [
'What can you do?',
'Demo writing and reading files',
'Make a snake game in a new folder',
'List files in my current directory',
'Take a screenshot and summarize',
];
const pills = activities || defaultPills;
// Find any pill that starts with "message:"
const messagePillIndex = pills.findIndex((pill) => pill.toLowerCase().startsWith('message:'));
// Extract the message pill and the remaining pills
const messagePill = messagePillIndex >= 0 ? pills[messagePillIndex] : null;
const remainingPills =
messagePillIndex >= 0
? [...pills.slice(0, messagePillIndex), ...pills.slice(messagePillIndex + 1)]
: pills;
return (
<div className="flex flex-col">
{messagePill && <ContextBlock content={messagePill} />}
<div className="flex flex-wrap gap-2 animate-[fadein_500ms_ease-in_forwards]">
{remainingPills.map((content, index) => (
<SplashPill key={index} content={content} append={append} />
))}
</div>
</div>
);
}
@@ -1,17 +0,0 @@
import React from 'react';
interface CLIChatViewProps {
sessionId: string;
// onSessionExit: () => void;
}
export const CLIChatView: React.FC<CLIChatViewProps> = ({ sessionId }) => {
return (
<div className="flex flex-col h-full">
<div className="flex-1 p-4">
<p className="text-muted-foreground">CLI Chat View for session: {sessionId}</p>
<p className="text-sm text-muted-foreground mt-2">This component is under development.</p>
</div>
</div>
);
};
-128
View File
@@ -1,128 +0,0 @@
import React, { useState, useEffect } from 'react';
import { CLIChatView } from './CLIChatView';
import { useNavigate } from 'react-router-dom';
import { Button } from '../ui/button';
import { Badge } from '../ui/badge';
import { Terminal, Settings, History, MessageSquare, ArrowLeft, RefreshCw } from 'lucide-react';
import { generateSessionId } from '../../sessions';
import { type View, ViewOptions } from '../../App';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
interface ChatMessage {
role: string;
content: string;
id?: string;
timestamp?: number;
[key: string]: unknown;
}
interface ChatState {
id: string;
title: string;
messageHistoryIndex: number;
messages: ChatMessage[];
}
interface CLIHubProps {
chat: ChatState;
setChat: (chat: ChatState) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
}
export const CLIHub: React.FC<CLIHubProps> = ({ chat, setChat, setView }) => {
const navigate = useNavigate();
const [sessionId, setSessionId] = useState(chat.id || generateSessionId());
// Update chat when session changes
useEffect(() => {
setChat({
...chat,
id: sessionId,
title: `CLI Session - ${sessionId}`,
messageHistoryIndex: 0,
messages: [],
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, setChat]);
const handleNewSession = () => {
const newSessionId = generateSessionId();
setSessionId(newSessionId);
};
const handleRestartSession = () => {
// Force restart by changing session ID
const newSessionId = generateSessionId();
setSessionId(newSessionId);
};
return (
<MainPanelLayout>
{/* Header */}
<div className="h-12 flex items-center justify-between">
<div className="flex items-center pr-4">
<Button variant="ghost" size="sm" onClick={() => navigate('/')} className="mr-2">
<ArrowLeft className="w-4 h-4" />
</Button>
<Terminal className="w-5 h-5 mr-2" />
<div>
<h1 className="text-lg font-semibold">Goose CLI Experience</h1>
<p className="text-xs text-muted-foreground">
Exact CLI behavior with GUI enhancements
</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="text-xs">
CLI Mode
</Badge>
<Button variant="outline" size="sm" onClick={handleNewSession}>
<MessageSquare className="w-4 h-4 mr-2" />
New Session
</Button>
<Button variant="outline" size="sm" onClick={handleRestartSession}>
<RefreshCw className="w-4 h-4 mr-2" />
Restart
</Button>
<Button variant="outline" size="sm" onClick={() => setView('settings')}>
<Settings className="w-4 h-4 mr-2" />
Settings
</Button>
</div>
</div>
{/* CLI Chat View */}
<div className="flex flex-col min-w-0 flex-1 overflow-y-auto relative pl-6 pr-4 pb-16 pt-2">
<CLIChatView sessionId={sessionId} />
</div>
{/* Footer */}
<div className="relative z-10 p-4 border-t bg-muted/30">
<div className="flex items-center justify-between text-sm text-muted-foreground">
<div className="flex items-center space-x-4">
<span>Session: {sessionId}</span>
<span></span>
<span>CLI Mode Active</span>
</div>
<div className="flex items-center space-x-4">
<Button variant="ghost" size="sm" onClick={() => setView('sessions')}>
<History className="w-4 h-4 mr-2" />
Sessions
</Button>
<Button variant="ghost" size="sm" onClick={() => setView('settings')}>
<Settings className="w-4 h-4 mr-2" />
Settings
</Button>
</div>
</div>
</div>
</MainPanelLayout>
);
};
-9
View File
@@ -1,9 +0,0 @@
export function Bars() {
return (
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="32" fill="#101010" />
<rect x="16" y="23" width="32" height="8" rx="4" fill="white" />
<rect x="16" y="34" width="20" height="8" rx="4" fill="white" />
</svg>
);
}
@@ -1,146 +0,0 @@
import React, { useState } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import { Button } from '../ui/button';
import { Session } from '../../sessions';
import { Project } from '../../projects';
import { addSessionToProject } from '../../projects';
import { toastError, toastSuccess } from '../../toasts';
import { ScrollArea } from '../ui/scroll-area';
import { Checkbox } from '../ui/checkbox';
import { formatDistanceToNow } from 'date-fns';
interface AddSessionToProjectModalProps {
isOpen: boolean;
onClose: () => void;
project: Project;
availableSessions: Session[];
onSessionsAdded: () => void;
}
const AddSessionToProjectModal: React.FC<AddSessionToProjectModalProps> = ({
isOpen,
onClose,
project,
availableSessions,
onSessionsAdded,
}) => {
const [selectedSessions, setSelectedSessions] = useState<string[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleToggleSession = (sessionId: string) => {
setSelectedSessions((prev) => {
if (prev.includes(sessionId)) {
return prev.filter((id) => id !== sessionId);
} else {
return [...prev, sessionId];
}
});
};
const handleClose = () => {
setSelectedSessions([]);
onClose();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (selectedSessions.length === 0) {
toastError({ title: 'Error', msg: 'Please select at least one session' });
return;
}
setIsSubmitting(true);
try {
// Add each selected session to the project
const promises = selectedSessions.map((sessionId) =>
addSessionToProject(project.id, sessionId)
);
await Promise.all(promises);
toastSuccess({
title: 'Success',
msg: `Added ${selectedSessions.length} ${selectedSessions.length === 1 ? 'session' : 'sessions'} to project`,
});
onSessionsAdded();
handleClose();
} catch (err) {
console.error('Failed to add sessions to project:', err);
toastError({ title: 'Error', msg: 'Failed to add sessions to project' });
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Add Sessions to Project</DialogTitle>
<DialogDescription>Select sessions to add to "{project.name}"</DialogDescription>
</DialogHeader>
{availableSessions.length === 0 ? (
<div className="py-6 text-center">
<p className="text-muted-foreground">
No available sessions to add. All sessions are already part of this project.
</p>
</div>
) : (
<ScrollArea className="h-[300px] mt-4 pr-4">
<div className="space-y-2">
{availableSessions.map((session) => (
<div
key={session.id}
className="flex items-center space-x-3 py-2 px-3 rounded-md hover:bg-muted/50"
>
<Checkbox
id={`session-${session.id}`}
checked={selectedSessions.includes(session.id)}
onCheckedChange={() => handleToggleSession(session.id)}
/>
<div className="flex-1 overflow-hidden">
<label
htmlFor={`session-${session.id}`}
className="text-sm font-medium leading-none cursor-pointer flex justify-between w-full"
>
<span className="truncate">{session.metadata.description}</span>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{formatDistanceToNow(new Date(session.modified))} ago
</span>
</label>
<p className="text-xs text-muted-foreground truncate mt-1">
{session.metadata.working_dir}
</p>
</div>
</div>
))}
</div>
</ScrollArea>
)}
<DialogFooter className="mt-6">
<Button variant="outline" onClick={handleClose} type="button">
Cancel
</Button>
<Button type="submit" disabled={isSubmitting || selectedSessions.length === 0}>
{isSubmitting ? 'Adding...' : 'Add Sessions'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export default AddSessionToProjectModal;
@@ -1,151 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import { FolderSearch } from 'lucide-react';
import { toastError } from '../../toasts';
interface CreateProjectModalProps {
isOpen: boolean;
onClose: () => void;
onCreate: (name: string, description: string, defaultDirectory: string) => void;
defaultDirectory?: string;
}
const CreateProjectModal: React.FC<CreateProjectModalProps> = ({
isOpen,
onClose,
onCreate,
defaultDirectory: defaultDirectoryProp,
}) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [defaultDirectory, setDefaultDirectory] = useState(defaultDirectoryProp || '');
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (isOpen) {
setDefaultDirectory(defaultDirectoryProp || '');
}
}, [defaultDirectoryProp, isOpen]);
const resetForm = () => {
setName('');
setDescription('');
setDefaultDirectory(defaultDirectoryProp || '');
setIsSubmitting(false);
};
const handleClose = () => {
resetForm();
onClose();
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
toastError({ title: 'Error', msg: 'Project name is required' });
return;
}
setIsSubmitting(true);
// Pass data to parent component
onCreate(name, description, defaultDirectory);
// Form will be reset when the modal is closed by the parent
// after successful creation
};
const handlePickDirectory = async () => {
try {
// Use Electron's dialog to pick a directory
const directory = await window.electron.directoryChooser();
if (!directory.canceled && directory.filePaths.length > 0) {
setDefaultDirectory(directory.filePaths[0]);
}
} catch (err) {
console.error('Failed to pick directory:', err);
toastError({ title: 'Error', msg: 'Failed to pick directory' });
}
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[425px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Create new project</DialogTitle>
<DialogDescription>
Create a project to group related sessions together
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Name*</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="My Project"
autoFocus
required
/>
</div>
<div className="grid gap-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="resize-none"
placeholder="Optional description"
rows={2}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="directory">Directory</Label>
<div className="flex gap-2">
<Input
id="directory"
value={defaultDirectory}
onChange={(e) => setDefaultDirectory(e.target.value)}
className="flex-grow"
placeholder="Default working directory for sessions"
/>
<Button type="button" variant="outline" onClick={handlePickDirectory}>
<FolderSearch className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<DialogFooter className="pt-2">
<Button variant="outline" onClick={handleClose} type="button">
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Creating...' : 'Create'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export default CreateProjectModal;
@@ -1,47 +0,0 @@
import React from 'react';
import { ProjectMetadata } from '../../projects';
import { Card, CardHeader, CardTitle, CardContent } from '../ui/card';
import { Folder, Calendar } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
interface ProjectCardProps {
project: ProjectMetadata;
onClick: () => void;
onRefresh: () => void;
}
const ProjectCard: React.FC<ProjectCardProps> = ({ project, onClick }) => {
return (
<Card
className="transition-all duration-200 hover:shadow-default hover:cursor-pointer min-h-[140px] flex flex-col"
onClick={onClick}
>
<CardHeader className="pb-2">
<CardTitle className="flex items-center gap-2">
<Folder className="w-4 h-4 text-text-muted flex-shrink-0" />
{project.name}
</CardTitle>
</CardHeader>
<CardContent className="px-4 text-sm flex-grow flex flex-col justify-between">
{project.description && (
<div className="mb-2">
<span className="text-text-muted line-clamp-2">{project.description}</span>
</div>
)}
<div className="flex items-center gap-4 text-xs text-text-muted mt-auto">
<div className="flex items-center">
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
<span>{formatDistanceToNow(new Date(project.updatedAt))} ago</span>
</div>
<span>
{project.sessionCount} {project.sessionCount === 1 ? 'session' : 'sessions'}
</span>
</div>
</CardContent>
</Card>
);
};
export default ProjectCard;
@@ -1,498 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Project } from '../../projects';
import { Session, fetchSessions } from '../../sessions';
import {
getProject as fetchProject,
removeSessionFromProject,
deleteProject,
addSessionToProject,
} from '../../projects';
import { Button } from '../ui/button';
import {
ArrowLeft,
Loader,
RefreshCcw,
Edit,
Trash2,
Folder,
MessageSquareText,
ChevronLeft,
LoaderCircle,
AlertCircle,
Calendar,
Target,
} from 'lucide-react';
import { toastError, toastSuccess } from '../../toasts';
import { formatMessageTimestamp } from '../../utils/timeUtils';
import AddSessionToProjectModal from './AddSessionToProjectModal';
import UpdateProjectModal from './UpdateProjectModal';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { ScrollArea } from '../ui/scroll-area';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '../ui/alert-dialog';
import { ChatSmart } from '../icons';
import { View, ViewOptions } from '../../App';
import { Card } from '../ui/card';
interface ProjectDetailsViewProps {
projectId: string;
onBack: () => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
}
// Custom ProjectHeader component similar to SessionHistoryView style
const ProjectHeader: React.FC<{
onBack: () => void;
children: React.ReactNode;
title: string;
actionButtons?: React.ReactNode;
}> = ({ onBack, children, title, actionButtons }) => {
return (
<div className="flex flex-col pb-8">
<div className="flex items-center pt-13 pb-2">
<Button onClick={onBack} size="xs" variant="outline">
<ChevronLeft />
Back
</Button>
</div>
<h1 className="text-4xl font-light mb-4">{title}</h1>
<div className="flex items-center">{children}</div>
{actionButtons && <div className="flex items-center space-x-3 mt-4">{actionButtons}</div>}
</div>
);
};
// New component for displaying project sessions with consistent styling
const ProjectSessions: React.FC<{
sessions: Session[];
isLoading: boolean;
error: string | null;
onRetry: () => void;
onRemoveSession: (sessionId: string) => void;
onAddSession: () => void;
}> = ({ sessions, isLoading, error, onRetry }) => {
return (
<ScrollArea className="h-full w-full">
<div className="pb-16">
<div className="flex flex-col space-y-6">
{isLoading ? (
<div className="flex justify-center items-center py-12">
<LoaderCircle className="animate-spin h-8 w-8 text-textStandard" />
</div>
) : error ? (
<div className="flex flex-col items-center justify-center py-8 text-textSubtle">
<div className="text-red-500 mb-4">
<AlertCircle size={32} />
</div>
<p className="text-md mb-2">Error Loading Project Details</p>
<p className="text-sm text-center mb-4">{error}</p>
<Button onClick={onRetry} variant="default">
Try Again
</Button>
</div>
) : sessions?.length > 0 ? (
<div className="w-full">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-4">
{sessions.map((session) => (
<Card
key={session.id}
className="h-full py-3 px-4 hover:shadow-default cursor-pointer transition-all duration-150 flex flex-col justify-between"
>
<div className="flex-1">
<h3 className="text-base truncate mb-1">
{session.metadata.description || session.id}
</h3>
<div className="flex items-center text-text-muted text-xs mb-1">
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
<span>{formatMessageTimestamp(Date.parse(session.modified) / 1000)}</span>
</div>
<div className="flex items-center text-text-muted text-xs mb-1">
<Folder className="w-3 h-3 mr-1 flex-shrink-0" />
<span className="truncate">{session.metadata.working_dir}</span>
</div>
</div>
<div className="flex items-center justify-between mt-1 pt-2">
<div className="flex items-center space-x-3 text-xs text-text-muted">
<div className="flex items-center">
<MessageSquareText className="w-3 h-3 mr-1" />
<span className="font-mono">{session.metadata.message_count}</span>
</div>
{session.metadata.total_tokens !== null && (
<div className="flex items-center">
<Target className="w-3 h-3 mr-1" />
<span className="font-mono">
{session.metadata.total_tokens.toLocaleString()}
</span>
</div>
)}
</div>
{/* <Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
onRemoveSession(session.id);
}}
className="text-xs"
>
Remove
</Button> */}
</div>
</Card>
))}
</div>
</div>
) : (
<div className="flex flex-col justify-center text-textSubtle">
<p className="text-lg mb-2">No sessions in this project</p>
<p className="text-sm mb-4 text-text-muted">
Add sessions to this project to keep your work organized
</p>
{/* <Button onClick={onAddSession}>
<Plus className="h-4 w-4 mr-2" />
Add Session
</Button> */}
</div>
)}
</div>
</div>
</ScrollArea>
);
};
const ProjectDetailsView: React.FC<ProjectDetailsViewProps> = ({ projectId, onBack, setView }) => {
const [project, setProject] = useState<Project | null>(null);
const [sessions, setSessions] = useState<Session[]>([]);
const [allSessions, setAllSessions] = useState<Session[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isAddSessionModalOpen, setIsAddSessionModalOpen] = useState(false);
const [isUpdateModalOpen, setIsUpdateModalOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const loadProjectData = useCallback(async () => {
setLoading(true);
setError(null);
try {
// Fetch the project details
const projectData = await fetchProject(projectId);
setProject(projectData);
// Fetch all sessions
const allSessionsData = await fetchSessions();
setAllSessions(allSessionsData);
// Filter sessions that belong to this project
const projectSessions = allSessionsData.filter((session: Session) =>
projectData.sessionIds.includes(session.id)
);
setSessions(projectSessions);
} catch (err) {
console.error('Failed to load project data:', err);
setError('Failed to load project data');
toastError({ title: 'Error', msg: 'Failed to load project data' });
} finally {
setLoading(false);
}
}, [projectId]);
// Fetch project details and associated sessions
useEffect(() => {
loadProjectData();
}, [projectId, loadProjectData]);
// Set up session creation listener to automatically associate new sessions with this project
useEffect(() => {
if (!project) return;
const handleSessionCreated = async () => {
console.log(
'ProjectDetailsView: Session created event received, checking for new sessions...'
);
// Wait a bit for the session to be fully created
setTimeout(async () => {
try {
// Fetch all sessions to find the newest one
const allSessionsData = await fetchSessions();
// Find sessions that are not in this project but were created recently
const recentSessions = allSessionsData.filter((session: Session) => {
const sessionDate = new Date(session.modified);
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const isRecent = sessionDate > fiveMinutesAgo;
const isNotInProject = !project.sessionIds.includes(session.id);
const isInProjectDirectory = session.metadata.working_dir === project.defaultDirectory;
return isRecent && isNotInProject && isInProjectDirectory;
});
// Add recent sessions to this project
for (const session of recentSessions) {
try {
await addSessionToProject(project.id, session.id);
console.log(`Automatically added session ${session.id} to project ${project.id}`);
} catch (err) {
console.error(`Failed to add session ${session.id} to project:`, err);
}
}
// Refresh project data if we added any sessions
if (recentSessions.length > 0) {
loadProjectData();
}
} catch (err) {
console.error('Error checking for new sessions:', err);
}
}, 2000); // Wait 2 seconds for session to be created
};
// Listen for session creation events
window.addEventListener('session-created', handleSessionCreated);
window.addEventListener('message-stream-finished', handleSessionCreated);
return () => {
window.removeEventListener('session-created', handleSessionCreated);
window.removeEventListener('message-stream-finished', handleSessionCreated);
};
}, [project, loadProjectData]);
const handleRemoveSession = async (sessionId: string) => {
if (!project) return;
try {
await removeSessionFromProject(project.id, sessionId);
// Update local state
setProject((prev) => {
if (!prev) return null;
return {
...prev,
sessionIds: prev.sessionIds.filter((id) => id !== sessionId),
};
});
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
toastSuccess({ title: 'Success', msg: 'Session removed from project' });
} catch (err) {
console.error('Failed to remove session from project:', err);
toastError({ title: 'Error', msg: 'Failed to remove session from project' });
}
};
const getSessionsNotInProject = () => {
if (!project) return [];
return allSessions.filter((session) => !project.sessionIds.includes(session.id));
};
const handleDeleteProject = async () => {
if (!project) return;
setIsDeleting(true);
try {
await deleteProject(project.id);
toastSuccess({ title: 'Success', msg: `Project "${project.name}" deleted successfully` });
onBack(); // Go back to projects list
} catch (err) {
console.error('Failed to delete project:', err);
toastError({ title: 'Error', msg: 'Failed to delete project' });
} finally {
setIsDeleting(false);
setIsDeleteDialogOpen(false);
}
};
const handleNewSession = () => {
if (!project) return;
console.log(`Navigating to chat page for project: ${project.name}`);
// Update the working directory in localStorage to the project's directory
try {
const currentConfig = JSON.parse(localStorage.getItem('gooseConfig') || '{}');
const updatedConfig = {
...currentConfig,
GOOSE_WORKING_DIR: project.defaultDirectory,
};
localStorage.setItem('gooseConfig', JSON.stringify(updatedConfig));
} catch (error) {
console.error('Failed to update working directory in localStorage:', error);
}
// Navigate to the pair page
setView('pair');
toastSuccess({
title: 'New Session',
msg: `Starting new session in ${project.name}`,
});
};
if (loading) {
return (
<MainPanelLayout>
<div className="flex flex-col h-full w-full items-center justify-center">
<Loader className="h-10 w-10 animate-spin opacity-70 mb-4" />
<p className="text-muted-foreground">Loading project...</p>
</div>
</MainPanelLayout>
);
}
if (error || !project) {
return (
<MainPanelLayout>
<div className="flex flex-col h-full w-full items-center justify-center">
<div className="text-center">
<p className="text-red-500 mb-4">{error || 'Project not found'}</p>
<div className="flex gap-2">
<Button onClick={onBack} variant="outline">
<ArrowLeft className="mr-2 h-4 w-4" /> Back
</Button>
<Button onClick={loadProjectData}>
<RefreshCcw className="mr-2 h-4 w-4" /> Retry
</Button>
</div>
</div>
</div>
</MainPanelLayout>
);
}
// Define action buttons
const actionButtons = (
<>
<Button onClick={handleNewSession} size="sm" className="flex items-center gap-1">
<ChatSmart className="h-4 w-4" />
<span>New session</span>
</Button>
<Button
onClick={() => setIsUpdateModalOpen(true)}
size="sm"
variant="outline"
className="flex items-center gap-1"
>
<Edit className="h-4 w-4" />
<span>Edit</span>
</Button>
<Button
onClick={() => setIsDeleteDialogOpen(true)}
size="sm"
variant="outline"
className="flex items-center gap-1"
>
<Trash2 className="h-4 w-4" />
<span>Delete</span>
</Button>
{/* <Button
onClick={() => setIsAddSessionModalOpen(true)}
size="sm"
className="flex items-center gap-1"
>
<Plus className="h-4 w-4" />
<span>Add Session</span>
</Button> */}
</>
);
return (
<>
<MainPanelLayout>
<div className="flex-1 flex flex-col min-h-0 px-8">
<ProjectHeader onBack={onBack} title={project.name} actionButtons={actionButtons}>
<div className="flex flex-col">
{!loading && (
<>
<div className="flex items-center text-text-muted text-sm space-x-5 font-mono">
<span className="flex items-center">
<MessageSquareText className="w-4 h-4 mr-1" />
{sessions.length} {sessions.length === 1 ? 'session' : 'sessions'}
</span>
</div>
<div className="flex items-center text-text-muted text-sm mt-1 font-mono">
<span className="flex items-center">
<Folder className="w-4 h-4 mr-1" />
{project.defaultDirectory}
</span>
</div>
{project.description && (
<div className="flex items-center text-text-muted text-sm mt-1">
<span>{project.description}</span>
</div>
)}
</>
)}
</div>
</ProjectHeader>
<ProjectSessions
sessions={sessions}
isLoading={loading}
error={error}
onRetry={loadProjectData}
onRemoveSession={handleRemoveSession}
onAddSession={() => setIsAddSessionModalOpen(true)}
/>
</div>
</MainPanelLayout>
<AddSessionToProjectModal
isOpen={isAddSessionModalOpen}
onClose={() => setIsAddSessionModalOpen(false)}
project={project}
availableSessions={getSessionsNotInProject()}
onSessionsAdded={loadProjectData}
/>
<UpdateProjectModal
isOpen={isUpdateModalOpen}
onClose={() => setIsUpdateModalOpen(false)}
project={{
...project,
sessionCount: sessions.length,
}}
onRefresh={loadProjectData}
/>
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to delete this project?</AlertDialogTitle>
<AlertDialogDescription>
This will delete the project "{project.name}". The sessions within this project won't
be deleted, but they will no longer be part of this project.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-red-500 hover:bg-red-600"
onClick={(e) => {
e.preventDefault();
handleDeleteProject();
}}
disabled={isDeleting}
>
{isDeleting ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
};
export default ProjectDetailsView;
@@ -1,33 +0,0 @@
import React, { useState } from 'react';
import ProjectsView from './ProjectsView';
import ProjectDetailsView from './ProjectDetailsView';
import { View, ViewOptions } from '../../App';
interface ProjectsContainerProps {
setView: (view: View, viewOptions?: ViewOptions) => void;
}
const ProjectsContainer: React.FC<ProjectsContainerProps> = ({ setView }) => {
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const handleSelectProject = (projectId: string) => {
setSelectedProjectId(projectId);
};
const handleBack = () => {
setSelectedProjectId(null);
// Trigger a refresh of the projects list when returning from details
setRefreshTrigger((prev) => prev + 1);
};
if (selectedProjectId) {
return (
<ProjectDetailsView projectId={selectedProjectId} onBack={handleBack} setView={setView} />
);
}
return <ProjectsView onSelectProject={handleSelectProject} refreshTrigger={refreshTrigger} />;
};
export default ProjectsContainer;
@@ -1,214 +0,0 @@
import React, { useState, useEffect } from 'react';
import { ProjectMetadata } from '../../projects';
import { fetchProjects, createProject } from '../../projects';
import ProjectCard from './ProjectCard';
import CreateProjectModal from './CreateProjectModal';
import { Button } from '../ui/button';
import { FolderPlus, AlertCircle } from 'lucide-react';
import { toastError, toastSuccess } from '../../toasts';
import { ScrollArea } from '../ui/scroll-area';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { Skeleton } from '../ui/skeleton';
interface ProjectsViewProps {
onSelectProject: (projectId: string) => void;
refreshTrigger?: number;
}
const ProjectsView: React.FC<ProjectsViewProps> = ({ onSelectProject, refreshTrigger = 0 }) => {
const [projects, setProjects] = useState<ProjectMetadata[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [showSkeleton, setShowSkeleton] = useState(true);
const [showContent, setShowContent] = useState(false);
// Load projects on component mount and when refreshTrigger changes
useEffect(() => {
loadProjects();
}, [refreshTrigger]);
// Minimum loading time to prevent skeleton flash
useEffect(() => {
if (!loading && showSkeleton) {
const timer = setTimeout(() => {
setShowSkeleton(false);
// Add a small delay before showing content for fade-in effect
setTimeout(() => {
setShowContent(true);
}, 50);
}, 300); // Show skeleton for at least 300ms
// eslint-disable-next-line no-undef
return () => clearTimeout(timer);
}
return () => void 0;
}, [loading, showSkeleton]);
const loadProjects = async () => {
try {
setLoading(true);
setShowSkeleton(true);
setShowContent(false);
setError(null);
const projectsList = await fetchProjects();
setProjects(projectsList);
} catch (err) {
console.error('Failed to load projects:', err);
setError('Failed to load projects. Please try again.');
toastError({ title: 'Error', msg: 'Failed to load projects' });
} finally {
setLoading(false);
}
};
// Get the current working directory or fallback to home
const getDefaultDirectory = () => {
if (window.appConfig && typeof window.appConfig.get === 'function') {
const dir = window.appConfig.get('GOOSE_WORKING_DIR');
return typeof dir === 'string' ? dir : '';
}
return typeof process !== 'undefined' && process.env && typeof process.env.HOME === 'string'
? process.env.HOME
: '';
};
const handleCreateProject = async (
name: string,
description: string,
defaultDirectory?: string
) => {
try {
await createProject({
name,
description: description.trim() === '' ? undefined : description,
defaultDirectory: defaultDirectory || getDefaultDirectory(),
});
setIsCreateModalOpen(false);
toastSuccess({ title: 'Success', msg: `Project "${name}" created successfully` });
// Refresh the projects list to get the updated data from the server
await loadProjects();
} catch (err) {
console.error('Failed to create project:', err);
toastError({ title: 'Error', msg: 'Failed to create project' });
}
};
// Render skeleton loader for project items
const ProjectSkeleton = () => (
<div className="p-2 mb-2 bg-background-default border border-border-subtle rounded-lg">
<div className="flex justify-between items-start gap-4">
<div className="min-w-0 flex-1">
<Skeleton className="h-5 w-3/4 mb-2" />
<Skeleton className="h-4 w-full mb-2" />
<Skeleton className="h-4 w-24" />
</div>
<div className="flex items-center gap-2 shrink-0">
<Skeleton className="h-8 w-20" />
<Skeleton className="h-8 w-8" />
</div>
</div>
</div>
);
const renderContent = () => {
if (loading || showSkeleton) {
return (
<div className="space-y-6">
<div className="space-y-3">
<Skeleton className="h-6 w-24" />
<div className="space-y-2">
<ProjectSkeleton />
<ProjectSkeleton />
<ProjectSkeleton />
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-full text-text-muted">
<AlertCircle className="h-12 w-12 text-red-500 mb-4" />
<p className="text-lg mb-2">Error Loading Projects</p>
<p className="text-sm text-center mb-4">{error}</p>
<Button onClick={loadProjects} variant="default">
Try Again
</Button>
</div>
);
}
if (projects.length === 0) {
return (
<div className="flex flex-col justify-center h-full">
<p className="text-lg">No projects yet</p>
<p className="text-sm mb-4 text-text-muted">
Create your first project to organize related sessions together
</p>
</div>
);
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
onClick={() => onSelectProject(project.id)}
onRefresh={loadProjects}
/>
))}
</div>
);
};
return (
<MainPanelLayout>
<div className="flex-1 flex flex-col min-h-0">
<div className="bg-background-default px-8 pb-8 pt-16">
<div className="flex flex-col page-transition">
<div className="flex justify-between items-center mb-1">
<h1 className="text-4xl font-light">Projects</h1>
</div>
<p className="text-sm text-text-muted mb-4">
Create and manage your projects to organize related sessions together.
</p>
{/* Action Buttons */}
<Button onClick={() => setIsCreateModalOpen(true)} className="self-start">
<FolderPlus className="h-4 w-4 mr-2" />
New project
</Button>
</div>
</div>
<div className="flex-1 min-h-0 relative px-8">
<ScrollArea className="h-full">
<div
className={`h-full relative transition-all duration-300 ${
showContent ? 'opacity-100 animate-in fade-in' : 'opacity-0'
}`}
>
{renderContent()}
</div>
</ScrollArea>
</div>
</div>
<CreateProjectModal
isOpen={isCreateModalOpen}
onClose={() => setIsCreateModalOpen(false)}
onCreate={handleCreateProject}
defaultDirectory={getDefaultDirectory()}
/>
</MainPanelLayout>
);
};
export default ProjectsView;
@@ -1,181 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Label } from '../ui/label';
import { Textarea } from '../ui/textarea';
import { FolderSearch } from 'lucide-react';
import { toastError, toastSuccess } from '../../toasts';
import { ProjectMetadata, updateProject, UpdateProjectRequest } from '../../projects';
interface UpdateProjectModalProps {
isOpen: boolean;
onClose: () => void;
project: ProjectMetadata;
onRefresh: () => void;
}
const UpdateProjectModal: React.FC<UpdateProjectModalProps> = ({
isOpen,
onClose,
project,
onRefresh,
}) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [defaultDirectory, setDefaultDirectory] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
// Initialize form with project data
useEffect(() => {
if (isOpen && project) {
setName(project.name);
setDescription(project.description || '');
setDefaultDirectory(project.defaultDirectory);
}
}, [isOpen, project]);
const handleClose = () => {
onClose();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!name.trim()) {
toastError({ title: 'Error', msg: 'Project name is required' });
return;
}
if (!defaultDirectory.trim()) {
toastError({ title: 'Error', msg: 'Default directory is required' });
return;
}
setIsSubmitting(true);
try {
// Create update object, only include changed fields
const updateData: UpdateProjectRequest = {};
if (name !== project.name) {
updateData.name = name;
}
if (description !== (project.description || '')) {
updateData.description = description || null;
}
if (defaultDirectory !== project.defaultDirectory) {
updateData.defaultDirectory = defaultDirectory;
}
// Only make the API call if there are changes
if (Object.keys(updateData).length > 0) {
await updateProject(project.id, updateData);
toastSuccess({ title: 'Success', msg: 'Project updated successfully' });
onRefresh();
}
onClose();
} catch (err) {
console.error('Failed to update project:', err);
toastError({ title: 'Error', msg: 'Failed to update project' });
} finally {
setIsSubmitting(false);
}
};
const handlePickDirectory = async () => {
try {
// Use Electron's dialog to pick a directory
const directory = await window.electron.directoryChooser();
if (!directory.canceled && directory.filePaths.length > 0) {
setDefaultDirectory(directory.filePaths[0]);
}
} catch (err) {
console.error('Failed to pick directory:', err);
toastError({ title: 'Error', msg: 'Failed to pick directory' });
}
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[425px]">
<form onSubmit={handleSubmit}>
<DialogHeader>
<DialogTitle>Edit Project</DialogTitle>
<DialogDescription>Update project information</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-2">
<Label htmlFor="name" className="text-right">
Name*
</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className="col-span-3"
autoFocus
required
/>
</div>
<div className="grid grid-cols-4 items-start gap-2">
<Label htmlFor="description" className="text-right pt-2">
Description
</Label>
<Textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="col-span-3 resize-none"
placeholder="Optional description"
rows={2}
/>
</div>
<div className="grid grid-cols-4 items-center gap-2">
<Label htmlFor="directory" className="text-right">
Directory*
</Label>
<div className="col-span-3 flex gap-2">
<Input
id="directory"
value={defaultDirectory}
onChange={(e) => setDefaultDirectory(e.target.value)}
className="flex-grow"
required
/>
<Button type="button" variant="outline" onClick={handlePickDirectory}>
<FolderSearch className="h-4 w-4" />
</Button>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={handleClose} type="button">
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Saving...' : 'Save Changes'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
export default UpdateProjectModal;
@@ -1,10 +1,8 @@
import { useEffect, useState } from 'react';
import { Card, CardContent, CardDescription } from '../ui/card';
// import { Folder } from 'lucide-react';
import { getApiUrl } from '../../config';
import { Greeting } from '../common/Greeting';
import { fetchSessions, fetchSessionDetails, type Session } from '../../sessions';
// import { fetchProjects, type ProjectMetadata } from '../../projects';
import { useNavigate } from 'react-router-dom';
import { Button } from '../ui/button';
import { ChatSmart } from '../icons/';
@@ -24,7 +22,6 @@ export function SessionInsights() {
const [recentSessions, setRecentSessions] = useState<Session[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingSessions, setIsLoadingSessions] = useState(true);
// const [recentProjects, setRecentProjects] = useState<ProjectMetadata[]>([]);
const navigate = useNavigate();
useEffect(() => {
@@ -75,15 +72,6 @@ export function SessionInsights() {
}
};
// const loadRecentProjects = async () => {
// try {
// const projects = await fetchProjects();
// setRecentProjects(projects.slice(0, 3));
// } catch (error) {
// console.error('Failed to load recent projects:', error);
// }
// };
// Set a maximum loading time to prevent infinite skeleton
loadingTimeout = setTimeout(() => {
// Only apply fallback if we still don't have insights data
@@ -107,7 +95,6 @@ export function SessionInsights() {
loadInsights();
loadRecentSessions();
// loadRecentProjects();
// Cleanup timeout on unmount
return () => {
@@ -141,17 +128,6 @@ export function SessionInsights() {
navigate('/sessions');
};
// const navigateToProjects = () => {
// navigate('/projects');
// };
//
// const handleProjectClick = (projectId: string) => {
// navigate('/projects', {
// state: { selectedProjectId: projectId },
// replace: true,
// });
// };
// Format date to show only the date part (without time)
const formatDateOnly = (dateStr: string) => {
const date = new Date(dateStr);
@@ -335,60 +311,6 @@ export function SessionInsights() {
{/* Recent Chats Card */}
<div className="grid grid-cols-1 gap-0.5">
{/* Recent Projects Card */}
{/*<Card className="w-full py-6 px-4 border-none rounded-tl-none rounded-bl-none">*/}
{/* <CardContent className="animate-in fade-in duration-500 px-4">*/}
{/* <div className="flex justify-between items-center mb-2 px-2">*/}
{/* <CardDescription className="mb-0">*/}
{/* <span className="text-lg text-text-default">Recent projects</span>*/}
{/* </CardDescription>*/}
{/* <Button*/}
{/* variant="ghost"*/}
{/* size="sm"*/}
{/* className="text-xs text-text-muted flex items-center gap-1 !px-0 hover:bg-transparent hover:underline hover:text-text-default"*/}
{/* onClick={navigateToProjects}*/}
{/* >*/}
{/* See all*/}
{/* </Button>*/}
{/* </div>*/}
{/* <div className="space-y-1 min-h-[96px] transition-all duration-300 ease-in-out">*/}
{/* <AnimatePresence>*/}
{/* {recentProjects.length > 0 ? (*/}
{/* recentProjects.map((project, index) => (*/}
{/* <motion.div*/}
{/* key={project.id}*/}
{/* className="flex items-center justify-between text-sm py-1 px-2 rounded-md hover:bg-background-muted cursor-pointer transition-colors"*/}
{/* onClick={() => handleProjectClick(project.id)}*/}
{/* role="button"*/}
{/* tabIndex={0}*/}
{/* initial={{ opacity: 0, y: 5 }}*/}
{/* animate={{ opacity: 1, y: 0 }}*/}
{/* transition={{ duration: 0.3, delay: index * 0.1 }}*/}
{/* onKeyDown={(e) => {*/}
{/* if (e.key === 'Enter' || e.key === ' ') {*/}
{/* handleProjectClick(project.id);*/}
{/* }*/}
{/* }}*/}
{/* >*/}
{/* <div className="flex items-center space-x-2">*/}
{/* <Folder className="h-4 w-4 text-text-muted" />*/}
{/* <span className="truncate max-w-[200px]">{project.name}</span>*/}
{/* </div>*/}
{/* <span className="text-text-muted font-mono font-light">*/}
{/* {formatDateOnly(project.updatedAt)}*/}
{/* </span>*/}
{/* </motion.div>*/}
{/* ))*/}
{/* ) : (*/}
{/* <div className="text-text-muted text-sm py-2 px-2">*/}
{/* No recent projects found.*/}
{/* </div>*/}
{/* )}*/}
{/* </AnimatePresence>*/}
{/* </div>*/}
{/* </CardContent>*/}
{/*</Card>*/}
{/* Recent Chats Card */}
<Card className="w-full py-6 px-6 border-none rounded-2xl bg-background-default">
<CardContent className="page-transition p-0">
@@ -1,170 +0,0 @@
import React, { useEffect, useState, useRef } from 'react';
import Model from '../modelInterface';
import { useRecentModels } from './recentModels';
import { useModelAndProvider } from '../../../ModelAndProviderContext';
import { toastInfo } from '../../../../toasts';
interface ModelRadioListProps {
renderItem: (props: {
model: Model;
isSelected: boolean;
onSelect: () => void;
}) => React.ReactNode;
className?: string;
providedModelList?: Model[];
}
// renders a model list and handles changing models when user clicks on them
export function BaseModelsList({
renderItem,
className = '',
providedModelList,
}: ModelRadioListProps) {
const { recentModels } = useRecentModels();
// allow for a custom model list to be passed if you don't want to use recent models
let modelList: Model[];
if (!providedModelList) {
modelList = recentModels;
} else {
modelList = providedModelList;
}
const { changeModel, getCurrentModelAndProvider, currentModel, currentProvider } =
useModelAndProvider();
const [selectedModel, setSelectedModel] = useState<Model | null>(null);
const [isInitialized, setIsInitialized] = useState(false);
// Load current model/provider once on component mount
useEffect(() => {
let isMounted = true;
const initializeCurrentModel = async () => {
try {
const result = await getCurrentModelAndProvider();
if (isMounted) {
// try to look up the model in the modelList
let currentModel: Model;
const match = modelList.find(
(model) => model.name == result.model && model.provider == result.provider
);
// no matches so just create a model object (maybe user updated config.yaml from CLI usage, manual editing etc)
if (!match) {
currentModel = { name: String(result.model), provider: String(result.provider) };
} else {
currentModel = match;
}
setSelectedModel(currentModel);
setIsInitialized(true);
}
} catch (error) {
console.error('Failed to load current model:', error);
if (isMounted) {
setIsInitialized(true); // Still mark as initialized even on error
}
}
};
initializeCurrentModel().then();
return () => {
isMounted = false;
};
}, [getCurrentModelAndProvider, modelList]);
const handleModelSelection = async (model: Model) => {
await changeModel(model);
};
const handleRadioChange = async (model: Model) => {
// Check if the selected model is already active
if (
selectedModel &&
selectedModel.name === model.name &&
selectedModel.provider === model.provider
) {
toastInfo({
title: 'No change',
msg: `Model "${model.name}" is already active.`,
});
return;
}
// OPTIMISTIC UPDATE: Update the UI immediately
setSelectedModel(model);
try {
// Then perform the actual model change
await handleModelSelection(model);
} catch (error) {
console.error('Error selecting model:', error);
// If the operation fails, revert to the previous state by simply
// re-calling the getCurrentModelAndProvider function
try {
const result = await getCurrentModelAndProvider();
const currentModel =
modelList.find((m) => m.name === result.model && m.provider === result.provider) ||
({ name: String(result.model), provider: String(result.provider) } as Model);
setSelectedModel(currentModel);
} catch (secondError) {
console.error('Failed to restore previous model:', secondError);
}
// Show an error toast
toastInfo({
title: 'Error',
msg: `Failed to switch to model "${model.name}".`,
});
}
};
// Update selected model when context changes - but only if they actually changed
const prevModelRef = useRef<string | null>(null);
const prevProviderRef = useRef<string | null>(null);
useEffect(() => {
if (
currentModel &&
currentProvider &&
isInitialized &&
(currentModel !== prevModelRef.current || currentProvider !== prevProviderRef.current)
) {
prevModelRef.current = currentModel;
prevProviderRef.current = currentProvider;
const match = modelList.find(
(model) => model.name === currentModel && model.provider === currentProvider
);
if (match) {
setSelectedModel(match);
} else {
// Create a model object if not found in list
setSelectedModel({ name: currentModel, provider: currentProvider });
}
}
}, [currentModel, currentProvider, modelList, isInitialized]);
// Don't render until we've loaded the initial model/provider
if (!isInitialized) {
return <div>Loading models...</div>;
}
return (
<div className={className}>
{modelList.map((model) =>
renderItem({
model,
isSelected: !!(
selectedModel &&
selectedModel.name === model.name &&
selectedModel.provider === model.provider
),
onSelect: () => handleRadioChange(model),
})
)}
</div>
);
}
@@ -1,30 +0,0 @@
import { useEffect, useState } from 'react';
import Model from '../modelInterface';
const MAX_RECENT_MODELS = 3;
export function useRecentModels() {
const [recentModels, setRecentModels] = useState<Model[]>([]);
useEffect(() => {
const storedModels = localStorage.getItem('recentModels');
if (storedModels) {
setRecentModels(JSON.parse(storedModels));
}
}, []);
const addRecentModel = (model: Model) => {
const modelWithTimestamp = { ...model, lastUsed: new Date().toISOString() }; // Add lastUsed field
setRecentModels((prevModels) => {
const updatedModels = [
modelWithTimestamp,
...prevModels.filter((m) => m.name !== model.name),
].slice(0, MAX_RECENT_MODELS);
localStorage.setItem('recentModels', JSON.stringify(updatedModels));
return updatedModels;
});
};
return { recentModels, addRecentModel };
}
@@ -1,6 +0,0 @@
import ProviderState from '../interfaces/ProviderState';
export default interface ButtonCallbacks {
onConfigure?: (provider: ProviderState) => void;
onLaunch?: (provider: ProviderState) => void;
}
@@ -1,7 +0,0 @@
// contains basic, common actions like edit, add, delete etc
// logic for whether or not these buttons get shown is stored in the actions/ folder
// specific providers may want specific methods to handle these operations -- TODO
export default interface ConfigurationAction {
id: string;
renderButton: () => React.JSX.Element;
}
@@ -1,3 +0,0 @@
export default interface OllamaMetadata {
location: 'app' | 'host' | null;
}
@@ -1,28 +0,0 @@
import { ExternalLink } from 'lucide-react';
import { QUICKSTART_GUIDE_URL } from '../constants';
interface ProviderSetupHeaderProps {
title: string;
body: string;
}
/**
* Renders the header (title + description + link to guide) for the modal.
*/
export default function ProviderSetupHeader({ title, body }: ProviderSetupHeaderProps) {
return (
<div className="text-center">
<h2 className="text-xl font-medium text-textStandard mb-3">{title}</h2>
<div className="text-lg text-gray-400 font-light mb-4">{body}</div>
<a
href={QUICKSTART_GUIDE_URL}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center text-textProminent text-sm"
>
<ExternalLink size={16} className="mr-1" />
View quick start guide
</a>
</div>
);
}
@@ -1,18 +0,0 @@
import React from 'react';
import ConfigurationAction from '../interfaces/ConfigurationAction';
interface CardActionsProps {
actions: ConfigurationAction[];
}
export default function CardActions({ actions }: CardActionsProps) {
return (
<div className="space-x-2">
{actions.map((action) => {
// Store the rendered button in a variable first
const ButtonElement = action.renderButton();
return <React.Fragment key={action.id}>{ButtonElement}</React.Fragment>;
})}
</div>
);
}
@@ -1,3 +0,0 @@
export default function ViewRecipe() {
return <div>ViewRecipe</div>;
}
-33
View File
@@ -1,33 +0,0 @@
export default function Box({ size }: { size: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
style={{ height: size, width: size }}
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
>
<path
d="M1 4.35L7 1.25L13 4.35M1 4.35L7 7.25M1 4.35V10.75L7 13.75M13 4.35L7 7.25M13 4.35V10.75L7 13.75M7 7.25V13.75"
stroke="url(#paint0_linear_113_4015)"
strokeWidth="2"
strokeLinecap="round"
/>
<defs>
<linearGradient
id="paint0_linear_113_4015"
x1="-5"
y1="-7.25"
x2="27.1928"
y2="20.7684"
gradientUnits="userSpaceOnUse"
>
<stop offset="0.230048" stopColor="#2E7CF6" />
<stop offset="0.430048" stopColor="#F200FF" stopOpacity="0.25" />
<stop offset="0.615048" stopColor="#FAC145" stopOpacity="0.669964" />
</linearGradient>
</defs>
</svg>
);
}
@@ -1,243 +0,0 @@
import React, { useState, useEffect, useRef } from 'react';
import Copy from '../icons/Copy';
import { Card } from './card';
import { Recipe, generateDeepLink } from '../../recipe';
interface DeepLinkModalProps {
recipe: Recipe;
onClose: () => void;
}
export function DeepLinkModal({ recipe: initialRecipe, onClose }: DeepLinkModalProps) {
// Create editable state for the recipe
const [recipe, setRecipe] = useState(initialRecipe);
const [instructions, setInstructions] = useState(initialRecipe.instructions || '');
const [activities, setActivities] = useState<string[]>(initialRecipe.activities || []);
const [activityInput, setActivityInput] = useState('');
// State for the deep link
const [deepLink, setDeepLink] = useState('');
const [isGeneratingLink, setIsGeneratingLink] = useState(false);
// Generate the deep link using the current bot config
useEffect(() => {
let isCancelled = false;
const generateLink = async () => {
setIsGeneratingLink(true);
try {
const currentConfig = {
...recipe,
instructions,
activities,
title: recipe.title || 'Generated Recipe',
description: recipe.description || 'Recipe created from chat',
};
const link = await generateDeepLink(currentConfig);
if (!isCancelled) {
setDeepLink(link);
}
} catch (error) {
console.error('Failed to generate deeplink:', error);
if (!isCancelled) {
setDeepLink('Error generating deeplink');
}
} finally {
if (!isCancelled) {
setIsGeneratingLink(false);
}
}
};
generateLink();
return () => {
isCancelled = true;
};
}, [recipe, instructions, activities]);
// Handle Esc key press
useEffect(() => {
const handleEscKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
// Add event listener
document.addEventListener('keydown', handleEscKey);
// Clean up
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [onClose]);
// Update the recipe when instructions or activities change
useEffect(() => {
setRecipe({
...recipe,
instructions,
activities,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [instructions, activities]);
// Handle adding a new activity
const handleAddActivity = () => {
if (activityInput.trim()) {
setActivities([...activities, activityInput.trim()]);
setActivityInput('');
}
};
// Handle removing an activity
const handleRemoveActivity = (index: number) => {
const newActivities = [...activities];
newActivities.splice(index, 1);
setActivities(newActivities);
};
// Reference for the modal content
const modalRef = useRef<HTMLDivElement>(null);
// Handle click outside the modal
const handleBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
onClose();
}
};
return (
<div
className="fixed inset-0 bg-black/20 dark:bg-white/20 backdrop-blur-sm transition-colors flex items-center justify-center p-4 z-50"
onClick={handleBackdropClick}
>
<Card
ref={modalRef}
className="relative w-[700px] max-w-full bg-background-default rounded-xl my-10 max-h-[90vh] flex flex-col shadow-lg"
>
<div className="p-8 overflow-y-auto" style={{ maxHeight: 'calc(90vh - 32px)' }}>
<div className="flex flex-col">
<h2 className="text-2xl font-bold mb-4 text-textStandard">Agent Created!</h2>
<p className="mb-4 text-textStandard">
Your agent has been created successfully. You can share or open it below:
</p>
{/* Sharable Goose Bot Section - Moved to top */}
<div className="mb-6">
<label className="block font-medium mb-1 text-textStandard">
Sharable Goose Bot:
</label>
<div className="flex items-center">
<input
type="text"
value={isGeneratingLink ? 'Generating deeplink...' : deepLink}
readOnly
className="flex-1 p-3 border border-borderSubtle rounded-l-md bg-transparent text-textStandard"
/>
<button
onClick={() => {
if (!isGeneratingLink && deepLink && deepLink !== 'Error generating deeplink') {
navigator.clipboard.writeText(deepLink);
window.electron.logInfo('Deep link copied to clipboard');
}
}}
disabled={
isGeneratingLink || !deepLink || deepLink === 'Error generating deeplink'
}
className="p-2 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 flex items-center justify-center min-w-[100px] disabled:bg-gray-400 disabled:cursor-not-allowed"
>
<Copy className="w-5 h-5 mr-1" />
Copy
</button>
</div>
</div>
{/* Action Buttons - Moved to top */}
<div className="flex mb-6">
<button
onClick={() => {
// Open the deep link with the current recipe config
const currentConfig = {
...recipe,
instructions,
activities,
title: recipe.title || 'DeepLink Recipe',
description: recipe.description || 'Recipe from deep link',
};
window.electron.createChatWindow(
undefined,
undefined,
undefined,
undefined,
currentConfig
);
// Don't close the modal
}}
className="px-5 py-2.5 bg-green-500 text-white rounded-md hover:bg-green-600 flex-1 mr-2"
>
Open Agent
</button>
<button
onClick={onClose}
className="px-5 py-2.5 bg-gray-500 text-white rounded-md hover:bg-gray-600 flex-1"
>
Close
</button>
</div>
<h3 className="text-lg font-medium mb-3 text-textStandard">Edit Instructions:</h3>
<div className="mb-4">
<div className="border border-borderSubtle rounded-md bg-transparent max-h-[120px] overflow-y-auto">
<textarea
id="instructions"
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
className="w-full p-3 bg-transparent text-textStandard focus:outline-none"
placeholder="Instructions for the agent..."
/>
</div>
</div>
{/* Activities Section */}
<div className="mb-4">
<label className="block font-medium mb-1 text-textStandard">Activities:</label>
<div className="border border-borderSubtle rounded-md bg-transparent max-h-[120px] overflow-y-auto mb-2">
<ul className="divide-y divide-borderSubtle">
{activities.map((activity, index) => (
<li key={index} className="flex items-center">
<span className="flex-1 p-2 text-textStandard">{activity}</span>
<button
onClick={() => handleRemoveActivity(index)}
className="p-1 bg-red-500 text-white rounded-md hover:bg-red-600 m-1"
>
</button>
</li>
))}
</ul>
</div>
<div className="flex">
<input
type="text"
value={activityInput}
onChange={(e) => setActivityInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddActivity()}
className="flex-1 p-2 border border-borderSubtle rounded-l-md bg-transparent text-textStandard focus:border-borderStandard hover:border-borderStandard"
placeholder="Add new activity..."
/>
<button
onClick={handleAddActivity}
className="p-2 bg-green-500 text-white rounded-r-md hover:bg-green-600"
>
+
</button>
</div>
</div>
</div>
</div>
</Card>
</div>
);
}
-18
View File
@@ -1,18 +0,0 @@
export default function Send({ size }: { size: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
style={{ height: size, width: size }}
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
>
<path
d="M22 12.5L2 4.5L4 12.5L2 20.5L22 12.5ZM5.81 13.5H14.11L4.88 17.19L5.81 13.5ZM14.11 11.5H5.81L4.89 7.81L14.11 11.5Z"
fill="#7A7EFB"
className="dark:fill-[#4A56E2]"
/>
</svg>
);
}
-25
View File
@@ -1,25 +0,0 @@
export default function VertDots({ size }: { size: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
className="text-gray-600 dark:text-prev-goose-text-dark"
>
<path
d="M10.4976 4.5C10.4976 5.32843 9.82599 6 8.99756 6C8.16913 6 7.49756 5.32843 7.49756 4.5C7.49756 3.67157 8.16913 3 8.99756 3C9.82599 3 10.4976 3.67157 10.4976 4.5Z"
fill="currentColor"
/>
<path
d="M10.4976 9C10.4976 9.82843 9.82599 10.5 8.99756 10.5C8.16913 10.5 7.49756 9.82843 7.49756 9C7.49756 8.17157 8.16913 7.5 8.99756 7.5C9.82599 7.5 10.4976 8.17157 10.4976 9Z"
fill="currentColor"
/>
<path
d="M8.99756 15C9.82599 15 10.4976 14.3284 10.4976 13.5C10.4976 12.6716 9.82599 12 8.99756 12C8.16913 12 7.49756 12.6716 7.49756 13.5C7.49756 14.3284 8.16913 15 8.99756 15Z"
fill="currentColor"
/>
</svg>
);
}
-20
View File
@@ -1,20 +0,0 @@
export default function X({ size }: { size: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.97237 6.00001L3.82593 3.85356L4.53303 3.14645L6.67948 5.2929L8.82593 3.14645L9.53303 3.85356L7.38659 6.00001L9.53303 8.14645L8.82593 8.85356L6.67948 6.70711L4.53303 8.85356L3.82593 8.14645L5.97237 6.00001Z"
fill="black"
className="dark:fill-white"
fillOpacity="0.6"
/>
</svg>
);
}
@@ -1,135 +0,0 @@
'use client';
import * as React from 'react';
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
import { cn } from '../../utils';
import { buttonVariants } from './button';
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
}
function AlertDialogTrigger({
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
}
function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
}
function AlertDialogOverlay({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
return (
<AlertDialogPrimitive.Overlay
data-slot="alert-dialog-overlay"
className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
className
)}
{...props}
/>
);
}
function AlertDialogContent({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
return (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
data-slot="alert-dialog-content"
className={cn(
'bg-background-default data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 duration-200 sm:max-w-lg',
className
)}
{...props}
/>
</AlertDialogPortal>
);
}
function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-dialog-header"
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
{...props}
/>
);
}
function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="alert-dialog-footer"
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
{...props}
/>
);
}
function AlertDialogTitle({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
return (
<AlertDialogPrimitive.Title
data-slot="alert-dialog-title"
className={cn('text-lg font-medium', className)}
{...props}
/>
);
}
function AlertDialogDescription({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
return (
<AlertDialogPrimitive.Description
data-slot="alert-dialog-description"
className={cn('text-text-muted text-sm', className)}
{...props}
/>
);
}
function AlertDialogAction({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />;
}
function AlertDialogCancel({
className,
...props
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
return (
<AlertDialogPrimitive.Cancel
className={cn(buttonVariants({ variant: 'outline' }), className)}
{...props}
/>
);
}
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
};
-45
View File
@@ -1,45 +0,0 @@
import * as React from 'react';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { cn } from '../../utils';
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-muted dark:bg-muted-dark',
className
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };
-33
View File
@@ -1,33 +0,0 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils';
const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
default: 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
secondary:
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
destructive:
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
outline: 'text-foreground',
},
},
defaultVariants: {
variant: 'default',
},
}
);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
}
export { Badge, badgeVariants };
-29
View File
@@ -1,29 +0,0 @@
'use client';
import * as React from 'react';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { CheckIcon } from 'lucide-react';
import { cn } from '../../utils';
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
'peer border-border-input data-[state=checked]:bg-background-accent data-[state=checked]:text-text-on-accent data-[state=checked]:border-border-inverse focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[1px] disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };
-21
View File
@@ -1,21 +0,0 @@
'use client';
import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '../../utils';
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
'flex gap-2 text-sm leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
className
)}
{...props}
/>
);
}
export { Label };
-98
View File
@@ -1,98 +0,0 @@
import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Close } from '../icons';
import { cn } from '../../utils';
const Modal = DialogPrimitive.Root;
const ModalTrigger = DialogPrimitive.Trigger;
const ModalPortal = DialogPrimitive.Portal;
const ModalClose = DialogPrimitive.Close;
const ModalOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn('fixed inset-0 z-50 bg-black/50', className)}
{...props}
/>
));
ModalOverlay.displayName = 'ModalOverlay';
const ModalContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<ModalPortal>
<ModalOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-white dark:bg-gray-800 p-6 shadow-lg sm:rounded-lg',
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Close className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</ModalPortal>
));
ModalContent.displayName = 'ModalContent';
const ModalHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
);
ModalHeader.displayName = 'ModalHeader';
const ModalFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
{...props}
/>
);
ModalFooter.displayName = 'ModalFooter';
const ModalTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn('text-lg font-semibold leading-none tracking-tight', className)}
{...props}
/>
));
ModalTitle.displayName = 'ModalTitle';
const ModalDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn('text-sm text-muted-foreground', className)}
{...props}
/>
));
ModalDescription.displayName = 'ModalDescription';
export {
Modal,
ModalPortal,
ModalOverlay,
ModalTrigger,
ModalClose,
ModalContent,
ModalHeader,
ModalFooter,
ModalTitle,
ModalDescription,
};
-36
View File
@@ -1,36 +0,0 @@
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../../utils';
const Popover = PopoverPrimitive.Root;
const PopoverPortal = PopoverPrimitive.Portal;
const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
'z-50 w-72 rounded-md border bg-app text-popover-foreground outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
style={{
position: 'fixed',
transform: 'none',
top: 'auto',
left: 'auto',
}}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent, PopoverPortal };
@@ -1,84 +0,0 @@
import { StylesConfig, ThemeConfig } from 'react-select';
export const createDarkSelectStyles = (minWidth?: string): StylesConfig => ({
control: (base) => ({
...base,
...(minWidth ? { minWidth } : {}),
backgroundColor: '#1a1b1e',
borderColor: '#2a2b2e',
color: '#ffffff',
}),
menu: (base) => ({
...base,
backgroundColor: '#1a1b1e',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
border: '1px solid #2a2b2e',
background: '#1a1b1e',
}),
menuList: (base) => ({
...base,
backgroundColor: '#1a1b1e',
background: '#1a1b1e',
padding: '4px',
}),
option: (base, state) => ({
...base,
backgroundColor: state.isFocused ? '#2a2b2e' : '#1a1b1e',
color: '#ffffff',
cursor: 'pointer',
background: state.isFocused ? '#2a2b2e' : '#1a1b1e',
':hover': {
backgroundColor: '#2a2b2e',
color: '#ffffff',
background: '#2a2b2e',
},
padding: '8px',
margin: '2px 0',
borderRadius: '4px',
}),
singleValue: (base) => ({
...base,
color: '#ffffff',
}),
input: (base) => ({
...base,
color: '#ffffff',
}),
placeholder: (base) => ({
...base,
color: '#9ca3af',
}),
dropdownIndicator: (base) => ({
...base,
color: '#9ca3af',
':hover': {
color: '#ffffff',
},
}),
indicatorSeparator: (base) => ({
...base,
backgroundColor: '#2a2b2e',
}),
});
export const darkSelectTheme: ThemeConfig = (theme) => ({
...theme,
colors: {
...theme.colors,
primary: '#2a2b2e',
primary75: '#2a2b2e',
primary50: '#2a2b2e',
primary25: '#2a2b2e',
neutral0: '#1a1b1e',
neutral5: '#1a1b1e',
neutral10: '#2a2b2e',
neutral20: '#2a2b2e',
neutral30: '#3a3b3e',
neutral40: '#ffffff',
neutral50: '#ffffff',
neutral60: '#ffffff',
neutral70: '#ffffff',
neutral80: '#ffffff',
neutral90: '#ffffff',
},
});
-18
View File
@@ -1,18 +0,0 @@
import * as React from 'react';
import { cn } from '../../utils';
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
<textarea
data-slot="textarea"
className={cn(
'border-border-input placeholder:text-text-muted focus-visible:border-ring focus-visible:ring-ring/50 shadow-xs aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base transition-[color,box-shadow] outline-none focus-visible:ring-[1px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
className
)}
{...props}
/>
);
}
export { Textarea };
@@ -1,68 +0,0 @@
'use client';
import * as React from 'react';
import { cn } from '../../utils';
import * as TabsPrimitive from '@radix-ui/react-tabs';
const Tabs = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Root>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Root
orientation="vertical"
ref={ref}
className={cn('flex rounded-md text-text-muted gap-1', className)}
{...props}
/>
));
Tabs.displayName = TabsPrimitive.List.displayName;
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
'flex flex-col h-auto justify-start rounded-md bg-background-default p-1 text-muted-foreground',
className
)}
{...props}
/>
));
TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
'flex items-center justify-start whitespace-nowrap rounded-lg px-3 py-1.5 text-sm ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background-muted data-[state=active]:text-text-default data-[state=active]:shadow-sm hover:bg-background-muted hover:text-text-default',
className
)}
{...props}
/>
));
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
'ml-4 mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 animate-in fade-in slide-in-from-right-8 duration-300',
className
)}
{...props}
/>
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsContent, TabsList, TabsTrigger };
-2
View File
@@ -1,2 +0,0 @@
export const GOOSE_PROVIDER = 'GOOSE_PROVIDER';
export const GOOSE_MODEL = 'GOOSE_MODEL';
View File
-28
View File
@@ -1,28 +0,0 @@
// Handle window movement and docking detection
let isDragging = false;
let startX, startY;
document.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.screenX - window.screenX;
startY = e.screenY - window.screenY;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
// Check for docking with parent window
if (window.electronFloating) {
window.electronFloating.checkDocking();
}
}
});
// Handle click to focus parent window
document.addEventListener('click', (e) => {
if (!isDragging && window.electronFloating) {
window.electronFloating.focusParent();
}
});
console.log('Floating button script loaded');
-42
View File
@@ -1,42 +0,0 @@
import { useEffect, useState } from 'react';
export function useDarkMode(): boolean {
const [isDarkMode, setIsDarkMode] = useState<boolean>(() => {
const html = document.documentElement;
return (
html.classList.contains('dark') || window.matchMedia('(prefers-color-scheme: dark)').matches
);
});
useEffect(() => {
const html = document.documentElement;
const updateDarkMode = () => {
setIsDarkMode(html.classList.contains('dark'));
};
// Observe class attribute changes
const observer = new MutationObserver(() => updateDarkMode());
observer.observe(html, { attributes: true, attributeFilter: ['class'] });
// Also handle system preference changes (if no dark class is set manually)
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
// eslint-disable-next-line no-undef
const handleMediaChange = (event: MediaQueryListEvent) => {
if (!html.classList.contains('dark') && !html.classList.contains('light')) {
setIsDarkMode(event.matches);
}
};
mediaQuery.addEventListener('change', handleMediaChange);
// Initial check
updateDarkMode();
return () => {
observer.disconnect();
mediaQuery.removeEventListener('change', handleMediaChange);
};
}, []);
return isDarkMode;
}
-225
View File
@@ -1,225 +0,0 @@
import { Session } from './sessions';
import { client } from './api/client.gen';
/**
* Interface for a project with all details
*/
export interface Project {
id: string;
name: string;
description: string | null;
defaultDirectory: string;
sessionIds: string[];
createdAt: string;
updatedAt: string;
}
/**
* Simplified project metadata for listings
*/
export interface ProjectMetadata {
id: string;
name: string;
description: string | null;
defaultDirectory: string;
sessionCount: number;
createdAt: string;
updatedAt: string;
}
/**
* Project with associated session objects
*/
export interface ProjectWithSessions extends Project {
sessions: Session[];
}
/**
* Request to create a new project
*/
export interface CreateProjectRequest {
name: string;
description?: string;
defaultDirectory: string;
}
/**
* Request to update an existing project
*/
export interface UpdateProjectRequest {
name?: string;
description?: string | null;
defaultDirectory?: string;
}
/**
* Ensure default directory is properly set
*/
function ensureDefaultDirectory(project: Partial<Project>): Project {
return {
id: project.id || '',
name: project.name || '',
description: project.description || null,
defaultDirectory: project.defaultDirectory || process.env.HOME || '',
sessionIds: project.sessionIds || [],
createdAt: project.createdAt || new Date().toISOString(),
updatedAt: project.updatedAt || new Date().toISOString(),
};
}
/**
* Fetches all available projects from the API
* @returns Promise with an array of ProjectMetadata objects
*/
export async function fetchProjects(): Promise<ProjectMetadata[]> {
try {
const response = await client.get<{ projects: ProjectMetadata[] }>({
url: '/projects',
});
if (response && response.data && response.data.projects) {
return response.data.projects;
} else {
throw new Error('Unexpected response format from list_projects');
}
} catch (error) {
console.error('Error fetching projects:', error);
throw error;
}
}
/**
* Creates a new project
* @param request The project creation request data
* @returns Promise with the created project
*/
export async function createProject(request: CreateProjectRequest): Promise<Project> {
const response = await client.post<{ project: Project }>({
url: '/projects',
body: request,
headers: {
'Content-Type': 'application/json',
},
});
console.log('Raw createProject response:', response);
return ensureDefaultDirectory(
(response as { project?: Project }).project ?? (response as unknown as Project)
);
}
/**
* Gets details for a specific project
* @param projectId The ID of the project to fetch
* @returns Promise with project details
*/
export async function getProject(projectId: string): Promise<Project> {
try {
const response = await client.get<{ project: Project }>({
url: `/projects/${projectId}`,
});
if (!response?.data?.project) {
throw new Error(`Unexpected response format from get_project_details for ID: ${projectId}`);
}
return ensureDefaultDirectory(response.data.project);
} catch (error) {
console.error(`Error fetching project ${projectId}:`, error);
throw error;
}
}
/**
* Updates an existing project
* @param projectId The ID of the project to update
* @param request The project update request data
* @returns Promise with the updated project
*/
export async function updateProject(
projectId: string,
request: UpdateProjectRequest
): Promise<Project> {
try {
const response = await client.put<{ project: Project }>({
url: `/projects/${projectId}`,
body: request,
headers: {
'Content-Type': 'application/json',
},
});
if (!response?.data?.project) {
throw new Error(`Unexpected response format from update_project for ID: ${projectId}`);
}
return ensureDefaultDirectory(response.data.project);
} catch (error) {
console.error(`Error updating project ${projectId}:`, error);
throw error;
}
}
/**
* Deletes a project
* @param projectId The ID of the project to delete
*/
export async function deleteProject(projectId: string): Promise<void> {
try {
await client.delete({
url: `/projects/${projectId}`,
});
} catch (error) {
console.error(`Error deleting project ${projectId}:`, error);
throw error;
}
}
/**
* Adds a session to a project
* @param projectId The ID of the project
* @param sessionId The ID of the session to add
*/
export async function addSessionToProject(projectId: string, sessionId: string): Promise<void> {
try {
await client.post({
url: `/projects/${projectId}/sessions/${sessionId}`,
});
} catch (error) {
console.error(`Error adding session ${sessionId} to project ${projectId}:`, error);
throw error;
}
}
/**
* Removes a session from a project
* @param projectId The ID of the project
* @param sessionId The ID of the session to remove
*/
export async function removeSessionFromProject(
projectId: string,
sessionId: string
): Promise<void> {
try {
await client.delete({
url: `/projects/${projectId}/sessions/${sessionId}`,
});
} catch (error) {
console.error(`Error removing session ${sessionId} from project ${projectId}:`, error);
throw error;
}
}
/**
* Generate a project ID in the format proj_yyyymmdd_hhmmss
*/
export function generateProjectId(): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `proj_${year}${month}${day}_${hours}${minutes}${seconds}`;
}
-71
View File
@@ -1,71 +0,0 @@
import { app } from 'electron';
import * as path from 'path';
import { spawn as spawnProcess } from 'child_process';
import * as fs from 'fs';
export function handleSquirrelEvent(): boolean {
if (process.argv.length === 1) {
return false;
}
const appFolder = path.resolve(process.execPath, '..');
const rootAtomFolder = path.resolve(appFolder, '..');
const updateDotExe = path.resolve(path.join(rootAtomFolder, 'Update.exe'));
const exeName = path.basename(process.execPath);
const spawnUpdate = function (args: string[]) {
try {
return spawnProcess(updateDotExe, args, { detached: true });
} catch (error) {
console.error('Failed to spawn update process:', error);
return null;
}
};
const squirrelEvent = process.argv[1];
switch (squirrelEvent) {
case '--squirrel-install':
case '--squirrel-updated': {
// Register protocol handler
spawnUpdate(['--createShortcut', exeName]);
// Register protocol
const regCommand = `Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\\goose]
@="URL:Goose Protocol"
"URL Protocol"=""
[HKEY_CLASSES_ROOT\\goose\\DefaultIcon]
@="\\"${process.execPath.replace(/\\/g, '\\\\')},1\\""
[HKEY_CLASSES_ROOT\\goose\\shell]
[HKEY_CLASSES_ROOT\\goose\\shell\\open]
[HKEY_CLASSES_ROOT\\goose\\shell\\open\\command]
@="\\"${process.execPath.replace(/\\/g, '\\\\')}\\" \\"%1\\""`;
fs.writeFileSync('goose-protocol.reg', regCommand);
spawnProcess('regedit.exe', ['/s', 'goose-protocol.reg']);
setTimeout(() => app.quit(), 1000);
return true;
}
case '--squirrel-uninstall': {
// Remove protocol handler
spawnUpdate(['--removeShortcut', exeName]);
setTimeout(() => app.quit(), 1000);
return true;
}
case '--squirrel-obsolete': {
app.quit();
return true;
}
default:
return false;
}
}
-14
View File
@@ -1,14 +0,0 @@
declare namespace ElectronTypes {
interface Event {
preventDefault: () => void;
sender: unknown;
}
interface IpcRendererEvent extends Event {
senderId: number;
}
interface MouseUpEvent extends Event {
button: number;
}
}
-1
View File
@@ -1 +0,0 @@
export * from './electron';
-73
View File
@@ -1,73 +0,0 @@
import { Session } from '../sessions';
/**
* Represents a project in the system
*/
export interface Project {
/** Unique identifier for the project */
id: string;
/** Display name of the project */
name: string;
/** Optional description of the project */
description?: string;
/** Default working directory for sessions in this project */
defaultDirectory: string;
/** When the project was created */
createdAt: string;
/** When the project was last updated */
updatedAt: string;
/** List of session IDs associated with this project */
sessionIds: string[];
}
/**
* Simplified project metadata for listings
*/
export interface ProjectMetadata {
/** Unique identifier for the project */
id: string;
/** Display name of the project */
name: string;
/** Optional description of the project */
description?: string;
/** Default working directory for sessions in this project */
defaultDirectory: string;
/** Number of sessions in this project */
sessionCount: number;
/** When the project was created */
createdAt: string;
/** When the project was last updated */
updatedAt: string;
}
/**
* Project with associated sessions
*/
export interface ProjectWithSessions extends Project {
/** Associated sessions */
sessions: Session[];
}
/**
* Request to create a new project
*/
export interface CreateProjectRequest {
/** Display name of the project */
name: string;
/** Optional description of the project */
description?: string;
/** Default working directory for sessions in this project */
defaultDirectory: string;
}
/**
* Request to update an existing project
*/
export interface UpdateProjectRequest {
/** Display name of the project */
name?: string;
/** Optional description of the project */
description?: string | null;
/** Default working directory for sessions in this project */
defaultDirectory?: string;
}
View File