Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 122 additions & 1 deletion crates/buzz-acp/src/setup_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ pub(crate) enum RequirementPayload {
/// missing and the probe was skipped.
availability: AcpAvailabilityStatus,
},
/// The CLI is installed but its config file could not be parsed.
/// Informational only — no in-app action can fix an external config file.
CliConfigInvalid {
probe_args: Vec<String>,
setup_copy: String,
/// One-line stderr excerpt identifying the parse error.
diagnostic: String,
},
}

impl RequirementPayload {
Expand Down Expand Up @@ -149,6 +157,18 @@ impl RequirementPayload {
format!("install {} (open Doctor in Settings to diagnose)", harness)
}
},
RequirementPayload::CliConfigInvalid {
probe_args,
diagnostic,
..
} => {
let cli = probe_args.first().map(String::as_str).unwrap_or("the CLI");
let config_file = format!("~/.{}/config.toml", cli);
format!(
"{} is invalid: {} — fix the config and restart the agent",
config_file, diagnostic
)
}
}
}
}
Expand Down Expand Up @@ -213,10 +233,32 @@ impl SetupPayload {
.map(|r| format!("- {}", r.instruction()))
.collect();

let all_external = self
.requirements
.iter()
.all(|r| matches!(r, RequirementPayload::CliConfigInvalid { .. }));
let any_external = self
.requirements
.iter()
.any(|r| matches!(r, RequirementPayload::CliConfigInvalid { .. }));

let footer = if all_external {
// All requirements are external config files — Edit Agent cannot
// help. Don't send the user there.
"Fix the config file(s) and restart the agent.".to_string()
} else if any_external {
// Mixed: some Buzz-managed fields, some external config.
"Open Edit Agent in the Buzz app for the Buzz-managed fields; fix the external CLI config files manually and restart the agent.".to_string()
} else {
// All Buzz-managed — original footer unchanged.
"Open Edit Agent in the Buzz app to set these.".to_string()
};

format!(
"**{}** needs configuration before it can respond:\n{}\n\nOpen Edit Agent in the Buzz app to set these.",
"**{}** needs configuration before it can respond:\n{}\n\n{}",
self.agent_name,
steps.join("\n"),
footer,
)
};

Expand Down Expand Up @@ -678,6 +720,85 @@ mod tests {
assert!(body.contains("needs configuration"));
}

// ── config-invalid footer tests ────────────────────────────────────────────

fn make_cli_config_invalid(cli: &str, diagnostic: &str) -> RequirementPayload {
RequirementPayload::CliConfigInvalid {
probe_args: vec![cli.to_string()],
setup_copy: String::new(),
diagnostic: diagnostic.to_string(),
}
}

#[test]
fn nudge_body_all_config_invalid_omits_edit_agent_footer() {
// An all-CliConfigInvalid requirements list must NOT send users to
// Edit Agent (which cannot fix an external config file).
let payload = SetupPayload {
agent_name: "Codex".to_string(),
agent_pubkey: "test".to_string(),
requirements: vec![make_cli_config_invalid(
"codex",
"unknown variant `ultra` for field `model_reasoning_effort`",
)],
};
let body = payload.nudge_body();
assert!(
!body.contains("Open Edit Agent"),
"all-config-invalid nudge must not mention Open Edit Agent; got: {body:?}"
);
assert!(
body.contains("config.toml"),
"nudge must name the config file; got: {body:?}"
);
assert!(
body.contains("restart the agent"),
"nudge must include restart guidance; got: {body:?}"
);
}

#[test]
fn nudge_body_mixed_requirements_uses_split_footer() {
// Mixed list: one Buzz-managed env key + one external config invalid.
// Footer must address both sides.
let payload = SetupPayload {
agent_name: "Codex".to_string(),
agent_pubkey: "test".to_string(),
requirements: vec![
RequirementPayload::EnvKey {
key: "SOME_API_KEY".to_string(),
},
make_cli_config_invalid("codex", "unknown variant `gpt-5.6-sol`"),
],
};
let body = payload.nudge_body();
assert!(
body.contains("Open Edit Agent"),
"mixed nudge must mention Open Edit Agent for managed fields; got: {body:?}"
);
assert!(
body.contains("fix the external CLI config"),
"mixed nudge must mention fixing the external config; got: {body:?}"
);
}

#[test]
fn nudge_body_all_buzz_managed_retains_original_footer() {
// Pure Buzz-managed requirements → original "Open Edit Agent" footer unchanged.
let payload = SetupPayload {
agent_name: "Fizz".to_string(),
agent_pubkey: "test".to_string(),
requirements: vec![RequirementPayload::EnvKey {
key: "ANTHROPIC_API_KEY".to_string(),
}],
};
let body = payload.nudge_body();
assert!(
body.contains("Open Edit Agent in the Buzz app to set these."),
"all-managed nudge must use the original Edit Agent footer; got: {body:?}"
);
}

// ── sentinel block tests ───────────────────────────────────────────────────

#[test]
Expand Down
5 changes: 4 additions & 1 deletion desktop/scripts/check-file-sizes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,10 @@ const overrides = new Map([
// +1 rebase merge: GlobalAgentConfig import added alongside AcpAvailabilityStatus.
// +2 rebase onto #1667: behavioral quad fields in PersonaRecord/ManagedAgentRecord.
// +3 rebase onto main (#1568 + #1613): identity-import-keyring + augmented-PATH probes.
["src-tauri/src/managed_agents/readiness.rs", 1565],
// +18: CliConfigInvalid requirement surface for config-parse probe classification —
// new Requirement variant + updated cli_login_requirements + 3 new probe-layer tests.
// Load-bearing UX fix (bad config → clear diagnostic, not "run codex login").
["src-tauri/src/managed_agents/readiness.rs", 1583],
// applyWorkspace reposDir parameter plus the validateReposDir binding,
// threaded through Tauri invokes for configurable repos_dir, plus the
// harness-persona-sync `harnessOverride` create-input bit — load-bearing
Expand Down
46 changes: 32 additions & 14 deletions desktop/src-tauri/src/managed_agents/readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ pub enum Requirement {
/// and route to Doctor with accurate context.
availability: AcpAvailabilityStatus,
},
/// The CLI is installed but its config file could not be parsed.
/// This is an informational surface only — there is no in-app destination
/// that can repair an external config file; the user must edit it manually.
CliConfigInvalid {
/// Arguments used in the probe (e.g. `["codex", "login", "status"]`);
/// `probe_args[0]` is the CLI name (e.g. `"codex"`).
probe_args: Vec<String>,
/// Human-readable hint shown when no structured copy is available.
setup_copy: String,
/// A one-line excerpt from the CLI's stderr (the parse-error line).
/// Shown verbatim in the nudge so the user can identify the problem.
diagnostic: String,
},
}

// ── AgentReadiness ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -505,20 +518,25 @@ fn cli_login_requirements(
};

let augmented_path = cli_probe::augmented_path();
let logged_in = cli_probe::login_probe_succeeds(
&binary_path,
probe_args,
augmented_path.as_deref(),
);

if logged_in {
vec![]
} else {
vec![Requirement::CliLogin {
probe_args: probe_args.iter().map(|s| s.to_string()).collect(),
setup_copy: setup_copy.to_string(),
availability: AcpAvailabilityStatus::Available,
}]
let outcome =
cli_probe::login_probe(&binary_path, probe_args, augmented_path.as_deref());

match outcome {
cli_probe::ProbeOutcome::LoggedIn => vec![],
cli_probe::ProbeOutcome::LoggedOut => {
vec![Requirement::CliLogin {
probe_args: probe_args.iter().map(|s| s.to_string()).collect(),
setup_copy: setup_copy.to_string(),
availability: AcpAvailabilityStatus::Available,
}]
}
cli_probe::ProbeOutcome::ConfigInvalid { stderr_excerpt } => {
vec![Requirement::CliConfigInvalid {
probe_args: probe_args.iter().map(|s| s.to_string()).collect(),
setup_copy: setup_copy.to_string(),
diagnostic: stderr_excerpt,
}]
}
}
}
// Tooling is not fully installed — emit CliLogin with the precise
Expand Down
Loading
Loading