From 52212a2d6c47202b697b0105407588e26cac6c0c Mon Sep 17 00:00:00 2001 From: Max Novich Date: Tue, 3 Dec 2024 16:24:28 -0800 Subject: [PATCH 01/10] Session management WIP --- ui/desktop/package-lock.json | 16 +++++- ui/desktop/package.json | 3 +- ui/desktop/src/ChatWindow.tsx | 66 +++++++++++++++++++----- ui/desktop/src/LauncherWindow.tsx | 6 ++- ui/desktop/src/main.ts | 70 ++++++++++++++++++++++++-- ui/desktop/src/preload.js | 2 + ui/desktop/src/types/electron.d.ts | 4 +- ui/desktop/src/utils/sessionManager.ts | 68 +++++++++++++++++++++++++ 8 files changed, 215 insertions(+), 20 deletions(-) create mode 100644 ui/desktop/src/utils/sessionManager.ts diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index b3e4b6ee3..bf065167d 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -36,7 +36,8 @@ "react-router-dom": "^6.28.0", "react-syntax-highlighter": "^15.6.1", "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.3" }, "devDependencies": { "@electron-forge/cli": "^7.5.0", @@ -11961,6 +11962,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/ui/desktop/package.json b/ui/desktop/package.json index da0999b83..63e19e05b 100644 --- a/ui/desktop/package.json +++ b/ui/desktop/package.json @@ -64,6 +64,7 @@ "react-router-dom": "^6.28.0", "react-syntax-highlighter": "^15.6.1", "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.3" } } diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index 82b838ff7..26567ab6f 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -1,16 +1,18 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { Message, useChat } from './ai-sdk-fork/useChat'; -import { Route, Routes, Navigate } from 'react-router-dom'; -import { getApiUrl } from './config'; -import { Card } from './components/ui/card'; -import { ScrollArea } from './components/ui/scroll-area'; +import React, {useEffect, useRef, useState} from 'react'; +import {Message,useChat} from './ai-sdk-fork/useChat'; +import {Navigate, Route, Routes} from 'react-router-dom'; +import {getApiUrl} from './config'; +import {Card} from './components/ui/card'; +import {ScrollArea} from './components/ui/scroll-area'; import Splash from './components/Splash'; import GooseMessage from './components/GooseMessage'; import UserMessage from './components/UserMessage'; import Input from './components/Input'; import MoreMenu from './components/MoreMenu'; +import {Bird} from './components/ui/icons'; import LoadingGoose from './components/LoadingGoose'; -import { ApiKeyWarning } from './components/ApiKeyWarning'; +import {ApiKeyWarning} from './components/ApiKeyWarning'; + import { askAi, getPromptTemplates } from './utils/askAI'; import WingToWing, { Working } from './components/WingToWing'; @@ -89,8 +91,23 @@ function ChatContent({ c.id === selectedChatId ? { ...c, messages } : c ); setChats(updatedChats); + const currentChat = chats.find(chat => chat.id === selectedChatId); + if (currentChat) { + const sessionToSave = { + messages: currentChat.messages, + directory: window.appConfig.get("GOOSE_WORKING_DIR") + }; + saveSession(sessionToSave); + } }, [messages, selectedChatId]); + // Function to save a session + const saveSession = (session) => { + if(session.messages === undefined || session.messages.length === 0) return + window.electron.saveSession(session); + }; + + const initialQueryAppended = useRef(false); useEffect(() => { if (initialQuery && !initialQueryAppended.current) { @@ -149,11 +166,11 @@ function ChatContent({ } }), }; - + const updatedMessages = [...messages.slice(0, -1), newLastMessage]; setMessages(updatedMessages); } - + }; return ( @@ -225,6 +242,26 @@ export default function ChatWindow() { window.electron.createChatWindow(); }; + // Function to get a session by ID + const getSession = (sessionId) => { + try { + const session = window.electron.getSession(sessionId); + console.log('Session loaded:', session); + return session + } catch (error) { + console.error('Failed to load session:', error); + } + }; + + const convertSessionToChat = (session) => { + const chat = { + id: 1, + title: session.name, + messages: session.messages, + }; + return chat; + } + // Add keyboard shortcut handler useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -259,6 +296,11 @@ export default function ChatWindow() { title: initialQuery || 'Chat 1', messages: initialHistory.length > 0 ? initialHistory : [], }; + const sessionId = window.appConfig.get("GOOSE_SESSION_ID"); + const session = getSession(sessionId); + if (session) { + return[convertSessionToChat(session)]; + } return [firstChat]; }); @@ -308,11 +350,11 @@ export default function ChatWindow() { } /> - + - + )} ); -} \ No newline at end of file +} diff --git a/ui/desktop/src/LauncherWindow.tsx b/ui/desktop/src/LauncherWindow.tsx index ce1955ced..fb7a48d95 100644 --- a/ui/desktop/src/LauncherWindow.tsx +++ b/ui/desktop/src/LauncherWindow.tsx @@ -3,6 +3,10 @@ import React, { useState, useRef } from 'react'; declare global { interface Window { electron: { + getConfig(): object; + getSession(arg0: string): object; + logInfo(arg0: string): object; + saveSession(arg0: { name: string; messages: Array; directory: string }): object; hideWindow: () => void; createChatWindow: (query: string) => void; }; @@ -41,4 +45,4 @@ export default function SpotlightWindow() { ); -} \ No newline at end of file +} diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 198daab86..438104548 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -7,6 +7,7 @@ import started from "electron-squirrel-startup"; import log from './utils/logger'; import { exec } from 'child_process'; import { addRecentDir, loadRecentDirs } from './utils/recentDirs'; +import { loadSessions, saveSession, clearAllSessions } from './utils/sessionManager'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) app.quit(); @@ -87,7 +88,7 @@ const createLauncher = () => { let windowCounter = 0; const windowMap = new Map(); -const createChat = async (app, query?: string, dir?: string) => { +const createChat = async (app, query?: string, dir?: string, sessionId?: string) => { const [port, working_dir] = await startGoosed(app, dir); const mainWindow = new BrowserWindow({ @@ -102,7 +103,7 @@ const createChat = async (app, query?: string, dir?: string) => { icon: path.join(__dirname, '../images/icon'), webPreferences: { preload: path.join(__dirname, 'preload.js'), - additionalArguments: [JSON.stringify({ ...appConfig, GOOSE_SERVER__PORT: port, GOOSE_WORKING_DIR: working_dir })], + additionalArguments: [JSON.stringify({ ...appConfig, GOOSE_SERVER__PORT: port, GOOSE_WORKING_DIR: working_dir, GOOSE_SESSION_ID: sessionId })], }, }); @@ -211,6 +212,17 @@ const buildRecentFilesMenu = () => { })); }; +// Add Recent Sessions submenu +const buildRecentSessionsMenu = () => { + const sessions = loadSessions(); + return sessions.map(session => ({ + label: session.name, + click: () => { + createChat(app, undefined, session.directory, session.name); + } + })); +}; + const openDirectoryDialog = async () => { const result = await dialog.showOpenDialog({ properties: ['openDirectory'] @@ -244,6 +256,23 @@ app.whenReady().then(async () => { }, })); + const recentSessionsSubmenu = buildRecentSessionsMenu(); + if (recentSessionsSubmenu.length > 0) { + fileMenu.submenu.append(new MenuItem({ type: 'separator' })); + fileMenu.submenu.append(new MenuItem({ + label: 'Recent Sessions', + submenu: recentSessionsSubmenu + })); + } + + // Add option to clear session history + fileMenu.submenu.append(new MenuItem({ + label: 'Clear Session History', + click() { + clearAllSessions(); + }, + })); + // Add Recent Files submenu const recentFilesSubmenu = buildRecentFilesMenu(); if (recentFilesSubmenu.length > 0) { @@ -274,8 +303,41 @@ app.whenReady().then(async () => { } }); + ipcMain.on('save-session', (_, session) => { + try { + return saveSession(session); + } catch (error) { + console.error('Failed to save session:', error); + throw error; + } +}); + + ipcMain.on('get-session', (_, sessionId) => { + try { + return loadSessions().find(session => session.name === sessionId); + } catch (error) { + console.error('Failed to load sessions:', error); + throw error; + } + }); + ipcMain.on('create-chat-window', (_, query) => { - createChat(app, query); + createChat(app, query); + }); + + ipcMain.on('clear-session-history', () => { + // Clear all stored session data + try { + // We'll simulate clearing session data - implement this using your session storage logic + clearAllSessions(); + console.log('All session history cleared'); + // Optionally notify all open chat windows to update/reset + windowMap.forEach(win => { + win.webContents.send('session-history-cleared'); + }); + } catch (error) { + console.error('Failed to clear session history:', error); + } }); ipcMain.on('directory-chooser', (_) => { @@ -330,4 +392,4 @@ app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } -}); \ No newline at end of file +}); diff --git a/ui/desktop/src/preload.js b/ui/desktop/src/preload.js index ccd02f0b4..1d71dbde3 100644 --- a/ui/desktop/src/preload.js +++ b/ui/desktop/src/preload.js @@ -16,6 +16,8 @@ contextBridge.exposeInMainWorld('electron', { logInfo: (txt) => ipcRenderer.send('logInfo', txt), showNotification: (data) => ipcRenderer.send('notify', data), createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query), + saveSession: (session) => ipcRenderer.send('save-session', session), + getSession: (sessionId) => ipcRenderer.send('get-session', sessionId), openInChrome: (url) => ipcRenderer.send('open-in-chrome', url), fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url), }) diff --git a/ui/desktop/src/types/electron.d.ts b/ui/desktop/src/types/electron.d.ts index 8c0b0bb8c..d3bcec4d0 100644 --- a/ui/desktop/src/types/electron.d.ts +++ b/ui/desktop/src/types/electron.d.ts @@ -6,10 +6,12 @@ interface IElectronAPI { GOOSE_API_HOST: string; apiCredsMissing: boolean; }; + getSession: (sessionId: string) => object; + saveSession: (session: { name: string; messages: Array; directory: string }) => string; } declare global { interface Window { electron: IElectronAPI; } -} \ No newline at end of file +} diff --git a/ui/desktop/src/utils/sessionManager.ts b/ui/desktop/src/utils/sessionManager.ts new file mode 100644 index 000000000..03687e8eb --- /dev/null +++ b/ui/desktop/src/utils/sessionManager.ts @@ -0,0 +1,68 @@ +import fs from 'fs'; +import path from 'path'; +import { app } from 'electron'; + +const SESSIONS_PATH = path.join(app.getPath('userData'), 'sessions'); +if (!fs.existsSync(SESSIONS_PATH)) { + fs.mkdirSync(SESSIONS_PATH); +} + +interface Session { + name: string; // Derived from a synopsis of the conversation + messages: Array<{ + id: number; + role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'; + content: string; + }>; + directory: string; +} + +function generateSessionName(messages: object[]): string { + // Create a session name based on the first message or a combination of initial messages + if (messages === undefined || messages.length === 0) return 'empty_session'; + return messages[0].content.split(' ').slice(0, 5).join(' '); +} + +export function saveSession(session: Session): string { + try { + const sessionData = { + ...session, + name: generateSessionName(session.messages) + }; + const filePath = path.join(SESSIONS_PATH, `${sessionData.name}.json`); + fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2)); + console.log('Session saved:', sessionData); + return sessionData.name; + } catch (error) { + console.error('Error saving session:', error); + } +} + +export function loadSessions(): Session[] { + try { + console.log('Attempting to load sessions from:', SESSIONS_PATH); + const files = fs.readdirSync(SESSIONS_PATH); + if (files.length === 0) { + console.warn('No session files found in directory'); + } else { + console.log('Session files found:', files); + } + return files.map(file => { + const data = fs.readFileSync(path.join(SESSIONS_PATH, file), 'utf8'); + return JSON.parse(data) as Session; + }); + } catch (error) { + console.error('Error loading sessions:', error); + return []; + } +} + +export function clearAllSessions(): void { + try { + const files = fs.readdirSync(SESSIONS_PATH); + files.forEach(file => fs.unlinkSync(path.join(SESSIONS_PATH, file))); + console.log('All sessions cleared'); + } catch (error) { + console.error('Error clearing sessions:', error); + } +} From 5f4c9371a8cc229bba722404d87e6a3ac700c8d1 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 14:10:45 +1100 Subject: [PATCH 02/10] shifting to invoke --- ui/desktop/src/ChatWindow.tsx | 8 +++++--- ui/desktop/src/LauncherWindow.tsx | 6 +++--- ui/desktop/src/main.ts | 19 +++++++++++-------- ui/desktop/src/preload.js | 2 +- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index 26567ab6f..223edf2b8 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -243,10 +243,12 @@ export default function ChatWindow() { }; // Function to get a session by ID - const getSession = (sessionId) => { + const getSession = async (sessionId) => { + try { - const session = window.electron.getSession(sessionId); - console.log('Session loaded:', session); + const session = await window.electron.getSession(sessionId); + window.electron.logInfo('GUI Session loading '); // + JSON.stringify(session, null,2)); + console.log('XSession loaded:', session); return session } catch (error) { console.error('Failed to load session:', error); diff --git a/ui/desktop/src/LauncherWindow.tsx b/ui/desktop/src/LauncherWindow.tsx index fb7a48d95..584d8b9b3 100644 --- a/ui/desktop/src/LauncherWindow.tsx +++ b/ui/desktop/src/LauncherWindow.tsx @@ -4,9 +4,9 @@ declare global { interface Window { electron: { getConfig(): object; - getSession(arg0: string): object; - logInfo(arg0: string): object; - saveSession(arg0: { name: string; messages: Array; directory: string }): object; + getSession(sessionId: string): object; + logInfo(info: string): object; + saveSession(sessionData: { name: string; messages: Array; directory: string }): object; hideWindow: () => void; createChatWindow: (query: string) => void; }; diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 438104548..6982996df 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -312,14 +312,6 @@ app.whenReady().then(async () => { } }); - ipcMain.on('get-session', (_, sessionId) => { - try { - return loadSessions().find(session => session.name === sessionId); - } catch (error) { - console.error('Failed to load sessions:', error); - throw error; - } - }); ipcMain.on('create-chat-window', (_, query) => { createChat(app, query); @@ -373,6 +365,17 @@ app.whenReady().then(async () => { } }); + ipcMain.handle('get-session', (_, sessionId) => { + try { + console.log("Loading session....."); + return loadSessions().find(session => session.name === sessionId); + } catch (error) { + console.error('Failed to load sessions:', error); + throw error; + } + }); + + ipcMain.on('open-in-chrome', (_, url) => { // On macOS, use the 'open' command with Chrome if (process.platform === 'darwin') { diff --git a/ui/desktop/src/preload.js b/ui/desktop/src/preload.js index 1d71dbde3..434f07bda 100644 --- a/ui/desktop/src/preload.js +++ b/ui/desktop/src/preload.js @@ -17,7 +17,7 @@ contextBridge.exposeInMainWorld('electron', { showNotification: (data) => ipcRenderer.send('notify', data), createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query), saveSession: (session) => ipcRenderer.send('save-session', session), - getSession: (sessionId) => ipcRenderer.send('get-session', sessionId), + getSession: (sessionId) => ipcRenderer.invoke('get-session', sessionId), openInChrome: (url) => ipcRenderer.send('open-in-chrome', url), fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url), }) From 29c7d14008a2b3b4d0ef386cfa05479e7c705b18 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 14:36:36 +1100 Subject: [PATCH 03/10] now kind of works --- ui/desktop/src/ChatWindow.tsx | 119 ++++++++++++---------------------- 1 file changed, 43 insertions(+), 76 deletions(-) diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index a637f4ec4..d88cfe73c 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -16,35 +16,41 @@ import {ApiKeyWarning} from './components/ApiKeyWarning'; import { askAi, getPromptTemplates } from './utils/askAI'; import WingToWing, { Working } from './components/WingToWing'; -export interface Chat { - id: number; - title: string; - messages: Array<{ - id: string; - role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'; - content: string; - }>; -} function ChatContent({ - chats, - setChats, - selectedChatId, - setSelectedChatId, initialQuery, setProgressMessage, setWorking, }: { - chats: Chat[]; - setChats: React.Dispatch>; - selectedChatId: number; - setSelectedChatId: React.Dispatch>; initialQuery: string | null; setProgressMessage: React.Dispatch>; setWorking: React.Dispatch>; }) { - const chat = chats.find((c: Chat) => c.id === selectedChatId); const [messageMetadata, setMessageMetadata] = useState>({}); + const [initialMessages, setInitialMessages] = useState([]); // Replace `any` with actual message type. + + + useEffect(() => { + async function fetchSession() { + const sessionId = window.appConfig.get("GOOSE_SESSION_ID"); + if (sessionId) { + window.electron.logInfo('We have a session ID: ' + sessionId); + try { + const session = await getSession(sessionId); + window.electron.logInfo('Session: ' + session); + + // Populate initialMessages based on session data + const sessionMessages = session ? session.messages || [] : []; + window.electron.logInfo("we have session: " + JSON.stringify(sessionMessages, null, 2)); + setInitialMessages(sessionMessages); + } catch (error) { + window.electron.logError('Error fetching session: ' + error); + } + } + } + + fetchSession(); + }, []); const { messages, @@ -53,10 +59,9 @@ function ChatContent({ isLoading, error, setMessages, - setInput, } = useChat({ api: getApiUrl('/reply'), - initialMessages: chat?.messages || [], + initialMessages, onToolCall: ({ toolCall }) => { setWorking(Working.Working); setProgressMessage(`Executing tool: ${toolCall.toolName}`); @@ -81,20 +86,14 @@ function ChatContent({ }); // Update chat messages when they change - useEffect(() => { - const updatedChats = chats.map((c) => - c.id === selectedChatId ? { ...c, messages } : c - ); - setChats(updatedChats); - const currentChat = chats.find(chat => chat.id === selectedChatId); - if (currentChat) { + useEffect(() => { const sessionToSave = { - messages: currentChat.messages, + messages: messages, directory: window.appConfig.get("GOOSE_WORKING_DIR") }; saveSession(sessionToSave); - } - }, [messages, selectedChatId]); + + }, [messages]); // Function to save a session const saveSession = (session) => { @@ -241,32 +240,6 @@ function ChatContent({ } export default function ChatWindow() { - // Shared function to create a chat window - const openNewChatWindow = () => { - window.electron.createChatWindow(); - }; - - // Function to get a session by ID - const getSession = async (sessionId) => { - - try { - const session = await window.electron.getSession(sessionId); - window.electron.logInfo('GUI Session loading '); // + JSON.stringify(session, null,2)); - console.log('XSession loaded:', session); - return session - } catch (error) { - console.error('Failed to load session:', error); - } - }; - - const convertSessionToChat = (session) => { - const chat = { - id: 1, - title: session.name, - messages: session.messages, - }; - return chat; - } // Add keyboard shortcut handler useEffect(() => { @@ -274,7 +247,7 @@ export default function ChatWindow() { // Check for Command+N (Mac) or Control+N (Windows/Linux) if ((event.metaKey || event.ctrlKey) && event.key === 'n') { event.preventDefault(); // Prevent default browser behavior - openNewChatWindow(); + window.electron.createChatWindow(); } }; @@ -296,20 +269,6 @@ export default function ChatWindow() { const historyParam = searchParams.get('history'); const initialHistory = historyParam ? JSON.parse(decodeURIComponent(historyParam)) : []; - const [chats, setChats] = useState(() => { - const firstChat = { - id: 1, - title: initialQuery || 'Chat 1', - messages: initialHistory.length > 0 ? initialHistory : [], - }; - const sessionId = window.appConfig.get("GOOSE_SESSION_ID"); - const session = getSession(sessionId); - if (session) { - return[convertSessionToChat(session)]; - } - return [firstChat]; - }); - const [selectedChatId, setSelectedChatId] = useState(1); const [mode, setMode] = useState<'expanded' | 'compact'>( initialQuery ? 'compact' : 'expanded' @@ -340,11 +299,6 @@ export default function ChatWindow() { path="/chat/:id" element={ ); } + + +const getSession = async (sessionId) => { + try { + const session = await window.electron.getSession(sessionId); + window.electron.logInfo('GUI Session loading '); // + JSON.stringify(session, null,2)); + console.log('XSession loaded:', session); + return session + } catch (error) { + console.error('Failed to load session:', error); + } +}; + From 54180978343eaedfac31ff74843e28623012e6b0 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 16:18:01 +1100 Subject: [PATCH 04/10] filter number of sessions and age --- ui/desktop/src/ChatWindow.tsx | 1 - ui/desktop/src/main.ts | 18 +++++++++++ ui/desktop/src/preload.js | 1 + ui/desktop/src/utils/sessionManager.ts | 43 +++++++++++++++++++++----- 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index d88cfe73c..5c6ffd31c 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -48,7 +48,6 @@ function ChatContent({ } } } - fetchSession(); }, []); diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 6982996df..c7f73b966 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -375,6 +375,24 @@ app.whenReady().then(async () => { } }); + // list sessions filtered by ones for the given directory + ipcMain.handle('list-sessions', (_, dir?: string) => { + try { + console.log("Loading session....."); + const sessions = loadSessions(); + if (dir) { + return sessions + .filter(session => session.directory === dir) + .map(session => session.name); + } else { + return sessions.map(session => session.name); + } + } catch (error) { + console.error('Failed to load sessions:', error); + throw error; + } + }); + ipcMain.on('open-in-chrome', (_, url) => { // On macOS, use the 'open' command with Chrome diff --git a/ui/desktop/src/preload.js b/ui/desktop/src/preload.js index 434f07bda..e633ab2f7 100644 --- a/ui/desktop/src/preload.js +++ b/ui/desktop/src/preload.js @@ -18,6 +18,7 @@ contextBridge.exposeInMainWorld('electron', { createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query), saveSession: (session) => ipcRenderer.send('save-session', session), getSession: (sessionId) => ipcRenderer.invoke('get-session', sessionId), + listSessions: (dir) => ipcRenderer.invoke('list-sessions', dir), openInChrome: (url) => ipcRenderer.send('open-in-chrome', url), fetchMetadata: (url) => ipcRenderer.invoke('fetch-metadata', url), }) diff --git a/ui/desktop/src/utils/sessionManager.ts b/ui/desktop/src/utils/sessionManager.ts index 03687e8eb..53cc4eb02 100644 --- a/ui/desktop/src/utils/sessionManager.ts +++ b/ui/desktop/src/utils/sessionManager.ts @@ -17,7 +17,7 @@ interface Session { directory: string; } -function generateSessionName(messages: object[]): string { +function generateSessionName(messages: {id: number, role: string, content: string}[]): string { // Create a session name based on the first message or a combination of initial messages if (messages === undefined || messages.length === 0) return 'empty_session'; return messages[0].content.split(' ').slice(0, 5).join(' '); @@ -41,13 +41,40 @@ export function saveSession(session: Session): string { export function loadSessions(): Session[] { try { console.log('Attempting to load sessions from:', SESSIONS_PATH); - const files = fs.readdirSync(SESSIONS_PATH); - if (files.length === 0) { - console.warn('No session files found in directory'); - } else { - console.log('Session files found:', files); - } - return files.map(file => { + const MAX_FILES = 100; + const MAX_AGE_DAYS = 10; + // Get the current date + const now = Date.now(); + const maxAgeMs = MAX_AGE_DAYS * 24 * 60 * 60 * 1000; + + // Get all files in the directory + const files = fs.readdirSync(SESSIONS_PATH); + + if (files.length === 0) { + console.warn('No session files found in directory'); + return []; + } + + // Filter files based on their age and limit to max 100 files + const filteredFiles = files + .map(file => { + const filePath = path.join(SESSIONS_PATH, file); + const stats = fs.statSync(filePath); + const age = now - stats.mtimeMs; + return { file, age }; + }) + .filter(({ age }) => age <= maxAgeMs) + .slice(0, MAX_FILES); + + if (filteredFiles.length === 0) { + console.warn('No session files meet the age criteria'); + return []; + } + + console.log('Filtered session files:', filteredFiles.map(f => f.file)); + + // Load the filtered files and parse them into sessions + return filteredFiles.map(({ file }) => { const data = fs.readFileSync(path.join(SESSIONS_PATH, file), 'utf8'); return JSON.parse(data) as Session; }); From 2701e2969fdbf2f7da6954c9c7bf8b0129cbfef7 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 17:17:11 +1100 Subject: [PATCH 05/10] working end to end --- ui/desktop/src/LauncherWindow.tsx | 3 +- ui/desktop/src/components/SessionPIlls.tsx | 43 ++++++++++++++++++++++ ui/desktop/src/components/Splash.tsx | 14 ++++++- ui/desktop/src/main.ts | 17 ++++++--- ui/desktop/src/preload.js | 2 +- ui/desktop/src/utils/sessionManager.ts | 1 - 6 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 ui/desktop/src/components/SessionPIlls.tsx diff --git a/ui/desktop/src/LauncherWindow.tsx b/ui/desktop/src/LauncherWindow.tsx index 584d8b9b3..b3054fa5b 100644 --- a/ui/desktop/src/LauncherWindow.tsx +++ b/ui/desktop/src/LauncherWindow.tsx @@ -5,10 +5,11 @@ declare global { electron: { getConfig(): object; getSession(sessionId: string): object; + listSessions(dir: string): Array; logInfo(info: string): object; saveSession(sessionData: { name: string; messages: Array; directory: string }): object; hideWindow: () => void; - createChatWindow: (query: string) => void; + createChatWindow: (query?: string, dir?: string, sessionId?: string) => void; }; } } diff --git a/ui/desktop/src/components/SessionPIlls.tsx b/ui/desktop/src/components/SessionPIlls.tsx new file mode 100644 index 000000000..5b90122bd --- /dev/null +++ b/ui/desktop/src/components/SessionPIlls.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from "react" + +export default function SessionPills() { + const [sessions, setSessions] = useState([]); + + const dir = window.appConfig.get("GOOSE_WORKING_DIR"); + useEffect(() => { + async function loadSessions() { + + window.electron.logInfo(`_------______________ Looking for sessions related to ${dir}`); + const sessions = await window.electron.listSessions(dir); + + window.electron.logInfo(`_------______________ Found ${sessions.length} sessions in ${dir}`); + window.electron.logInfo(`Sessions: ${JSON.stringify(sessions)}`); + setSessions(sessions); + }; + loadSessions(); + }, []); + + return ( +
+ + {sessions.map((session) => ( + +
{ + window.electron.createChatWindow(undefined, dir, session); + + }}> + {session} +
+ + + ))} + +
+ ) +} + + + + diff --git a/ui/desktop/src/components/Splash.tsx b/ui/desktop/src/components/Splash.tsx index afcd7814b..071081002 100644 --- a/ui/desktop/src/components/Splash.tsx +++ b/ui/desktop/src/components/Splash.tsx @@ -1,8 +1,11 @@ import React from 'react'; import GooseSplashLogo from './GooseSplashLogo'; import SplashPills from './SplashPills'; +import SessionPills from './SessionPIlls'; export default function Splash({ append }) { + + return (
@@ -26,17 +29,26 @@ export default function Splash({ append }) { What can goose do?
+ + + +
+ +
+
{ window.electron.directoryChooser(); }}> Working in: {window.appConfig.get("GOOSE_WORKING_DIR")} -
+
+
+
diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index c7f73b966..6aff06154 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; import { loadZshEnv } from './utils/loadEnv'; -import { app, BrowserWindow, Tray, Menu, globalShortcut, ipcMain, Notification, MenuItem, dialog } from 'electron'; +import { app, BrowserWindow, Tray, Menu, globalShortcut, ipcMain, Notification, MenuItem, dialog, session } from 'electron'; import path from 'node:path'; import { findAvailablePort, startGoosed } from './goosed'; import started from "electron-squirrel-startup"; @@ -313,9 +313,10 @@ app.whenReady().then(async () => { }); - ipcMain.on('create-chat-window', (_, query) => { - createChat(app, query); - }); + ipcMain.on('create-chat-window', (_, query, dir?, sessionId?) => { + //(app, query?: string, dir?: string, sessionId?: string) + createChat(app, query, dir, sessionId); + }); ipcMain.on('clear-session-history', () => { // Clear all stored session data @@ -378,12 +379,16 @@ app.whenReady().then(async () => { // list sessions filtered by ones for the given directory ipcMain.handle('list-sessions', (_, dir?: string) => { try { - console.log("Loading session....."); + const sessions = loadSessions(); + if (dir) { - return sessions + console.log("server: looking for sessions that match directory", dir); + const results = sessions .filter(session => session.directory === dir) .map(session => session.name); + console.log("server: found sessions:", results); + return results; } else { return sessions.map(session => session.name); } diff --git a/ui/desktop/src/preload.js b/ui/desktop/src/preload.js index e633ab2f7..8952959a9 100644 --- a/ui/desktop/src/preload.js +++ b/ui/desktop/src/preload.js @@ -12,7 +12,7 @@ contextBridge.exposeInMainWorld('electron', { getConfig: () => config, hideWindow: () => ipcRenderer.send('hide-window'), directoryChooser: () => ipcRenderer.send('directory-chooser'), - createChatWindow: (query) => ipcRenderer.send('create-chat-window', query), + createChatWindow: (query, dir, sessionId) => ipcRenderer.send('create-chat-window', query, dir, sessionId), logInfo: (txt) => ipcRenderer.send('logInfo', txt), showNotification: (data) => ipcRenderer.send('notify', data), createWingToWingWindow: (query) => ipcRenderer.send('create-wing-to-wing-window', query), diff --git a/ui/desktop/src/utils/sessionManager.ts b/ui/desktop/src/utils/sessionManager.ts index 53cc4eb02..ae54f0f1e 100644 --- a/ui/desktop/src/utils/sessionManager.ts +++ b/ui/desktop/src/utils/sessionManager.ts @@ -71,7 +71,6 @@ export function loadSessions(): Session[] { return []; } - console.log('Filtered session files:', filteredFiles.map(f => f.file)); // Load the filtered files and parse them into sessions return filteredFiles.map(({ file }) => { From 732bf0b304a4d89a4378c85417ff34afa0e073ff Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 17:34:36 +1100 Subject: [PATCH 06/10] better sorting --- ui/desktop/src/main.ts | 7 +++--- ui/desktop/src/utils/sessionManager.ts | 33 +++++++++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 6aff06154..2ddd34c1c 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -7,7 +7,7 @@ import started from "electron-squirrel-startup"; import log from './utils/logger'; import { exec } from 'child_process'; import { addRecentDir, loadRecentDirs } from './utils/recentDirs'; -import { loadSessions, saveSession, clearAllSessions } from './utils/sessionManager'; +import { loadSessions, saveSession, clearAllSessions, loadSession } from './utils/sessionManager'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) app.quit(); @@ -369,7 +369,7 @@ app.whenReady().then(async () => { ipcMain.handle('get-session', (_, sessionId) => { try { console.log("Loading session....."); - return loadSessions().find(session => session.name === sessionId); + return loadSession(sessionId) } catch (error) { console.error('Failed to load sessions:', error); throw error; @@ -380,12 +380,11 @@ app.whenReady().then(async () => { ipcMain.handle('list-sessions', (_, dir?: string) => { try { - const sessions = loadSessions(); + const sessions = loadSessions(dir); if (dir) { console.log("server: looking for sessions that match directory", dir); const results = sessions - .filter(session => session.directory === dir) .map(session => session.name); console.log("server: found sessions:", results); return results; diff --git a/ui/desktop/src/utils/sessionManager.ts b/ui/desktop/src/utils/sessionManager.ts index ae54f0f1e..dbabd590e 100644 --- a/ui/desktop/src/utils/sessionManager.ts +++ b/ui/desktop/src/utils/sessionManager.ts @@ -38,10 +38,26 @@ export function saveSession(session: Session): string { } } -export function loadSessions(): Session[] { +export function loadSession(sessionId: string): Session | undefined { + try { + const filePath = path.join(SESSIONS_PATH, `${sessionId}.json`); + if (!fs.existsSync(filePath)) { + console.warn('Session file not found:', sessionId); + return undefined; + } + const data = fs.readFileSync(filePath, 'utf8'); + const session = JSON.parse(data) as Session; + console.log('Session loaded:', session); + return session; + } catch (error) { + console.error('Error loading session:', error); + } +} + +// load sessions that are relevant to the directory supplied (not where they are stored, but where user is operating) +export function loadSessions(dir?: string): Session[] { try { console.log('Attempting to load sessions from:', SESSIONS_PATH); - const MAX_FILES = 100; const MAX_AGE_DAYS = 10; // Get the current date const now = Date.now(); @@ -63,20 +79,25 @@ export function loadSessions(): Session[] { const age = now - stats.mtimeMs; return { file, age }; }) - .filter(({ age }) => age <= maxAgeMs) - .slice(0, MAX_FILES); + .filter(({ age }) => age <= maxAgeMs); if (filteredFiles.length === 0) { console.warn('No session files meet the age criteria'); return []; } - // Load the filtered files and parse them into sessions - return filteredFiles.map(({ file }) => { + const sessions = filteredFiles.map(({ file }) => { const data = fs.readFileSync(path.join(SESSIONS_PATH, file), 'utf8'); return JSON.parse(data) as Session; }); + if (dir) { + // Filter sessions based on the directory + return sessions.filter(session => session.directory === dir).splice(0, 4); + } else { + // just recent sessions + return sessions.splice(0, 20); + } } catch (error) { console.error('Error loading sessions:', error); return []; From a8cc1429a468c14a4e60407769c0345d0a5f8a6e Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 17:41:13 +1100 Subject: [PATCH 07/10] style closer to design --- ui/desktop/src/components/SessionPIlls.tsx | 38 ++++++++++------------ 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/ui/desktop/src/components/SessionPIlls.tsx b/ui/desktop/src/components/SessionPIlls.tsx index 5b90122bd..9179c7b42 100644 --- a/ui/desktop/src/components/SessionPIlls.tsx +++ b/ui/desktop/src/components/SessionPIlls.tsx @@ -17,27 +17,25 @@ export default function SessionPills() { loadSessions(); }, []); + if (sessions.length === 0) { + return null; + } + return ( -
+
- {sessions.map((session) => ( - -
{ - window.electron.createChatWindow(undefined, dir, session); - - }}> - {session} -
- - - ))} - +
+
Previous gooses:
+ {sessions.map((session) => ( +
{ + window.electron.createChatWindow(undefined, dir, session); + }}> + {session} +
+ ))} +
) -} - - - - +} \ No newline at end of file From a9f18336a32b01627aea434cee0f54f35dab5639 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 17:48:04 +1100 Subject: [PATCH 08/10] safe filenames --- ui/desktop/src/utils/sessionManager.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/utils/sessionManager.ts b/ui/desktop/src/utils/sessionManager.ts index dbabd590e..4572b9466 100644 --- a/ui/desktop/src/utils/sessionManager.ts +++ b/ui/desktop/src/utils/sessionManager.ts @@ -23,13 +23,23 @@ function generateSessionName(messages: {id: number, role: string, content: strin return messages[0].content.split(' ').slice(0, 5).join(' '); } +function createSafeFilename(name: string): string { + // Replace unsafe characters with underscores and limit length + return name + .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore + .replace(/_{2,}/g, '_') // Replace multiple underscores with single + .replace(/^_|_$/g, '') // Remove leading/trailing underscores + .substring(0, 100); // Limit length to 100 chars +} + export function saveSession(session: Session): string { try { const sessionData = { ...session, name: generateSessionName(session.messages) }; - const filePath = path.join(SESSIONS_PATH, `${sessionData.name}.json`); + const safeFileName = createSafeFilename(sessionData.name); + const filePath = path.join(SESSIONS_PATH, `${safeFileName}.json`); fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2)); console.log('Session saved:', sessionData); return sessionData.name; @@ -40,7 +50,8 @@ export function saveSession(session: Session): string { export function loadSession(sessionId: string): Session | undefined { try { - const filePath = path.join(SESSIONS_PATH, `${sessionId}.json`); + const safeFileName = createSafeFilename(sessionId); + const filePath = path.join(SESSIONS_PATH, `${safeFileName}.json`); if (!fs.existsSync(filePath)) { console.warn('Session file not found:', sessionId); return undefined; @@ -112,4 +123,4 @@ export function clearAllSessions(): void { } catch (error) { console.error('Error clearing sessions:', error); } -} +} \ No newline at end of file From 32c84f68e5b0d06f4182a7b788c8c0625a072958 Mon Sep 17 00:00:00 2001 From: Mic Neale Date: Wed, 4 Dec 2024 19:02:44 +1100 Subject: [PATCH 09/10] show recent ones always --- ui/desktop/src/LauncherWindow.tsx | 2 +- ui/desktop/src/components/SessionPIlls.tsx | 68 +++++++++++++++++----- ui/desktop/src/main.ts | 4 +- 3 files changed, 58 insertions(+), 16 deletions(-) diff --git a/ui/desktop/src/LauncherWindow.tsx b/ui/desktop/src/LauncherWindow.tsx index b3054fa5b..adfadc485 100644 --- a/ui/desktop/src/LauncherWindow.tsx +++ b/ui/desktop/src/LauncherWindow.tsx @@ -5,7 +5,7 @@ declare global { electron: { getConfig(): object; getSession(sessionId: string): object; - listSessions(dir: string): Array; + listSessions(dir?: string): Array; logInfo(info: string): object; saveSession(sessionData: { name: string; messages: Array; directory: string }): object; hideWindow: () => void; diff --git a/ui/desktop/src/components/SessionPIlls.tsx b/ui/desktop/src/components/SessionPIlls.tsx index 9179c7b42..647a88425 100644 --- a/ui/desktop/src/components/SessionPIlls.tsx +++ b/ui/desktop/src/components/SessionPIlls.tsx @@ -2,39 +2,81 @@ import React, { useEffect, useState } from "react" export default function SessionPills() { const [sessions, setSessions] = useState([]); + const [latestSessions, setLatestSessions] = useState([]); - const dir = window.appConfig.get("GOOSE_WORKING_DIR"); + const workingDir = window.appConfig.get("GOOSE_WORKING_DIR"); + useEffect(() => { async function loadSessions() { - - window.electron.logInfo(`_------______________ Looking for sessions related to ${dir}`); - const sessions = await window.electron.listSessions(dir); + window.electron.logInfo(`_------______________ Looking for sessions related to ${workingDir}`); + const sessions = await window.electron.listSessions(workingDir); - window.electron.logInfo(`_------______________ Found ${sessions.length} sessions in ${dir}`); + window.electron.logInfo(`_------______________ Found ${sessions.length} sessions in ${workingDir}`); window.electron.logInfo(`Sessions: ${JSON.stringify(sessions)}`); setSessions(sessions); }; loadSessions(); }, []); - if (sessions.length === 0) { + useEffect(() => { + async function loadSessions() { + const sessions = await window.electron.listSessions(); + setLatestSessions(sessions); + }; + loadSessions(); + }, []); + + if (sessions.length === 0 && latestSessions.length === 0) { return null; } + // Create a combined list of sessions, prioritizing latest ones and removing duplicates + const combinedSessions = []; + const seenNames = new Set(); + + // Add at least one latest session if available + if (latestSessions.length > 0) { + const latest = latestSessions[0]; + combinedSessions.push({ ...latest, isLatest: true }); + seenNames.add(latest.name); + } + + // Add remaining latest sessions (up to 5 total) + for (let i = 1; i < latestSessions.length && combinedSessions.length < 5; i++) { + const session = latestSessions[i]; + if (!seenNames.has(session.name)) { + combinedSessions.push({ ...session, isLatest: true }); + seenNames.add(session.name); + } + } + + // Fill remaining slots with regular sessions (up to 5 total) + for (const session of sessions) { + if (combinedSessions.length >= 5) break; + if (!seenNames.has(session.name)) { + combinedSessions.push({ ...session, isLatest: false }); + seenNames.add(session.name); + } + } + return (
-
-
Previous gooses:
- {sessions.map((session) => ( + {combinedSessions.map((session) => (
{ - window.electron.createChatWindow(undefined, dir, session); - }}> - {session} + window.electron.createChatWindow(undefined, session.directory, session.name); + }} + title={session.directory}> + + {`${session.name.slice(0, 50)}`} + {session.isLatest && !(session.directory === workingDir) && ( + (recent) + )}
- ))} + ))}
) diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 2ddd34c1c..ceb42fc71 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -385,11 +385,11 @@ app.whenReady().then(async () => { if (dir) { console.log("server: looking for sessions that match directory", dir); const results = sessions - .map(session => session.name); + .map(session => ({ name: session.name, directory: session.directory })); console.log("server: found sessions:", results); return results; } else { - return sessions.map(session => session.name); + return sessions.map(session => ({ name: session.name, directory: session.directory })); } } catch (error) { console.error('Failed to load sessions:', error); From 7bce83ae23fa8dd1d20e22ab7e407e6118477ac8 Mon Sep 17 00:00:00 2001 From: Alex Hancock Date: Wed, 4 Dec 2024 10:18:38 -0500 Subject: [PATCH 10/10] SessionPills file rename + hook --- ui/desktop/src/components/SessionPIlls.tsx | 83 ---------- ui/desktop/src/components/SessionPills.tsx | 85 ++++++++++ ui/desktop/src/components/Splash.tsx | 2 +- ui/desktop/src/utils/sessionManager.ts | 180 ++++++++++----------- 4 files changed, 176 insertions(+), 174 deletions(-) delete mode 100644 ui/desktop/src/components/SessionPIlls.tsx create mode 100644 ui/desktop/src/components/SessionPills.tsx diff --git a/ui/desktop/src/components/SessionPIlls.tsx b/ui/desktop/src/components/SessionPIlls.tsx deleted file mode 100644 index 647a88425..000000000 --- a/ui/desktop/src/components/SessionPIlls.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useEffect, useState } from "react" - -export default function SessionPills() { - const [sessions, setSessions] = useState([]); - const [latestSessions, setLatestSessions] = useState([]); - - const workingDir = window.appConfig.get("GOOSE_WORKING_DIR"); - - useEffect(() => { - async function loadSessions() { - window.electron.logInfo(`_------______________ Looking for sessions related to ${workingDir}`); - const sessions = await window.electron.listSessions(workingDir); - - window.electron.logInfo(`_------______________ Found ${sessions.length} sessions in ${workingDir}`); - window.electron.logInfo(`Sessions: ${JSON.stringify(sessions)}`); - setSessions(sessions); - }; - loadSessions(); - }, []); - - useEffect(() => { - async function loadSessions() { - const sessions = await window.electron.listSessions(); - setLatestSessions(sessions); - }; - loadSessions(); - }, []); - - if (sessions.length === 0 && latestSessions.length === 0) { - return null; - } - - // Create a combined list of sessions, prioritizing latest ones and removing duplicates - const combinedSessions = []; - const seenNames = new Set(); - - // Add at least one latest session if available - if (latestSessions.length > 0) { - const latest = latestSessions[0]; - combinedSessions.push({ ...latest, isLatest: true }); - seenNames.add(latest.name); - } - - // Add remaining latest sessions (up to 5 total) - for (let i = 1; i < latestSessions.length && combinedSessions.length < 5; i++) { - const session = latestSessions[i]; - if (!seenNames.has(session.name)) { - combinedSessions.push({ ...session, isLatest: true }); - seenNames.add(session.name); - } - } - - // Fill remaining slots with regular sessions (up to 5 total) - for (const session of sessions) { - if (combinedSessions.length >= 5) break; - if (!seenNames.has(session.name)) { - combinedSessions.push({ ...session, isLatest: false }); - seenNames.add(session.name); - } - } - - return ( -
-
- {combinedSessions.map((session) => ( -
{ - window.electron.createChatWindow(undefined, session.directory, session.name); - }} - title={session.directory}> - - {`${session.name.slice(0, 50)}`} - {session.isLatest && !(session.directory === workingDir) && ( - (recent) - )} -
- ))} -
-
- ) -} \ No newline at end of file diff --git a/ui/desktop/src/components/SessionPills.tsx b/ui/desktop/src/components/SessionPills.tsx new file mode 100644 index 000000000..c95eb01f9 --- /dev/null +++ b/ui/desktop/src/components/SessionPills.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from "react" + +const useCombinedSessions = (workingDir: string) => { + const [sessions, setSessions] = useState([]); + const [latestSessions, setLatestSessions] = useState([]); + + useEffect(() => { + async function loadSessions() { + const sessions = await window.electron.listSessions(workingDir); + setSessions(sessions); + const latestSessions = await window.electron.listSessions(); + setLatestSessions(latestSessions); + }; + loadSessions(); + }, [workingDir]); + + const getCombinedSessions = () => { + if (sessions.length === 0 && latestSessions.length === 0) { + return []; + } + + const combinedSessions = []; + const seenNames = new Set(); + + // Add at least one latest session if available + if (latestSessions.length > 0) { + const latest = latestSessions[0]; + combinedSessions.push({ ...latest, isLatest: true }); + seenNames.add(latest.name); + } + + // Add remaining latest sessions (up to 5 total) + for (let i = 1; i < latestSessions.length && combinedSessions.length < 5; i++) { + const session = latestSessions[i]; + if (!seenNames.has(session.name)) { + combinedSessions.push({ ...session, isLatest: true }); + seenNames.add(session.name); + } + } + + // Fill remaining slots with regular sessions (up to 5 total) + for (const session of sessions) { + if (combinedSessions.length >= 5) break; + if (!seenNames.has(session.name)) { + combinedSessions.push({ ...session, isLatest: false }); + seenNames.add(session.name); + } + } + + return combinedSessions; + }; + + return getCombinedSessions(); +}; + +export default function SessionPills() { + const workingDir = window.appConfig.get("GOOSE_WORKING_DIR"); + const combinedSessions = useCombinedSessions(workingDir); + + if (combinedSessions.length === 0) { + return null; + } + + return ( +
+
+ {combinedSessions.map((session) => ( +
{ + window.electron.createChatWindow(undefined, session.directory, session.name); + }} + title={session.directory} + > + {`${session.name.slice(0, 50)}`} + {session.isLatest && !(session.directory === workingDir) && ( + (recent) + )} +
+ ))} +
+
+ ) +} \ No newline at end of file diff --git a/ui/desktop/src/components/Splash.tsx b/ui/desktop/src/components/Splash.tsx index 071081002..10dc19326 100644 --- a/ui/desktop/src/components/Splash.tsx +++ b/ui/desktop/src/components/Splash.tsx @@ -1,7 +1,7 @@ import React from 'react'; import GooseSplashLogo from './GooseSplashLogo'; import SplashPills from './SplashPills'; -import SessionPills from './SessionPIlls'; +import SessionPills from './SessionPills'; export default function Splash({ append }) { diff --git a/ui/desktop/src/utils/sessionManager.ts b/ui/desktop/src/utils/sessionManager.ts index 4572b9466..9b048ded6 100644 --- a/ui/desktop/src/utils/sessionManager.ts +++ b/ui/desktop/src/utils/sessionManager.ts @@ -4,123 +4,123 @@ import { app } from 'electron'; const SESSIONS_PATH = path.join(app.getPath('userData'), 'sessions'); if (!fs.existsSync(SESSIONS_PATH)) { - fs.mkdirSync(SESSIONS_PATH); + fs.mkdirSync(SESSIONS_PATH); } interface Session { - name: string; // Derived from a synopsis of the conversation - messages: Array<{ - id: number; - role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'; - content: string; - }>; - directory: string; + name: string; // Derived from a synopsis of the conversation + messages: Array<{ + id: number; + role: 'function' | 'system' | 'user' | 'assistant' | 'data' | 'tool'; + content: string; + }>; + directory: string; } function generateSessionName(messages: {id: number, role: string, content: string}[]): string { - // Create a session name based on the first message or a combination of initial messages - if (messages === undefined || messages.length === 0) return 'empty_session'; - return messages[0].content.split(' ').slice(0, 5).join(' '); + // Create a session name based on the first message or a combination of initial messages + if (messages === undefined || messages.length === 0) return 'empty_session'; + return messages[0].content.split(' ').slice(0, 5).join(' '); } function createSafeFilename(name: string): string { - // Replace unsafe characters with underscores and limit length - return name - .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore - .replace(/_{2,}/g, '_') // Replace multiple underscores with single - .replace(/^_|_$/g, '') // Remove leading/trailing underscores - .substring(0, 100); // Limit length to 100 chars + // Replace unsafe characters with underscores and limit length + return name + .replace(/[^a-zA-Z0-9-_]/g, '_') // Replace unsafe chars with underscore + .replace(/_{2,}/g, '_') // Replace multiple underscores with single + .replace(/^_|_$/g, '') // Remove leading/trailing underscores + .substring(0, 100); // Limit length to 100 chars } export function saveSession(session: Session): string { - try { - const sessionData = { - ...session, - name: generateSessionName(session.messages) - }; - const safeFileName = createSafeFilename(sessionData.name); - const filePath = path.join(SESSIONS_PATH, `${safeFileName}.json`); - fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2)); - console.log('Session saved:', sessionData); - return sessionData.name; - } catch (error) { - console.error('Error saving session:', error); - } + try { + const sessionData = { + ...session, + name: generateSessionName(session.messages) + }; + const safeFileName = createSafeFilename(sessionData.name); + const filePath = path.join(SESSIONS_PATH, `${safeFileName}.json`); + fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2)); + console.log('Session saved:', sessionData); + return sessionData.name; + } catch (error) { + console.error('Error saving session:', error); + } } export function loadSession(sessionId: string): Session | undefined { - try { - const safeFileName = createSafeFilename(sessionId); - const filePath = path.join(SESSIONS_PATH, `${safeFileName}.json`); - if (!fs.existsSync(filePath)) { - console.warn('Session file not found:', sessionId); - return undefined; - } - const data = fs.readFileSync(filePath, 'utf8'); - const session = JSON.parse(data) as Session; - console.log('Session loaded:', session); - return session; - } catch (error) { - console.error('Error loading session:', error); + try { + const safeFileName = createSafeFilename(sessionId); + const filePath = path.join(SESSIONS_PATH, `${safeFileName}.json`); + if (!fs.existsSync(filePath)) { + console.warn('Session file not found:', sessionId); + return undefined; } + const data = fs.readFileSync(filePath, 'utf8'); + const session = JSON.parse(data) as Session; + console.log('Session loaded:', session); + return session; + } catch (error) { + console.error('Error loading session:', error); + } } // load sessions that are relevant to the directory supplied (not where they are stored, but where user is operating) export function loadSessions(dir?: string): Session[] { - try { - console.log('Attempting to load sessions from:', SESSIONS_PATH); - const MAX_AGE_DAYS = 10; - // Get the current date - const now = Date.now(); - const maxAgeMs = MAX_AGE_DAYS * 24 * 60 * 60 * 1000; + try { + console.log('Attempting to load sessions from:', SESSIONS_PATH); + const MAX_AGE_DAYS = 10; + // Get the current date + const now = Date.now(); + const maxAgeMs = MAX_AGE_DAYS * 24 * 60 * 60 * 1000; - // Get all files in the directory - const files = fs.readdirSync(SESSIONS_PATH); + // Get all files in the directory + const files = fs.readdirSync(SESSIONS_PATH); - if (files.length === 0) { - console.warn('No session files found in directory'); - return []; - } + if (files.length === 0) { + console.warn('No session files found in directory'); + return []; + } - // Filter files based on their age and limit to max 100 files - const filteredFiles = files - .map(file => { - const filePath = path.join(SESSIONS_PATH, file); - const stats = fs.statSync(filePath); - const age = now - stats.mtimeMs; - return { file, age }; - }) - .filter(({ age }) => age <= maxAgeMs); + // Filter files based on their age and limit to max 100 files + const filteredFiles = files + .map(file => { + const filePath = path.join(SESSIONS_PATH, file); + const stats = fs.statSync(filePath); + const age = now - stats.mtimeMs; + return { file, age }; + }) + .filter(({ age }) => age <= maxAgeMs); - if (filteredFiles.length === 0) { - console.warn('No session files meet the age criteria'); - return []; - } + if (filteredFiles.length === 0) { + console.warn('No session files meet the age criteria'); + return []; + } - // Load the filtered files and parse them into sessions - const sessions = filteredFiles.map(({ file }) => { - const data = fs.readFileSync(path.join(SESSIONS_PATH, file), 'utf8'); - return JSON.parse(data) as Session; - }); - if (dir) { - // Filter sessions based on the directory - return sessions.filter(session => session.directory === dir).splice(0, 4); - } else { - // just recent sessions - return sessions.splice(0, 20); - } - } catch (error) { - console.error('Error loading sessions:', error); - return []; + // Load the filtered files and parse them into sessions + const sessions = filteredFiles.map(({ file }) => { + const data = fs.readFileSync(path.join(SESSIONS_PATH, file), 'utf8'); + return JSON.parse(data) as Session; + }); + if (dir) { + // Filter sessions based on the directory + return sessions.filter(session => session.directory === dir).splice(0, 4); + } else { + // just recent sessions + return sessions.splice(0, 20); } + } catch (error) { + console.error('Error loading sessions:', error); + return []; + } } export function clearAllSessions(): void { - try { - const files = fs.readdirSync(SESSIONS_PATH); - files.forEach(file => fs.unlinkSync(path.join(SESSIONS_PATH, file))); - console.log('All sessions cleared'); - } catch (error) { - console.error('Error clearing sessions:', error); - } + try { + const files = fs.readdirSync(SESSIONS_PATH); + files.forEach(file => fs.unlinkSync(path.join(SESSIONS_PATH, file))); + console.log('All sessions cleared'); + } catch (error) { + console.error('Error clearing sessions:', error); + } } \ No newline at end of file