Write

//! Picker **data gather** — read-only projections over the EXISTING surfaces
//! the rest of the CLI already uses. No new daemon surface, no new store
//! (doyle ruling): adapters from the registry, endpoints from the roster + the
//! WAN registry snapshots, descriptions from `info.json`, project history
//! best-effort from the context store, resume rows from the session ledger.
//!
//! Everything here degrades to empty rather than failing — the picker is a
//! front-end; a missing surface means "nothing to show", never a crash.
// [impl->REQ-RUN-PICKER]

use std::path::Path;

use spt_store::perch::{self, ParentHint};
use spt_store::{info, project, sessions};

use super::model::{AdapterInfo, AdapterOption, Category, EndpointRow, EpStatus, ResumeRow};

/// The cwd project name — the Project category label + the project-history match
/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
/// folder → dir name).
pub fn project_name() -> String {
    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
    project::project_id_for_dir(&dir)
}

/// The selectable adapter tree: every active `kind="harness"` adapter with its
/// shipped + local profiles nested under it.
pub fn adapter_options() -> Vec<AdapterOption> {
    let adapters_dir = perch::adapters_dir();
    let mut infos: Vec<AdapterInfo> = Vec::new();
    for (record, manifest) in spt_runtime::registry::registered(&adapters_dir) {
        if manifest.adapter.kind != spt_runtime::manifest::AdapterKind::Harness {
            continue;
        }
        let mut shipped: Vec<String> = manifest.profiles.keys().cloned().collect();
        shipped.sort();
        let local = spt_runtime::registry::local_profile_names(&adapters_dir, &record.name);
        infos.push(AdapterInfo {
            name: record.name,
            shipped,
            local,
        });
    }
    infos.sort_by(|a, b| a.name.cmp(&b.name));
    super::model::build_adapter_tree(&infos)
}

/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    let mut rows = Vec::new();
    rows.extend(local_rows());
    rows.extend(subnet_rows());
    rows
}

/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
fn local_rows() -> Vec<EndpointRow> {
    crate::roster::enumerate()
        .into_iter()
        .map(|p| {
            let perch_path = perch::resolve_perch_path(&p.id, ParentHint::Infer);
            let rec = info::read_info(&perch_path);
            let status = if p.alive {
                EpStatus::Online
            } else {
                EpStatus::Offline
            };
            let adapter_profile = rec
                .as_ref()
                .and_then(|r| r.adapter.clone())
                .unwrap_or_else(|| "(unknown adapter)".to_string());
            let description = rec
                .as_ref()
                .and_then(|r| r.resources.clone())
                .unwrap_or_default();
            let project_history = project_history_for(&p.id);
            // Resume-from-history is offline + LOCAL only (doyle Q3): only an
            // offline local endpoint reads its ledger.
            let resume_rows = if status == EpStatus::Offline {
                resume_rows_for(&perch_path, &project_history)
            } else {
                Vec::new()
            };
            EndpointRow {
                id: p.id,
                group: "local".to_string(),
                node: "LOCAL".to_string(),
                status,
                is_local: true,
                adapter_profile,
                project_history,
                description,
                resume_rows,
            }
        })
        .collect()
}

/// Subnet endpoints, from the WAN registry snapshots, visibility-gated exactly
/// like `spt endpoint list`.
fn subnet_rows() -> Vec<EndpointRow> {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let mut out = Vec::new();
    for s in &subnets.subnets {
        let Some(reg) = regs.get(&s.name) else {
            continue;
        };
        let hide_new = s.hide_new_endpoints;
        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
            let status = match row.status {
                spt_net::net::registry::Status::Active
                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
                // Suspended (cold) + Offline render offline in the W2 duality.
                _ => EpStatus::Offline,
            };
            out.push(EndpointRow {
                id: row.endpoint_id,
                group: format!("{}:{}", s.name, row.node),
                node: row.node,
                status,
                is_local: false,
                adapter_profile: row.resources.clone().unwrap_or_default(),
                project_history: Vec::new(),
                description: row.resources.unwrap_or_default(),
                resume_rows: Vec::new(),
            });
        }
    }
    out
}

/// Best-effort project history for a local endpoint, newest→oldest (doyle Q2):
/// scan the context store's `projects/<project>/<id>/` tree for the projects
/// this endpoint carries context in, ordered by the context file's mtime.
/// Empty if the store has nothing — an informational pane, degrade-to-empty.
fn project_history_for(id: &str) -> Vec<String> {
    let root = perch::tracked_dir();
    let projects_dir = root.join("projects");
    let Ok(entries) = std::fs::read_dir(&projects_dir) else {
        return Vec::new();
    };
    let mut found: Vec<(std::time::SystemTime, String)> = Vec::new();
    for e in entries.flatten() {
        let project_id = e.file_name().to_string_lossy().into_owned();
        let ctx = spt_store::contextstore::project_context_file(&root, &project_id, id);
        let Ok(meta) = std::fs::metadata(&ctx) else {
            continue; // this endpoint has no context in this project
        };
        let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
        found.push((mtime, project_id));
    }
    found.sort_by(|a, b| b.0.cmp(&a.0)); // newest first
    found.into_iter().map(|(_, p)| p).collect()
}

/// Resume rows from the per-endpoint session ledger (newest→oldest), titled in
/// the model. The ledger row carries no project, so the project field is a
/// best-effort fill from the endpoint's most-recent project history; the title
/// falls back to the boundary trigger when that is empty.
fn resume_rows_for(perch_path: &Path, project_history: &[String]) -> Vec<ResumeRow> {
    let project = project_history.first().cloned().unwrap_or_default();
    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
        .into_iter()
        .map(|e| ResumeRow {
            session_id: e.session_id,
            ts: e.ts,
            trigger: e.trigger.token().to_string(),
            project: project.clone(),
        })
        .collect();
    rows.reverse(); // ledger is oldest→newest; show newest first
    rows
}

/// The categories that actually have endpoints (for the "(no endpoints — press
/// n to create)" empty-state hint per category).
pub fn nonempty_categories(endpoints: &[EndpointRow], project_name: &str) -> Vec<Category> {
    Category::ALL
        .into_iter()
        .filter(|c| {
            endpoints.iter().any(|e| match c {
                Category::Local => e.is_local,
                Category::Subnet => !e.is_local,
                Category::Project => {
                    e.is_local && e.project_history.iter().any(|p| p == project_name)
                }
            })
        })
        .collect()
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\data.rs (file state is current in your context — no need to Read it back)

//! Picker **data gather** — read-only projections over the EXISTING surfaces
//! the rest of the CLI already uses. No new daemon surface, no new store
//! (doyle ruling): adapters from the registry, endpoints from the roster + the
//! WAN registry snapshots, descriptions from `info.json`, project history
//! best-effort from the context store, resume rows from the session ledger.
//!
//! Everything here degrades to empty rather than failing — the picker is a
//! front-end; a missing surface means "nothing to show", never a crash.
// [impl->REQ-RUN-PICKER]

use std::path::Path;

use spt_store::perch::{self, ParentHint};
use spt_store::{info, project, sessions};

use super::model::{AdapterInfo, AdapterOption, Category, EndpointRow, EpStatus, ResumeRow};

/// The cwd project name — the Project category label + the project-history match
/// key. Derived exactly like the daemon derives it (remote-url slug → toplevel
/// folder → dir name).
pub fn project_name() -> String {
    let dir = std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf());
    project::project_id_for_dir(&dir)
}

/// The selectable adapter tree: every active `kind="harness"` adapter with its
/// shipped + local profiles nested under it.
pub fn adapter_options() -> Vec<AdapterOption> {
    let adapters_dir = perch::adapters_dir();
    let mut infos: Vec<AdapterInfo> = Vec::new();
    for (record, manifest) in spt_runtime::registry::registered(&adapters_dir) {
        if manifest.adapter.kind != spt_runtime::manifest::AdapterKind::Harness {
            continue;
        }
        let mut shipped: Vec<String> = manifest.profiles.keys().cloned().collect();
        shipped.sort();
        let local = spt_runtime::registry::local_profile_names(&adapters_dir, &record.name);
        infos.push(AdapterInfo {
            name: record.name,
            shipped,
            local,
        });
    }
    infos.sort_by(|a, b| a.name.cmp(&b.name));
    super::model::build_adapter_tree(&infos)
}

/// Every discoverable endpoint, across all three categories. The model
/// partitions by category; this just produces the union (local perches +
/// subnet registry rows).
// [impl->REQ-RUN-PICKER]
pub fn gather_endpoints() -> Vec<EndpointRow> {
    let mut rows = Vec::new();
    rows.extend(local_rows());
    rows.extend(subnet_rows());
    rows
}

/// This node's local perches (from the roster). Local rows feed both the Local
/// and Project categories (the model filters Project by project-history match).
fn local_rows() -> Vec<EndpointRow> {
    crate::roster::enumerate()
        .into_iter()
        .map(|p| {
            let perch_path = perch::resolve_perch_path(&p.id, ParentHint::Infer);
            let rec = info::read_info(&perch_path);
            let status = if p.alive {
                EpStatus::Online
            } else {
                EpStatus::Offline
            };
            let adapter_profile = rec
                .as_ref()
                .and_then(|r| r.adapter.clone())
                .unwrap_or_else(|| "(unknown adapter)".to_string());
            let description = rec
                .as_ref()
                .and_then(|r| r.resources.clone())
                .unwrap_or_default();
            let project_history = project_history_for(&p.id);
            // Resume-from-history is offline + LOCAL only (doyle Q3): only an
            // offline local endpoint reads its ledger.
            let resume_rows = if status == EpStatus::Offline {
                resume_rows_for(&perch_path, &project_history)
            } else {
                Vec::new()
            };
            EndpointRow {
                id: p.id,
                group: "local".to_string(),
                node: "LOCAL".to_string(),
                status,
                is_local: true,
                adapter_profile,
                project_history,
                description,
                resume_rows,
            }
        })
        .collect()
}

/// Subnet endpoints, from the WAN registry snapshots, visibility-gated exactly
/// like `spt endpoint list`.
fn subnet_rows() -> Vec<EndpointRow> {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let mut out = Vec::new();
    for s in &subnets.subnets {
        let Some(reg) = regs.get(&s.name) else {
            continue;
        };
        let hide_new = s.hide_new_endpoints;
        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
            let status = match row.status {
                spt_net::net::registry::Status::Active
                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
                // Suspended (cold) + Offline render offline in the W2 duality.
                _ => EpStatus::Offline,
            };
            out.push(EndpointRow {
                id: row.endpoint_id,
                group: format!("{}:{}", s.name, row.node),
                node: row.node,
                status,
                is_local: false,
                adapter_profile: row.resources.clone().unwrap_or_default(),
                project_history: Vec::new(),
                description: row.resources.unwrap_or_default(),
                resume_rows: Vec::new(),
            });
        }
    }
    out
}

/// Best-effort project history for a local endpoint, newest→oldest (doyle Q2):
/// scan the context store's `projects/<project>/<id>/` tree for the projects
/// this endpoint carries context in, ordered by the context file's mtime.
/// Empty if the store has nothing — an informational pane, degrade-to-empty.
fn project_history_for(id: &str) -> Vec<String> {
    let root = perch::tracked_dir();
    let projects_dir = root.join("projects");
    let Ok(entries) = std::fs::read_dir(&projects_dir) else {
        return Vec::new();
    };
    let mut found: Vec<(std::time::SystemTime, String)> = Vec::new();
    for e in entries.flatten() {
        let project_id = e.file_name().to_string_lossy().into_owned();
        let ctx = spt_store::contextstore::project_context_file(&root, &project_id, id);
        let Ok(meta) = std::fs::metadata(&ctx) else {
            continue; // this endpoint has no context in this project
        };
        let mtime = meta.modified().unwrap_or(std::time::UNIX_EPOCH);
        found.push((mtime, project_id));
    }
    found.sort_by(|a, b| b.0.cmp(&a.0)); // newest first
    found.into_iter().map(|(_, p)| p).collect()
}

/// Resume rows from the per-endpoint session ledger (newest→oldest), titled in
/// the model. The ledger row carries no project, so the project field is a
/// best-effort fill from the endpoint's most-recent project history; the title
/// falls back to the boundary trigger when that is empty.
fn resume_rows_for(perch_path: &Path, project_history: &[String]) -> Vec<ResumeRow> {
    let project = project_history.first().cloned().unwrap_or_default();
    let mut rows: Vec<ResumeRow> = sessions::last_k(perch_path, sessions::MAX_LEDGER)
        .into_iter()
        .map(|e| ResumeRow {
            session_id: e.session_id,
            ts: e.ts,
            trigger: e.trigger.token().to_string(),
            project: project.clone(),
        })
        .collect();
    rows.reverse(); // ledger is oldest→newest; show newest first
    rows
}

/// The categories that actually have endpoints (for the "(no endpoints — press
/// n to create)" empty-state hint per category).
pub fn nonempty_categories(endpoints: &[EndpointRow], project_name: &str) -> Vec<Category> {
    Category::ALL
        .into_iter()
        .filter(|c| {
            endpoints.iter().any(|e| match c {
                Category::Local => e.is_local,
                Category::Subnet => !e.is_local,
                Category::Project => {
                    e.is_local && e.project_history.iter().any(|p| p == project_name)
                }
            })
        })
        .collect()
}


Prompt: commit as needed to keep code history tidy.


data.rs done. Operator authorized incremental commits — I'll finish the module to a compiling+green state first (cli.rs already references `picker::run`, so partial = broken build), then commit tidy. Continue: shortcut.rs.


Updated task #5 status


Updated task #7 status


write the JIT plan for W2, verify it with doyle, execute on their greenlight


//! `cc-<id>` launcher shortcut generation (the picker `s` keybind,
//! REQ-RUN-SHORTCUT).
//!
//! From any pre-start options set the picker bakes the current selection's
//! NON-INTERACTIVE `spt endpoint run` flags into a launcher script at the
//! project root, so a user can re-launch this exact endpoint with one command
//! (and a `cc-doyle` muscle-memory shortcut).
//!
//! - **Current-OS native** (doyle Q4): a `.ps1` on Windows, a POSIX `sh` (+`chmod
//!   +x`) on Unix — a single portable form can't be both.
//! - **Sentinel-guarded overwrite:** the generator writes + checks a generated-by
//!   header [`SENTINEL`]. It overwrites its OWN prior output freely ("Update"),
//!   but REFUSES a same-named file that lacks the sentinel — never clobbering a
//!   user file.
//! - **Terminal actions only:** adapter[:profile] + id + (create|resume) +
//!   (start|attach|view). Kick/Instantiate/Change-adapter/Fork are
//!   interactive-only and not bakeable.
// [impl->REQ-RUN-SHORTCUT]

use std::path::{Path, PathBuf};

use super::model::RunMode;

/// The generated-by marker. Present on line 1 of every script we write; its
/// presence (and ONLY its presence) authorizes an overwrite.
pub const SENTINEL: &str = "spt-generated cc-shortcut — safe to regenerate (do not remove this line)";

/// What a write did (or refused to do).
#[derive(Debug, PartialEq, Eq)]
pub enum WriteOutcome {
    /// A new shortcut was written at this path.
    Created(PathBuf),
    /// An existing spt-generated shortcut was overwritten.
    Updated(PathBuf),
    /// A same-named file exists but lacks the sentinel — refused (the path is
    /// returned so the caller can warn precisely).
    RefusedForeign(PathBuf),
}

/// The current-OS shortcut filename for an endpoint id.
pub fn shortcut_filename(id: &str) -> String {
    if cfg!(windows) {
        format!("cc-{id}.ps1")
    } else {
        format!("cc-{id}")
    }
}

/// Render the script body for the current OS. Pure (no I/O) so the content is
/// unit-assertable. The first line is the sentinel comment (`#` is a comment in
/// both PowerShell and POSIX sh).
// [impl->REQ-RUN-SHORTCUT]
pub fn render_script(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let args = baked_args(id, adapter, resume, mode);
    if cfg!(windows) {
        // PowerShell: `&` call operator forwards $args; `$args` passes through any
        // extra runtime args the user appends to the shortcut.
        format!(
            "# {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             spt endpoint run {args} @args\n"
        )
    } else {
        format!(
            "#!/bin/sh\n\
             # {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             exec spt endpoint run {args} \"$@\"\n"
        )
    }
}

/// The non-interactive flag string baked into the shortcut — terminal actions
/// only. `--create` (the default-fresh made explicit) is mutually exclusive with
/// `--resume`; attach is the default action (no flag).
fn baked_args(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let mut parts = vec![
        "--adapter".to_string(),
        shell_quote(adapter),
        "--id".to_string(),
        shell_quote(id),
    ];
    match resume {
        Some(session) => {
            parts.push("--resume".to_string());
            parts.push(shell_quote(session));
        }
        None => parts.push("--create".to_string()),
    }
    match mode {
        RunMode::Start => parts.push("--start".to_string()),
        RunMode::View => parts.push("--view".to_string()),
        RunMode::Attach => {} // the default action — no flag
    }
    parts.join(" ")
}

/// Conservative quoting: endpoint ids / adapter names / session ids are charset-
/// constrained (alphanumeric, `-`, `_`, plus `:` in an adapter:profile), so a
/// bare token is safe; only quote if something unexpected appears.
fn shell_quote(s: &str) -> String {
    let safe = s
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | ':' | '.'));
    if safe && !s.is_empty() {
        s.to_string()
    } else {
        // Single-quote for sh; PowerShell also accepts single quotes as literals.
        format!("'{}'", s.replace('\'', "'\\''"))
    }
}

/// Whether `contents` was written by us (carries the [`SENTINEL`] on any of its
/// first few lines).
pub fn has_sentinel(contents: &str) -> bool {
    contents.lines().take(3).any(|l| l.contains(SENTINEL))
}

/// Write (or refuse to overwrite) the shortcut in `dir`. Sentinel-guarded:
/// a fresh path is created; an existing OURS path is updated; an existing
/// foreign path (no sentinel) is refused untouched.
// [impl->REQ-RUN-SHORTCUT]
pub fn write_shortcut(
    dir: &Path,
    id: &str,
    adapter: &str,
    resume: Option<&str>,
    mode: RunMode,
) -> std::io::Result<WriteOutcome> {
    let path = dir.join(shortcut_filename(id));
    let exists = path.exists();
    if exists {
        let current = std::fs::read_to_string(&path).unwrap_or_default();
        if !has_sentinel(&current) {
            return Ok(WriteOutcome::RefusedForeign(path));
        }
    }
    let body = render_script(id, adapter, resume, mode);
    std::fs::write(&path, &body)?;
    set_executable(&path);
    Ok(if exists {
        WriteOutcome::Updated(path)
    } else {
        WriteOutcome::Created(path)
    })
}

#[cfg(unix)]
fn set_executable(path: &Path) {
    use std::os::unix::fs::PermissionsExt;
    if let Ok(meta) = std::fs::metadata(path) {
        let mut perms = meta.permissions();
        perms.set_mode(perms.mode() | 0o755);
        let _ = std::fs::set_permissions(path, perms);
    }
}

#[cfg(not(unix))]
fn set_executable(_path: &Path) {}

#[cfg(test)]
mod tests {
    use super::*;

    // [unit->REQ-RUN-SHORTCUT] the script bakes the terminal-action flags; a
    // fresh run defaults to --create + attach (no action flag).
    #[test]
    fn bakes_create_attach_by_default() {
        let body = render_script("doyle", "claude-spt:fast", None, RunMode::Attach);
        assert!(body.contains("--adapter claude-spt:fast"));
        assert!(body.contains("--id doyle"));
        assert!(body.contains("--create"));
        assert!(!body.contains("--resume"));
        assert!(!body.contains("--start") && !body.contains("--view"));
        assert!(body.lines().next().unwrap().contains(SENTINEL), "sentinel on line 1");
    }

    // [unit->REQ-RUN-SHORTCUT] resume bakes --resume (not --create); view/start
    // bake their action flag.
    #[test]
    fn bakes_resume_and_action() {
        let body = render_script("d", "ccs", Some("sess123"), RunMode::View);
        assert!(body.contains("--resume sess123"));
        assert!(!body.contains("--create"));
        assert!(body.contains("--view"));

        let start = render_script("d", "ccs", None, RunMode::Start);
        assert!(start.contains("--create") && start.contains("--start"));
    }

    // [unit->REQ-RUN-SHORTCUT] sentinel detection only trusts our own header.
    #[test]
    fn sentinel_detection() {
        assert!(has_sentinel(&render_script("x", "a", None, RunMode::Attach)));
        assert!(!has_sentinel("#!/bin/sh\necho hi\n"), "a user script has no sentinel");
    }

    // [unit->REQ-RUN-SHORTCUT] create → update (our own) → refuse (foreign).
    #[test]
    fn write_create_update_refuse() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path();

        // 1. fresh → Created.
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        let path = match out {
            WriteOutcome::Created(path) => path,
            other => panic!("expected Created, got {other:?}"),
        };
        assert!(path.exists());

        // 2. re-generate over our own → Updated (different selection rewrites).
        let out = write_shortcut(p, "doyle", "ccs", Some("s9"), RunMode::Start).unwrap();
        assert!(matches!(out, WriteOutcome::Updated(_)));
        let body = std::fs::read_to_string(&path).unwrap();
        assert!(body.contains("--adapter ccs") && body.contains("--resume s9"));

        // 3. a foreign same-named file → RefusedForeign, untouched.
        std::fs::write(&path, "#!/bin/sh\necho mine\n").unwrap();
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        assert!(matches!(out, WriteOutcome::RefusedForeign(_)));
        assert_eq!(
            std::fs::read_to_string(&path).unwrap(),
            "#!/bin/sh\necho mine\n",
            "a foreign file is never clobbered"
        );
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\shortcut.rs (file state is current in your context — no need to Read it back)

//! `cc-<id>` launcher shortcut generation (the picker `s` keybind,
//! REQ-RUN-SHORTCUT).
//!
//! From any pre-start options set the picker bakes the current selection's
//! NON-INTERACTIVE `spt endpoint run` flags into a launcher script at the
//! project root, so a user can re-launch this exact endpoint with one command
//! (and a `cc-doyle` muscle-memory shortcut).
//!
//! - **Current-OS native** (doyle Q4): a `.ps1` on Windows, a POSIX `sh` (+`chmod
//!   +x`) on Unix — a single portable form can't be both.
//! - **Sentinel-guarded overwrite:** the generator writes + checks a generated-by
//!   header [`SENTINEL`]. It overwrites its OWN prior output freely ("Update"),
//!   but REFUSES a same-named file that lacks the sentinel — never clobbering a
//!   user file.
//! - **Terminal actions only:** adapter[:profile] + id + (create|resume) +
//!   (start|attach|view). Kick/Instantiate/Change-adapter/Fork are
//!   interactive-only and not bakeable.
// [impl->REQ-RUN-SHORTCUT]

use std::path::{Path, PathBuf};

use super::model::RunMode;

/// The generated-by marker. Present on line 1 of every script we write; its
/// presence (and ONLY its presence) authorizes an overwrite.
pub const SENTINEL: &str = "spt-generated cc-shortcut — safe to regenerate (do not remove this line)";

/// What a write did (or refused to do).
#[derive(Debug, PartialEq, Eq)]
pub enum WriteOutcome {
    /// A new shortcut was written at this path.
    Created(PathBuf),
    /// An existing spt-generated shortcut was overwritten.
    Updated(PathBuf),
    /// A same-named file exists but lacks the sentinel — refused (the path is
    /// returned so the caller can warn precisely).
    RefusedForeign(PathBuf),
}

/// The current-OS shortcut filename for an endpoint id.
pub fn shortcut_filename(id: &str) -> String {
    if cfg!(windows) {
        format!("cc-{id}.ps1")
    } else {
        format!("cc-{id}")
    }
}

/// Render the script body for the current OS. Pure (no I/O) so the content is
/// unit-assertable. The first line is the sentinel comment (`#` is a comment in
/// both PowerShell and POSIX sh).
// [impl->REQ-RUN-SHORTCUT]
pub fn render_script(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let args = baked_args(id, adapter, resume, mode);
    if cfg!(windows) {
        // PowerShell: `&` call operator forwards $args; `$args` passes through any
        // extra runtime args the user appends to the shortcut.
        format!(
            "# {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             spt endpoint run {args} @args\n"
        )
    } else {
        format!(
            "#!/bin/sh\n\
             # {SENTINEL}\n\
             # cc-{id}: launch the `{id}` endpoint with the baked selection.\n\
             exec spt endpoint run {args} \"$@\"\n"
        )
    }
}

/// The non-interactive flag string baked into the shortcut — terminal actions
/// only. `--create` (the default-fresh made explicit) is mutually exclusive with
/// `--resume`; attach is the default action (no flag).
fn baked_args(id: &str, adapter: &str, resume: Option<&str>, mode: RunMode) -> String {
    let mut parts = vec![
        "--adapter".to_string(),
        shell_quote(adapter),
        "--id".to_string(),
        shell_quote(id),
    ];
    match resume {
        Some(session) => {
            parts.push("--resume".to_string());
            parts.push(shell_quote(session));
        }
        None => parts.push("--create".to_string()),
    }
    match mode {
        RunMode::Start => parts.push("--start".to_string()),
        RunMode::View => parts.push("--view".to_string()),
        RunMode::Attach => {} // the default action — no flag
    }
    parts.join(" ")
}

/// Conservative quoting: endpoint ids / adapter names / session ids are charset-
/// constrained (alphanumeric, `-`, `_`, plus `:` in an adapter:profile), so a
/// bare token is safe; only quote if something unexpected appears.
fn shell_quote(s: &str) -> String {
    let safe = s
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | ':' | '.'));
    if safe && !s.is_empty() {
        s.to_string()
    } else {
        // Single-quote for sh; PowerShell also accepts single quotes as literals.
        format!("'{}'", s.replace('\'', "'\\''"))
    }
}

/// Whether `contents` was written by us (carries the [`SENTINEL`] on any of its
/// first few lines).
pub fn has_sentinel(contents: &str) -> bool {
    contents.lines().take(3).any(|l| l.contains(SENTINEL))
}

/// Write (or refuse to overwrite) the shortcut in `dir`. Sentinel-guarded:
/// a fresh path is created; an existing OURS path is updated; an existing
/// foreign path (no sentinel) is refused untouched.
// [impl->REQ-RUN-SHORTCUT]
pub fn write_shortcut(
    dir: &Path,
    id: &str,
    adapter: &str,
    resume: Option<&str>,
    mode: RunMode,
) -> std::io::Result<WriteOutcome> {
    let path = dir.join(shortcut_filename(id));
    let exists = path.exists();
    if exists {
        let current = std::fs::read_to_string(&path).unwrap_or_default();
        if !has_sentinel(&current) {
            return Ok(WriteOutcome::RefusedForeign(path));
        }
    }
    let body = render_script(id, adapter, resume, mode);
    std::fs::write(&path, &body)?;
    set_executable(&path);
    Ok(if exists {
        WriteOutcome::Updated(path)
    } else {
        WriteOutcome::Created(path)
    })
}

#[cfg(unix)]
fn set_executable(path: &Path) {
    use std::os::unix::fs::PermissionsExt;
    if let Ok(meta) = std::fs::metadata(path) {
        let mut perms = meta.permissions();
        perms.set_mode(perms.mode() | 0o755);
        let _ = std::fs::set_permissions(path, perms);
    }
}

#[cfg(not(unix))]
fn set_executable(_path: &Path) {}

#[cfg(test)]
mod tests {
    use super::*;

    // [unit->REQ-RUN-SHORTCUT] the script bakes the terminal-action flags; a
    // fresh run defaults to --create + attach (no action flag).
    #[test]
    fn bakes_create_attach_by_default() {
        let body = render_script("doyle", "claude-spt:fast", None, RunMode::Attach);
        assert!(body.contains("--adapter claude-spt:fast"));
        assert!(body.contains("--id doyle"));
        assert!(body.contains("--create"));
        assert!(!body.contains("--resume"));
        assert!(!body.contains("--start") && !body.contains("--view"));
        assert!(body.lines().next().unwrap().contains(SENTINEL), "sentinel on line 1");
    }

    // [unit->REQ-RUN-SHORTCUT] resume bakes --resume (not --create); view/start
    // bake their action flag.
    #[test]
    fn bakes_resume_and_action() {
        let body = render_script("d", "ccs", Some("sess123"), RunMode::View);
        assert!(body.contains("--resume sess123"));
        assert!(!body.contains("--create"));
        assert!(body.contains("--view"));

        let start = render_script("d", "ccs", None, RunMode::Start);
        assert!(start.contains("--create") && start.contains("--start"));
    }

    // [unit->REQ-RUN-SHORTCUT] sentinel detection only trusts our own header.
    #[test]
    fn sentinel_detection() {
        assert!(has_sentinel(&render_script("x", "a", None, RunMode::Attach)));
        assert!(!has_sentinel("#!/bin/sh\necho hi\n"), "a user script has no sentinel");
    }

    // [unit->REQ-RUN-SHORTCUT] create → update (our own) → refuse (foreign).
    #[test]
    fn write_create_update_refuse() {
        let dir = tempfile::tempdir().unwrap();
        let p = dir.path();

        // 1. fresh → Created.
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        let path = match out {
            WriteOutcome::Created(path) => path,
            other => panic!("expected Created, got {other:?}"),
        };
        assert!(path.exists());

        // 2. re-generate over our own → Updated (different selection rewrites).
        let out = write_shortcut(p, "doyle", "ccs", Some("s9"), RunMode::Start).unwrap();
        assert!(matches!(out, WriteOutcome::Updated(_)));
        let body = std::fs::read_to_string(&path).unwrap();
        assert!(body.contains("--adapter ccs") && body.contains("--resume s9"));

        // 3. a foreign same-named file → RefusedForeign, untouched.
        std::fs::write(&path, "#!/bin/sh\necho mine\n").unwrap();
        let out = write_shortcut(p, "doyle", "claude-spt", None, RunMode::Attach).unwrap();
        assert!(matches!(out, WriteOutcome::RefusedForeign(_)));
        assert_eq!(
            std::fs::read_to_string(&path).unwrap(),
            "#!/bin/sh\necho mine\n",
            "a foreign file is never clobbered"
        );
    }
}


Shortcut done. Now `view.rs` (ratatui render, Buffer-testable).


Updated task #6 status


//! Picker **view** — render a `&PickerModel` to a ratatui frame.
//!
//! ratatui's double-buffered renderer removes the manual erase/redraw +
//! wrapped-row-counting hazard the raw-ANSI reference (`vercel-labs/skills`)
//! carried. Rendering is a pure function of the model + the frame area, so every
//! screen is asserted against a `TestBackend` `Buffer` in the unit tests (no
//! live terminal) — doyle's "assert the rendered buffer" gate.
// [impl->REQ-RUN-PICKER]

use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
use ratatui::Frame;

use super::model::{glyph, Category, ConfirmOption, EpStatus, PickerModel, Screen};

/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
/// surface the same actions without the keys (the phone caveat).
const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";

/// Highlight style for the row under a cursor.
fn cursor_style() -> Style {
    Style::default()
        .fg(Color::Cyan)
        .add_modifier(Modifier::BOLD)
}

/// A status square span in its tri-state color (green ■ / gray ▢; blue is the
/// deferred attached state — not rendered in W2).
fn square_span(status: EpStatus) -> Span<'static> {
    let color = match status {
        EpStatus::Online => Color::Green,
        EpStatus::Offline => Color::DarkGray,
    };
    Span::styled(status.square().to_string(), Style::default().fg(color))
}

/// Render the whole picker for the current screen.
pub fn render(model: &PickerModel, f: &mut Frame) {
    let area = f.area();
    match model.screen {
        Screen::Kind => render_kind(model, f, area),
        Screen::CreateAdapter => render_create_adapter(model, f, area),
        Screen::CreateId => render_create_id(model, f, area),
        Screen::PickExisting => render_pick(model, f, area),
        Screen::Confirm => render_confirm(model, f, area),
    }
}

fn titled_block(title: &str) -> Block<'_> {
    Block::default().borders(Borders::ALL).title(title)
}

/// A cursor-aware list item: prefix the cursor row with the caret glyph.
fn item(label: String, selected: bool) -> ListItem<'static> {
    if selected {
        ListItem::new(Line::from(vec![
            Span::styled(format!("{} ", glyph::CURSOR), cursor_style()),
            Span::styled(label, cursor_style()),
        ]))
    } else {
        ListItem::new(Line::from(format!("  {label}")))
    }
}

// ── Layer 1: kind ────────────────────────────────────────────────────────
fn render_kind(model: &PickerModel, f: &mut Frame, area: Rect) {
    let items = vec![
        item("Create new".to_string(), model.kind_cursor == 0),
        item("Pick existing".to_string(), model.kind_cursor == 1),
    ];
    let list = List::new(items).block(titled_block("What kind of endpoint?"));
    f.render_widget(list, area);
}

// ── Create-new: adapter tree ───────────────────────────────────────────────
fn render_create_adapter(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(1)])
        .split(area);
    let items: Vec<ListItem> = model
        .adapter_rows
        .iter()
        .enumerate()
        .map(|(i, row)| {
            let label = match &row.profile {
                Some(p) => format!("{} {}:{p}", glyph::BRANCH, row.adapter),
                None => row.adapter.clone(),
            };
            item(label, i == model.adapter_cursor)
        })
        .collect();
    let list = if items.is_empty() {
        List::new(vec![ListItem::new(
            "(no harness adapters registered — `spt adapter list`)",
        )])
    } else {
        List::new(items)
    }
    .block(titled_block("Choose your harness adapter for this endpoint:"));
    f.render_widget(list, chunks[0]);
    f.render_widget(legend(), chunks[1]);
}

// ── Create-new: id entry ────────────────────────────────────────────────────
fn render_create_id(model: &PickerModel, f: &mut Frame, area: Rect) {
    let mut lines = vec![
        Line::from("Enter an endpoint id. Alphanumeric + hyphen + underscore only."),
        Line::from(""),
        Line::from(vec![
            Span::raw("id: "),
            Span::styled(
                format!("{}_", model.id_buffer),
                Style::default().add_modifier(Modifier::BOLD),
            ),
        ]),
    ];
    if let Some(err) = &model.id_error {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            format!("invalid: {err}"),
            Style::default().fg(Color::Red),
        )));
    }
    let adapter = model
        .selected_adapter()
        .map(|a| a.address())
        .unwrap_or_default();
    let p = Paragraph::new(lines).block(titled_block(&format!("New {adapter} endpoint")));
    f.render_widget(p, area);
}

// ── Pick-existing ───────────────────────────────────────────────────────────
fn category_tabs(model: &PickerModel) -> Line<'static> {
    let mut spans = vec![Span::raw("[ ")];
    for (i, cat) in Category::ALL.iter().enumerate() {
        let label = cat.label(&model.project_name);
        let style = if *cat == model.category {
            cursor_style()
        } else {
            Style::default().fg(Color::DarkGray)
        };
        spans.push(Span::styled(label, style));
        if i + 1 < Category::ALL.len() {
            spans.push(Span::raw(" | "));
        }
    }
    spans.push(Span::raw(" ]"));
    Line::from(spans)
}

fn legend() -> Paragraph<'static> {
    Paragraph::new(Line::from(Span::styled(
        LEGEND,
        Style::default().fg(Color::DarkGray),
    )))
}

fn render_pick(model: &PickerModel, f: &mut Frame, area: Rect) {
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1), // category tabs
            Constraint::Min(1),    // body (list | description)
            Constraint::Length(1), // filter / legend
        ])
        .split(area);
    f.render_widget(Paragraph::new(category_tabs(model)), rows[0]);

    let body = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(rows[1]);

    // Left: grouped endpoint list with status squares.
    let vis = model.visible_indices();
    if vis.is_empty() {
        let empty = Paragraph::new("no endpoints — press n to create")
            .block(titled_block("Available endpoints:"));
        f.render_widget(empty, body[0]);
    } else {
        let mut items: Vec<ListItem> = Vec::new();
        let mut last_group: Option<&str> = None;
        // The visible-list cursor maps to the i-th visible row; track that the
        // group headers we interleave don't shift it (headers are non-selectable
        // display rows, the cursor counts only endpoint rows).
        for (vi, &ei) in vis.iter().enumerate() {
            let ep = &model.endpoints[ei];
            if last_group != Some(ep.group.as_str()) {
                items.push(ListItem::new(Span::styled(
                    ep.group.clone(),
                    Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM),
                )));
                last_group = Some(ep.group.as_str());
            }
            let selected = vi == model.item_cursor;
            let caret = if selected { glyph::CURSOR } else { " " };
            let line = Line::from(vec![
                Span::styled(format!("{caret} "), cursor_style()),
                square_span(ep.status),
                Span::raw(format!(" {}", ep.id)),
            ]);
            let li = ListItem::new(line);
            items.push(if selected {
                li.style(cursor_style())
            } else {
                li
            });
        }
        f.render_widget(
            List::new(items).block(titled_block("Available endpoints:")),
            body[0],
        );
    }

    // Right: the two-pane description for the highlighted endpoint.
    f.render_widget(description_pane(model), body[1]);

    // Bottom: the filter line (when filtering) else the legend.
    if model.filtering || !model.query.is_empty() {
        let q = Paragraph::new(Line::from(vec![
            Span::styled("/", Style::default().fg(Color::Cyan)),
            Span::raw(model.query.clone()),
            Span::styled("_", Style::default().add_modifier(Modifier::BOLD)),
        ]));
        f.render_widget(q, rows[2]);
    } else {
        f.render_widget(legend(), rows[2]);
    }
}

fn description_pane(model: &PickerModel) -> Paragraph<'static> {
    let Some(ep) = model.selected_endpoint() else {
        return Paragraph::new("").block(titled_block("Description"));
    };
    let history = if ep.project_history.is_empty() {
        "(none)".to_string()
    } else {
        ep.project_history.join(", ")
    };
    let desc = if ep.description.is_empty() {
        "(no description)".to_string()
    } else {
        ep.description.clone()
    };
    let lines = vec![
        Line::from(vec![
            Span::styled("harness: ", Style::default().fg(Color::DarkGray)),
            Span::raw(ep.adapter_profile.clone()),
        ]),
        Line::from(vec![
            Span::styled("project history: ", Style::default().fg(Color::DarkGray)),
            Span::raw(history),
        ]),
        Line::from(""),
        Line::from(desc),
    ];
    Paragraph::new(lines)
        .wrap(Wrap { trim: true })
        .block(titled_block("Description"))
}

// ── Confirm ─────────────────────────────────────────────────────────────────
fn confirm_option_label(opt: ConfirmOption) -> &'static str {
    match opt {
        ConfirmOption::Attach => "Attach now",
        ConfirmOption::Start => "Start now",
        ConfirmOption::View => "View now (read-only)",
        ConfirmOption::Resume => "Resume from history",
        ConfirmOption::ChangeAdapter => "Change harness adapter",
        ConfirmOption::Instantiate => "Instantiate locally",
        ConfirmOption::Fork => "Fork endpoint",
        ConfirmOption::Shortcut => "New/Update cc-<id> shortcut (s)",
    }
}

fn render_confirm(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(7), Constraint::Min(1), Constraint::Length(1)])
        .split(area);

    // The selection summary block.
    if let Some(ep) = model.selected_endpoint() {
        let status_word = match ep.status {
            EpStatus::Online => "online",
            EpStatus::Offline => "offline",
        };
        let loc = if ep.is_local {
            "LOCAL".to_string()
        } else {
            ep.node.clone()
        };
        let history = if ep.project_history.is_empty() {
            "(none)".to_string()
        } else {
            ep.project_history.join(", ")
        };
        let summary = vec![
            Line::from(Span::styled(
                ep.id.clone(),
                Style::default().add_modifier(Modifier::BOLD),
            )),
            Line::from(vec![
                square_span(ep.status),
                Span::raw(format!(" {status_word} • {loc}")),
            ]),
            Line::from(ep.adapter_profile.clone()),
            Line::from(format!("Project history: {history}")),
            Line::from(ep.description.clone()),
        ];
        f.render_widget(
            Paragraph::new(summary).block(titled_block("Confirm selection")),
            chunks[0],
        );
    }

    // The status-dependent option set.
    let opts = model.confirm_options();
    let items: Vec<ListItem> = opts
        .iter()
        .enumerate()
        .map(|(i, opt)| item(confirm_option_label(*opt).to_string(), i == model.confirm_cursor))
        .collect();
    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(legend(), chunks[2]);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::picker::model::{AdapterInfo, EndpointRow, ResumeRow};
    use ratatui::backend::TestBackend;
    use ratatui::Terminal;

    /// Flatten a rendered backend to one big string (row-major) for `contains`
    /// assertions — the buffer-level gate doyle asked for.
    fn rendered(model: &PickerModel) -> String {
        let backend = TestBackend::new(100, 30);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| render(model, f)).unwrap();
        let buf = terminal.backend().buffer().clone();
        let mut out = String::new();
        for y in 0..buf.area.height {
            for x in 0..buf.area.width {
                out.push_str(buf[(x, y)].symbol());
            }
            out.push('\n');
        }
        out
    }

    fn ep(id: &str, status: EpStatus, is_local: bool) -> EndpointRow {
        EndpointRow {
            id: id.to_string(),
            group: "local".to_string(),
            node: "LOCAL".to_string(),
            status,
            is_local,
            adapter_profile: "claude-spt:fast".to_string(),
            project_history: vec!["spt-core".to_string()],
            description: "the gatekeeper".to_string(),
            resume_rows: vec![],
        }
    }

    // [unit->REQ-RUN-PICKER] the kind screen renders both choices + the caret on
    // the cursor row.
    #[test]
    fn kind_buffer() {
        let m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
        let s = rendered(&m);
        assert!(s.contains("What kind of endpoint?"));
        assert!(s.contains("Create new"));
        assert!(s.contains("Pick existing"));
        assert!(s.contains(glyph::CURSOR), "the cursor caret renders");
    }

    // [unit->REQ-RUN-PICKER] the adapter screen renders the tree-nested profiles
    // under their adapter with the branch glyph.
    #[test]
    fn adapter_tree_buffer() {
        let rows = crate::picker::model::build_adapter_tree(&[AdapterInfo {
            name: "claude-spt".into(),
            shipped: vec!["fast".into()],
            local: vec![],
        }]);
        let mut m = PickerModel::new("p".into(), rows, vec![], None, None);
        m.screen = Screen::CreateAdapter;
        let s = rendered(&m);
        assert!(s.contains("Choose your harness adapter"));
        assert!(s.contains("claude-spt"));
        assert!(s.contains(glyph::BRANCH), "profile nests under the adapter");
        assert!(s.contains("claude-spt:fast"));
    }

    // [unit->REQ-RUN-PICKER] pick-existing renders the category tabs, the status
    // squares, and the two-pane description for the highlighted endpoint.
    #[test]
    fn pick_buffer_has_squares_and_description() {
        let eps = vec![
            ep("doyle", EpStatus::Online, true),
            ep("offline-one", EpStatus::Offline, true),
        ];
        let mut m = PickerModel::new("spt-core".into(), vec![], eps, None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        let s = rendered(&m);
        assert!(s.contains("Local node"), "category tab");
        assert!(s.contains(glyph::ONLINE), "online square");
        assert!(s.contains(glyph::OFFLINE), "offline square");
        assert!(s.contains("doyle"));
        // description pane for the highlighted (first) endpoint:
        assert!(s.contains("claude-spt:fast"), "adapter:profile in the pane");
        assert!(s.contains("spt-core"), "project history in the pane");
        assert!(s.contains("the gatekeeper"), "endpoint description in the pane");
    }

    // [unit->REQ-RUN-PICKER] an empty category renders the create hint.
    #[test]
    fn pick_empty_category_hint() {
        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        let s = rendered(&m);
        assert!(s.contains("no endpoints — press n to create"));
    }

    // [unit->REQ-RUN-PICKER] the confirm screen renders the status-dependent
    // options; an offline+local endpoint with a ledger offers Resume.
    #[test]
    fn confirm_buffer_options() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        let s = rendered(&m);
        assert!(s.contains("Confirm selection"));
        assert!(s.contains("offline • LOCAL"));
        assert!(s.contains("Attach now"));
        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\view.rs (file state is current in your context — no need to Read it back)

//! Picker **view** — render a `&PickerModel` to a ratatui frame.
//!
//! ratatui's double-buffered renderer removes the manual erase/redraw +
//! wrapped-row-counting hazard the raw-ANSI reference (`vercel-labs/skills`)
//! carried. Rendering is a pure function of the model + the frame area, so every
//! screen is asserted against a `TestBackend` `Buffer` in the unit tests (no
//! live terminal) — doyle's "assert the rendered buffer" gate.
// [impl->REQ-RUN-PICKER]

use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
use ratatui::Frame;

use super::model::{glyph, Category, ConfirmOption, EpStatus, PickerModel, Screen};

/// The pinned keybind legend (bottom row). Designed so a future tap-mode can
/// surface the same actions without the keys (the phone caveat).
const LEGEND: &str = "←→ category · ↑↓ item · enter confirm · esc back · / filter · n new · s shortcut";

/// Highlight style for the row under a cursor.
fn cursor_style() -> Style {
    Style::default()
        .fg(Color::Cyan)
        .add_modifier(Modifier::BOLD)
}

/// A status square span in its tri-state color (green ■ / gray ▢; blue is the
/// deferred attached state — not rendered in W2).
fn square_span(status: EpStatus) -> Span<'static> {
    let color = match status {
        EpStatus::Online => Color::Green,
        EpStatus::Offline => Color::DarkGray,
    };
    Span::styled(status.square().to_string(), Style::default().fg(color))
}

/// Render the whole picker for the current screen.
pub fn render(model: &PickerModel, f: &mut Frame) {
    let area = f.area();
    match model.screen {
        Screen::Kind => render_kind(model, f, area),
        Screen::CreateAdapter => render_create_adapter(model, f, area),
        Screen::CreateId => render_create_id(model, f, area),
        Screen::PickExisting => render_pick(model, f, area),
        Screen::Confirm => render_confirm(model, f, area),
    }
}

fn titled_block(title: &str) -> Block<'_> {
    Block::default().borders(Borders::ALL).title(title)
}

/// A cursor-aware list item: prefix the cursor row with the caret glyph.
fn item(label: String, selected: bool) -> ListItem<'static> {
    if selected {
        ListItem::new(Line::from(vec![
            Span::styled(format!("{} ", glyph::CURSOR), cursor_style()),
            Span::styled(label, cursor_style()),
        ]))
    } else {
        ListItem::new(Line::from(format!("  {label}")))
    }
}

// ── Layer 1: kind ────────────────────────────────────────────────────────
fn render_kind(model: &PickerModel, f: &mut Frame, area: Rect) {
    let items = vec![
        item("Create new".to_string(), model.kind_cursor == 0),
        item("Pick existing".to_string(), model.kind_cursor == 1),
    ];
    let list = List::new(items).block(titled_block("What kind of endpoint?"));
    f.render_widget(list, area);
}

// ── Create-new: adapter tree ───────────────────────────────────────────────
fn render_create_adapter(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Min(1), Constraint::Length(1)])
        .split(area);
    let items: Vec<ListItem> = model
        .adapter_rows
        .iter()
        .enumerate()
        .map(|(i, row)| {
            let label = match &row.profile {
                Some(p) => format!("{} {}:{p}", glyph::BRANCH, row.adapter),
                None => row.adapter.clone(),
            };
            item(label, i == model.adapter_cursor)
        })
        .collect();
    let list = if items.is_empty() {
        List::new(vec![ListItem::new(
            "(no harness adapters registered — `spt adapter list`)",
        )])
    } else {
        List::new(items)
    }
    .block(titled_block("Choose your harness adapter for this endpoint:"));
    f.render_widget(list, chunks[0]);
    f.render_widget(legend(), chunks[1]);
}

// ── Create-new: id entry ────────────────────────────────────────────────────
fn render_create_id(model: &PickerModel, f: &mut Frame, area: Rect) {
    let mut lines = vec![
        Line::from("Enter an endpoint id. Alphanumeric + hyphen + underscore only."),
        Line::from(""),
        Line::from(vec![
            Span::raw("id: "),
            Span::styled(
                format!("{}_", model.id_buffer),
                Style::default().add_modifier(Modifier::BOLD),
            ),
        ]),
    ];
    if let Some(err) = &model.id_error {
        lines.push(Line::from(""));
        lines.push(Line::from(Span::styled(
            format!("invalid: {err}"),
            Style::default().fg(Color::Red),
        )));
    }
    let adapter = model
        .selected_adapter()
        .map(|a| a.address())
        .unwrap_or_default();
    let p = Paragraph::new(lines).block(titled_block(&format!("New {adapter} endpoint")));
    f.render_widget(p, area);
}

// ── Pick-existing ───────────────────────────────────────────────────────────
fn category_tabs(model: &PickerModel) -> Line<'static> {
    let mut spans = vec![Span::raw("[ ")];
    for (i, cat) in Category::ALL.iter().enumerate() {
        let label = cat.label(&model.project_name);
        let style = if *cat == model.category {
            cursor_style()
        } else {
            Style::default().fg(Color::DarkGray)
        };
        spans.push(Span::styled(label, style));
        if i + 1 < Category::ALL.len() {
            spans.push(Span::raw(" | "));
        }
    }
    spans.push(Span::raw(" ]"));
    Line::from(spans)
}

fn legend() -> Paragraph<'static> {
    Paragraph::new(Line::from(Span::styled(
        LEGEND,
        Style::default().fg(Color::DarkGray),
    )))
}

fn render_pick(model: &PickerModel, f: &mut Frame, area: Rect) {
    let rows = Layout::default()
        .direction(Direction::Vertical)
        .constraints([
            Constraint::Length(1), // category tabs
            Constraint::Min(1),    // body (list | description)
            Constraint::Length(1), // filter / legend
        ])
        .split(area);
    f.render_widget(Paragraph::new(category_tabs(model)), rows[0]);

    let body = Layout::default()
        .direction(Direction::Horizontal)
        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
        .split(rows[1]);

    // Left: grouped endpoint list with status squares.
    let vis = model.visible_indices();
    if vis.is_empty() {
        let empty = Paragraph::new("no endpoints — press n to create")
            .block(titled_block("Available endpoints:"));
        f.render_widget(empty, body[0]);
    } else {
        let mut items: Vec<ListItem> = Vec::new();
        let mut last_group: Option<&str> = None;
        // The visible-list cursor maps to the i-th visible row; track that the
        // group headers we interleave don't shift it (headers are non-selectable
        // display rows, the cursor counts only endpoint rows).
        for (vi, &ei) in vis.iter().enumerate() {
            let ep = &model.endpoints[ei];
            if last_group != Some(ep.group.as_str()) {
                items.push(ListItem::new(Span::styled(
                    ep.group.clone(),
                    Style::default().fg(Color::DarkGray).add_modifier(Modifier::DIM),
                )));
                last_group = Some(ep.group.as_str());
            }
            let selected = vi == model.item_cursor;
            let caret = if selected { glyph::CURSOR } else { " " };
            let line = Line::from(vec![
                Span::styled(format!("{caret} "), cursor_style()),
                square_span(ep.status),
                Span::raw(format!(" {}", ep.id)),
            ]);
            let li = ListItem::new(line);
            items.push(if selected {
                li.style(cursor_style())
            } else {
                li
            });
        }
        f.render_widget(
            List::new(items).block(titled_block("Available endpoints:")),
            body[0],
        );
    }

    // Right: the two-pane description for the highlighted endpoint.
    f.render_widget(description_pane(model), body[1]);

    // Bottom: the filter line (when filtering) else the legend.
    if model.filtering || !model.query.is_empty() {
        let q = Paragraph::new(Line::from(vec![
            Span::styled("/", Style::default().fg(Color::Cyan)),
            Span::raw(model.query.clone()),
            Span::styled("_", Style::default().add_modifier(Modifier::BOLD)),
        ]));
        f.render_widget(q, rows[2]);
    } else {
        f.render_widget(legend(), rows[2]);
    }
}

fn description_pane(model: &PickerModel) -> Paragraph<'static> {
    let Some(ep) = model.selected_endpoint() else {
        return Paragraph::new("").block(titled_block("Description"));
    };
    let history = if ep.project_history.is_empty() {
        "(none)".to_string()
    } else {
        ep.project_history.join(", ")
    };
    let desc = if ep.description.is_empty() {
        "(no description)".to_string()
    } else {
        ep.description.clone()
    };
    let lines = vec![
        Line::from(vec![
            Span::styled("harness: ", Style::default().fg(Color::DarkGray)),
            Span::raw(ep.adapter_profile.clone()),
        ]),
        Line::from(vec![
            Span::styled("project history: ", Style::default().fg(Color::DarkGray)),
            Span::raw(history),
        ]),
        Line::from(""),
        Line::from(desc),
    ];
    Paragraph::new(lines)
        .wrap(Wrap { trim: true })
        .block(titled_block("Description"))
}

// ── Confirm ─────────────────────────────────────────────────────────────────
fn confirm_option_label(opt: ConfirmOption) -> &'static str {
    match opt {
        ConfirmOption::Attach => "Attach now",
        ConfirmOption::Start => "Start now",
        ConfirmOption::View => "View now (read-only)",
        ConfirmOption::Resume => "Resume from history",
        ConfirmOption::ChangeAdapter => "Change harness adapter",
        ConfirmOption::Instantiate => "Instantiate locally",
        ConfirmOption::Fork => "Fork endpoint",
        ConfirmOption::Shortcut => "New/Update cc-<id> shortcut (s)",
    }
}

fn render_confirm(model: &PickerModel, f: &mut Frame, area: Rect) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints([Constraint::Length(7), Constraint::Min(1), Constraint::Length(1)])
        .split(area);

    // The selection summary block.
    if let Some(ep) = model.selected_endpoint() {
        let status_word = match ep.status {
            EpStatus::Online => "online",
            EpStatus::Offline => "offline",
        };
        let loc = if ep.is_local {
            "LOCAL".to_string()
        } else {
            ep.node.clone()
        };
        let history = if ep.project_history.is_empty() {
            "(none)".to_string()
        } else {
            ep.project_history.join(", ")
        };
        let summary = vec![
            Line::from(Span::styled(
                ep.id.clone(),
                Style::default().add_modifier(Modifier::BOLD),
            )),
            Line::from(vec![
                square_span(ep.status),
                Span::raw(format!(" {status_word} • {loc}")),
            ]),
            Line::from(ep.adapter_profile.clone()),
            Line::from(format!("Project history: {history}")),
            Line::from(ep.description.clone()),
        ];
        f.render_widget(
            Paragraph::new(summary).block(titled_block("Confirm selection")),
            chunks[0],
        );
    }

    // The status-dependent option set.
    let opts = model.confirm_options();
    let items: Vec<ListItem> = opts
        .iter()
        .enumerate()
        .map(|(i, opt)| item(confirm_option_label(*opt).to_string(), i == model.confirm_cursor))
        .collect();
    f.render_widget(List::new(items).block(titled_block("Options")), chunks[1]);
    f.render_widget(legend(), chunks[2]);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::picker::model::{AdapterInfo, EndpointRow, ResumeRow};
    use ratatui::backend::TestBackend;
    use ratatui::Terminal;

    /// Flatten a rendered backend to one big string (row-major) for `contains`
    /// assertions — the buffer-level gate doyle asked for.
    fn rendered(model: &PickerModel) -> String {
        let backend = TestBackend::new(100, 30);
        let mut terminal = Terminal::new(backend).unwrap();
        terminal.draw(|f| render(model, f)).unwrap();
        let buf = terminal.backend().buffer().clone();
        let mut out = String::new();
        for y in 0..buf.area.height {
            for x in 0..buf.area.width {
                out.push_str(buf[(x, y)].symbol());
            }
            out.push('\n');
        }
        out
    }

    fn ep(id: &str, status: EpStatus, is_local: bool) -> EndpointRow {
        EndpointRow {
            id: id.to_string(),
            group: "local".to_string(),
            node: "LOCAL".to_string(),
            status,
            is_local,
            adapter_profile: "claude-spt:fast".to_string(),
            project_history: vec!["spt-core".to_string()],
            description: "the gatekeeper".to_string(),
            resume_rows: vec![],
        }
    }

    // [unit->REQ-RUN-PICKER] the kind screen renders both choices + the caret on
    // the cursor row.
    #[test]
    fn kind_buffer() {
        let m = PickerModel::new("spt-core".into(), vec![], vec![], None, None);
        let s = rendered(&m);
        assert!(s.contains("What kind of endpoint?"));
        assert!(s.contains("Create new"));
        assert!(s.contains("Pick existing"));
        assert!(s.contains(glyph::CURSOR), "the cursor caret renders");
    }

    // [unit->REQ-RUN-PICKER] the adapter screen renders the tree-nested profiles
    // under their adapter with the branch glyph.
    #[test]
    fn adapter_tree_buffer() {
        let rows = crate::picker::model::build_adapter_tree(&[AdapterInfo {
            name: "claude-spt".into(),
            shipped: vec!["fast".into()],
            local: vec![],
        }]);
        let mut m = PickerModel::new("p".into(), rows, vec![], None, None);
        m.screen = Screen::CreateAdapter;
        let s = rendered(&m);
        assert!(s.contains("Choose your harness adapter"));
        assert!(s.contains("claude-spt"));
        assert!(s.contains(glyph::BRANCH), "profile nests under the adapter");
        assert!(s.contains("claude-spt:fast"));
    }

    // [unit->REQ-RUN-PICKER] pick-existing renders the category tabs, the status
    // squares, and the two-pane description for the highlighted endpoint.
    #[test]
    fn pick_buffer_has_squares_and_description() {
        let eps = vec![
            ep("doyle", EpStatus::Online, true),
            ep("offline-one", EpStatus::Offline, true),
        ];
        let mut m = PickerModel::new("spt-core".into(), vec![], eps, None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        let s = rendered(&m);
        assert!(s.contains("Local node"), "category tab");
        assert!(s.contains(glyph::ONLINE), "online square");
        assert!(s.contains(glyph::OFFLINE), "offline square");
        assert!(s.contains("doyle"));
        // description pane for the highlighted (first) endpoint:
        assert!(s.contains("claude-spt:fast"), "adapter:profile in the pane");
        assert!(s.contains("spt-core"), "project history in the pane");
        assert!(s.contains("the gatekeeper"), "endpoint description in the pane");
    }

    // [unit->REQ-RUN-PICKER] an empty category renders the create hint.
    #[test]
    fn pick_empty_category_hint() {
        let mut m = PickerModel::new("p".into(), vec![], vec![], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        let s = rendered(&m);
        assert!(s.contains("no endpoints — press n to create"));
    }

    // [unit->REQ-RUN-PICKER] the confirm screen renders the status-dependent
    // options; an offline+local endpoint with a ledger offers Resume.
    #[test]
    fn confirm_buffer_options() {
        let mut e = ep("doyle", EpStatus::Offline, true);
        e.resume_rows = vec![ResumeRow {
            session_id: "s".into(),
            ts: "t".into(),
            trigger: "boot".into(),
            project: String::new(),
        }];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.screen = Screen::PickExisting;
        m.category = Category::Local;
        m.enter_pick();
        let s = rendered(&m);
        assert!(s.contains("Confirm selection"));
        assert!(s.contains("offline • LOCAL"));
        assert!(s.contains("Attach now"));
        assert!(s.contains("Resume from history"), "offline+local+ledger ⇒ Resume option");
        assert!(s.contains("Fork endpoint"));
    }
}


View done. Now I need to extend the model for the Resume sub-screen + existing-endpoint terminal actions + create-new re-entry (ChangeAdapter/Instantiate/Fork all re-enter create-new per spec → single bringup path). Updating `model.rs`.


Edit

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
    /// Bring up (or resume) an endpoint then start/attach/view.
    Run {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// Write/update a `cc-<id>` shortcut, then stay (re-enter the loop).
    Shortcut {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// User backed all the way out.
    Cancelled,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
    /// Bring up (or resume) an endpoint then start/attach/view — routes through
    /// `cmd_endpoint_run` (the one bringup core).
    Run {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// Attach to an ALREADY-LIVE endpoint's PTY — routes through the rc pump
    /// (`rc::run_attach`), not a re-bringup.
    Attach { id: String, view: bool },
    /// Write/update a `cc-<id>` shortcut, then stay (re-enter the loop).
    Shortcut {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// User backed all the way out.
    Cancelled,
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\picker\model.rs has been updated successfully. (file state is current in your context — no need to Read it back)

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
    /// Bring up (or resume) an endpoint then start/attach/view.
    Run {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// Write/update a `cc-<id>` shortcut, then stay (re-enter the loop).
    Shortcut {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// User backed all the way out.
    Cancelled,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
    /// Bring up (or resume) an endpoint then start/attach/view — routes through
    /// `cmd_endpoint_run` (the one bringup core).
    Run {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// Attach to an ALREADY-LIVE endpoint's PTY — routes through the rc pump
    /// (`rc::run_attach`), not a re-bringup.
    Attach { id: String, view: bool },
    /// Write/update a `cc-<id>` shortcut, then stay (re-enter the loop).
    Shortcut {
        adapter: String,
        id: String,
        resume: Option<String>,
        mode: RunMode,
    },
    /// User backed all the way out.
    Cancelled,
}

-    /// Bring up (or resume) an endpoint then start/attach/view.

+    /// Bring up (or resume) an endpoint then start/attach/view — routes through

+    /// `cmd_endpoint_run` (the one bringup core).

+    /// Attach to an ALREADY-LIVE endpoint's PTY — routes through the rc pump

+    /// (`rc::run_attach`), not a re-bringup.


    /// Pick-existing L3 — confirm + status-dependent options.
    Confirm,
}

    /// Pick-existing L3 — confirm + status-dependent options.
    Confirm,
    /// Resume-from-history sub-screen — pick a prior session from the ledger.
    Resume,
}


    /// Pick-existing L3 — confirm + status-dependent options.
    Confirm,
}

    /// Pick-existing L3 — confirm + status-dependent options.
    Confirm,
    /// Resume-from-history sub-screen — pick a prior session from the ledger.
    Resume,
}

+    /// Resume-from-history sub-screen — pick a prior session from the ledger.


    // Confirm.
    pub confirm_cursor: usize,
}

    // Confirm.
    pub confirm_cursor: usize,

    // Resume sub-screen.
    pub resume_cursor: usize,
}


    // Confirm.
    pub confirm_cursor: usize,
}

    // Confirm.
    pub confirm_cursor: usize,

    // Resume sub-screen.
    pub resume_cursor: usize,
}

// Confirm.
pub confirm_cursor: usize,
+
+    // Resume sub-screen.
+    pub resume_cursor: usize,
}
impl PickerModel {


            query: String::new(),
            filtering: false,
            confirm_cursor: 0,
        }
    }

            query: String::new(),
            filtering: false,
            confirm_cursor: 0,
            resume_cursor: 0,
        }
    }


            query: String::new(),
            filtering: false,
            confirm_cursor: 0,
        }
    }

            query: String::new(),
            filtering: false,
            confirm_cursor: 0,
            resume_cursor: 0,
        }
    }

query: String::new(),
filtering: false,
confirm_cursor: 0,
+            resume_cursor: 0,
}
}


Now add the confirm-terminal-action + resume + re-entry methods, and wire `back()` for the Resume screen:


    pub fn selected_confirm(&self) -> Option<ConfirmOption> {
        self.confirm_options().get(self.confirm_cursor).copied()
    }

    pub fn selected_confirm(&self) -> Option<ConfirmOption> {
        self.confirm_options().get(self.confirm_cursor).copied()
    }

    /// The terminal [`Outcome`] a confirm option yields IMMEDIATELY (no further
    /// screen). Online endpoints attach via the rc pump; offline endpoints
    /// bring up via `cmd_endpoint_run` with the endpoint's recorded adapter.
    /// `Shortcut` bakes the current selection. The re-entrant options
    /// (Resume / ChangeAdapter / Instantiate / Fork) return `None` — they are
    /// SCREEN transitions, driven by [`enter_resume`](Self::enter_resume) /
    /// [`reenter_create`](Self::reenter_create). Every yielded variant routes
    /// through an existing bringup fn — no second bringup path.
    // [impl->REQ-RUN-PICKER]
    pub fn confirm_terminal(&self, opt: ConfirmOption) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        let online = ep.status == EpStatus::Online;
        let bringup = |mode| Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: None,
            mode,
        };
        match opt {
            // An online endpoint's PTY is live → attach the rc pump; an offline
            // one must be brought up first.
            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {
                Outcome::Attach { id: ep.id.clone(), view: false }
            } else {
                bringup(match opt {
                    ConfirmOption::Start => RunMode::Start,
                    _ => RunMode::Attach,
                })
            }),
            ConfirmOption::View => Some(if online {
                Outcome::Attach { id: ep.id.clone(), view: true }
            } else {
                bringup(RunMode::View)
            }),
            ConfirmOption::Shortcut => Some(Outcome::Shortcut {
                adapter: ep.adapter_profile.clone(),
                id: ep.id.clone(),
                resume: None,
                mode: RunMode::Attach,
            }),
            // Screen transitions, not immediate outcomes.
            ConfirmOption::Resume
            | ConfirmOption::ChangeAdapter
            | ConfirmOption::Instantiate
            | ConfirmOption::Fork => None,
        }
    }

    // ── Resume sub-screen (offline+local only) ───────────────────────────
    pub fn enter_resume(&mut self) {
        self.screen = Screen::Resume;
        self.resume_cursor = 0;
    }

    pub fn resume_rows(&self) -> &[ResumeRow] {
        self.selected_endpoint().map(|e| e.resume_rows.as_slice()).unwrap_or(&[])
    }

    pub fn move_resume(&mut self, delta: isize) {
        let len = self.resume_rows().len();
        self.resume_cursor = Self::step(self.resume_cursor, len, delta);
    }

    pub fn selected_resume(&self) -> Option<&ResumeRow> {
        self.resume_rows().get(self.resume_cursor)
    }

    /// Resume-from-history outcome: bring the endpoint up against the chosen
    /// prior session id (`cmd_endpoint_run --resume`).
    pub fn resume_outcome(&self) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        let row = ep.resume_rows.get(self.resume_cursor)?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(row.session_id.clone()),
            mode: RunMode::Attach,
        })
    }

    /// Re-enter the create-new adapter screen, the spec's "L2@create-new"
    /// path shared by Change-harness-adapter / Instantiate-locally (keep the id)
    /// and Fork (fresh id). All three converge on `cmd_endpoint_run` — one
    /// bringup core. `keep_id` carries the existing endpoint's id forward.
    pub fn reenter_create(&mut self, keep_id: bool) {
        let id = if keep_id {
            self.selected_endpoint().map(|e| e.id.clone()).unwrap_or_default()
        } else {
            String::new()
        };
        self.id_buffer = id;
        self.revalidate_id();
        self.adapter_cursor = 0;
        self.screen = Screen::CreateAdapter;
    }


    pub fn selected_confirm(&self) -> Option<ConfirmOption> {
        self.confirm_options().get(self.confirm_cursor).copied()
    }

    /// The terminal [`Outcome`] a confirm option yields IMMEDIATELY (no further
    /// screen). Online endpoints attach via the rc pump; offline endpoints
    /// bring up via `cmd_endpoint_run` with the endpoint's recorded adapter.
    /// `Shortcut` bakes the current selection. The re-entrant options
    /// (Resume / ChangeAdapter / Instantiate / Fork) return `None` — they are
    /// SCREEN transitions, driven by [`enter_resume`](Self::enter_resume) /
    /// [`reenter_create`](Self::reenter_create). Every yielded variant routes
    /// through an existing bringup fn — no second bringup path.
    // [impl->REQ-RUN-PICKER]
    pub fn confirm_terminal(&self, opt: ConfirmOption) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        let online = ep.status == EpStatus::Online;
        let bringup = |mode| Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: None,
            mode,
        };
        match opt {
            // An online endpoint's PTY is live → attach the rc pump; an offline
            // one must be brought up first.
            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {
                Outcome::Attach { id: ep.id.clone(), view: false }
            } else {
                bringup(match opt {
                    ConfirmOption::Start => RunMode::Start,
                    _ => RunMode::Attach,
                })
            }),
            ConfirmOption::View => Some(if online {
                Outcome::Attach { id: ep.id.clone(), view: true }
            } else {
                bringup(RunMode::View)
            }),
            ConfirmOption::Shortcut => Some(Outcome::Shortcut {
                adapter: ep.adapter_profile.clone(),
                id: ep.id.clone(),
                resume: None,
                mode: RunMode::Attach,
            }),
            // Screen transitions, not immediate outcomes.
            ConfirmOption::Resume
            | ConfirmOption::ChangeAdapter
            | ConfirmOption::Instantiate
            | ConfirmOption::Fork => None,
        }
    }

    // ── Resume sub-screen (offline+local only) ───────────────────────────
    pub fn enter_resume(&mut self) {
        self.screen = Screen::Resume;
        self.resume_cursor = 0;
    }

    pub fn resume_rows(&self) -> &[ResumeRow] {
        self.selected_endpoint().map(|e| e.resume_rows.as_slice()).unwrap_or(&[])
    }

    pub fn move_resume(&mut self, delta: isize) {
        let len = self.resume_rows().len();
        self.resume_cursor = Self::step(self.resume_cursor, len, delta);
    }

    pub fn selected_resume(&self) -> Option<&ResumeRow> {
        self.resume_rows().get(self.resume_cursor)
    }

    /// Resume-from-history outcome: bring the endpoint up against the chosen
    /// prior session id (`cmd_endpoint_run --resume`).
    pub fn resume_outcome(&self) -> Option<Outcome> {
        let ep = self.selected_endpoint()?;
        let row = ep.resume_rows.get(self.resume_cursor)?;
        Some(Outcome::Run {
            adapter: ep.adapter_profile.clone(),
            id: ep.id.clone(),
            resume: Some(row.session_id.clone()),
            mode: RunMode::Attach,
        })
    }

    /// Re-enter the create-new adapter screen, the spec's "L2@create-new"
    /// path shared by Change-harness-adapter / Instantiate-locally (keep the id)
    /// and Fork (fresh id). All three converge on `cmd_endpoint_run` — one
    /// bringup core. `keep_id` carries the existing endpoint's id forward.
    pub fn reenter_create(&mut self, keep_id: bool) {
        let id = if keep_id {
            self.selected_endpoint().map(|e| e.id.clone()).unwrap_or_default()
        } else {
            String::new()
        };
        self.id_buffer = id;
        self.revalidate_id();
        self.adapter_cursor = 0;
        self.screen = Screen::CreateAdapter;
    }

+    /// The terminal [`Outcome`] a confirm option yields IMMEDIATELY (no further

+    /// screen). Online endpoints attach via the rc pump; offline endpoints

+    /// bring up via `cmd_endpoint_run` with the endpoint's recorded adapter.

+    /// `Shortcut` bakes the current selection. The re-entrant options

+    /// (Resume / ChangeAdapter / Instantiate / Fork) return `None` — they are

+    /// SCREEN transitions, driven by [`enter_resume`](Self::enter_resume) /

+    /// [`reenter_create`](Self::reenter_create). Every yielded variant routes

+    /// through an existing bringup fn — no second bringup path.

+    pub fn confirm_terminal(&self, opt: ConfirmOption) -> Option<Outcome> {

+        let ep = self.selected_endpoint()?;

+        let online = ep.status == EpStatus::Online;

+        let bringup = |mode| Outcome::Run {

+            adapter: ep.adapter_profile.clone(),

+            // An online endpoint's PTY is live → attach the rc pump; an offline

+            // one must be brought up first.

+            ConfirmOption::Attach | ConfirmOption::Start => Some(if online {

+                Outcome::Attach { id: ep.id.clone(), view: false }

+                    ConfirmOption::Start => RunMode::Start,

+                    _ => RunMode::Attach,

+            ConfirmOption::View => Some(if online {

+                Outcome::Attach { id: ep.id.clone(), view: true }

+            ConfirmOption::Shortcut => Some(Outcome::Shortcut {

+                adapter: ep.adapter_profile.clone(),

+            // Screen transitions, not immediate outcomes.

+            | ConfirmOption::ChangeAdapter

+            | ConfirmOption::Instantiate

+            | ConfirmOption::Fork => None,

+    // ── Resume sub-screen (offline+local only) ───────────────────────────

+    pub fn resume_rows(&self) -> &[ResumeRow] {

+        self.selected_endpoint().map(|e| e.resume_rows.as_slice()).unwrap_or(&[])

+    pub fn move_resume(&mut self, delta: isize) {

+        let len = self.resume_rows().len();

+        self.resume_cursor = Self::step(self.resume_cursor, len, delta);

+    pub fn selected_resume(&self) -> Option<&ResumeRow> {

+        self.resume_rows().get(self.resume_cursor)

+    /// Resume-from-history outcome: bring the endpoint up against the chosen

+    /// prior session id (`cmd_endpoint_run --resume`).

+    pub fn resume_outcome(&self) -> Option<Outcome> {

+        let ep = self.selected_endpoint()?;

+        let row = ep.resume_rows.get(self.resume_cursor)?;

+            adapter: ep.adapter_profile.clone(),

+            resume: Some(row.session_id.clone()),

+    /// Re-enter the create-new adapter screen, the spec's "L2@create-new"

+    /// path shared by Change-harness-adapter / Instantiate-locally (keep the id)

+    /// and Fork (fresh id). All three converge on `cmd_endpoint_run` — one

+    /// bringup core. `keep_id` carries the existing endpoint's id forward.

+    pub fn reenter_create(&mut self, keep_id: bool) {

+            self.selected_endpoint().map(|e| e.id.clone()).unwrap_or_default()

+        self.screen = Screen::CreateAdapter;

/// Back out one screen; returns `true` if it backed out of the whole picker

/// (the caller then yields `Outcome::Cancelled`).


            Screen::Confirm => {
                self.screen = Screen::PickExisting;
                false
            }
        }
    }
}

            Screen::Confirm => {
                self.screen = Screen::PickExisting;
                false
            }
            Screen::Resume => {
                self.screen = Screen::Confirm;
                false
            }
        }
    }
}


            Screen::Confirm => {
                self.screen = Screen::PickExisting;
                false
            }
        }
    }
}

            Screen::Confirm => {
                self.screen = Screen::PickExisting;
                false
            }
            Screen::Resume => {
                self.screen = Screen::Confirm;
                false
            }
        }
    }
}

+                self.screen = Screen::Confirm;


Add model tests for the new terminal-action + resume + re-entry logic:


    // [unit->REQ-RUN-PICKER] a lone --id prefills and jumps into create-new.
    #[test]
    fn prefill_jumps_to_create() {

    // [unit->REQ-RUN-PICKER] confirm terminal actions route correctly: an online
    // endpoint attaches via the rc pump; an offline one brings up via Run with
    // its recorded adapter; Shortcut bakes the selection.
    #[test]
    fn confirm_terminal_routes() {
        let online = ep("live", "g", EpStatus::Online, true);
        let mut m = PickerModel::new("p".into(), vec![], vec![online], None, None);
        m.category = Category::Local;
        m.enter_pick();
        assert_eq!(
            m.confirm_terminal(ConfirmOption::Attach),
            Some(Outcome::Attach { id: "live".into(), view: false })
        );
        assert_eq!(
            m.confirm_terminal(ConfirmOption::View),
            Some(Outcome::Attach { id: "live".into(), view: true })
        );
        match m.confirm_terminal(ConfirmOption::Shortcut).unwrap() {
            Outcome::Shortcut { adapter, id, .. } => {
                assert_eq!((adapter.as_str(), id.as_str()), ("claude-spt", "live"));
            }
            o => panic!("expected Shortcut, got {o:?}"),
        }

        // offline → bringup (Run) with the endpoint's adapter.
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m2 = PickerModel::new("p".into(), vec![], vec![off], None, None);
        m2.category = Category::Local;
        m2.enter_pick();
        assert_eq!(
            m2.confirm_terminal(ConfirmOption::Start),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Start,
            })
        );
        // the re-entrant options yield no immediate outcome.
        assert_eq!(m2.confirm_terminal(ConfirmOption::Fork), None);
        assert_eq!(m2.confirm_terminal(ConfirmOption::ChangeAdapter), None);
    }

    // [unit->REQ-RUN-PICKER] resume picks a prior session and bakes --resume.
    #[test]
    fn resume_outcome_bakes_session() {
        let mut e = ep("cold", "g", EpStatus::Offline, true);
        e.resume_rows = vec![
            ResumeRow { session_id: "old".into(), ts: "t1".into(), trigger: "boot".into(), project: String::new() },
            ResumeRow { session_id: "new".into(), ts: "t2".into(), trigger: "clear".into(), project: String::new() },
        ];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.category = Category::Local;
        m.enter_pick();
        m.enter_resume();
        assert_eq!(m.screen, Screen::Resume);
        m.move_resume(1);
        assert_eq!(m.selected_resume().unwrap().session_id, "new");
        assert_eq!(
            m.resume_outcome(),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Attach,
            })
        );
        assert!(!m.back() && m.screen == Screen::Confirm, "back → confirm");
    }

    // [unit->REQ-RUN-PICKER] Change-adapter / Instantiate re-enter create keeping
    // the id; Fork re-enters with a fresh id — all converging on cmd_endpoint_run.
    #[test]
    fn reenter_create_keeps_or_clears_id() {
        let rows = build_adapter_tree(&[info("claude-spt", &[], &[])]);
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m = PickerModel::new("p".into(), rows, vec![off], None, None);
        m.category = Category::Local;
        m.enter_pick();
        m.reenter_create(true);
        assert_eq!(m.screen, Screen::CreateAdapter);
        assert_eq!(m.id_buffer, "cold", "change-adapter/instantiate keep the id");
        m.reenter_create(false);
        assert_eq!(m.id_buffer, "", "fork starts a fresh id");
    }

    // [unit->REQ-RUN-PICKER] a lone --id prefills and jumps into create-new.
    #[test]
    fn prefill_jumps_to_create() {


    // [unit->REQ-RUN-PICKER] confirm terminal actions route correctly: an online
    // endpoint attaches via the rc pump; an offline one brings up via Run with
    // its recorded adapter; Shortcut bakes the selection.
    #[test]
    fn confirm_terminal_routes() {
        let online = ep("live", "g", EpStatus::Online, true);
        let mut m = PickerModel::new("p".into(), vec![], vec![online], None, None);
        m.category = Category::Local;
        m.enter_pick();
        assert_eq!(
            m.confirm_terminal(ConfirmOption::Attach),
            Some(Outcome::Attach { id: "live".into(), view: false })
        );
        assert_eq!(
            m.confirm_terminal(ConfirmOption::View),
            Some(Outcome::Attach { id: "live".into(), view: true })
        );
        match m.confirm_terminal(ConfirmOption::Shortcut).unwrap() {
            Outcome::Shortcut { adapter, id, .. } => {
                assert_eq!((adapter.as_str(), id.as_str()), ("claude-spt", "live"));
            }
            o => panic!("expected Shortcut, got {o:?}"),
        }

        // offline → bringup (Run) with the endpoint's adapter.
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m2 = PickerModel::new("p".into(), vec![], vec![off], None, None);
        m2.category = Category::Local;
        m2.enter_pick();
        assert_eq!(
            m2.confirm_terminal(ConfirmOption::Start),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: None,
                mode: RunMode::Start,
            })
        );
        // the re-entrant options yield no immediate outcome.
        assert_eq!(m2.confirm_terminal(ConfirmOption::Fork), None);
        assert_eq!(m2.confirm_terminal(ConfirmOption::ChangeAdapter), None);
    }

    // [unit->REQ-RUN-PICKER] resume picks a prior session and bakes --resume.
    #[test]
    fn resume_outcome_bakes_session() {
        let mut e = ep("cold", "g", EpStatus::Offline, true);
        e.resume_rows = vec![
            ResumeRow { session_id: "old".into(), ts: "t1".into(), trigger: "boot".into(), project: String::new() },
            ResumeRow { session_id: "new".into(), ts: "t2".into(), trigger: "clear".into(), project: String::new() },
        ];
        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);
        m.category = Category::Local;
        m.enter_pick();
        m.enter_resume();
        assert_eq!(m.screen, Screen::Resume);
        m.move_resume(1);
        assert_eq!(m.selected_resume().unwrap().session_id, "new");
        assert_eq!(
            m.resume_outcome(),
            Some(Outcome::Run {
                adapter: "claude-spt".into(),
                id: "cold".into(),
                resume: Some("new".into()),
                mode: RunMode::Attach,
            })
        );
        assert!(!m.back() && m.screen == Screen::Confirm, "back → confirm");
    }

    // [unit->REQ-RUN-PICKER] Change-adapter / Instantiate re-enter create keeping
    // the id; Fork re-enters with a fresh id — all converging on cmd_endpoint_run.
    #[test]
    fn reenter_create_keeps_or_clears_id() {
        let rows = build_adapter_tree(&[info("claude-spt", &[], &[])]);
        let off = ep("cold", "g", EpStatus::Offline, true);
        let mut m = PickerModel::new("p".into(), rows, vec![off], None, None);
        m.category = Category::Local;
        m.enter_pick();
        m.reenter_create(true);
        assert_eq!(m.screen, Screen::CreateAdapter);
        assert_eq!(m.id_buffer, "cold", "change-adapter/instantiate keep the id");
        m.reenter_create(false);
        assert_eq!(m.id_buffer, "", "fork starts a fresh id");
    }

    // [unit->REQ-RUN-PICKER] a lone --id prefills and jumps into create-new.
    #[test]
    fn prefill_jumps_to_create() {

assert!(m.back(), "back from kind cancels the picker");

+    // [unit->REQ-RUN-PICKER] confirm terminal actions route correctly: an online

+    // endpoint attaches via the rc pump; an offline one brings up via Run with

+    // its recorded adapter; Shortcut bakes the selection.

+        let online = ep("live", "g", EpStatus::Online, true);

+        let mut m = PickerModel::new("p".into(), vec![], vec![online], None, None);

+            m.confirm_terminal(ConfirmOption::Attach),

+            Some(Outcome::Attach { id: "live".into(), view: false })

+            m.confirm_terminal(ConfirmOption::View),

+            Some(Outcome::Attach { id: "live".into(), view: true })

+        match m.confirm_terminal(ConfirmOption::Shortcut).unwrap() {

+            Outcome::Shortcut { adapter, id, .. } => {

+                assert_eq!((adapter.as_str(), id.as_str()), ("claude-spt", "live"));

+            o => panic!("expected Shortcut, got {o:?}"),

+        // offline → bringup (Run) with the endpoint's adapter.

+        let off = ep("cold", "g", EpStatus::Offline, true);

+        let mut m2 = PickerModel::new("p".into(), vec![], vec![off], None, None);

+            m2.confirm_terminal(ConfirmOption::Start),

+                adapter: "claude-spt".into(),

+        // the re-entrant options yield no immediate outcome.

+        assert_eq!(m2.confirm_terminal(ConfirmOption::Fork), None);

+        assert_eq!(m2.confirm_terminal(ConfirmOption::ChangeAdapter), None);

+    // [unit->REQ-RUN-PICKER] resume picks a prior session and bakes --resume.

+    fn resume_outcome_bakes_session() {

+        let mut e = ep("cold", "g", EpStatus::Offline, true);

+            ResumeRow { session_id: "old".into(), ts: "t1".into(), trigger: "boot".into(), project: String::new() },

+            ResumeRow { session_id: "new".into(), ts: "t2".into(), trigger: "clear".into(), project: String::new() },

+        let mut m = PickerModel::new("p".into(), vec![], vec![e], None, None);

+        assert_eq!(m.screen, Screen::Resume);

+        assert_eq!(m.selected_resume().unwrap().session_id, "new");

+                resume: Some("new".into()),

+        assert!(!m.back() && m.screen == Screen::Confirm, "back → confirm");

+    // [unit->REQ-RUN-PICKER] Change-adapter / Instantiate re-enter create keeping

+    // the id; Fork re-enters with a fresh id — all converging on cmd_endpoint_run.

+    fn reenter_create_keeps_or_clears_id() {

+        let rows = build_adapter_tree(&[info("claude-spt", &[], &[])]);

+        let mut m = PickerModel::new("p".into(), rows, vec![off], None, None);

+        assert_eq!(m.screen, Screen::CreateAdapter);

+        assert_eq!(m.id_buffer, "cold", "change-adapter/instantiate keep the id");

+        assert_eq!(m.id_buffer, "", "fork starts a fresh id");


Update M12-W2-PLAN.md with crystallized post-ruling design

Append a "Crystallized design (post-ruling)" section reflecting doyle's M12-W2-RULING.md: Q1 defer, Q2 best-effort, Q3 BUILD resume via sessions::last_k, Q4 sentinel-guarded per-OS shortcut, Q5 additive --create, 2 REQs doc+impl+unit no-int, single-bringup-path invariant.

Add REQ-RUN-PICKER + REQ-RUN-SHORTCUT to traceable-reqs.toml

Add both REQ-* entries (rule 3, before building). Stages doc+impl+unit, NO int (TUI manual-verify leg note per REQ-PAIR-6).

Add ratatui + nucleo-matcher deps; make Run{} adapter/id optional + add --create

spt/Cargo.toml: ratatui 0.29 (pairs crossterm 0.28) + nucleo-matcher. cli.rs Run{}: adapter/id → Option<String>, add --create flag; dispatch bare (no adapter/id) → picker, flags-present → cmd_endpoint_run untouched.

Build picker state model (pure, testable)

spt/src/picker/model.rs: screens (Kind/CreateNew/PickExisting/Confirm), nav transitions, action enum (single source of truth), status→square, adapter→profile tree, ledger→resume titles, filter narrowing, cursor windowing. Heavy unit tests.

Build picker data gather over existing surfaces

spt/src/picker/data.rs: harness adapters+profiles (registered/local_profile_names), endpoints by category project/local/subnet (roster + wansend snapshots + resource_projection), description (adapter:profile, project history best-effort contextstore, endpoint description), sessions::last_k resume rows. No new daemon surface.

Build picker ratatui view (Buffer-testable)

spt/src/picker/view.rs: render fns &Model→Buffer; kind screen, create-new adapter/profile tree + id entry, pick-existing category L/R + grouped list + status squares + two-pane description, confirm layer + options, pinned legend. Glyph set lifted. Buffer asserts via ratatui TestBackend.

Build cc-<id> shortcut generation (REQ-RUN-SHORTCUT)

spt/src/picker/shortcut.rs: per-OS native (.ps1 Win / sh+chmod Unix), bake terminal-action flags (adapter[:profile]+id+create|resume+start|attach|view), sentinel-guarded overwrite (generated-by header marker; overwrite own freely, refuse+warn if no sentinel). Unit-tested content + create-vs-update.

Wire picker live loop + terminal-action dispatch (single bringup path)

spt/src/picker/mod.rs: crossterm raw + ratatui Terminal live loop (manual-verify leg); every terminal action routes through cmd_endpoint_run / cmd_fork / rc pump — NO second bringup path (the gate invariant).

Tag evidence, full suite + clippy -D + traceable check, ping doyle to gate

Tag [impl->]/[unit->] in same change; run full test suite, clippy -D warnings, traceable-reqs check EXIT=0. Then ping doyle to gate (reproduce traceable + read load-bearing tests + confirm single-bringup-path).


Add Resume screen render to view.rs:


        Screen::Confirm => render_confirm(model, f, area),
    }
}

        Screen::Confirm => render_confirm(model, f, area),
        Screen::Resume => render_resume(model, f, area),
    }
}