Skip to main content

supercode_harness/
startup_prompts.rs

1//! The flags that take each harness past its routine startup prompts, in one table
2//! (`startup_prompts.json`, which the TypeScript SDK reads as its own copy) for every launch path:
3//! `supercode open --new`, a resume, a native start. `startup` pairs answer the prompts every launch
4//! meets (Codex's new-version check, its folder and hook trust questions) and keep a pane scrollable (Codex
5//! drawn inline, not on the alternate screen, where a pane's wheel turns into arrow keys); `unattended` flags answer the
6//! approval prompts of a launch nobody watches, unless the caller chose its own
7//! (`unattended_chosen_by`). `{cwd}` in a flag is the launch's directory as a JSON string, the form a
8//! Codex `-c projects={…}` table takes it in; a pair that needs it is left out without one.
9
10use std::path::Path;
11
12use serde::Deserialize;
13
14pub(crate) fn codex_program() -> std::result::Result<String, String> {
15    if let Some(program) = std::env::var_os(crate::sessions_control::CODEX_BIN_ENV) {
16        if !program.is_empty() {
17            let path = std::path::PathBuf::from(program);
18            if !path.is_absolute() {
19                return Err("SUPERCODE_CODEX_BIN must name an absolute executable path".into());
20            }
21            return Ok(path.to_string_lossy().into_owned());
22        }
23    }
24    let home = supercode_interchange::user_home()
25        .ok_or_else(|| "the executing user has no home for its consolidated Codex".to_string())?;
26    Ok(home
27        .join(".local/bin")
28        .join(if cfg!(windows) { "codex.cmd" } else { "codex" })
29        .to_string_lossy()
30        .into_owned())
31}
32
33pub(crate) fn codex_arguments(given: &[String]) -> Vec<String> {
34    let mut normalized = vec!["-c".into(), "check_for_update_on_startup=false".into()];
35    let mut index = 0;
36    while index < given.len() {
37        let argument = &given[index];
38        if argument == "--" {
39            normalized.extend_from_slice(&given[index..]);
40            break;
41        }
42        if argument == "-c" || argument == "--config" {
43            if given.get(index + 1).is_some_and(|value| {
44                value.split('=').next().map(str::trim) == Some("check_for_update_on_startup")
45            }) {
46                index += 2;
47                continue;
48            }
49        } else if argument
50            .strip_prefix("--config=")
51            .or_else(|| {
52                argument
53                    .strip_prefix("-c")
54                    .map(|value| value.trim_start_matches('='))
55            })
56            .is_some_and(|value| {
57                value.split('=').next().map(str::trim) == Some("check_for_update_on_startup")
58            })
59        {
60            index += 1;
61            continue;
62        }
63        normalized.push(argument.clone());
64        index += 1;
65    }
66    normalized
67}
68
69#[derive(Deserialize)]
70struct Prompts {
71    startup: Vec<Vec<String>>,
72    unattended: Vec<String>,
73    unattended_chosen_by: Vec<String>,
74}
75
76fn table() -> std::collections::BTreeMap<String, Prompts> {
77    serde_json::from_str(include_str!("startup_prompts.json"))
78        .expect("startup_prompts.json is the harness crate's own table")
79}
80
81/// The arguments `harness` is launched with to open past its routine prompts, ahead of the
82/// caller's own `given` arguments. A startup flag the caller already sets (its key, the text before
83/// `=`, appears in `given`) is left to the caller.
84pub fn startup_arguments(
85    harness: &str,
86    cwd: Option<&Path>,
87    given: &[String],
88    unattended: bool,
89) -> Vec<String> {
90    let table = table();
91    let Some(prompts) = table.get(harness) else {
92        return Vec::new();
93    };
94    let cwd = cwd.map(|cwd| {
95        serde_json::to_string(cwd.to_string_lossy().as_ref())
96            .expect("a filesystem path always serializes as JSON text")
97    });
98    let mut arguments = Vec::new();
99    for pair in &prompts.startup {
100        let needs_cwd = pair.iter().any(|flag| flag.contains("{cwd}"));
101        if needs_cwd && cwd.is_none() {
102            continue;
103        }
104        let key = pair
105            .last()
106            .and_then(|flag| flag.split('=').next())
107            .unwrap_or_default();
108        if !key.is_empty() && given.iter().any(|argument| argument.contains(key)) {
109            continue;
110        }
111        arguments.extend(pair.iter().map(|flag| match &cwd {
112            Some(cwd) => flag.replace("{cwd}", cwd),
113            None => flag.clone(),
114        }));
115    }
116    let chosen = given.iter().any(|argument| {
117        prompts
118            .unattended_chosen_by
119            .iter()
120            .any(|flag| argument == flag || argument.starts_with(&format!("{flag}=")))
121    });
122    if unattended && !chosen {
123        arguments.extend(prompts.unattended.iter().cloned());
124    }
125    arguments
126}