UI update with sidebar and settings tabs (#3288)

Co-authored-by: Nahiyan Khan <nahiyan@squareup.com>
Co-authored-by: Taylor Ho <taylorkmho@gmail.com>
Co-authored-by: Lily Delalande <119957291+lily-de@users.noreply.github.com>
Co-authored-by: Spence <spencrmartin@gmail.com>
Co-authored-by: spencrmartin <spencermartin@squareup.com>
Co-authored-by: Judson Stephenson <Jud@users.noreply.github.com>
Co-authored-by: Max Novich <mnovich@squareup.com>
Co-authored-by: Best Codes <106822363+The-Best-Codes@users.noreply.github.com>
Co-authored-by: caroline-a-mckenzie <cmckenzie@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Zane
2025-07-15 17:24:41 -07:00
committed by GitHub
parent b22f50d1a1
commit 77ea27f5f5
195 changed files with 20633 additions and 7238 deletions
+1
View File
@@ -4,6 +4,7 @@ pub mod context_mgmt;
pub mod message;
pub mod model;
pub mod permission;
pub mod project;
pub mod prompt_template;
pub mod providers;
pub mod recipe;
+68
View File
@@ -0,0 +1,68 @@
pub mod storage;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use utoipa::ToSchema;
/// Main project structure that holds project metadata and associated sessions
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Project {
/// Unique identifier for the project
pub id: String,
/// Display name of the project
pub name: String,
/// Optional description of the project
pub description: Option<String>,
/// Default working directory for sessions in this project
#[schema(value_type = String, example = "/home/user/projects/my-project")]
pub default_directory: PathBuf,
/// When the project was created
pub created_at: DateTime<Utc>,
/// When the project was last updated
pub updated_at: DateTime<Utc>,
/// List of session IDs associated with this project
pub session_ids: Vec<String>,
}
/// Simplified project metadata for listing
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ProjectMetadata {
/// Unique identifier for the project
pub id: String,
/// Display name of the project
pub name: String,
/// Optional description of the project
pub description: Option<String>,
/// Default working directory for sessions in this project
#[schema(value_type = String)]
pub default_directory: PathBuf,
/// Number of sessions in this project
pub session_count: usize,
/// When the project was created
pub created_at: DateTime<Utc>,
/// When the project was last updated
pub updated_at: DateTime<Utc>,
}
impl From<&Project> for ProjectMetadata {
fn from(project: &Project) -> Self {
ProjectMetadata {
id: project.id.clone(),
name: project.name.clone(),
description: project.description.clone(),
default_directory: project.default_directory.clone(),
session_count: project.session_ids.len(),
created_at: project.created_at,
updated_at: project.updated_at,
}
}
}
// Re-export storage functions
pub use storage::{
add_session_to_project, create_project, delete_project, ensure_project_dir, get_project,
list_projects, remove_session_from_project, update_project,
};
+239
View File
@@ -0,0 +1,239 @@
use crate::project::{Project, ProjectMetadata};
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs};
use serde_json;
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use tracing::{error, info};
const APP_NAME: &str = "goose";
/// Ensure the project directory exists and return its path
pub fn ensure_project_dir() -> Result<PathBuf> {
let app_strategy = AppStrategyArgs {
top_level_domain: "Block".to_string(),
author: "Block".to_string(),
app_name: APP_NAME.to_string(),
};
let data_dir = choose_app_strategy(app_strategy)
.context("goose requires a home dir")?
.data_dir()
.join("projects");
if !data_dir.exists() {
fs::create_dir_all(&data_dir)?;
}
Ok(data_dir)
}
/// Generate a unique project ID
fn generate_project_id() -> String {
use rand::Rng;
let timestamp = Utc::now().timestamp();
let random: u32 = rand::thread_rng().gen();
format!("proj_{}_{}", timestamp, random)
}
/// Get the path for a specific project file
fn get_project_path(project_id: &str) -> Result<PathBuf> {
let project_dir = ensure_project_dir()?;
Ok(project_dir.join(format!("{}.json", project_id)))
}
/// Create a new project
pub fn create_project(
name: String,
description: Option<String>,
default_directory: PathBuf,
) -> Result<Project> {
let project_dir = ensure_project_dir()?;
// Validate the default directory exists
if !default_directory.exists() {
return Err(anyhow!(
"Default directory does not exist: {:?}",
default_directory
));
}
let now = Utc::now();
let project = Project {
id: generate_project_id(),
name,
description,
default_directory,
created_at: now,
updated_at: now,
session_ids: Vec::new(),
};
// Save the project
let project_path = project_dir.join(format!("{}.json", project.id));
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Created project {} at {:?}", project.id, project_path);
Ok(project)
}
/// Update an existing project
pub fn update_project(
project_id: &str,
name: Option<String>,
description: Option<Option<String>>,
default_directory: Option<PathBuf>,
) -> Result<Project> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
// Read existing project
let mut project: Project = serde_json::from_reader(File::open(&project_path)?)?;
// Update fields
if let Some(new_name) = name {
project.name = new_name;
}
if let Some(new_description) = description {
project.description = new_description;
}
if let Some(new_directory) = default_directory {
if !new_directory.exists() {
return Err(anyhow!(
"Default directory does not exist: {:?}",
new_directory
));
}
project.default_directory = new_directory;
}
project.updated_at = Utc::now();
// Save updated project
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Updated project {}", project_id);
Ok(project)
}
/// Delete a project (does not delete associated sessions)
pub fn delete_project(project_id: &str) -> Result<()> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
fs::remove_file(&project_path)?;
info!("Deleted project {}", project_id);
Ok(())
}
/// List all projects
pub fn list_projects() -> Result<Vec<ProjectMetadata>> {
let project_dir = ensure_project_dir()?;
let mut projects = Vec::new();
if let Ok(entries) = fs::read_dir(&project_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
match serde_json::from_reader::<_, Project>(File::open(&path)?) {
Ok(project) => {
projects.push(ProjectMetadata::from(&project));
}
Err(e) => {
error!("Failed to read project file {:?}: {}", path, e);
}
}
}
}
}
// Sort by updated_at descending
projects.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
Ok(projects)
}
/// Get a specific project
pub fn get_project(project_id: &str) -> Result<Project> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
let project: Project = serde_json::from_reader(File::open(&project_path)?)?;
Ok(project)
}
/// Add a session to a project
pub fn add_session_to_project(project_id: &str, session_id: &str) -> Result<()> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
// Read project
let mut project: Project = serde_json::from_reader(File::open(&project_path)?)?;
// Check if session already exists in project
if project.session_ids.contains(&session_id.to_string()) {
return Ok(()); // Already added
}
// Add session and update timestamp
project.session_ids.push(session_id.to_string());
project.updated_at = Utc::now();
// Save updated project
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Added session {} to project {}", session_id, project_id);
Ok(())
}
/// Remove a session from a project
pub fn remove_session_from_project(project_id: &str, session_id: &str) -> Result<()> {
let project_path = get_project_path(project_id)?;
if !project_path.exists() {
return Err(anyhow!("Project not found: {}", project_id));
}
// Read project
let mut project: Project = serde_json::from_reader(File::open(&project_path)?)?;
// Remove session
let original_len = project.session_ids.len();
project.session_ids.retain(|id| id != session_id);
if project.session_ids.len() == original_len {
return Ok(()); // Session wasn't in project
}
project.updated_at = Utc::now();
// Save updated project
let mut file = File::create(&project_path)?;
let json = serde_json::to_string_pretty(&project)?;
file.write_all(json.as_bytes())?;
info!("Removed session {} from project {}", session_id, project_id);
Ok(())
}
+1
View File
@@ -1267,6 +1267,7 @@ async fn run_scheduled_job_internal(
working_dir: current_dir.clone(),
description: String::new(),
schedule_id: Some(job.id.clone()),
project_id: None,
message_count: all_session_messages.len(),
total_tokens: None,
input_tokens: None,
+5
View File
@@ -41,6 +41,8 @@ pub struct SessionMetadata {
pub description: String,
/// ID of the schedule that triggered this session, if any
pub schedule_id: Option<String>,
/// ID of the project this session belongs to, if any
pub project_id: Option<String>,
/// Number of messages in the session
pub message_count: usize,
/// The total number of tokens used in the session. Retrieved from the provider's last usage.
@@ -68,6 +70,7 @@ impl<'de> Deserialize<'de> for SessionMetadata {
description: String,
message_count: usize,
schedule_id: Option<String>, // For backward compatibility
project_id: Option<String>, // For backward compatibility
total_tokens: Option<i32>,
input_tokens: Option<i32>,
output_tokens: Option<i32>,
@@ -89,6 +92,7 @@ impl<'de> Deserialize<'de> for SessionMetadata {
description: helper.description,
message_count: helper.message_count,
schedule_id: helper.schedule_id,
project_id: helper.project_id,
total_tokens: helper.total_tokens,
input_tokens: helper.input_tokens,
output_tokens: helper.output_tokens,
@@ -113,6 +117,7 @@ impl SessionMetadata {
working_dir,
description: String::new(),
schedule_id: None,
project_id: None,
message_count: 0,
total_tokens: None,
input_tokens: None,
+2
View File
@@ -32,6 +32,7 @@ pub struct ConfigurableMockScheduler {
sessions_data: Arc<Mutex<HashMap<String, Vec<(String, SessionMetadata)>>>>,
}
#[allow(dead_code)]
impl ConfigurableMockScheduler {
pub fn new() -> Self {
Self {
@@ -404,6 +405,7 @@ pub fn create_test_session_metadata(message_count: usize, working_dir: &str) ->
working_dir: PathBuf::from(working_dir),
description: "Test session".to_string(),
schedule_id: Some("test_job".to_string()),
project_id: None,
total_tokens: Some(100),
input_tokens: Some(50),
output_tokens: Some(50),