use crate::app::agent::shell::git_std_command;
use std::fmt::Write;
use std::path::Path;
const CLI_VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) async fn create_cli_session(project_dir: &Path, title: Option<String>) -> String {
let directory = project_dir.to_string_lossy().to_string();
let fallback = crate::app::agent::id::descending(crate::app::agent::id::Prefix::Session, None)
.unwrap_or_else(|_| "cli".to_string());
let created =
crate::app::agent::project::instance::provide(project_dir.to_path_buf(), None, move || {
let directory = directory.clone();
let title = title.clone();
Box::pin(async move {
crate::app::agent::session::session::create_next(
crate::app::agent::session::session::CreateInput {
parent_id: None,
title,
directory,
permission: None,
},
)
.await
})
})
.await;
match created {
Ok(Ok(info)) => info.id,
_ => fallback,
}
}
pub(crate) fn initial_cli_session_title_for_input(input: &str) -> String {
let normalized = input.split_whitespace().collect::<Vec<_>>().join(" ");
let trimmed = normalized.trim();
if trimmed.is_empty() {
return "CLI session".to_string();
}
let mut chars = trimmed.chars();
let title: String = chars.by_ref().take(50).collect();
if chars.next().is_some() { format!("{}...", title.trim_end()) } else { title }
}
pub(crate) async fn maybe_refresh_cli_session_title(
session_id: &str,
first_user_content: &str,
preferred_model: Option<String>,
) {
let fallback = initial_cli_session_title_for_input(first_user_content);
let fallback_for_update = fallback.clone();
let _ = crate::app::agent::session::session::update_any(session_id, move |s| {
if s.title.trim().is_empty()
|| crate::app::agent::session::session::is_default_title(&s.title)
{
s.title = fallback_for_update;
}
})
.await;
let generated = crate::app::agent::session::title::generate_from_content(
session_id.to_string(),
first_user_content.to_string(),
preferred_model,
None,
)
.await;
let Ok(title) = generated else { return };
let _ = crate::app::agent::session::session::update_any(session_id, move |s| {
s.title = title;
})
.await;
}
pub(crate) fn build_project_info(worktree: &Path) -> String {
let base = worktree.display().to_string();
let branch = current_branch(worktree);
let mut out = if let Some(branch) = branch { format!("{base}:{branch}") } else { base };
let _ = write!(out, " • VibeWindow {CLI_VERSION}");
out
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub(crate) enum GitWorkspaceStatus {
ReadyClean,
ReadyDirty(Vec<String>),
#[default]
Unavailable,
}
impl GitWorkspaceStatus {
pub(crate) fn modified_files(&self) -> &[String] {
match self {
Self::ReadyDirty(files) => files.as_slice(),
Self::ReadyClean | Self::Unavailable => &[],
}
}
}
pub(crate) fn collect_modified_files(worktree: &Path) -> Vec<String> {
collect_git_workspace_status(worktree).modified_files().to_vec()
}
pub(crate) fn collect_git_workspace_status(worktree: &Path) -> GitWorkspaceStatus {
let output = git_std_command()
.args(["-c", "core.quotepath=false", "status", "--porcelain", "--untracked-files=all"])
.current_dir(worktree)
.output();
let Ok(out) = output else {
return GitWorkspaceStatus::Unavailable;
};
if !out.status.success() {
return GitWorkspaceStatus::Unavailable;
}
let stdout = String::from_utf8_lossy(&out.stdout);
let mut files = Vec::new();
for line in stdout.lines().filter(|line| !line.trim().is_empty()) {
let Some(path) = parse_git_status_porcelain_path(line) else {
return GitWorkspaceStatus::Unavailable;
};
files.push(path);
}
files.sort();
if files.is_empty() {
GitWorkspaceStatus::ReadyClean
} else {
GitWorkspaceStatus::ReadyDirty(files)
}
}
fn parse_git_status_porcelain_path(line: &str) -> Option<String> {
let path = line.get(3..)?.trim();
if path.is_empty() {
return None;
}
Some(path.split_once(" -> ").map(|(_, renamed_path)| renamed_path).unwrap_or(path).to_string())
}
fn current_branch(worktree: &Path) -> Option<String> {
let out = git_std_command()
.current_dir(worktree)
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
if s.is_empty() { None } else { Some(s) }
}