Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ target/
./ui/desktop/node_modules
./ui/desktop/out

# Desktop app binaries (built at build time, not checked in)
Goose-Desktop-App-Fixed/

# Generated Goose DLLs (built at build time, not checked in)
ui/desktop/src/bin/goose_ffi.dll
ui/desktop/src/bin/goose_llm.dll
Expand Down
77 changes: 77 additions & 0 deletions ui/desktop/src/components/sessions/DeleteSessionModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useState } from 'react';
import { Button } from '../ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import { Loader2, Trash2 } from 'lucide-react';

interface DeleteSessionModalProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => Promise<void>;
sessionName: string;
}

export function DeleteSessionModal({
isOpen,
onClose,
onConfirm,
sessionName,
}: DeleteSessionModalProps) {
const [isDeleting, setIsDeleting] = useState(false);

const handleConfirm = async () => {
setIsDeleting(true);
try {
await onConfirm();
onClose();
} catch (error) {
console.error('Error deleting session:', error);
} finally {
setIsDeleting(false);
}
};

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Trash2 className="h-5 w-5 text-destructive" />
Delete Session
</DialogTitle>
<DialogDescription>
Are you sure you want to delete the session "{sessionName}"? This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={isDeleting}>
Cancel
</Button>
<Button
variant="destructive"
onClick={handleConfirm}
disabled={isDeleting}
>
{isDeleting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Deleting...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Delete Session
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
73 changes: 69 additions & 4 deletions ui/desktop/src/components/sessions/SessionListView.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useRef, useCallback, useMemo, startTransition } from 'react';
import { MessageSquareText, Target, AlertCircle, Calendar, Folder } from 'lucide-react';
import { fetchSessions, type Session } from '../../sessions';
import { MessageSquareText, Target, AlertCircle, Calendar, Folder, Trash2 } from 'lucide-react';
import { fetchSessions, deleteSessionById, type Session } from '../../sessions';
import { Card } from '../ui/card';
import { Button } from '../ui/button';
import { ScrollArea } from '../ui/scroll-area';
Expand All @@ -11,6 +11,7 @@ import { SearchHighlighter } from '../../utils/searchHighlighter';
import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { groupSessionsByDate, type DateGroup } from '../../utils/dateUtils';
import { Skeleton } from '../ui/skeleton';
import { DeleteSessionModal } from './DeleteSessionModal';

// Debounce hook for search
function useDebounce<T>(value: T, delay: number): T {
Expand Down Expand Up @@ -52,6 +53,10 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
currentIndex: number;
} | null>(null);

// Delete modal state
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [sessionToDelete, setSessionToDelete] = useState<Session | null>(null);

// Search state for debouncing
const [searchTerm, setSearchTerm] = useState('');
const [caseSensitive, setCaseSensitive] = useState(false);
Expand Down Expand Up @@ -184,12 +189,49 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
}
};

// Handle delete session
const handleDeleteSession = (session: Session) => {
setSessionToDelete(session);
setDeleteModalOpen(true);
};

const handleConfirmDelete = async () => {
if (!sessionToDelete) return;

try {
await deleteSessionById(sessionToDelete.id);

// Remove the session from local state
setSessions((prev) => prev.filter((s) => s.id !== sessionToDelete.id));
setFilteredSessions((prev) => prev.filter((s) => s.id !== sessionToDelete.id));

setDeleteModalOpen(false);
setSessionToDelete(null);
} catch (error) {
console.error('Failed to delete session:', error);
// You could show a toast notification here
}
};

// Render a session item
const SessionItem = React.memo(function SessionItem({ session }: { session: Session }) {
const handleCardClick = (e: React.MouseEvent) => {
// Don't trigger session selection if clicking on delete button
if ((e.target as HTMLElement).closest('.delete-button')) {
return;
}
onSelectSession(session.id);
};

const handleDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation();
handleDeleteSession(session);
};

return (
<Card
onClick={() => onSelectSession(session.id)}
className="session-item h-full py-3 px-4 hover:shadow-default cursor-pointer transition-all duration-150 flex flex-col justify-between"
onClick={handleCardClick}
className="session-item h-full py-3 px-4 hover:shadow-default cursor-pointer transition-all duration-150 flex flex-col justify-between relative group"
>
<div className="flex-1">
<h3 className="text-base truncate mb-1">{session.metadata.description || session.id}</h3>
Expand All @@ -216,6 +258,16 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
</div>
)}
</div>

{/* Delete button */}
<Button
variant="ghost"
size="sm"
onClick={handleDeleteClick}
className="delete-button opacity-0 group-hover:opacity-100 transition-opacity duration-200 hover:bg-destructive hover:text-destructive-foreground"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</Card>
);
Expand Down Expand Up @@ -326,6 +378,19 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(({ onSelectSe
</div>
</div>

{/* Delete Session Modal */}
{sessionToDelete && (
<DeleteSessionModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
setSessionToDelete(null);
}}
onConfirm={handleConfirmDelete}
sessionName={sessionToDelete.metadata.description || sessionToDelete.id}
/>
)}

<div className="flex-1 min-h-0 relative px-8">
<ScrollArea className="h-full" data-search-scroll-area>
<div ref={containerRef} className="h-full relative">
Expand Down
77 changes: 77 additions & 0 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,83 @@ ipcMain.handle('get-dock-icon-state', () => {
}
});

// Window transparency handlers
ipcMain.handle('set-window-opacity', async (_event, opacity: number) => {
try {
// Validate opacity value (0.03 to 1.0, where 0.03 = 97% transparency)
const validOpacity = Math.max(0.03, Math.min(1.0, opacity));

console.log(`Setting window opacity to: ${validOpacity}`);

// Get all windows and apply opacity
const windows = BrowserWindow.getAllWindows();
for (const window of windows) {
window.setOpacity(validOpacity);
console.log(`Applied opacity ${validOpacity} to window: ${window.id}`);
}

// Save opacity setting
const settings = loadSettings();
settings.windowOpacity = validOpacity;
saveSettings(settings);

console.log(`Saved opacity setting: ${validOpacity}`);
return true;
} catch (error) {
console.error('Error setting window opacity:', error);
return false;
}
});

ipcMain.handle('get-window-opacity', async () => {
try {
const settings = loadSettings();
const opacity = settings.windowOpacity || 1.0;
console.log(`Retrieved window opacity: ${opacity}`);
return opacity;
} catch (error) {
console.error('Error getting window opacity:', error);
return 1.0;
}
});

// Session deletion handler
ipcMain.handle('delete-session-file', async (_event, sessionId: string) => {
try {
// Validate session ID for security
if (!sessionId || typeof sessionId !== 'string' || sessionId.length > 255) {
console.warn('Invalid session ID provided for deletion');
return false;
}

// Check for path traversal attempts
if (sessionId.includes('..') || sessionId.includes('/') || sessionId.includes('\\')) {
console.warn('Invalid characters in session ID');
return false;
}

// Use the same session directory logic as the Rust backend
// The Rust backend uses AppStrategyArgs with top_level_domain: "Block", author: "Block", app_name: "goose"
// This creates the path structure: Block/goose/data/sessions/
const appDataPath = path.dirname(app.getPath('userData')); // Get the parent directory
const sessionsDir = path.join(appDataPath, 'Block', 'goose', 'data', 'sessions');
const sessionFile = path.join(sessionsDir, `${sessionId}.jsonl`);

// Check if file exists before deleting
if (fsSync.existsSync(sessionFile)) {
fsSync.unlinkSync(sessionFile);
console.log(`[Main] Deleted session file: ${sessionFile}`);
return true;
} else {
console.warn(`[Main] Session file not found: ${sessionFile}`);
return false;
}
} catch (error) {
console.error('[Main] Error deleting session file:', error);
return false;
}
});

// Handle opening system notifications preferences
ipcMain.handle('open-notifications-settings', async () => {
try {
Expand Down
10 changes: 10 additions & 0 deletions ui/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ type ElectronAPI = {
closeWindow: () => void;
hasAcceptedRecipeBefore: (recipeConfig: Recipe) => Promise<boolean>;
recordRecipeHash: (recipeConfig: Recipe) => Promise<boolean>;
// Window transparency functions
setWindowOpacity: (opacity: number) => Promise<boolean>;
getWindowOpacity: () => Promise<number>;
// Session deletion function
deleteSessionFile: (sessionId: string) => Promise<boolean>;
};

type AppConfigAPI = {
Expand Down Expand Up @@ -240,6 +245,11 @@ const electronAPI: ElectronAPI = {
ipcRenderer.invoke('has-accepted-recipe-before', recipeConfig),
recordRecipeHash: (recipeConfig: Recipe) =>
ipcRenderer.invoke('record-recipe-hash', recipeConfig),
// Window transparency functions
setWindowOpacity: (opacity: number) => ipcRenderer.invoke('set-window-opacity', opacity),
getWindowOpacity: () => ipcRenderer.invoke('get-window-opacity'),
// Session deletion function
deleteSessionFile: (sessionId: string) => ipcRenderer.invoke('delete-session-file', sessionId),
};

const appConfigAPI: AppConfigAPI = {
Expand Down
19 changes: 19 additions & 0 deletions ui/desktop/src/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,22 @@ export async function fetchSessionDetails(sessionId: string): Promise<SessionDet
throw error;
}
}

/**
* Deletes a session by its ID
* @param sessionId The ID of the session to delete
* @returns Promise that resolves when the session is deleted
*/
export async function deleteSessionById(sessionId: string): Promise<void> {
try {
// Use the Electron API to delete the session file
const success = await window.electron.deleteSessionFile(sessionId);

if (!success) {
throw new Error('Failed to delete session file');
}
} catch (error) {
console.error(`Error deleting session ${sessionId}:`, error);
throw error;
}
}
1 change: 1 addition & 0 deletions ui/desktop/src/utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface Settings {
schedulingEngine: SchedulingEngine;
showQuitConfirmation: boolean;
enableWakelock: boolean;
windowOpacity?: number;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You say clean ...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry im new in PR.
Do i have to remove the comments?
Its not allowed to use AI?

...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Icarus-B4 - again I like your enthusiasm and you are encourage to use AI!

however (and maybe we should post some guidelines here) it is your responsibility to make sure the PR the AI produces is actually ready for review. so you can't have comments in there that the AI left there (like "you could show a toast here") or in this case left overs from the previous PR (windowOpacity is not related to the current change). it would also behoove you to check out @zanesq link to the other implementation of this feature.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clear guidance. Good point – I'll be more careful to clean up my PRs in the future and review existing implementations beforehand. I'll checked out @zanesq's link. its "Privat"
Any way i close the PR and let the professionals work.

}

// Constants
Expand Down
Loading