Merge sessions/interactive/664ee473
This commit is contained in:
@@ -308,6 +308,7 @@ async fn handle_command(
|
|||||||
workspace_dir,
|
workspace_dir,
|
||||||
cli.new_session,
|
cli.new_session,
|
||||||
None, // agent_name (not in agent mode)
|
None, // agent_name (not in agent mode)
|
||||||
|
None, // initial_project (not supported in accumulative mode yet)
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ pub async fn run_agent_mode(
|
|||||||
&workspace_dir,
|
&workspace_dir,
|
||||||
new_session,
|
new_session,
|
||||||
Some(agent_name), // agent name for prompt (e.g., "butler>")
|
Some(agent_name), // agent name for prompt (e.g., "butler>")
|
||||||
|
None, // initial_project (not supported in agent mode yet)
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,4 +122,8 @@ pub struct Cli {
|
|||||||
/// Disable automatic memory update reminder at end of agent mode
|
/// Disable automatic memory update reminder at end of agent mode
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub no_auto_memory: bool,
|
pub no_auto_memory: bool,
|
||||||
|
|
||||||
|
/// Load a project from the given path at startup (like /project but without auto-prompt)
|
||||||
|
#[arg(long, value_name = "PATH")]
|
||||||
|
pub project: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rustyline::Editor;
|
use rustyline::Editor;
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use g3_core::ui_writer::UiWriter;
|
use g3_core::ui_writer::UiWriter;
|
||||||
use g3_core::Agent;
|
use g3_core::Agent;
|
||||||
@@ -13,6 +12,7 @@ use crate::completion::G3Helper;
|
|||||||
use crate::g3_status::{G3Status, Status};
|
use crate::g3_status::{G3Status, Status};
|
||||||
use crate::simple_output::SimpleOutput;
|
use crate::simple_output::SimpleOutput;
|
||||||
use crate::project::Project;
|
use crate::project::Project;
|
||||||
|
use crate::project::load_and_validate_project;
|
||||||
use crate::template::process_template;
|
use crate::template::process_template;
|
||||||
use crate::task_execution::execute_task_with_retry;
|
use crate::task_execution::execute_task_with_retry;
|
||||||
|
|
||||||
@@ -336,33 +336,10 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
output.print("Loads project files (brief.md, contacts.yaml, status.md) from the given path.");
|
output.print("Loads project files (brief.md, contacts.yaml, status.md) from the given path.");
|
||||||
} else {
|
} else {
|
||||||
let project_path_str = parts[1].trim();
|
let project_path_str = parts[1].trim();
|
||||||
|
|
||||||
// Expand tilde if present
|
|
||||||
let project_path = if project_path_str.starts_with("~/") {
|
|
||||||
if let Some(home) = dirs::home_dir() {
|
|
||||||
home.join(&project_path_str[2..])
|
|
||||||
} else {
|
|
||||||
PathBuf::from(project_path_str)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
PathBuf::from(project_path_str)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate path is absolute
|
// Use shared helper for validation and loading
|
||||||
if !project_path.is_absolute() {
|
match load_and_validate_project(project_path_str, workspace_dir) {
|
||||||
output.print("❌ Project path must be absolute (e.g., /Users/name/projects/myproject)");
|
Ok(project) => {
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate path exists
|
|
||||||
if !project_path.exists() {
|
|
||||||
output.print(&format!("❌ Project path does not exist: {}", project_path.display()));
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the project
|
|
||||||
match Project::load(&project_path, workspace_dir) {
|
|
||||||
Some(project) => {
|
|
||||||
// Set project content in agent's system message
|
// Set project content in agent's system message
|
||||||
if agent.set_project_content(Some(project.content.clone())) {
|
if agent.set_project_content(Some(project.content.clone())) {
|
||||||
// Set project path on UI writer for path shortening
|
// Set project path on UI writer for path shortening
|
||||||
@@ -377,10 +354,10 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
let project_name = project.path.file_name()
|
let project_name = project.path.file_name()
|
||||||
.and_then(|n| n.to_str()).unwrap_or("project");
|
.and_then(|n| n.to_str()).unwrap_or("project");
|
||||||
G3Status::loading_project(project_name, &project.format_loaded_status());
|
G3Status::loading_project(project_name, &project.format_loaded_status());
|
||||||
|
|
||||||
// Store active project
|
// Store active project
|
||||||
*active_project = Some(project);
|
*active_project = Some(project);
|
||||||
|
|
||||||
// Auto-submit the project status prompt
|
// Auto-submit the project status prompt
|
||||||
let prompt = "what is the current state of the project? and what is your suggested next best step?";
|
let prompt = "what is the current state of the project? and what is your suggested next best step?";
|
||||||
execute_task_with_retry(agent, prompt, show_prompt, show_code, output).await;
|
execute_task_with_retry(agent, prompt, show_prompt, show_code, output).await;
|
||||||
@@ -388,8 +365,8 @@ pub async fn handle_command<W: UiWriter>(
|
|||||||
output.print("❌ Failed to set project content in agent context.");
|
output.print("❌ Failed to set project content in agent context.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
Err(e) => {
|
||||||
output.print("❌ No project files found (brief.md, contacts.yaml, status.md).");
|
output.print(&format!("❌ {}", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ async fn execute_user_input<W: UiWriter>(
|
|||||||
/// Run interactive mode with console output.
|
/// Run interactive mode with console output.
|
||||||
/// If `agent_name` is Some, we're in agent+chat mode: skip session resume/verbose welcome,
|
/// If `agent_name` is Some, we're in agent+chat mode: skip session resume/verbose welcome,
|
||||||
/// and use the agent name as the prompt (e.g., "butler>").
|
/// and use the agent name as the prompt (e.g., "butler>").
|
||||||
|
/// If `initial_project` is Some, the project is pre-loaded (from --project flag).
|
||||||
pub async fn run_interactive<W: UiWriter>(
|
pub async fn run_interactive<W: UiWriter>(
|
||||||
mut agent: Agent<W>,
|
mut agent: Agent<W>,
|
||||||
show_prompt: bool,
|
show_prompt: bool,
|
||||||
@@ -77,6 +78,7 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
workspace_path: &Path,
|
workspace_path: &Path,
|
||||||
new_session: bool,
|
new_session: bool,
|
||||||
agent_name: Option<&str>,
|
agent_name: Option<&str>,
|
||||||
|
initial_project: Option<Project>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let output = SimpleOutput::new();
|
let output = SimpleOutput::new();
|
||||||
let from_agent_mode = agent_name.is_some();
|
let from_agent_mode = agent_name.is_some();
|
||||||
@@ -187,8 +189,22 @@ pub async fn run_interactive<W: UiWriter>(
|
|||||||
let mut multiline_buffer = String::new();
|
let mut multiline_buffer = String::new();
|
||||||
let mut in_multiline = false;
|
let mut in_multiline = false;
|
||||||
|
|
||||||
// Track active project
|
// Track active project (may be pre-loaded from --project flag)
|
||||||
let mut active_project: Option<Project> = None;
|
let mut active_project: Option<Project> = initial_project;
|
||||||
|
|
||||||
|
// If we have an initial project, display its status
|
||||||
|
if let Some(ref project) = active_project {
|
||||||
|
let project_name = project.path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("project");
|
||||||
|
G3Status::loading_project(project_name, &project.format_loaded_status());
|
||||||
|
|
||||||
|
// Print newline after the loading message (G3Status::loading_project doesn't add one)
|
||||||
|
use std::io::Write;
|
||||||
|
println!();
|
||||||
|
std::io::stdout().flush().ok();
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Display context window progress bar before each prompt
|
// Display context window progress bar before each prompt
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ use ui_writer_impl::ConsoleUiWriter;
|
|||||||
use g3_core::ui_writer::UiWriter;
|
use g3_core::ui_writer::UiWriter;
|
||||||
use utils::{initialize_logging, load_config_with_cli_overrides, setup_workspace_directory};
|
use utils::{initialize_logging, load_config_with_cli_overrides, setup_workspace_directory};
|
||||||
use template::process_template;
|
use template::process_template;
|
||||||
|
use project::load_and_validate_project;
|
||||||
|
|
||||||
pub async fn run() -> Result<()> {
|
pub async fn run() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
@@ -194,6 +195,34 @@ async fn run_console_mode(
|
|||||||
agent.set_acd_enabled(true);
|
agent.set_acd_enabled(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load CLI project if --project flag was specified
|
||||||
|
let initial_project: Option<project::Project> = if let Some(ref project_path) = cli.project {
|
||||||
|
match load_and_validate_project(&project_path.to_string_lossy(), &workspace_dir) {
|
||||||
|
Ok(cli_project) => {
|
||||||
|
// Set project content in agent's system message
|
||||||
|
if agent.set_project_content(Some(cli_project.content.clone())) {
|
||||||
|
// Set project path on UI writer for path shortening
|
||||||
|
let project_name = cli_project.path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("project")
|
||||||
|
.to_string();
|
||||||
|
agent.ui_writer().set_project_path(cli_project.path.clone(), project_name);
|
||||||
|
Some(cli_project)
|
||||||
|
} else {
|
||||||
|
eprintln!("Warning: Failed to set project content in agent context.");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error loading project: {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
if cli.autonomous {
|
if cli.autonomous {
|
||||||
let _agent = run_autonomous(
|
let _agent = run_autonomous(
|
||||||
agent,
|
agent,
|
||||||
@@ -232,6 +261,7 @@ async fn run_console_mode(
|
|||||||
project.workspace(),
|
project.workspace(),
|
||||||
cli.new_session,
|
cli.new_session,
|
||||||
None, // agent_name (not in agent mode)
|
None, // agent_name (not in agent mode)
|
||||||
|
initial_project,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
//! Projects allow loading context from a specific project directory that persists
|
//! Projects allow loading context from a specific project directory that persists
|
||||||
//! in the system message and survives compaction/dehydration.
|
//! in the system message and survives compaction/dehydration.
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Represents an active project with its loaded content.
|
/// Represents an active project with its loaded content.
|
||||||
@@ -96,6 +97,45 @@ impl Project {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load and validate a project from a path string.
|
||||||
|
///
|
||||||
|
/// This is the shared logic used by both `--project` CLI flag and `/project` command.
|
||||||
|
/// It handles:
|
||||||
|
/// - Tilde expansion for home directory
|
||||||
|
/// - Validation that path is absolute
|
||||||
|
/// - Validation that path exists
|
||||||
|
/// - Loading project files
|
||||||
|
///
|
||||||
|
/// Returns the loaded Project or an error with a user-friendly message.
|
||||||
|
pub fn load_and_validate_project(project_path_str: &str, workspace_dir: &Path) -> Result<Project> {
|
||||||
|
// Expand tilde if present
|
||||||
|
let project_path = if project_path_str.starts_with("~/") {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
home.join(&project_path_str[2..])
|
||||||
|
} else {
|
||||||
|
PathBuf::from(project_path_str)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PathBuf::from(project_path_str)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate path is absolute
|
||||||
|
if !project_path.is_absolute() {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"Project path must be absolute (e.g., /Users/name/projects/myproject)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate path exists
|
||||||
|
if !project_path.exists() {
|
||||||
|
return Err(anyhow!("Project path does not exist: {}", project_path.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the project
|
||||||
|
Project::load(&project_path, workspace_dir)
|
||||||
|
.ok_or_else(|| anyhow!("No project files found (brief.md, contacts.yaml, status.md)"))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -191,4 +231,60 @@ mod tests {
|
|||||||
|
|
||||||
assert!(project.is_none());
|
assert!(project.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_and_validate_project_success() {
|
||||||
|
let workspace = TempDir::new().unwrap();
|
||||||
|
let project_dir = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
// Create project files
|
||||||
|
fs::write(project_dir.path().join("brief.md"), "Project brief").unwrap();
|
||||||
|
|
||||||
|
let result = load_and_validate_project(
|
||||||
|
project_dir.path().to_str().unwrap(),
|
||||||
|
workspace.path(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let project = result.unwrap();
|
||||||
|
assert!(project.loaded_files.contains(&"brief.md".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_and_validate_project_relative_path_error() {
|
||||||
|
let workspace = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
let result = load_and_validate_project("relative/path", workspace.path());
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err().to_string();
|
||||||
|
assert!(err.contains("must be absolute"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_and_validate_project_nonexistent_path_error() {
|
||||||
|
let workspace = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
let result = load_and_validate_project("/nonexistent/path/12345", workspace.path());
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err().to_string();
|
||||||
|
assert!(err.contains("does not exist"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_and_validate_project_no_files_error() {
|
||||||
|
let workspace = TempDir::new().unwrap();
|
||||||
|
let project_dir = TempDir::new().unwrap();
|
||||||
|
|
||||||
|
// No project files created
|
||||||
|
let result = load_and_validate_project(
|
||||||
|
project_dir.path().to_str().unwrap(),
|
||||||
|
workspace.path(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err = result.unwrap_err().to_string();
|
||||||
|
assert!(err.contains("No project files found"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user