mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-19 07:04:21 +01:00
feat: store working directory for sessions (#1559)
This commit is contained in:
@@ -35,7 +35,59 @@ pub async fn build_session(
|
|||||||
let mut agent = AgentFactory::create(&AgentFactory::configured_version(), provider)
|
let mut agent = AgentFactory::create(&AgentFactory::configured_version(), provider)
|
||||||
.expect("Failed to create agent");
|
.expect("Failed to create agent");
|
||||||
|
|
||||||
|
// Handle session file resolution and resuming
|
||||||
|
let session_file = if resume {
|
||||||
|
if let Some(identifier) = identifier {
|
||||||
|
let session_file = session::get_path(identifier);
|
||||||
|
if !session_file.exists() {
|
||||||
|
output::render_error(&format!(
|
||||||
|
"Cannot resume session {} - no such session exists",
|
||||||
|
style(session_file.display()).cyan()
|
||||||
|
));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
session_file
|
||||||
|
} else {
|
||||||
|
// Try to resume most recent session
|
||||||
|
match session::get_most_recent_session() {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => {
|
||||||
|
output::render_error("Cannot resume - no previous sessions found");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new session with provided name/path or generated name
|
||||||
|
let id = match identifier {
|
||||||
|
Some(identifier) => identifier,
|
||||||
|
None => Identifier::Name(session::generate_session_id()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Just get the path - file will be created when needed
|
||||||
|
session::get_path(id)
|
||||||
|
};
|
||||||
|
|
||||||
|
if resume {
|
||||||
|
// Read the session metadata
|
||||||
|
let metadata = session::read_metadata(&session_file).unwrap_or_else(|e| {
|
||||||
|
output::render_error(&format!("Failed to read session metadata: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ask user if they want to change the working directory
|
||||||
|
let change_workdir = cliclack::confirm(format!("{} The working directory of this session was set to {}. It does not match the current working directory. Would you like to change it?", style("WARNING:").yellow(), style(metadata.working_dir.display()).cyan()))
|
||||||
|
.initial_value(true)
|
||||||
|
.interact().expect("Failed to get user input");
|
||||||
|
|
||||||
|
if change_workdir {
|
||||||
|
std::env::set_current_dir(metadata.working_dir).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Setup extensions for the agent
|
// Setup extensions for the agent
|
||||||
|
// Extensions need to be added after the session is created because we change directory when resuming a session
|
||||||
for extension in ExtensionManager::get_all().expect("should load extensions") {
|
for extension in ExtensionManager::get_all().expect("should load extensions") {
|
||||||
if extension.enabled {
|
if extension.enabled {
|
||||||
let config = extension.config.clone();
|
let config = extension.config.clone();
|
||||||
@@ -59,39 +111,6 @@ pub async fn build_session(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle session file resolution and resuming
|
|
||||||
let session_file = if resume {
|
|
||||||
if let Some(identifier) = identifier {
|
|
||||||
let session_file = session::get_path(identifier);
|
|
||||||
if !session_file.exists() {
|
|
||||||
output::render_error(&format!(
|
|
||||||
"Cannot resume session {} - no such session exists",
|
|
||||||
style(session_file.display()).cyan()
|
|
||||||
));
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
session_file
|
|
||||||
} else {
|
|
||||||
// Try to resume most recent session
|
|
||||||
match session::get_most_recent_session() {
|
|
||||||
Ok(file) => file,
|
|
||||||
Err(_) => {
|
|
||||||
output::render_error("Cannot resume - no previous sessions found");
|
|
||||||
process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Create new session with provided name/path or generated name
|
|
||||||
let id = match identifier {
|
|
||||||
Some(identifier) => identifier,
|
|
||||||
None => Identifier::Name(session::generate_session_id()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Just get the path - file will be created when needed
|
|
||||||
session::get_path(id)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new session
|
// Create new session
|
||||||
let mut session = Session::new(agent, session_file.clone(), debug);
|
let mut session = Session::new(agent, session_file.clone(), debug);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use completion::GooseCompleter;
|
|||||||
use etcetera::choose_app_strategy;
|
use etcetera::choose_app_strategy;
|
||||||
use etcetera::AppStrategy;
|
use etcetera::AppStrategy;
|
||||||
use goose::agents::extension::{Envs, ExtensionConfig};
|
use goose::agents::extension::{Envs, ExtensionConfig};
|
||||||
use goose::agents::Agent;
|
use goose::agents::{Agent, SessionConfig};
|
||||||
use goose::config::Config;
|
use goose::config::Config;
|
||||||
use goose::message::{Message, MessageContent};
|
use goose::message::{Message, MessageContent};
|
||||||
use goose::session;
|
use goose::session;
|
||||||
@@ -199,7 +199,6 @@ impl Session {
|
|||||||
/// Process a single message and get the response
|
/// Process a single message and get the response
|
||||||
async fn process_message(&mut self, message: String) -> Result<()> {
|
async fn process_message(&mut self, message: String) -> Result<()> {
|
||||||
self.messages.push(Message::user().with_text(&message));
|
self.messages.push(Message::user().with_text(&message));
|
||||||
|
|
||||||
// Get the provider from the agent for description generation
|
// Get the provider from the agent for description generation
|
||||||
let provider = self.agent.provider().await;
|
let provider = self.agent.provider().await;
|
||||||
|
|
||||||
@@ -436,7 +435,17 @@ impl Session {
|
|||||||
|
|
||||||
async fn process_agent_response(&mut self, interactive: bool) -> Result<()> {
|
async fn process_agent_response(&mut self, interactive: bool) -> Result<()> {
|
||||||
let session_id = session::Identifier::Path(self.session_file.clone());
|
let session_id = session::Identifier::Path(self.session_file.clone());
|
||||||
let mut stream = self.agent.reply(&self.messages, Some(session_id)).await?;
|
let mut stream = self
|
||||||
|
.agent
|
||||||
|
.reply(
|
||||||
|
&self.messages,
|
||||||
|
Some(SessionConfig {
|
||||||
|
id: session_id,
|
||||||
|
working_dir: std::env::current_dir()
|
||||||
|
.expect("failed to get current session working directory"),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
loop {
|
loop {
|
||||||
|
|||||||
@@ -479,6 +479,13 @@ pub fn display_session_info(resume: bool, provider: &str, model: &str, session_f
|
|||||||
style("logging to").dim(),
|
style("logging to").dim(),
|
||||||
style(session_file.display()).dim().cyan(),
|
style(session_file.display()).dim().cyan(),
|
||||||
);
|
);
|
||||||
|
println!(
|
||||||
|
" {} {}",
|
||||||
|
style("working directory:").dim(),
|
||||||
|
style(std::env::current_dir().unwrap().display())
|
||||||
|
.cyan()
|
||||||
|
.dim()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn display_greeting() {
|
pub fn display_greeting() {
|
||||||
|
|||||||
@@ -8,14 +8,18 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures::{stream::StreamExt, Stream};
|
use futures::{stream::StreamExt, Stream};
|
||||||
use goose::message::{Message, MessageContent};
|
|
||||||
use goose::session;
|
use goose::session;
|
||||||
|
use goose::{
|
||||||
|
agents::SessionConfig,
|
||||||
|
message::{Message, MessageContent},
|
||||||
|
};
|
||||||
|
|
||||||
use mcp_core::role::Role;
|
use mcp_core::role::Role;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
|
path::PathBuf,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
@@ -29,6 +33,7 @@ use tokio_stream::wrappers::ReceiverStream;
|
|||||||
struct ChatRequest {
|
struct ChatRequest {
|
||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
|
session_working_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom SSE response type for streaming messages
|
// Custom SSE response type for streaming messages
|
||||||
@@ -108,8 +113,8 @@ async fn handler(
|
|||||||
let (tx, rx) = mpsc::channel(100);
|
let (tx, rx) = mpsc::channel(100);
|
||||||
let stream = ReceiverStream::new(rx);
|
let stream = ReceiverStream::new(rx);
|
||||||
|
|
||||||
// Get messages directly from the request
|
|
||||||
let messages = request.messages;
|
let messages = request.messages;
|
||||||
|
let session_working_dir = request.session_working_dir;
|
||||||
|
|
||||||
// Generate a new session ID if not provided in the request
|
// Generate a new session ID if not provided in the request
|
||||||
let session_id = request
|
let session_id = request
|
||||||
@@ -149,7 +154,10 @@ async fn handler(
|
|||||||
let mut stream = match agent
|
let mut stream = match agent
|
||||||
.reply(
|
.reply(
|
||||||
&messages,
|
&messages,
|
||||||
Some(session::Identifier::Name(session_id.clone())),
|
Some(SessionConfig {
|
||||||
|
id: session::Identifier::Name(session_id.clone()),
|
||||||
|
working_dir: PathBuf::from(session_working_dir),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -246,6 +254,7 @@ async fn handler(
|
|||||||
struct AskRequest {
|
struct AskRequest {
|
||||||
prompt: String,
|
prompt: String,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
|
session_working_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
@@ -269,6 +278,8 @@ async fn ask_handler(
|
|||||||
return Err(StatusCode::UNAUTHORIZED);
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let session_working_dir = request.session_working_dir;
|
||||||
|
|
||||||
// Generate a new session ID if not provided in the request
|
// Generate a new session ID if not provided in the request
|
||||||
let session_id = request
|
let session_id = request
|
||||||
.session_id
|
.session_id
|
||||||
@@ -289,7 +300,10 @@ async fn ask_handler(
|
|||||||
let mut stream = match agent
|
let mut stream = match agent
|
||||||
.reply(
|
.reply(
|
||||||
&messages,
|
&messages,
|
||||||
Some(session::Identifier::Name(session_id.clone())),
|
Some(SessionConfig {
|
||||||
|
id: session::Identifier::Name(session_id.clone()),
|
||||||
|
working_dir: PathBuf::from(session_working_dir),
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -464,6 +478,7 @@ mod tests {
|
|||||||
serde_json::to_string(&AskRequest {
|
serde_json::to_string(&AskRequest {
|
||||||
prompt: "test prompt".to_string(),
|
prompt: "test prompt".to_string(),
|
||||||
session_id: Some("test-session".to_string()),
|
session_id: Some("test-session".to_string()),
|
||||||
|
session_working_dir: "test-working-dir".to_string(),
|
||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use futures::stream::BoxStream;
|
use futures::stream::BoxStream;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -13,6 +15,15 @@ use crate::session;
|
|||||||
use mcp_core::prompt::Prompt;
|
use mcp_core::prompt::Prompt;
|
||||||
use mcp_core::protocol::GetPromptResult;
|
use mcp_core::protocol::GetPromptResult;
|
||||||
|
|
||||||
|
/// Session configuration for an agent
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SessionConfig {
|
||||||
|
/// Unique identifier for the session
|
||||||
|
pub id: session::Identifier,
|
||||||
|
/// Working directory for the session
|
||||||
|
pub working_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
/// Core trait defining the behavior of an Agent
|
/// Core trait defining the behavior of an Agent
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Agent: Send + Sync {
|
pub trait Agent: Send + Sync {
|
||||||
@@ -20,7 +31,7 @@ pub trait Agent: Send + Sync {
|
|||||||
async fn reply(
|
async fn reply(
|
||||||
&self,
|
&self,
|
||||||
messages: &[Message],
|
messages: &[Message],
|
||||||
session_id: Option<session::Identifier>,
|
session: Option<SessionConfig>,
|
||||||
) -> Result<BoxStream<'_, Result<Message>>>;
|
) -> Result<BoxStream<'_, Result<Message>>>;
|
||||||
|
|
||||||
/// Add a new MCP client to the agent
|
/// Add a new MCP client to the agent
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ mod reference;
|
|||||||
mod summarize;
|
mod summarize;
|
||||||
mod truncate;
|
mod truncate;
|
||||||
|
|
||||||
pub use agent::Agent;
|
pub use agent::{Agent, SessionConfig};
|
||||||
pub use capabilities::Capabilities;
|
pub use capabilities::Capabilities;
|
||||||
pub use extension::ExtensionConfig;
|
pub use extension::ExtensionConfig;
|
||||||
pub use factory::{register_agent, AgentFactory};
|
pub use factory::{register_agent, AgentFactory};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use std::sync::Arc;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{debug, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
use super::agent::SessionConfig;
|
||||||
use super::Agent;
|
use super::Agent;
|
||||||
use crate::agents::capabilities::Capabilities;
|
use crate::agents::capabilities::Capabilities;
|
||||||
use crate::agents::extension::{ExtensionConfig, ExtensionResult};
|
use crate::agents::extension::{ExtensionConfig, ExtensionResult};
|
||||||
@@ -70,11 +71,11 @@ impl Agent for ReferenceAgent {
|
|||||||
// TODO implement
|
// TODO implement
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, messages), fields(user_message))]
|
#[instrument(skip(self, messages, session), fields(user_message))]
|
||||||
async fn reply(
|
async fn reply(
|
||||||
&self,
|
&self,
|
||||||
messages: &[Message],
|
messages: &[Message],
|
||||||
session_id: Option<session::Identifier>,
|
session: Option<SessionConfig>,
|
||||||
) -> anyhow::Result<BoxStream<'_, anyhow::Result<Message>>> {
|
) -> anyhow::Result<BoxStream<'_, anyhow::Result<Message>>> {
|
||||||
let mut messages = messages.to_vec();
|
let mut messages = messages.to_vec();
|
||||||
let reply_span = tracing::Span::current();
|
let reply_span = tracing::Span::current();
|
||||||
@@ -148,10 +149,11 @@ impl Agent for ReferenceAgent {
|
|||||||
capabilities.record_usage(usage.clone()).await;
|
capabilities.record_usage(usage.clone()).await;
|
||||||
|
|
||||||
// record usage for the session in the session file
|
// record usage for the session in the session file
|
||||||
if let Some(session_id) = session_id.clone() {
|
if let Some(session) = session.clone() {
|
||||||
// TODO: track session_id in langfuse tracing
|
// TODO: track session_id in langfuse tracing
|
||||||
let session_file = session::get_path(session_id);
|
let session_file = session::get_path(session.id);
|
||||||
let mut metadata = session::read_metadata(&session_file)?;
|
let mut metadata = session::read_metadata(&session_file)?;
|
||||||
|
metadata.working_dir = session.working_dir;
|
||||||
metadata.total_tokens = usage.usage.total_tokens;
|
metadata.total_tokens = usage.usage.total_tokens;
|
||||||
// The message count is the number of messages in the session + 1 for the response
|
// The message count is the number of messages in the session + 1 for the response
|
||||||
// The message count does not include the tool response till next iteration
|
// The message count does not include the tool response till next iteration
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use tokio::sync::mpsc;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{debug, error, instrument, warn};
|
use tracing::{debug, error, instrument, warn};
|
||||||
|
|
||||||
|
use super::agent::SessionConfig;
|
||||||
use super::detect_read_only_tools;
|
use super::detect_read_only_tools;
|
||||||
use super::Agent;
|
use super::Agent;
|
||||||
use crate::agents::capabilities::Capabilities;
|
use crate::agents::capabilities::Capabilities;
|
||||||
@@ -162,11 +163,11 @@ impl Agent for SummarizeAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, messages), fields(user_message))]
|
#[instrument(skip(self, messages, session), fields(user_message))]
|
||||||
async fn reply(
|
async fn reply(
|
||||||
&self,
|
&self,
|
||||||
messages: &[Message],
|
messages: &[Message],
|
||||||
session_id: Option<session::Identifier>,
|
session: Option<SessionConfig>,
|
||||||
) -> anyhow::Result<BoxStream<'_, anyhow::Result<Message>>> {
|
) -> anyhow::Result<BoxStream<'_, anyhow::Result<Message>>> {
|
||||||
let mut messages = messages.to_vec();
|
let mut messages = messages.to_vec();
|
||||||
let reply_span = tracing::Span::current();
|
let reply_span = tracing::Span::current();
|
||||||
@@ -246,10 +247,11 @@ impl Agent for SummarizeAgent {
|
|||||||
capabilities.record_usage(usage.clone()).await;
|
capabilities.record_usage(usage.clone()).await;
|
||||||
|
|
||||||
// record usage for the session in the session file
|
// record usage for the session in the session file
|
||||||
if let Some(session_id) = session_id.clone() {
|
if let Some(session) = session.clone() {
|
||||||
// TODO: track session_id in langfuse tracing
|
// TODO: track session_id in langfuse tracing
|
||||||
let session_file = session::get_path(session_id);
|
let session_file = session::get_path(session.id);
|
||||||
let mut metadata = session::read_metadata(&session_file)?;
|
let mut metadata = session::read_metadata(&session_file)?;
|
||||||
|
metadata.working_dir = session.working_dir;
|
||||||
metadata.total_tokens = usage.usage.total_tokens;
|
metadata.total_tokens = usage.usage.total_tokens;
|
||||||
// The message count is the number of messages in the session + 1 for the response
|
// The message count is the number of messages in the session + 1 for the response
|
||||||
// The message count does not include the tool response till next iteration
|
// The message count does not include the tool response till next iteration
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use tokio::sync::mpsc;
|
|||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{debug, error, instrument, warn};
|
use tracing::{debug, error, instrument, warn};
|
||||||
|
|
||||||
|
use super::agent::SessionConfig;
|
||||||
use super::detect_read_only_tools;
|
use super::detect_read_only_tools;
|
||||||
use super::Agent;
|
use super::Agent;
|
||||||
use crate::agents::capabilities::Capabilities;
|
use crate::agents::capabilities::Capabilities;
|
||||||
@@ -145,11 +146,11 @@ impl Agent for TruncateAgent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, messages, session_id), fields(user_message))]
|
#[instrument(skip(self, messages, session), fields(user_message))]
|
||||||
async fn reply(
|
async fn reply(
|
||||||
&self,
|
&self,
|
||||||
messages: &[Message],
|
messages: &[Message],
|
||||||
session_id: Option<session::Identifier>,
|
session: Option<SessionConfig>,
|
||||||
) -> anyhow::Result<BoxStream<'_, anyhow::Result<Message>>> {
|
) -> anyhow::Result<BoxStream<'_, anyhow::Result<Message>>> {
|
||||||
let mut messages = messages.to_vec();
|
let mut messages = messages.to_vec();
|
||||||
let reply_span = tracing::Span::current();
|
let reply_span = tracing::Span::current();
|
||||||
@@ -229,10 +230,11 @@ impl Agent for TruncateAgent {
|
|||||||
capabilities.record_usage(usage.clone()).await;
|
capabilities.record_usage(usage.clone()).await;
|
||||||
|
|
||||||
// record usage for the session in the session file
|
// record usage for the session in the session file
|
||||||
if let Some(session_id) = session_id.clone() {
|
if let Some(session) = session.clone() {
|
||||||
// TODO: track session_id in langfuse tracing
|
// TODO: track session_id in langfuse tracing
|
||||||
let session_file = session::get_path(session_id);
|
let session_file = session::get_path(session.id);
|
||||||
let mut metadata = session::read_metadata(&session_file)?;
|
let mut metadata = session::read_metadata(&session_file)?;
|
||||||
|
metadata.working_dir = session.working_dir;
|
||||||
metadata.total_tokens = usage.usage.total_tokens;
|
metadata.total_tokens = usage.usage.total_tokens;
|
||||||
// The message count is the number of messages in the session + 1 for the response
|
// The message count is the number of messages in the session + 1 for the response
|
||||||
// The message count does not include the tool response till next iteration
|
// The message count does not include the tool response till next iteration
|
||||||
|
|||||||
@@ -9,9 +9,18 @@ use std::io::{self, BufRead, Write};
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
fn get_home_dir() -> PathBuf {
|
||||||
|
choose_app_strategy(crate::config::APP_STRATEGY.clone())
|
||||||
|
.expect("goose requires a home dir")
|
||||||
|
.home_dir()
|
||||||
|
.to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
/// Metadata for a session, stored as the first line in the session file
|
/// Metadata for a session, stored as the first line in the session file
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct SessionMetadata {
|
pub struct SessionMetadata {
|
||||||
|
/// Working directory for the session
|
||||||
|
pub working_dir: PathBuf,
|
||||||
/// A short description of the session, typically 3 words or less
|
/// A short description of the session, typically 3 words or less
|
||||||
pub description: String,
|
pub description: String,
|
||||||
/// Number of messages in the session
|
/// Number of messages in the session
|
||||||
@@ -20,9 +29,35 @@ pub struct SessionMetadata {
|
|||||||
pub total_tokens: Option<i32>,
|
pub total_tokens: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom deserializer to handle old sessions without working_dir
|
||||||
|
impl<'de> Deserialize<'de> for SessionMetadata {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Helper {
|
||||||
|
description: String,
|
||||||
|
message_count: usize,
|
||||||
|
total_tokens: Option<i32>,
|
||||||
|
working_dir: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let helper = Helper::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
Ok(SessionMetadata {
|
||||||
|
description: helper.description,
|
||||||
|
message_count: helper.message_count,
|
||||||
|
total_tokens: helper.total_tokens,
|
||||||
|
working_dir: helper.working_dir.unwrap_or_else(get_home_dir),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SessionMetadata {
|
impl SessionMetadata {
|
||||||
pub fn new() -> Self {
|
pub fn new(working_dir: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
working_dir,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
message_count: 0,
|
message_count: 0,
|
||||||
total_tokens: None,
|
total_tokens: None,
|
||||||
@@ -32,7 +67,7 @@ impl SessionMetadata {
|
|||||||
|
|
||||||
impl Default for SessionMetadata {
|
impl Default for SessionMetadata {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new(get_home_dir())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +203,7 @@ pub fn read_messages(session_file: &Path) -> Result<Vec<Message>> {
|
|||||||
/// Returns default empty metadata if the file doesn't exist or has no metadata.
|
/// Returns default empty metadata if the file doesn't exist or has no metadata.
|
||||||
pub fn read_metadata(session_file: &Path) -> Result<SessionMetadata> {
|
pub fn read_metadata(session_file: &Path) -> Result<SessionMetadata> {
|
||||||
if !session_file.exists() {
|
if !session_file.exists() {
|
||||||
return Ok(SessionMetadata::new());
|
return Ok(SessionMetadata::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = fs::File::open(session_file)?;
|
let file = fs::File::open(session_file)?;
|
||||||
@@ -182,12 +217,12 @@ pub fn read_metadata(session_file: &Path) -> Result<SessionMetadata> {
|
|||||||
Ok(metadata) => Ok(metadata),
|
Ok(metadata) => Ok(metadata),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// If the first line isn't metadata, return default
|
// If the first line isn't metadata, return default
|
||||||
Ok(SessionMetadata::new())
|
Ok(SessionMetadata::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Empty file, return default
|
// Empty file, return default
|
||||||
Ok(SessionMetadata::new())
|
Ok(SessionMetadata::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { ConfirmationModal } from './components/ui/ConfirmationModal';
|
|||||||
import { ToastContainer } from 'react-toastify';
|
import { ToastContainer } from 'react-toastify';
|
||||||
import { extractExtensionName } from './components/settings/extensions/utils';
|
import { extractExtensionName } from './components/settings/extensions/utils';
|
||||||
import { GoosehintsModal } from './components/GoosehintsModal';
|
import { GoosehintsModal } from './components/GoosehintsModal';
|
||||||
|
import { SessionDetails, fetchSessionDetails } from './sessions';
|
||||||
|
|
||||||
import WelcomeView from './components/WelcomeView';
|
import WelcomeView from './components/WelcomeView';
|
||||||
import ChatView from './components/ChatView';
|
import ChatView from './components/ChatView';
|
||||||
@@ -37,7 +38,12 @@ export type View =
|
|||||||
|
|
||||||
export type ViewConfig = {
|
export type ViewConfig = {
|
||||||
view: View;
|
view: View;
|
||||||
viewOptions?: SettingsViewOptions | Record<any, any>;
|
viewOptions?:
|
||||||
|
| SettingsViewOptions
|
||||||
|
| {
|
||||||
|
resumedSession?: SessionDetails;
|
||||||
|
}
|
||||||
|
| Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -51,6 +57,7 @@ export default function App() {
|
|||||||
viewOptions: {},
|
viewOptions: {},
|
||||||
});
|
});
|
||||||
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
|
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
|
||||||
|
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
||||||
|
|
||||||
const { switchModel } = useModel();
|
const { switchModel } = useModel();
|
||||||
const { addRecentModel } = useRecentModels();
|
const { addRecentModel } = useRecentModels();
|
||||||
@@ -135,6 +142,7 @@ export default function App() {
|
|||||||
addRecentModel(model);
|
addRecentModel(model);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// TODO: add sessionError state and show error screen with option to start fresh
|
||||||
console.error('Failed to initialize with stored provider:', error);
|
console.error('Failed to initialize with stored provider:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +151,39 @@ export default function App() {
|
|||||||
setupStoredProvider();
|
setupStoredProvider();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Check for resumeSessionId in URL parameters
|
||||||
|
useEffect(() => {
|
||||||
|
const checkForResumeSession = async () => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const resumeSessionId = urlParams.get('resumeSessionId');
|
||||||
|
|
||||||
|
if (!resumeSessionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoadingSession(true);
|
||||||
|
try {
|
||||||
|
const sessionDetails = await fetchSessionDetails(resumeSessionId);
|
||||||
|
|
||||||
|
// Only set view if we have valid session details
|
||||||
|
if (sessionDetails && sessionDetails.session_id) {
|
||||||
|
setView('chat', {
|
||||||
|
resumedSession: sessionDetails,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('Invalid session details received');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch session details:', error);
|
||||||
|
} finally {
|
||||||
|
// Always clear the loading state
|
||||||
|
setIsLoadingSession(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkForResumeSession();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleFatalError = (_: any, errorMessage: string) => {
|
const handleFatalError = (_: any, errorMessage: string) => {
|
||||||
setFatalError(errorMessage);
|
setFatalError(errorMessage);
|
||||||
@@ -160,6 +201,13 @@ export default function App() {
|
|||||||
return () => window.electron.off('set-view', handleSetView);
|
return () => window.electron.off('set-view', handleSetView);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Add cleanup for session states when view changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (view !== 'chat') {
|
||||||
|
setIsLoadingSession(false);
|
||||||
|
}
|
||||||
|
}, [view]);
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
if (pendingLink && !isInstalling) {
|
if (pendingLink && !isInstalling) {
|
||||||
setIsInstalling(true);
|
setIsInstalling(true);
|
||||||
@@ -250,13 +298,18 @@ export default function App() {
|
|||||||
{view === 'alphaConfigureProviders' && (
|
{view === 'alphaConfigureProviders' && (
|
||||||
<ProviderSettings onClose={() => setView('chat')} />
|
<ProviderSettings onClose={() => setView('chat')} />
|
||||||
)}
|
)}
|
||||||
{view === 'chat' && (
|
{view === 'chat' && !isLoadingSession && (
|
||||||
<ChatView
|
<ChatView
|
||||||
setView={setView}
|
setView={setView}
|
||||||
viewOptions={viewOptions}
|
viewOptions={viewOptions}
|
||||||
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{view === 'chat' && isLoadingSession && (
|
||||||
|
<div className="flex justify-center items-center py-12">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-textStandard"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{view === 'sessions' && <SessionsView setView={setView} />}
|
{view === 'sessions' && <SessionsView setView={setView} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,63 +48,28 @@ export default function ChatView({
|
|||||||
const resumedSession = viewOptions?.resumedSession;
|
const resumedSession = viewOptions?.resumedSession;
|
||||||
|
|
||||||
// Generate or retrieve session ID
|
// Generate or retrieve session ID
|
||||||
const [sessionId] = useState(() => {
|
// The session ID should not change for the duration of the chat
|
||||||
// If resuming a session, use that session ID
|
const sessionId = resumedSession?.session_id || generateSessionId();
|
||||||
if (resumedSession?.session_id) {
|
|
||||||
// Store the resumed session ID in sessionStorage
|
|
||||||
window.sessionStorage.setItem('goose-session-id', resumedSession.session_id);
|
|
||||||
return resumedSession.session_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For a new chat, generate a new session ID
|
|
||||||
const newId = generateSessionId();
|
|
||||||
window.sessionStorage.setItem('goose-session-id', newId);
|
|
||||||
return newId;
|
|
||||||
});
|
|
||||||
|
|
||||||
const [chat, setChat] = useState<ChatType>(() => {
|
const [chat, setChat] = useState<ChatType>(() => {
|
||||||
// If resuming a session, convert the session messages to our format
|
// If resuming a session, convert the session messages to our format
|
||||||
if (resumedSession) {
|
if (resumedSession) {
|
||||||
try {
|
return {
|
||||||
// Convert the resumed session messages to the expected format
|
id: resumedSession.session_id,
|
||||||
const convertedMessages = resumedSession.messages.map((msg): Message => {
|
title: resumedSession.metadata?.description || `ID: ${resumedSession.session_id}`,
|
||||||
return {
|
messages: resumedSession.messages,
|
||||||
id: `${msg.role}-${msg.created}`,
|
messageHistoryIndex: resumedSession.messages.length,
|
||||||
role: msg.role,
|
};
|
||||||
created: msg.created,
|
|
||||||
content: msg.content,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: Date.now(),
|
|
||||||
title: resumedSession.metadata?.description || `ID: ${resumedSession.session_id}`,
|
|
||||||
messageHistoryIndex: convertedMessages.length,
|
|
||||||
messages: convertedMessages,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse resumed session:', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load saved chat from sessionStorage
|
|
||||||
const savedChat = window.sessionStorage.getItem(`goose-chat-${sessionId}`);
|
|
||||||
if (savedChat) {
|
|
||||||
try {
|
|
||||||
return JSON.parse(savedChat);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse saved chat:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return default chat if no saved chat exists
|
|
||||||
return {
|
return {
|
||||||
id: Date.now(),
|
id: sessionId,
|
||||||
title: 'Chat 1',
|
title: 'New Chat',
|
||||||
messages: [],
|
messages: [],
|
||||||
messageHistoryIndex: 0,
|
messageHistoryIndex: 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
|
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
|
||||||
const [hasMessages, setHasMessages] = useState(false);
|
const [hasMessages, setHasMessages] = useState(false);
|
||||||
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
|
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
|
||||||
@@ -124,8 +89,8 @@ export default function ChatView({
|
|||||||
handleSubmit: _submitMessage,
|
handleSubmit: _submitMessage,
|
||||||
} = useMessageStream({
|
} = useMessageStream({
|
||||||
api: getApiUrl('/reply'),
|
api: getApiUrl('/reply'),
|
||||||
initialMessages: chat?.messages || [],
|
initialMessages: resumedSession ? resumedSession.messages : chat?.messages || [],
|
||||||
body: { session_id: sessionId },
|
body: { session_id: sessionId, session_working_dir: window.appConfig.get('GOOSE_WORKING_DIR') },
|
||||||
onFinish: async (message, _reason) => {
|
onFinish: async (message, _reason) => {
|
||||||
window.electron.stopPowerSaveBlocker();
|
window.electron.stopPowerSaveBlocker();
|
||||||
|
|
||||||
@@ -155,15 +120,9 @@ export default function ChatView({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setChat((prevChat) => {
|
setChat((prevChat) => {
|
||||||
const updatedChat = { ...prevChat, messages };
|
const updatedChat = { ...prevChat, messages };
|
||||||
// Save to sessionStorage
|
|
||||||
try {
|
|
||||||
window.sessionStorage.setItem(`goose-chat-${sessionId}`, JSON.stringify(updatedChat));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to save chat to sessionStorage:', e);
|
|
||||||
}
|
|
||||||
return updatedChat;
|
return updatedChat;
|
||||||
});
|
});
|
||||||
}, [messages, sessionId]);
|
}, [messages, sessionId, resumedSession]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
|
|||||||
@@ -247,7 +247,10 @@ export default function MoreMenu({
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
window.electron.createChatWindow();
|
window.electron.createChatWindow(
|
||||||
|
undefined,
|
||||||
|
window.appConfig.get('GOOSE_WORKING_DIR')
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
className="w-full text-left p-2 text-sm hover:bg-bgSubtle transition-colors"
|
className="w-full text-left p-2 text-sm hover:bg-bgSubtle transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Clock, MessageSquare, ArrowLeft, AlertCircle } from 'lucide-react';
|
import { Clock, MessageSquare, Folder, AlertCircle } from 'lucide-react';
|
||||||
import { type SessionDetails } from '../../sessions';
|
import { type SessionDetails } from '../../sessions';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
@@ -69,6 +69,10 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
|
|||||||
<Clock className="w-4 h-4 mr-1" />
|
<Clock className="w-4 h-4 mr-1" />
|
||||||
{new Date(session.messages[0]?.created * 1000).toLocaleString()}
|
{new Date(session.messages[0]?.created * 1000).toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<Folder className="w-4 h-4 mr-1" />
|
||||||
|
{session.metadata.working_dir}
|
||||||
|
</span>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<MessageSquare className="w-4 h-4 mr-1" />
|
<MessageSquare className="w-4 h-4 mr-1" />
|
||||||
{session.metadata.message_count} messages
|
{session.metadata.message_count} messages
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { ViewConfig } from '../../App';
|
import { ViewConfig } from '../../App';
|
||||||
import { MessageSquare, Loader, AlertCircle, Calendar, ChevronRight } from 'lucide-react';
|
import { MessageSquare, Loader, AlertCircle, Calendar, ChevronRight, Folder } from 'lucide-react';
|
||||||
import { fetchSessions, type Session } from '../../sessions';
|
import { fetchSessions, type Session } from '../../sessions';
|
||||||
import { Card } from '../ui/card';
|
import { Card } from '../ui/card';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
@@ -104,9 +104,15 @@ const SessionListView: React.FC<SessionListViewProps> = ({ setView, onSelectSess
|
|||||||
<h3 className="text-base font-medium text-textStandard truncate">
|
<h3 className="text-base font-medium text-textStandard truncate">
|
||||||
{session.metadata.description || session.id}
|
{session.metadata.description || session.id}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center mt-1 text-textSubtle text-sm">
|
<div className="flex gap-3">
|
||||||
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
<div className="flex items-center text-textSubtle text-sm">
|
||||||
<span className="truncate">{formatDate(session.modified)}</span>
|
<Calendar className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||||
|
<span className="truncate">{formatDate(session.modified)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-textSubtle text-sm">
|
||||||
|
<Folder className="w-3 h-3 mr-1 flex-shrink-0" />
|
||||||
|
<span className="truncate">{session.metadata.working_dir}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,26 @@ const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
|
|||||||
|
|
||||||
const handleResumeSession = () => {
|
const handleResumeSession = () => {
|
||||||
if (selectedSession) {
|
if (selectedSession) {
|
||||||
// Pass the session to ChatView for resuming
|
// Get the working directory from the session metadata
|
||||||
setView('chat', {
|
const workingDir = selectedSession.metadata.working_dir;
|
||||||
resumedSession: selectedSession,
|
|
||||||
});
|
if (workingDir) {
|
||||||
|
console.log(
|
||||||
|
`Resuming session with ID: ${selectedSession.session_id}, in working dir: ${workingDir}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a new chat window with the working directory and session ID
|
||||||
|
window.electron.createChatWindow(
|
||||||
|
undefined,
|
||||||
|
workingDir,
|
||||||
|
undefined,
|
||||||
|
selectedSession.session_id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fallback if no working directory is found
|
||||||
|
console.error('No working directory found in session metadata');
|
||||||
|
// We could show a toast or alert here
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -268,8 +268,11 @@ export function useMessageStream({
|
|||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
abortControllerRef.current = abortController;
|
abortControllerRef.current = abortController;
|
||||||
|
|
||||||
// Log the request messages for debugging
|
// Log request details for debugging
|
||||||
console.log('Sending messages to server:', JSON.stringify(requestMessages, null, 2));
|
console.log('Request details:', {
|
||||||
|
messages: requestMessages,
|
||||||
|
body: extraMetadataRef.current.body,
|
||||||
|
});
|
||||||
|
|
||||||
// Send request to the server
|
// Send request to the server
|
||||||
const response = await fetch(api, {
|
const response = await fetch(api, {
|
||||||
|
|||||||
@@ -115,7 +115,13 @@ let appConfig = {
|
|||||||
let windowCounter = 0;
|
let windowCounter = 0;
|
||||||
const windowMap = new Map<number, BrowserWindow>();
|
const windowMap = new Map<number, BrowserWindow>();
|
||||||
|
|
||||||
const createChat = async (app, query?: string, dir?: string, version?: string) => {
|
const createChat = async (
|
||||||
|
app,
|
||||||
|
query?: string,
|
||||||
|
dir?: string,
|
||||||
|
version?: string,
|
||||||
|
resumeSessionId?: string
|
||||||
|
) => {
|
||||||
// Apply current environment settings before creating chat
|
// Apply current environment settings before creating chat
|
||||||
updateEnvironmentVariables(envToggles);
|
updateEnvironmentVariables(envToggles);
|
||||||
|
|
||||||
@@ -158,7 +164,18 @@ const createChat = async (app, query?: string, dir?: string, version?: string) =
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Load the index.html of the app.
|
// Load the index.html of the app.
|
||||||
const queryParam = query ? `?initialQuery=${encodeURIComponent(query)}` : '';
|
let queryParams = '';
|
||||||
|
if (query) {
|
||||||
|
queryParams = `?initialQuery=${encodeURIComponent(query)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resumeSessionId to query params if provided
|
||||||
|
if (resumeSessionId) {
|
||||||
|
queryParams = queryParams
|
||||||
|
? `${queryParams}&resumeSessionId=${encodeURIComponent(resumeSessionId)}`
|
||||||
|
: `?resumeSessionId=${encodeURIComponent(resumeSessionId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
const primaryDisplay = electron.screen.getPrimaryDisplay();
|
||||||
const { width } = primaryDisplay.workAreaSize;
|
const { width } = primaryDisplay.workAreaSize;
|
||||||
|
|
||||||
@@ -173,13 +190,13 @@ const createChat = async (app, query?: string, dir?: string, version?: string) =
|
|||||||
mainWindow.setPosition(baseXPosition + xOffset, 100);
|
mainWindow.setPosition(baseXPosition + xOffset, 100);
|
||||||
|
|
||||||
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
|
||||||
mainWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${queryParam}`);
|
mainWindow.loadURL(`${MAIN_WINDOW_VITE_DEV_SERVER_URL}${queryParams}`);
|
||||||
} else {
|
} else {
|
||||||
// In production, we need to use a proper file protocol URL with correct base path
|
// In production, we need to use a proper file protocol URL with correct base path
|
||||||
const indexPath = path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`);
|
const indexPath = path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`);
|
||||||
console.log('Loading production path:', indexPath);
|
console.log('Loading production path:', indexPath);
|
||||||
mainWindow.loadFile(indexPath, {
|
mainWindow.loadFile(indexPath, {
|
||||||
search: queryParam ? queryParam.slice(1) : undefined,
|
search: queryParams ? queryParams.slice(1) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,12 +477,12 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('create-chat-window', (_, query, dir, version) => {
|
ipcMain.on('create-chat-window', (_, query, dir, version, resumeSessionId) => {
|
||||||
if (!dir?.trim()) {
|
if (!dir?.trim()) {
|
||||||
const recentDirs = loadRecentDirs();
|
const recentDirs = loadRecentDirs();
|
||||||
dir = recentDirs.length > 0 ? recentDirs[0] : null;
|
dir = recentDirs.length > 0 ? recentDirs[0] : null;
|
||||||
}
|
}
|
||||||
createChat(app, query, dir, version);
|
createChat(app, query, dir, version, resumeSessionId);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('directory-chooser', (_, replace: boolean = false) => {
|
ipcMain.on('directory-chooser', (_, replace: boolean = false) => {
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ type ElectronAPI = {
|
|||||||
getConfig: () => Record<string, any>;
|
getConfig: () => Record<string, any>;
|
||||||
hideWindow: () => void;
|
hideWindow: () => void;
|
||||||
directoryChooser: (replace: string) => void;
|
directoryChooser: (replace: string) => void;
|
||||||
createChatWindow: (query?: string, dir?: string, version?: string) => void;
|
createChatWindow: (
|
||||||
|
query?: string,
|
||||||
|
dir?: string,
|
||||||
|
version?: string,
|
||||||
|
resumeSessionId?: string
|
||||||
|
) => void;
|
||||||
logInfo: (txt: string) => void;
|
logInfo: (txt: string) => void;
|
||||||
showNotification: (data: any) => void;
|
showNotification: (data: any) => void;
|
||||||
openInChrome: (url: string) => void;
|
openInChrome: (url: string) => void;
|
||||||
@@ -18,7 +23,9 @@ type ElectronAPI = {
|
|||||||
startPowerSaveBlocker: () => Promise<number>;
|
startPowerSaveBlocker: () => Promise<number>;
|
||||||
stopPowerSaveBlocker: () => Promise<void>;
|
stopPowerSaveBlocker: () => Promise<void>;
|
||||||
getBinaryPath: (binaryName: string) => Promise<string>;
|
getBinaryPath: (binaryName: string) => Promise<string>;
|
||||||
readFile: (directory: string) => Promise<{ file: string; filePath: string; error: string; found: boolean }>;
|
readFile: (
|
||||||
|
directory: string
|
||||||
|
) => Promise<{ file: string; filePath: string; error: string; found: boolean }>;
|
||||||
writeFile: (directory: string, content: string) => Promise<boolean>;
|
writeFile: (directory: string, content: string) => Promise<boolean>;
|
||||||
on: (
|
on: (
|
||||||
channel: string,
|
channel: string,
|
||||||
@@ -40,8 +47,8 @@ const electronAPI: ElectronAPI = {
|
|||||||
getConfig: () => config,
|
getConfig: () => config,
|
||||||
hideWindow: () => ipcRenderer.send('hide-window'),
|
hideWindow: () => ipcRenderer.send('hide-window'),
|
||||||
directoryChooser: (replace: string) => ipcRenderer.send('directory-chooser', replace),
|
directoryChooser: (replace: string) => ipcRenderer.send('directory-chooser', replace),
|
||||||
createChatWindow: (query?: string, dir?: string, version?: string) =>
|
createChatWindow: (query?: string, dir?: string, version?: string, resumeSessionId?: string) =>
|
||||||
ipcRenderer.send('create-chat-window', query, dir, version),
|
ipcRenderer.send('create-chat-window', query, dir, version, resumeSessionId),
|
||||||
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
|
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
|
||||||
showNotification: (data: any) => ipcRenderer.send('notify', data),
|
showNotification: (data: any) => ipcRenderer.send('notify', data),
|
||||||
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
|
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
|
||||||
@@ -53,7 +60,8 @@ const electronAPI: ElectronAPI = {
|
|||||||
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
stopPowerSaveBlocker: () => ipcRenderer.invoke('stop-power-save-blocker'),
|
||||||
getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName),
|
getBinaryPath: (binaryName: string) => ipcRenderer.invoke('get-binary-path', binaryName),
|
||||||
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
|
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
|
||||||
writeFile: (filePath: string, content: string) => ipcRenderer.invoke('write-file', filePath, content),
|
writeFile: (filePath: string, content: string) =>
|
||||||
|
ipcRenderer.invoke('write-file', filePath, content),
|
||||||
on: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
on: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
|
||||||
ipcRenderer.on(channel, callback);
|
ipcRenderer.on(channel, callback);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ export interface SessionMetadata {
|
|||||||
description: string;
|
description: string;
|
||||||
message_count: number;
|
message_count: number;
|
||||||
total_tokens: number | null;
|
total_tokens: number | null;
|
||||||
|
working_dir: string; // Required in type, but may be missing in old sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to ensure working directory is set
|
||||||
|
export function ensureWorkingDir(metadata: Partial<SessionMetadata>): SessionMetadata {
|
||||||
|
return {
|
||||||
|
description: metadata.description || '',
|
||||||
|
message_count: metadata.message_count || 0,
|
||||||
|
total_tokens: metadata.total_tokens || null,
|
||||||
|
working_dir: metadata.working_dir || process.env.HOME || '',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Session {
|
export interface Session {
|
||||||
@@ -67,9 +78,13 @@ export async function fetchSessions(): Promise<SessionsResponse> {
|
|||||||
|
|
||||||
// TODO: remove this logic once everyone migrates to the new sessions format
|
// TODO: remove this logic once everyone migrates to the new sessions format
|
||||||
// for now, filter out sessions whose description is empty (old CLI sessions)
|
// for now, filter out sessions whose description is empty (old CLI sessions)
|
||||||
const sessions = (await response.json()).sessions.filter(
|
const rawSessions = await response.json();
|
||||||
(session: Session) => session.metadata.description !== ''
|
const sessions = rawSessions.sessions
|
||||||
);
|
.filter((session: Session) => session.metadata.description !== '')
|
||||||
|
.map((session: Session) => ({
|
||||||
|
...session,
|
||||||
|
metadata: ensureWorkingDir(session.metadata),
|
||||||
|
}));
|
||||||
|
|
||||||
// order sessions by 'modified' date descending
|
// order sessions by 'modified' date descending
|
||||||
sessions.sort(
|
sessions.sort(
|
||||||
@@ -102,7 +117,11 @@ export async function fetchSessionDetails(sessionId: string): Promise<SessionDet
|
|||||||
throw new Error(`Failed to fetch session details: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to fetch session details: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await response.json();
|
const details = await response.json();
|
||||||
|
return {
|
||||||
|
...details,
|
||||||
|
metadata: ensureWorkingDir(details.metadata),
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching session details for ${sessionId}:`, error);
|
console.error(`Error fetching session details for ${sessionId}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
Reference in New Issue
Block a user