Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: session management #402

Draft
wants to merge 33 commits into
base: v1.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
52212a2
Session management WIP
Kvadratni Dec 4, 2024
5f4c937
shifting to invoke
michaelneale Dec 4, 2024
37c8802
Merge branch 'v1.0' into mnovich/session-management
michaelneale Dec 4, 2024
29c7d14
now kind of works
michaelneale Dec 4, 2024
5418097
filter number of sessions and age
michaelneale Dec 4, 2024
2701e29
working end to end
michaelneale Dec 4, 2024
732bf0b
better sorting
michaelneale Dec 4, 2024
a8cc142
style closer to design
michaelneale Dec 4, 2024
a9f1833
safe filenames
michaelneale Dec 4, 2024
32c84f6
show recent ones always
michaelneale Dec 4, 2024
7bce83a
SessionPills file rename + hook
alexhancock Dec 4, 2024
65e16e8
Session management WIP
Kvadratni Dec 4, 2024
521bce1
shifting to invoke
michaelneale Dec 4, 2024
8e05b78
now kind of works
michaelneale Dec 4, 2024
02f444a
filter number of sessions and age
michaelneale Dec 4, 2024
fe80009
working end to end
michaelneale Dec 4, 2024
691afc2
better sorting
michaelneale Dec 4, 2024
61aea75
style closer to design
michaelneale Dec 4, 2024
78f8b79
safe filenames
michaelneale Dec 4, 2024
c4e6d4c
show recent ones always
michaelneale Dec 4, 2024
81a570e
SessionPills file rename + hook
alexhancock Dec 4, 2024
af6a0d0
Merge remote-tracking branch 'origin/mnovich/session-management' into…
Kvadratni Dec 4, 2024
64ba52e
fixed some session display stuff
Kvadratni Dec 4, 2024
ac88a08
New UI for session management
Kvadratni Dec 4, 2024
ca411f6
Merge branch 'refs/heads/v1.0' into mnovich/session-management
Kvadratni Dec 4, 2024
9db788c
post merge fixes
Kvadratni Dec 4, 2024
df80ce9
added session caching for performance.
Kvadratni Dec 5, 2024
f8968f2
Merge branch 'refs/heads/v1.0' into mnovich/session-management
Kvadratni Dec 5, 2024
b15988a
Merge branch 'refs/heads/v1.0' into mnovich/session-management
Kvadratni Dec 5, 2024
7e1cd34
CLI -UI session interop
Kvadratni Dec 6, 2024
1d326fb
Merge branch 'refs/heads/v1.0' into mnovich/session-management
Kvadratni Dec 10, 2024
9ba22a6
Merge branch 'refs/heads/v1.0' into mnovich/session-management
Kvadratni Dec 10, 2024
3a46c41
Merge branch 'v1.0' into mnovich/session-management
michaelneale Dec 10, 2024
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
2 changes: 1 addition & 1 deletion crates/goose-cli/src/commands/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn session_path(
retry_on_conflict: bool,
) -> PathBuf {
let session_name = provided_session_name.unwrap_or(random_session_name());
let session_file = session_dir.join(format!("{}.jsonl", session_name));
let session_file = session_dir.join(format!("{}.json", session_name));

if session_file.exists() && retry_on_conflict {
generate_new_session_path(session_dir)
Expand Down
74 changes: 36 additions & 38 deletions crates/goose-cli/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use core::panic;
use futures::StreamExt;
use serde_json;
use std::fs::{self, File};
use std::io::{self, BufRead, Write};
use std::io::{self, BufRead};
use std::path::PathBuf;

use serde_json::json;
use crate::agents::agent::Agent;
use crate::prompt::{InputType, Prompt};
use goose::developer::DeveloperSystem;
Expand Down Expand Up @@ -38,20 +38,15 @@ pub fn readable_session_file(session_file: &PathBuf) -> Result<File> {
}
}

pub fn persist_messages(session_file: &PathBuf, messages: &[Message]) -> Result<()> {
let file = fs::File::create(session_file)?; // Create or truncate the file
persist_messages_internal(file, messages)
}

fn persist_messages_internal(session_file: File, messages: &[Message]) -> Result<()> {
let mut writer = std::io::BufWriter::new(session_file);

for message in messages {
serde_json::to_writer(&mut writer, &message)?;
writeln!(writer)?;
}

writer.flush()?;
pub fn persist_messages(session_file: &PathBuf, messages: &[Message], directory: &PathBuf) -> Result<()> {
let session_data = json!({
"name": session_file.file_stem().unwrap().to_str().unwrap(),
"messages": messages,
"directory": directory
});

let writer = std::io::BufWriter::new(fs::File::create(session_file)?);
serde_json::to_writer(writer, &session_data)?;
Ok(())
}

Expand All @@ -76,27 +71,27 @@ pub struct Session<'a> {

impl<'a> Session<'a> {
pub fn new(agent: Box<dyn Agent>, prompt: Box<dyn Prompt + 'a>, session_file: PathBuf) -> Self {
let messages = match readable_session_file(&session_file) {
Ok(file) => deserialize_messages(file).unwrap_or_else(|e| {
eprintln!(
"Failed to read messages from session file. Starting fresh.\n{}",
e
);
Vec::<Message>::new()
}),
Err(e) => {
eprintln!("Failed to load session file. Starting fresh.\n{}", e);
Vec::<Message>::new()
}
};

Session {
agent,
prompt,
session_file,
messages,
let messages = match readable_session_file(&session_file) {
Ok(file) => deserialize_messages(file).unwrap_or_else(|e| {
eprintln!(
"Failed to read messages from session file. Starting fresh.\n{}",
e
);
Vec::<Message>::new()
}),
Err(e) => {
eprintln!("Failed to load session file. Starting fresh.\n{}", e);
Vec::<Message>::new()
}
};

Session {
agent,
prompt,
session_file,
messages,
}
}

pub async fn start(&mut self) -> Result<(), Box<dyn std::error::Error>> {
self.setup_session();
Expand All @@ -108,7 +103,8 @@ impl<'a> Session<'a> {
InputType::Message => {
if let Some(content) = &input.content {
self.messages.push(Message::user().with_text(content));
persist_messages(&self.session_file, &self.messages)?;
let directory: PathBuf = std::env::current_dir().expect("Failed to get current directory");
persist_messages(&self.session_file, &self.messages, &directory)?
}
}
InputType::Exit => break,
Expand All @@ -131,7 +127,8 @@ impl<'a> Session<'a> {

self.messages
.push(Message::user().with_text(initial_message.as_str()));
persist_messages(&self.session_file, &self.messages)?;
let directory: PathBuf = std::env::current_dir().expect("Failed to get current directory");
persist_messages(&self.session_file, &self.messages, &directory)?;

self.agent_process_messages().await;

Expand All @@ -153,7 +150,8 @@ impl<'a> Session<'a> {
match response {
Some(Ok(message)) => {
self.messages.push(message.clone());
persist_messages(&self.session_file, &self.messages).unwrap_or_else(|e| eprintln!("Failed to persist messages: {}", e));
let directory: PathBuf = std::env::current_dir().expect("Failed to get current directory");
persist_messages(&self.session_file, &self.messages, &directory).unwrap_or_else(|e| eprintln!("Failed to persist messages: {}", e));
self.prompt.hide_busy();
self.prompt.render(Box::new(message.clone()));
self.prompt.show_busy();
Expand Down
16 changes: 15 additions & 1 deletion ui/desktop/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion ui/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,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"
}
}
108 changes: 58 additions & 50 deletions ui/desktop/src/ChatWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +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 BottomMenu from './components/BottomMenu';
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';
import { WelcomeScreen } from './components/WelcomeScreen';
Expand All @@ -24,38 +25,42 @@ const getLastSeenVersion = () => localStorage.getItem('lastSeenVersion');
const setLastSeenVersion = (version: string) => localStorage.setItem('lastSeenVersion', version);


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<React.SetStateAction<Chat[]>>;
selectedChatId: number;
setSelectedChatId: React.Dispatch<React.SetStateAction<number>>;
initialQuery: string | null;
setProgressMessage: React.Dispatch<React.SetStateAction<string>>;
setWorking: React.Dispatch<React.SetStateAction<Working>>;
}) {
const chat = chats.find((c: Chat) => c.id === selectedChatId);
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
const [initialMessages, setInitialMessages] = useState<Message[]>([]); // Replace `any` with actual message type.
const [hasMessages, setHasMessages] = useState(false);


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,
append,
Expand All @@ -65,7 +70,7 @@ function ChatContent({
setMessages,
} = useChat({
api: getApiUrl('/reply'),
initialMessages: chat?.messages || [],
initialMessages,
onToolCall: ({ toolCall }) => {
setWorking(Working.Working);
setProgressMessage(`Executing tool: ${toolCall.toolName}`);
Expand All @@ -91,11 +96,20 @@ function ChatContent({

// Update chat messages when they change
useEffect(() => {
const updatedChats = chats.map((c) =>
c.id === selectedChatId ? { ...c, messages } : c
);
setChats(updatedChats);
}, [messages, selectedChatId]);
const sessionToSave = {
messages: messages,
directory: window.appConfig.get("GOOSE_WORKING_DIR")
};
saveSession(sessionToSave);

}, [messages]);

// 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(() => {
Expand Down Expand Up @@ -256,18 +270,14 @@ function ChatContent({
}

export default function ChatWindow() {
// Shared function to create a chat window
const openNewChatWindow = () => {
window.electron.createChatWindow();
};

// Add keyboard shortcut handler
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// 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();
}
};

Expand All @@ -289,15 +299,6 @@ export default function ChatWindow() {
const historyParam = searchParams.get('history');
const initialHistory = historyParam ? JSON.parse(decodeURIComponent(historyParam)) : [];

const [chats, setChats] = useState<Chat[]>(() => {
const firstChat = {
id: 1,
title: initialQuery || 'Chat 1',
messages: initialHistory.length > 0 ? initialHistory : [],
};
return [firstChat];
});

const [selectedChatId, setSelectedChatId] = useState(1);
const [mode, setMode] = useState<'expanded' | 'compact'>(
initialQuery ? 'compact' : 'expanded'
Expand Down Expand Up @@ -343,11 +344,6 @@ export default function ChatWindow() {
path="/chat/:id"
element={
<ChatContent
key={selectedChatId}
chats={chats}
setChats={setChats}
selectedChatId={selectedChatId}
setSelectedChatId={setSelectedChatId}
initialQuery={initialQuery}
setProgressMessage={setProgressMessage}
setWorking={setWorking}
Expand All @@ -365,3 +361,15 @@ export default function ChatWindow() {
</div>
);
}


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);
}
};
9 changes: 7 additions & 2 deletions ui/desktop/src/LauncherWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ import React, { useState, useRef } from 'react';
declare global {
interface Window {
electron: {
getConfig(): object;
getSession(sessionId: string): object;
listSessions(): Array<object>;
logInfo(info: string): object;
saveSession(sessionData: { name: string; messages: Array<object>; directory: string }): object;
hideWindow: () => void;
createChatWindow: (query: string) => void;
createChatWindow: (query?: string, dir?: string, sessionId?: string) => void;
};
}
}
Expand Down Expand Up @@ -41,4 +46,4 @@ export default function SpotlightWindow() {
</form>
</div>
);
}
}
Loading
Loading