numeric prefix

This commit is contained in:
Ryan Ragona
2025-04-26 07:32:26 -07:00
parent 342ac711ca
commit 9aaa947828
4 changed files with 395 additions and 104 deletions

238
codex-rs/Cargo.lock generated
View File

@@ -235,6 +235,17 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi 0.1.19",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@@ -387,6 +398,23 @@ dependencies = [
"windows-link",
]
[[package]]
name = "clap"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
"bitflags 1.3.2",
"clap_derive 3.2.25",
"clap_lex 0.2.4",
"indexmap 1.9.3",
"once_cell",
"strsim 0.10.0",
"termcolor",
"textwrap 0.16.2",
]
[[package]]
name = "clap"
version = "4.5.37"
@@ -394,7 +422,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
"clap_derive 4.5.32",
]
[[package]]
@@ -405,23 +433,45 @@ checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"clap_lex 0.7.4",
"strsim 0.11.1",
"terminal_size",
]
[[package]]
name = "clap_derive"
version = "3.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
dependencies = [
"heck 0.4.1",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "clap_lex"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
@@ -472,7 +522,7 @@ name = "codex-cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"clap 4.5.37",
"codex-core",
"codex-exec",
"codex-interactive",
@@ -493,7 +543,7 @@ dependencies = [
"async-channel",
"base64 0.21.7",
"bytes",
"clap",
"clap 4.5.37",
"codex-apply-patch",
"dirs 6.0.0",
"env-flags",
@@ -506,7 +556,7 @@ dependencies = [
"openssl-sys",
"patch",
"predicates",
"rand",
"rand 0.9.1",
"reqwest",
"seccompiler",
"serde",
@@ -527,7 +577,7 @@ name = "codex-exec"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"clap 4.5.37",
"codex-core",
"tokio",
"tracing",
@@ -540,7 +590,7 @@ version = "0.1.0"
dependencies = [
"allocative",
"anyhow",
"clap",
"clap 4.5.37",
"derive_more",
"env_logger",
"log",
@@ -559,7 +609,7 @@ name = "codex-interactive"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"clap 4.5.37",
"codex-core",
"tokio",
]
@@ -569,10 +619,10 @@ name = "codex-repl"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"clap 4.5.37",
"codex-core",
"owo-colors 4.2.0",
"rand",
"rand 0.9.1",
"tokio",
"tracing",
"tracing-subscriber",
@@ -584,16 +634,18 @@ version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"clap",
"clap 4.5.37",
"codex-core",
"codex-exec",
"comfy-table",
"dirs 5.0.1",
"libc",
"names",
"nix 0.27.1",
"serde",
"serde_json",
"serde_yaml",
"sysinfo",
"tabwriter",
"tokio",
"tracing",
"tracing-subscriber",
@@ -605,7 +657,7 @@ name = "codex-tui"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"clap 4.5.37",
"codex-ansi-escape",
"codex-core",
"color-eyre",
@@ -653,17 +705,6 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "comfy-table"
version = "7.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a65ebfec4fb190b6f90e944a817d60499ee0744e582530e2c9900a22e591d9a"
dependencies = [
"crossterm",
"unicode-segmentation",
"unicode-width 0.2.0",
]
[[package]]
name = "compact_str"
version = "0.8.1"
@@ -1418,12 +1459,27 @@ dependencies = [
"foldhash",
]
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.9"
@@ -2102,6 +2158,16 @@ dependencies = [
"serde",
]
[[package]]
name = "names"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc"
dependencies = [
"clap 3.2.25",
"rand 0.8.5",
]
[[package]]
name = "native-tls"
version = "0.2.14"
@@ -2322,6 +2388,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "os_str_bytes"
version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
[[package]]
name = "overload"
version = "0.1.1"
@@ -2523,6 +2595,30 @@ dependencies = [
"yansi",
]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -2557,14 +2653,35 @@ dependencies = [
"nibble_vec",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
"rand_chacha 0.9.0",
"rand_core 0.9.3",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
@@ -2574,7 +2691,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.9.3",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.16",
]
[[package]]
@@ -3071,6 +3197,19 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.9.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@@ -3201,7 +3340,7 @@ dependencies = [
"starlark_syntax",
"static_assertions",
"strsim 0.10.0",
"textwrap",
"textwrap 0.11.0",
"thiserror 1.0.69",
]
@@ -3306,7 +3445,7 @@ version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
@@ -3397,6 +3536,15 @@ dependencies = [
"libc",
]
[[package]]
name = "tabwriter"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fce91f2f0ec87dff7e6bcbbeb267439aa1188703003c6055193c821487400432"
dependencies = [
"unicode-width 0.2.0",
]
[[package]]
name = "tempfile"
version = "3.19.1"
@@ -3421,6 +3569,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.4.2"
@@ -3446,6 +3603,12 @@ dependencies = [
"unicode-width 0.1.14",
]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -3856,6 +4019,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -4062,6 +4231,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@@ -31,8 +31,10 @@ chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
dirs = "5"
comfy-table = "7"
sysinfo = "0.29"
tabwriter = "1.3"
serde_yaml = "0.9"
names = "0.14"
# Re-use the codex-exec library for its CLI definition
codex_exec = { package = "codex-exec", path = "../exec" }

View File

@@ -1,12 +1,12 @@
//! Command-line interface definition and dispatch.
//! CLI command definitions and implementation.
use crate::{spawn, store};
use anyhow::Result;
use clap::{Args, Parser, Subcommand};
use clap::{Args, Parser, Subcommand, ValueEnum};
use serde::Serialize;
/// Top-level CLI entry (re-exported by the crate).
#[derive(Parser)]
#[command(name = "codex-session", about = "Manage detached codex-exec sessions")]
#[command(name = "codex-session", about = "Manage codex-exec background sessions")]
pub struct Cli {
#[command(subcommand)]
cmd: Commands,
@@ -24,21 +24,28 @@ impl Cli {
}
}
fn human_bytes(b: u64) -> String {
const KB: f64 = 1024.0;
const MB: f64 = KB * 1024.0;
const GB: f64 = MB * 1024.0;
let f = b as f64;
if f >= GB {
format!("{:.1}G", f / GB)
} else if f >= MB {
format!("{:.1}M", f / MB)
} else if f >= KB {
format!("{:.1}K", f / KB)
} else {
format!("{}B", b)
}
}
#[derive(Subcommand)]
enum Commands {
/// Spawn a new, detached agent.
Create(CreateCmd),
/// Kill a running session and delete on-disk artefacts.
Delete(DeleteCmd),
/// Show (and optionally follow) stdout / stderr logs of a session.
Logs(LogsCmd),
/// Execute a one-shot command inside an existing session.
Exec(ExecCmd),
/// List all known session IDs.
List(ListCmd),
}
@@ -47,61 +54,87 @@ enum Commands {
#[derive(Args)]
pub struct CreateCmd {
/// Session identifier. Generates a random UUIDv4 when omitted.
/// Explicit session name. If omitted, a memorable random one is generated.
#[arg(long)]
id: Option<String>,
/// All flags following `create` are forwarded to `codex-exec`.
/// Flags passed through to codex-exec.
#[clap(flatten)]
exec_cli: codex_exec::Cli,
}
impl CreateCmd {
pub async fn run(self) -> Result<()> {
let id = self
.id
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
let id = match self.id {
Some(id) => id,
None => generate_session_id()?,
};
let paths = store::paths_for(&id)?;
store::prepare_dirs(&paths)?;
let exec_args = build_exec_args(&self.exec_cli);
// Spawn the background agent and immediately detach.
// Preview first 40 printable chars of prompt for status listing
let prompt_preview = self
.exec_cli
.prompt
.as_ref()
.map(|p| {
let slice: String = p.chars().take(40).collect();
if p.len() > 40 {
format!("{}", slice)
} else {
slice
}
});
// Spawn process
let child = spawn::spawn_agent(&paths, &exec_args)?;
// Record metadata (with PID) *after* successful spawn.
let meta = store::SessionMeta {
id: id.clone(),
pid: child.id().unwrap_or_default(),
created_at: chrono::Utc::now(),
prompt_preview,
};
store::write_meta(&paths, &meta)?;
println!("{id}");
Ok(())
}
}
/// Re-serialize a `codex_exec::Cli` struct back into the exact CLI args.
fn generate_session_id() -> Result<String> {
let mut generator = names::Generator::with_naming(names::Name::Numbered);
loop {
let candidate = generator.next().unwrap();
let paths = store::paths_for(&candidate)?;
if !paths.dir.exists() {
return Ok(candidate);
}
}
}
fn build_exec_args(cli: &codex_exec::Cli) -> Vec<String> {
let mut args = Vec::new();
for path in &cli.images {
args.push("--image".to_string());
args.push(path.to_string_lossy().into_owned());
for img in &cli.images {
args.push("--image".into());
args.push(img.to_string_lossy().into_owned());
}
if let Some(model) = &cli.model {
args.push("--model".to_string());
args.push("--model".into());
args.push(model.clone());
}
if cli.skip_git_repo_check {
args.push("--skip-git-repo-check".to_string());
args.push("--skip-git-repo-check".into());
}
if cli.disable_response_storage {
args.push("--disable-response-storage".to_string());
args.push("--disable-response-storage".into());
}
if let Some(prompt) = &cli.prompt {
@@ -116,14 +149,14 @@ fn build_exec_args(cli: &codex_exec::Cli) -> Vec<String> {
#[derive(Args)]
pub struct DeleteCmd {
/// Session ID to terminate and remove.
id: String,
}
impl DeleteCmd {
pub async fn run(self) -> Result<()> {
store::kill_session(&self.id).await?;
store::purge(&self.id)?;
let id = store::resolve_selector(&self.id)?;
store::kill_session(&id).await?;
store::purge(&id)?;
Ok(())
}
}
@@ -133,101 +166,141 @@ impl DeleteCmd {
#[derive(Args)]
pub struct LogsCmd {
/// Session ID whose logs should be printed.
id: String,
/// Follow the file and stream appended lines (like `tail -f`).
#[arg(short, long)]
follow: bool,
/// Show stderr instead of stdout.
#[arg(long)]
stderr: bool,
}
impl LogsCmd {
pub async fn run(self) -> Result<()> {
use tokio::io::AsyncBufReadExt;
let paths = store::paths_for(&self.id)?;
let target = if self.stderr {
&paths.stderr
} else {
&paths.stdout
};
let id = store::resolve_selector(&self.id)?;
let paths = store::paths_for(&id)?;
let target = if self.stderr { &paths.stderr } else { &paths.stdout };
let file = tokio::fs::File::open(target).await?;
if self.follow {
let reader = tokio::io::BufReader::new(file);
let mut lines = reader.lines();
use tokio::io::AsyncBufReadExt;
let mut lines = tokio::io::BufReader::new(file).lines();
while let Some(line) = lines.next_line().await? {
println!("{line}");
}
} else {
// Simply dump the file contents to stdout.
let mut stdout = tokio::io::stdout();
tokio::io::copy(&mut tokio::io::BufReader::new(file), &mut stdout).await?;
tokio::io::copy(&mut tokio::io::BufReader::new(file), &mut tokio::io::stdout()).await?;
}
Ok(())
}
}
// -----------------------------------------------------------------------------
// exec (not implemented yet)
// exec (TODO)
#[derive(Args)]
pub struct ExecCmd {
id: String,
/// Remaining arguments form the command to execute.
#[arg(trailing_var_arg = true)]
cmd: Vec<String>,
}
impl ExecCmd {
pub async fn run(self) -> Result<()> {
anyhow::bail!("exec inside an existing session is not yet implemented");
let _id = store::resolve_selector(&self.id)?;
anyhow::bail!("exec inside session not implemented yet");
}
}
// -----------------------------------------------------------------------------
// list
#[derive(Copy, Clone, ValueEnum, Debug)]
enum OutputFormat { Table, Json, Yaml }
#[derive(Args)]
pub struct ListCmd;
pub struct ListCmd {
#[arg(short = 'o', long = "output", value_enum, default_value_t = OutputFormat::Table)]
output: OutputFormat,
}
#[derive(Serialize)]
struct StatusRow {
idx: usize,
id: String,
pid: u32,
status: String,
created: String,
prompt: String,
out: String,
err: String,
}
impl ListCmd {
pub async fn run(self) -> Result<()> {
use comfy_table::{Cell, Table};
use sysinfo::{SystemExt, Pid, PidExt};
use sysinfo::{SystemExt, PidExt};
let sessions = store::list_sessions()?;
let mut sys = sysinfo::System::new_all();
let metas = store::list_sessions_sorted()?;
let mut sys = sysinfo::System::new();
sys.refresh_processes();
let mut table = Table::new();
table.set_header(["ID", "PID", "STATUS", "CREATED"]);
let rows: Vec<StatusRow> = metas
.into_iter()
.enumerate()
.map(|(idx, m)| {
let status = if m.pid == 0 {
"unknown"
} else if sys.process(sysinfo::Pid::from_u32(m.pid)).is_some() {
"running"
} else {
"exited"
};
for meta in sessions {
let status: &str = if meta.pid == 0 {
"unknown"
} else if sys.process(Pid::from_u32(meta.pid)).is_some() {
"running"
} else {
"exited"
};
// file sizes
let paths = store::paths_for(&m.id).ok();
let (out, err) = if let Some(p) = &paths {
let osz = std::fs::metadata(&p.stdout).map(|m| m.len()).unwrap_or(0);
let esz = std::fs::metadata(&p.stderr).map(|m| m.len()).unwrap_or(0);
(human_bytes(osz), human_bytes(esz))
} else {
("-".into(), "-".into())
};
table.add_row([
Cell::new(&meta.id),
Cell::new(meta.pid),
Cell::new(status),
Cell::new(meta.created_at.to_rfc3339()),
]);
StatusRow {
idx,
id: m.id,
pid: m.pid,
status: status.into(),
created: m.created_at.to_rfc3339(),
prompt: m.prompt_preview.unwrap_or_default(),
out,
err,
}
})
.collect();
match self.output {
OutputFormat::Table => print_table(&rows)?,
OutputFormat::Json => println!("{}", serde_json::to_string_pretty(&rows)?),
OutputFormat::Yaml => println!("{}", serde_yaml::to_string(&rows)?),
}
println!("{table}");
Ok(())
}
}
fn print_table(rows: &[StatusRow]) -> Result<()> {
use std::io::Write;
use tabwriter::TabWriter;
let mut tw = TabWriter::new(Vec::new()).padding(2);
writeln!(tw, "#\tID\tPID\tSTATUS\tOUT\tERR\tCREATED\tPROMPT")?;
for r in rows {
writeln!(tw, "{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", r.idx, r.id, r.pid, r.status, r.out, r.err, r.created, r.prompt)?;
}
let out = String::from_utf8(tw.into_inner()?)?;
print!("{out}");
Ok(())
}

View File

@@ -34,6 +34,8 @@ pub struct SessionMeta {
pub id: String,
pub pid: u32,
pub created_at: chrono::DateTime<chrono::Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_preview: Option<String>,
}
/// Create the on-disk directory structure and write metadata + empty log files.
@@ -74,6 +76,42 @@ pub fn list_sessions() -> Result<Vec<SessionMeta>> {
Ok(res)
}
/// List sessions sorted by newest first (created_at desc).
pub fn list_sessions_sorted() -> Result<Vec<SessionMeta>> {
let mut v = list_sessions()?;
v.sort_by(|a, b| b.created_at.cmp(&a.created_at));
Ok(v)
}
/// Resolve a user-supplied selector to a concrete session id.
///
/// Rules:
/// 1. Pure integer ⇒ index into newest-first list (0 = most recent)
/// 2. Otherwise try exact id match, then unique prefix match.
pub fn resolve_selector(sel: &str) -> Result<String> {
let list = list_sessions_sorted()?;
// numeric index
if let Ok(idx) = sel.parse::<usize>() {
return list.get(idx)
.map(|m| m.id.clone())
.context(format!("no session at index {idx}"));
}
// exact match
if let Some(m) = list.iter().find(|m| m.id == sel) {
return Ok(m.id.clone());
}
// unique prefix match
let mut matches: Vec<&SessionMeta> = list.iter().filter(|m| m.id.starts_with(sel)).collect();
match matches.len() {
1 => Ok(matches.remove(0).id.clone()),
0 => anyhow::bail!("no session matching '{sel}'"),
_ => anyhow::bail!("selector '{sel}' is ambiguous ({} matches)", matches.len()),
}
}
/// Send a polite termination request to the sessions process.
///
/// NOTE: Full PID accounting is a future improvement; for now the function