use std::path::{Path, PathBuf};
use ishou_tokens::{FleetSessionNames, SessionName, SessionNameStyle};
use tear_types::id::SessionId;
use tear_types::{DefinitionId, InstanceId};
use crate::binding::ProjectBinding;
use crate::index::SessionIndex;
use crate::project::project_root;
use crate::record::SessionRecord;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AttachDecision {
Stay,
SwitchTo(SessionId),
SpawnNew {
project_root: PathBuf,
name: SessionName,
},
Suggest(Box<AttachDecision>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AttachAction {
Stay,
SwitchTo(InstanceId),
Instantiate(DefinitionId),
SpawnNew {
project_root: PathBuf,
name: SessionName,
},
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum AttachPolicy {
#[default]
AutoSwitch,
SuggestOnly,
PickerOnly,
}
#[must_use]
pub fn decide(
policy: AttachPolicy,
current: Option<&SessionRecord>,
new_cwd: &Path,
binding: &ProjectBinding,
index: &SessionIndex,
style: SessionNameStyle,
) -> AttachDecision {
let root = project_root(new_cwd);
decide_with_root(policy, current, &root, binding, index, style)
}
#[must_use]
pub fn decide_with_root(
policy: AttachPolicy,
current: Option<&SessionRecord>,
root: &Path,
binding: &ProjectBinding,
index: &SessionIndex,
style: SessionNameStyle,
) -> AttachDecision {
if policy == AttachPolicy::PickerOnly {
return AttachDecision::Stay;
}
if let Some(cur) = current {
if cur.project_root == root {
return AttachDecision::Stay;
}
}
let raw = match binding.lookup(root) {
Some(id) if index.get(id).is_some() => AttachDecision::SwitchTo(id),
_ => AttachDecision::SpawnNew {
project_root: root.to_path_buf(),
name: FleetSessionNames::from_project_path(root, style),
},
};
match policy {
AttachPolicy::AutoSwitch => raw,
AttachPolicy::SuggestOnly => AttachDecision::Suggest(Box::new(raw)),
AttachPolicy::PickerOnly => AttachDecision::Stay, }
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn sid(s: &str) -> SessionId {
SessionId::from_seed(s)
}
fn record_for(id: &str, root: &str) -> SessionRecord {
SessionRecord::for_project(sid(id), PathBuf::from(root), SessionNameStyle::Emoji, 0)
}
#[test]
fn same_project_stays() {
let cur = record_for("cur", "/code/mado");
let binding = ProjectBinding::new();
let index = SessionIndex::new();
let d = decide_with_root(
AttachPolicy::AutoSwitch,
Some(&cur),
Path::new("/code/mado"),
&binding,
&index,
SessionNameStyle::Emoji,
);
assert_eq!(d, AttachDecision::Stay);
}
#[test]
fn bound_other_project_switches() {
let cur = record_for("cur", "/code/mado");
let mut binding = ProjectBinding::new();
binding.bind(PathBuf::from("/code/tear"), sid("tear-sess"));
let mut index = SessionIndex::new();
index.upsert(record_for("tear-sess", "/code/tear"));
let d = decide_with_root(
AttachPolicy::AutoSwitch,
Some(&cur),
Path::new("/code/tear"),
&binding,
&index,
SessionNameStyle::Emoji,
);
assert_eq!(d, AttachDecision::SwitchTo(sid("tear-sess")));
}
#[test]
fn bound_but_session_gone_spawns_new() {
let mut binding = ProjectBinding::new();
binding.bind(PathBuf::from("/code/tear"), sid("dead"));
let index = SessionIndex::new(); let d = decide_with_root(
AttachPolicy::AutoSwitch,
None,
Path::new("/code/tear"),
&binding,
&index,
SessionNameStyle::Emoji,
);
match d {
AttachDecision::SpawnNew { project_root, name } => {
assert_eq!(project_root, PathBuf::from("/code/tear"));
let expected =
FleetSessionNames::from_project_path(Path::new("/code/tear"), SessionNameStyle::Emoji);
assert_eq!(name.to_string(), expected.to_string());
}
other => panic!("expected SpawnNew, got {other:?}"),
}
}
#[test]
fn brand_new_project_spawns_with_deterministic_name() {
let binding = ProjectBinding::new();
let index = SessionIndex::new();
let d = decide_with_root(
AttachPolicy::AutoSwitch,
None,
Path::new("/code/pleme-io/brand-new"),
&binding,
&index,
SessionNameStyle::Emoji,
);
match d {
AttachDecision::SpawnNew { project_root, name } => {
assert_eq!(project_root, PathBuf::from("/code/pleme-io/brand-new"));
let expected = FleetSessionNames::from_project_path(
Path::new("/code/pleme-io/brand-new"),
SessionNameStyle::Emoji,
);
assert_eq!(name.to_string(), expected.to_string());
let d2 = decide_with_root(
AttachPolicy::AutoSwitch,
None,
Path::new("/code/pleme-io/brand-new"),
&binding,
&index,
SessionNameStyle::Emoji,
);
assert_eq!(AttachDecision::SpawnNew { project_root, name }, d2);
}
other => panic!("expected SpawnNew, got {other:?}"),
}
}
#[test]
fn picker_only_always_stays() {
let mut binding = ProjectBinding::new();
binding.bind(PathBuf::from("/code/tear"), sid("tear-sess"));
let mut index = SessionIndex::new();
index.upsert(record_for("tear-sess", "/code/tear"));
let d = decide_with_root(
AttachPolicy::PickerOnly,
None,
Path::new("/code/tear"),
&binding,
&index,
SessionNameStyle::Emoji,
);
assert_eq!(d, AttachDecision::Stay);
}
#[test]
fn suggest_only_wraps_switch() {
let mut binding = ProjectBinding::new();
binding.bind(PathBuf::from("/code/tear"), sid("tear-sess"));
let mut index = SessionIndex::new();
index.upsert(record_for("tear-sess", "/code/tear"));
let d = decide_with_root(
AttachPolicy::SuggestOnly,
None,
Path::new("/code/tear"),
&binding,
&index,
SessionNameStyle::Emoji,
);
assert_eq!(
d,
AttachDecision::Suggest(Box::new(AttachDecision::SwitchTo(sid("tear-sess"))))
);
}
#[test]
fn suggest_only_does_not_wrap_stay() {
let cur = record_for("cur", "/code/mado");
let binding = ProjectBinding::new();
let index = SessionIndex::new();
let d = decide_with_root(
AttachPolicy::SuggestOnly,
Some(&cur),
Path::new("/code/mado"),
&binding,
&index,
SessionNameStyle::Emoji,
);
assert_eq!(d, AttachDecision::Stay);
}
}