use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use agentty::db::{DB_DIR, DB_FILE, Database};
use assert_cmd::cargo::cargo_bin;
type ShowcaseResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
fn default_output_dir() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir.join("../../docs/site/static/features")
}
fn feature_output_dir() -> ShowcaseResult<PathBuf> {
let path = std::env::var("FEATURE_OUTPUT").map_or_else(|_| default_output_dir(), PathBuf::from);
std::fs::create_dir_all(&path)?;
Ok(path.canonicalize()?)
}
fn build_tape(
output_path: &Path,
binary_path: &Path,
agentty_root: &Path,
workdir: &Path,
steps: &str,
) -> String {
let mut tape = String::new();
let _ = writeln!(tape, "Set Shell \"bash\"");
let _ = writeln!(tape, "Set FontSize 36");
let _ = writeln!(tape, "Set Width 3200");
let _ = writeln!(tape, "Set Height 1600");
let _ = writeln!(tape, "Set Padding 0");
let _ = writeln!(tape, "Set Framerate 30");
let _ = writeln!(tape, "Set TypingSpeed 0");
let _ = writeln!(tape, "Set Theme \"OneDark\"");
let _ = writeln!(tape);
let _ = writeln!(tape, "Output \"{}\"", output_path.display());
let _ = writeln!(tape);
let _ = writeln!(tape, "Hide");
let _ = writeln!(
tape,
"Type \"export AGENTTY_ROOT='{}'\"",
agentty_root.display()
);
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Type \"export HOME='{}'\"", agentty_root.display());
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Type \"cd '{}'\"", workdir.display());
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Type \"clear\"");
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape, "Sleep 200ms");
let _ = writeln!(tape, "Show");
let _ = writeln!(tape);
let _ = writeln!(tape, "Type \"'{}'\"", binary_path.display());
let _ = writeln!(tape, "Enter");
let _ = writeln!(tape);
let _ = write!(tape, "{steps}");
let _ = writeln!(tape);
let _ = writeln!(tape, "Hide");
let _ = writeln!(tape, "Type \"q\"");
let _ = writeln!(tape, "Sleep 1s");
tape
}
fn execute_tape(tape_content: &str, name: &str, output_path: &Path) -> ShowcaseResult {
let tape_path = std::env::temp_dir().join(format!("{name}.tape"));
std::fs::write(&tape_path, tape_content)?;
let output = Command::new("vhs").arg(&tape_path).output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(std::io::Error::other(format!("VHS execution failed: {stderr}")).into());
}
assert!(
output_path.exists(),
"VHS did not produce output at {}",
output_path.display()
);
Ok(())
}
async fn seed_database(agentty_root: &Path, workdir: &Path) -> ShowcaseResult {
let db_path = agentty_root.join(DB_DIR).join(DB_FILE);
let database = Database::open(&db_path).await?;
let project_id = database
.projects()
.upsert_project(&workdir.to_string_lossy(), Some("main".to_string()))
.await?;
database
.projects()
.touch_project_last_opened(project_id)
.await?;
let sessions = [
(
"a1b2c3d4-0001",
"claude-opus-4-8",
"Review",
"Fix auth token refresh on session expiry",
"M",
),
(
"a1b2c3d4-0002",
"claude-sonnet-4-6",
"InProgress",
"Add dark mode toggle to settings page",
"L",
),
(
"a1b2c3d4-0003",
"gemini-3-flash-preview",
"Review",
"Optimize database query performance",
"S",
),
(
"a1b2c3d4-0004",
"claude-opus-4-8",
"Question",
"Refactor API error handling",
"M",
),
(
"a1b2c3d4-0005",
"claude-sonnet-4-6",
"Done",
"Update README with new install steps",
"XS",
),
(
"a1b2c3d4-0006",
"gemini-3-flash-preview",
"Done",
"Add unit tests for auth middleware",
"S",
),
(
"a1b2c3d4-0007",
"claude-opus-4-8",
"Queued",
"Migrate config to TOML format",
"L",
),
];
let wt_dir = agentty_root.join("wt");
for (session_id, model, status, title, size) in &sessions {
database
.sessions()
.insert_session(session_id, model, "main", status, project_id)
.await?;
database
.sessions()
.update_session_title(session_id, title)
.await?;
database
.sessions()
.update_session_diff_stats(10, 5, session_id, size)
.await?;
let session_wt = wt_dir.join(&session_id[..8]);
std::fs::create_dir_all(&session_wt)?;
}
Ok(())
}
async fn setup_feature_env(temp: &tempfile::TempDir) -> ShowcaseResult<(PathBuf, PathBuf)> {
let agentty_root = temp.path().join("agentty_root");
let workdir = temp.path().join("my-project");
std::fs::create_dir_all(&agentty_root)?;
std::fs::create_dir_all(&workdir)?;
let agentty_root = agentty_root.canonicalize()?;
let workdir = workdir.canonicalize()?;
seed_database(&agentty_root, &workdir).await?;
Ok((agentty_root, workdir))
}
#[tokio::test]
#[ignore = "requires VHS and a renderable terminal — run explicitly with --ignored"]
async fn feature_sessions_tab() -> ShowcaseResult {
let temp = tempfile::TempDir::new()?;
let (agentty_root, workdir) = setup_feature_env(&temp).await?;
let binary_path = cargo_bin("agentty");
let gif_path = feature_output_dir()?.join("sessions.gif");
let steps = "\
Sleep 2s
Tab
Sleep 1s
Sleep 2s
";
let tape = build_tape(&gif_path, &binary_path, &agentty_root, &workdir, steps);
execute_tape(&tape, "feature_sessions", &gif_path)
}
#[tokio::test]
#[ignore = "requires VHS and a renderable terminal — run explicitly with --ignored"]
async fn feature_tab_tour() -> ShowcaseResult {
let temp = tempfile::TempDir::new()?;
let (agentty_root, workdir) = setup_feature_env(&temp).await?;
let binary_path = cargo_bin("agentty");
let gif_path = feature_output_dir()?.join("tab_tour.gif");
let steps = "\
Sleep 2s
Tab
Sleep 2s
Tab
Sleep 2s
Tab
Sleep 2s
";
let tape = build_tape(&gif_path, &binary_path, &agentty_root, &workdir, steps);
execute_tape(&tape, "feature_tab_tour", &gif_path)
}