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:
@@ -1,6 +1,6 @@
|
||||
//! Session management for studio
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
@@ -12,9 +12,17 @@ use uuid::Uuid;
|
||||
pub enum SessionStatus {
|
||||
Running,
|
||||
Complete,
|
||||
Paused,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Session type
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SessionType {
|
||||
OneShot,
|
||||
Interactive,
|
||||
}
|
||||
|
||||
/// A studio session representing a g3 agent run in a worktree
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
@@ -30,6 +38,9 @@ pub struct Session {
|
||||
pub pid: Option<u32>,
|
||||
/// Path to the worktree
|
||||
pub worktree_path: Option<PathBuf>,
|
||||
/// Type of session
|
||||
#[serde(default = "default_session_type")]
|
||||
pub session_type: SessionType,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -46,6 +57,23 @@ impl Session {
|
||||
status: SessionStatus::Running,
|
||||
pid: None,
|
||||
worktree_path: None,
|
||||
session_type: SessionType::OneShot,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new interactive session
|
||||
pub fn new_interactive() -> Self {
|
||||
let full_uuid = Uuid::new_v4();
|
||||
let short_id = full_uuid.to_string()[..8].to_string();
|
||||
|
||||
Self {
|
||||
id: short_id,
|
||||
agent: "interactive".to_string(),
|
||||
created_at: Utc::now(),
|
||||
status: SessionStatus::Running,
|
||||
pid: None,
|
||||
worktree_path: None,
|
||||
session_type: SessionType::Interactive,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +138,20 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark session as paused (for interactive sessions)
|
||||
pub fn mark_paused(&self, repo_root: &Path) -> Result<()> {
|
||||
let path = self.metadata_path(repo_root);
|
||||
let content = fs::read_to_string(&path).context("Failed to read session metadata")?;
|
||||
let mut session: Session = serde_json::from_str(&content)?;
|
||||
session.status = SessionStatus::Paused;
|
||||
session.pid = None;
|
||||
|
||||
let json = serde_json::to_string_pretty(&session)?;
|
||||
fs::write(&path, json).context("Failed to write session metadata")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load a session by ID
|
||||
pub fn load(repo_root: &Path, session_id: &str) -> Result<Session> {
|
||||
let path = Self::sessions_dir(repo_root).join(format!("{}.json", session_id));
|
||||
@@ -164,3 +206,8 @@ impl Session {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Default session type for backwards compatibility with existing sessions
|
||||
fn default_session_type() -> SessionType {
|
||||
SessionType::OneShot
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user