Add interactive mode to studio
New commands: - studio cli (alias: c) - Start a new interactive g3 session in an isolated worktree - studio resume <id> (alias: r) - Resume a paused interactive session - Bare 'studio' now defaults to 'studio cli' Session changes: - Added SessionStatus::Paused for sessions that can be resumed - Added SessionType enum (OneShot, Interactive) for future use - Interactive sessions use inherited stdio for direct TTY access - Sessions are marked as Paused when user exits g3 Workflow: 1. studio # creates worktree, runs g3 interactively 2. (work in g3, exit when done) 3. studio resume <id> # continue working 4. studio accept <id> # merge to main when finished
This commit is contained in:
@@ -16,15 +16,26 @@ use session::{Session, SessionStatus};
|
||||
|
||||
/// Studio - Multi-agent workspace manager for g3
|
||||
#[derive(Parser)]
|
||||
#[command(name = "studio")]
|
||||
#[command(name = "studio", subcommand_required = false)]
|
||||
#[command(about = "Manage multiple g3 agent sessions using git worktrees")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
command: Option<Commands>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Start a new interactive g3 session in an isolated worktree
|
||||
#[command(alias = "c")]
|
||||
Cli,
|
||||
|
||||
/// Resume a paused interactive session
|
||||
#[command(alias = "r")]
|
||||
Resume {
|
||||
/// Session ID to resume
|
||||
session_id: String,
|
||||
},
|
||||
|
||||
/// Run a new g3 session (tails output until complete)
|
||||
Run {
|
||||
/// Agent name (e.g., carmack, torvalds). If omitted, runs g3 in one-shot mode.
|
||||
@@ -76,7 +87,9 @@ enum Commands {
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
match cli.command.unwrap_or(Commands::Cli) {
|
||||
Commands::Cli => cmd_cli(),
|
||||
Commands::Resume { session_id } => cmd_resume(&session_id),
|
||||
Commands::Run { agent, accept, g3_args } => cmd_run(agent.as_deref(), accept, &g3_args),
|
||||
Commands::Exec { agent, g3_args } => cmd_exec(agent.as_deref(), &g3_args),
|
||||
Commands::List => cmd_list(),
|
||||
@@ -86,6 +99,98 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a new interactive g3 session
|
||||
fn cmd_cli() -> Result<()> {
|
||||
let g3_binary = get_g3_binary_path()?;
|
||||
let repo_root = get_repo_root()?;
|
||||
let session = Session::new_interactive();
|
||||
|
||||
// Create worktree
|
||||
let worktree = GitWorktree::new(&repo_root);
|
||||
let worktree_path = worktree.create(&session)?;
|
||||
|
||||
println!("📁 Worktree: {}", worktree_path.display());
|
||||
println!("🌿 Branch: {}", session.branch_name());
|
||||
println!("🆔 Session: {}", session.id);
|
||||
println!();
|
||||
|
||||
// Save session metadata
|
||||
session.save(&repo_root, &worktree_path)?;
|
||||
|
||||
// Build g3 command with inherited stdio for interactive use
|
||||
let mut cmd = Command::new(&g3_binary);
|
||||
cmd.arg("--workspace").arg(&worktree_path);
|
||||
cmd.current_dir(&worktree_path);
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
|
||||
// Spawn and wait
|
||||
let status = cmd.status().context("Failed to run g3")?;
|
||||
|
||||
// Mark session as paused (user can resume later or accept/discard)
|
||||
session.mark_paused(&repo_root)?;
|
||||
|
||||
println!();
|
||||
println!("Session {} paused.", session.id);
|
||||
println!();
|
||||
println!("Next steps:");
|
||||
println!(" studio resume {} - Continue working", session.id);
|
||||
println!(" studio accept {} - Merge changes to main", session.id);
|
||||
println!(" studio discard {} - Discard changes", session.id);
|
||||
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resume a paused interactive session
|
||||
fn cmd_resume(session_id: &str) -> Result<()> {
|
||||
let g3_binary = get_g3_binary_path()?;
|
||||
let repo_root = get_repo_root()?;
|
||||
let session = Session::load(&repo_root, session_id)?;
|
||||
|
||||
let worktree_path = session.worktree_path.as_ref()
|
||||
.ok_or_else(|| anyhow!("Session {} has no worktree path", session_id))?;
|
||||
|
||||
if !worktree_path.exists() {
|
||||
bail!("Worktree for session {} no longer exists at {}", session_id, worktree_path.display());
|
||||
}
|
||||
|
||||
println!("📁 Resuming session {} in {}", session_id, worktree_path.display());
|
||||
println!();
|
||||
|
||||
// Build g3 command with inherited stdio
|
||||
let mut cmd = Command::new(&g3_binary);
|
||||
cmd.arg("--workspace").arg(worktree_path);
|
||||
cmd.current_dir(worktree_path);
|
||||
cmd.stdin(Stdio::inherit());
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.stderr(Stdio::inherit());
|
||||
|
||||
// Spawn and wait
|
||||
let status = cmd.status().context("Failed to run g3")?;
|
||||
|
||||
// Mark session as paused again
|
||||
session.mark_paused(&repo_root)?;
|
||||
|
||||
println!();
|
||||
println!("Session {} paused.", session_id);
|
||||
println!();
|
||||
println!("Next steps:");
|
||||
println!(" studio resume {} - Continue working", session_id);
|
||||
println!(" studio accept {} - Merge changes to main", session_id);
|
||||
println!(" studio discard {} - Discard changes", session_id);
|
||||
|
||||
if !status.success() {
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the path to the g3 binary (same directory as studio)
|
||||
fn get_g3_binary_path() -> Result<PathBuf> {
|
||||
let current_exe = env::current_exe().context("Failed to get current executable path")?;
|
||||
@@ -286,6 +391,7 @@ fn cmd_list() -> Result<()> {
|
||||
for session in sessions {
|
||||
let status_str = match session.status {
|
||||
SessionStatus::Running => "🔄 running",
|
||||
SessionStatus::Paused => "⏸️ paused",
|
||||
SessionStatus::Complete => "✅ complete",
|
||||
SessionStatus::Failed => "❌ failed",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user