Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a0c93e8
fix: resolve false Go/gofmt not-installed warnings on Windows and uni…
Zireael May 24, 2026
563157c
fix(rust): gate configure warnings and resolve tools on Windows
Zireael May 29, 2026
bc973da
fix(opencode): toast configure warnings and document delivery option
Zireael May 29, 2026
1a0a044
Merge upstream/main and address cubic review on PR #72
Zireael May 29, 2026
27bc99a
fix(rust): satisfy clippy needless_return in tool_path
Zireael May 29, 2026
c38ab7e
fix: CI test for resolved checker paths and configure warning tests
Zireael May 30, 2026
8860ec9
test: stabilize CI fixtures and process-group kill regression
Zireael May 30, 2026
7e89898
fix(test): safe pid path in kill_all test and Windows e2e binary probe
Zireael May 30, 2026
9d20992
Merge branch 'main' into fix/configure-warnings-and-path
Zireael May 30, 2026
31bce8b
Merge remote-tracking branch 'upstream/main' into fix/configure-warni…
Zireael May 30, 2026
1717590
fix(semantic_test): resolve merge conflicts with upstream/main
Zireael May 30, 2026
4cc9e6e
Merge remote-tracking branch 'upstream/main' into fix/configure-warni…
Zireael May 30, 2026
ef3f52c
chore: remove unused well_known_windows_search_paths
Zireael May 30, 2026
96a3e72
Merge remote-tracking branch 'upstream/main' into fix/configure-warni…
Zireael May 31, 2026
8a3d723
fix: normalize CRLF to LF, fix biome/fmt lint errors
Zireael May 31, 2026
30f33d9
fix(tests): normalize CRLF in import_golden_corpus comparison
Zireael May 31, 2026
f2b59a7
fix(tests): increase move_file_cross_fs timeout from 60s to 120s
Zireael May 31, 2026
3c29336
fix(tests): normalize path separators in semantic watcher test assert…
Zireael May 31, 2026
026d130
Merge remote-tracking branch 'upstream/main' into fix/configure-warni…
Zireael Jun 1, 2026
085d885
fix(ci): remove undefined TARGET_DEBUG_BINARY_EXE and fix move_file c…
Zireael Jun 1, 2026
ec3fcf1
fix(tests): use cross-platform absolute path in from_bytes dimension …
Zireael Jun 1, 2026
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,14 @@ The schema is identical across harnesses. Only file location differs.
"typescript": "biome"
},

// How missing formatter/checker/LSP warnings appear after configure.
// Default: "toast" — 10s TUI/HTTP toast, no session chat pollution.
// "log" — plugin log only. "chat" — legacy ignored messages in the transcript.
// Formatter warnings run only when format_on_edit is true or formatter.<lang> is set.
// Checker warnings run only when validate_on_edit is "syntax"/"full" or checker.<lang> is set.
// (There is no top-level "formatters" key — use format_on_edit / formatter / checker.)
"configure_warnings_delivery": "toast",

// Tool surface level: "minimal" | "recommended" (default) | "all"
// minimal: aft_outline, aft_zoom, aft_safety only (no hoisting)
// recommended: minimal + hoisted tools (read/write/edit/apply_patch/bash)
Expand Down
10 changes: 10 additions & 0 deletions assets/aft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@
},
"description": "Per-language type checker overrides keyed by language (e.g. 'typescript', 'python', 'rust', 'go')."
},
"configure_warnings_delivery": {
"type": "string",
"enum": [
"toast",
"log",
"chat"
],
"default": "toast",
"description": "How missing formatter/checker/LSP binary warnings are shown after configure. 'toast' (default) uses a 10s TUI or HTTP toast without adding session chat messages. 'log' writes to the plugin log only. 'chat' uses legacy ignored user messages in the session transcript. Warnings for formatters/checkers are only emitted when format_on_edit is true or a per-language formatter is set; checker warnings require validate_on_edit 'syntax' or 'full' or an explicit checker. There is no top-level 'formatters' key — use format_on_edit, formatter, and checker instead."
},
"hoist_builtin_tools": {
"type": "boolean",
"default": true,
Expand Down
173 changes: 72 additions & 101 deletions crates/aft/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -847,85 +847,18 @@ fn resolve_tool_cached(
return *is_available;
}

let is_available = resolve_tool_uncached(tool, project_root);
let is_available = crate::format::tool_available_for_missing_warning(tool, project_root);
cache.insert(tool.to_string(), is_available);
is_available
}

fn resolve_tool_uncached(tool: &str, project_root: Option<&Path>) -> bool {
if tool == "ruff" {
return ruff_format_available(project_root);
}

if let Some(root) = project_root {
if root.join("node_modules").join(".bin").join(tool).exists() {
return true;
}
}

let mut child = match std::process::Command::new(tool)
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
{
Ok(child) => child,
Err(_) => return false,
};

let start = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(2);
loop {
match child.try_wait() {
Ok(Some(status)) => return status.success(),
Ok(None) if start.elapsed() > timeout => {
let _ = child.kill();
let _ = child.wait();
return false;
}
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(50)),
Err(_) => return false,
}
}
fn should_warn_missing_formatters(config: &crate::config::Config, lang: LangId) -> bool {
config.format_on_edit || config.formatter.contains_key(lang_key(lang))
}

fn ruff_format_available(project_root: Option<&Path>) -> bool {
let command = if let Some(root) = project_root {
let local = root.join("node_modules").join(".bin").join("ruff");
if local.exists() {
local
} else {
PathBuf::from("ruff")
}
} else {
PathBuf::from("ruff")
};

let output = match std::process::Command::new(command)
.arg("--version")
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output()
{
Ok(output) => output,
Err(_) => return false,
};

let version = String::from_utf8_lossy(&output.stdout);
let version = version
.trim()
.strip_prefix("ruff ")
.unwrap_or(version.trim());
let parts = version
.split('.')
.take(3)
.map(str::parse::<u32>)
.collect::<Result<Vec<_>, _>>();
match parts.as_deref() {
Ok([major, minor, patch]) => (*major, *minor, *patch) >= (0, 1, 2),
_ => false,
}
fn should_warn_missing_checkers(config: &crate::config::Config, lang: LangId) -> bool {
let mode = config.validate_on_edit.as_deref().unwrap_or("off");
(mode == "syntax" || mode == "full") || config.checker.contains_key(lang_key(lang))
}

fn missing_tool_warning(
Expand Down Expand Up @@ -967,38 +900,42 @@ fn detect_missing_tools_for_languages(
for &lang in languages {
let language = lang_key(lang);

for candidate in formatter_candidates(lang, config) {
if let Some(warning) = missing_tool_warning(
"formatter_not_installed",
language,
&candidate,
config.project_root.as_deref(),
&mut tool_cache,
) {
if seen.insert((
warning.kind.clone(),
warning.language.clone(),
warning.tool.clone(),
)) {
warnings.push(warning);
if should_warn_missing_formatters(config, lang) {
for candidate in formatter_candidates(lang, config) {
if let Some(warning) = missing_tool_warning(
"formatter_not_installed",
language,
&candidate,
config.project_root.as_deref(),
&mut tool_cache,
) {
if seen.insert((
warning.kind.clone(),
warning.language.clone(),
warning.tool.clone(),
)) {
warnings.push(warning);
}
}
}
}

for candidate in checker_candidates(lang, config) {
if let Some(warning) = missing_tool_warning(
"checker_not_installed",
language,
&candidate,
config.project_root.as_deref(),
&mut tool_cache,
) {
if seen.insert((
warning.kind.clone(),
warning.language.clone(),
warning.tool.clone(),
)) {
warnings.push(warning);
if should_warn_missing_checkers(config, lang) {
for candidate in checker_candidates(lang, config) {
if let Some(warning) = missing_tool_warning(
"checker_not_installed",
language,
&candidate,
config.project_root.as_deref(),
&mut tool_cache,
) {
if seen.insert((
warning.kind.clone(),
warning.language.clone(),
warning.tool.clone(),
)) {
warnings.push(warning);
}
}
}
}
Expand Down Expand Up @@ -2338,6 +2275,40 @@ mod tests {
assert_eq!(warning.tool, "oxfmt");
}

#[test]
fn detect_missing_tools_skips_formatters_when_format_on_edit_disabled() {
let temp = tempfile::tempdir().unwrap();
std::fs::write(temp.path().join("biome.json"), "{}\n").unwrap();
let config = Config {
project_root: Some(temp.path().to_path_buf()),
format_on_edit: false,
..Config::default()
};
let languages = std::collections::HashSet::from([crate::parser::LangId::TypeScript]);
let warnings = super::detect_missing_tools_for_languages(&languages, &config);
assert!(
warnings.is_empty(),
"format_on_edit:false should suppress derived formatter warnings: {warnings:?}"
);
}

#[test]
fn detect_missing_tools_still_warns_explicit_formatter_when_format_on_edit_disabled() {
let temp = tempfile::tempdir().unwrap();
let mut config = Config {
project_root: Some(temp.path().to_path_buf()),
format_on_edit: false,
..Config::default()
};
config
.formatter
.insert("typescript".to_string(), "biome".to_string());
let languages = std::collections::HashSet::from([crate::parser::LangId::TypeScript]);
let warnings = super::detect_missing_tools_for_languages(&languages, &config);
assert_eq!(warnings.len(), 1);
assert_eq!(warnings[0].tool, "biome");
}

#[test]
fn configure_missing_tools_warns_for_oxfmt_project_config() {
let temp = tempfile::tempdir().unwrap();
Expand Down
Loading