feat(validation): polish settings UI and disable defaults

This commit is contained in:
James Peter
2025-09-22 08:53:24 +10:00
parent 03853a6183
commit 96b0507f7c
6 changed files with 195 additions and 55 deletions
+2 -2
View File
@@ -237,7 +237,7 @@ pub struct GithubConfig {
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub struct ValidationConfig {
/// Master toggle for the patch validation harness.
#[serde(default = "default_true")]
#[serde(default)]
pub patch_harness: bool,
/// Optional allowlist restricting which external tools may run.
@@ -256,7 +256,7 @@ pub struct ValidationConfig {
impl Default for ValidationConfig {
fn default() -> Self {
Self {
patch_harness: true,
patch_harness: false,
tools_allowlist: None,
timeout_seconds: None,
tools: ValidationTools::default(),
+7
View File
@@ -1153,6 +1153,13 @@ impl App<'_> {
widget.handle_terminal_after(after);
}
}
AppEvent::RequestValidationToolInstall { name, command } => {
if let AppState::Chat { widget } = &mut self.app_state {
if let Some(launch) = widget.launch_validation_tool_install(&name, &command) {
self.app_event_tx.send(AppEvent::OpenTerminal(launch));
}
}
}
#[cfg(not(debug_assertions))]
AppEvent::RunUpdateCommand { command, display, latest_version } => {
if let AppState::Chat { widget } = &mut self.app_state {
+2
View File
@@ -123,6 +123,8 @@ pub(crate) enum AppEvent {
UpdateValidationPatchHarness(bool),
/// Enable/disable a specific validation tool
UpdateValidationTool { name: String, enable: bool },
/// Start installing a validation tool through the terminal overlay
RequestValidationToolInstall { name: String, command: String },
/// Enable/disable a specific MCP server
UpdateMcpServer { name: String, enable: bool },
@@ -14,6 +14,7 @@ use super::BottomPane;
#[derive(Clone, Debug)]
pub(crate) struct ToolStatus {
pub name: &'static str,
pub description: &'static str,
pub installed: bool,
pub install_hint: String,
}
@@ -49,6 +50,9 @@ impl ValidationSettingsView {
fn toggle_tool(&mut self, idx: usize) {
if let Some((status, enabled)) = self.tools.get_mut(idx) {
if !status.installed {
return;
}
let new_value = !*enabled;
*enabled = new_value;
self.app_event_tx.send(AppEvent::UpdateValidationTool {
@@ -88,10 +92,26 @@ impl<'a> BottomPaneView<'a> for ValidationSettingsView {
} else if self.selected_row == max_row {
self.is_complete = true;
} else if let Some((status, _)) = self.tools.get(self.selected_row - 1) {
if !status.installed && !status.install_hint.is_empty() {
pane.flash_footer_notice(format!("Prefilled install command for {}", status.name));
self.app_event_tx
.send(AppEvent::PrefillComposer(status.install_hint.clone()));
if !status.installed {
let command = status.install_hint.trim();
if command.is_empty() {
pane.flash_footer_notice(format!(
"No install command available for {}",
status.name
));
} else {
pane.flash_footer_notice(format!(
"Opening terminal to install {}",
status.name
));
self.is_complete = true;
self.app_event_tx.send(AppEvent::RequestValidationToolInstall {
name: status.name.to_string(),
command: command.to_string(),
});
}
} else {
self.toggle_tool(self.selected_row - 1);
}
}
}
@@ -114,7 +134,8 @@ impl<'a> BottomPaneView<'a> for ValidationSettingsView {
}
fn desired_height(&self, _width: u16) -> u16 {
(8 + self.tools.len() as u16).min(20)
let rows = 9 + (self.tools.len() as u16 * 2);
rows.min(24)
}
fn render(&self, area: Rect, buf: &mut Buffer) {
@@ -140,13 +161,21 @@ impl<'a> BottomPaneView<'a> for ValidationSettingsView {
if self.selected_row == 0 {
status_style = status_style.bg(crate::colors::selection()).add_modifier(Modifier::BOLD);
}
lines.push(Line::from(vec![
let mut header_spans = vec![
Span::styled("Validate New Code: ", Style::default().fg(crate::colors::text_dim())),
Span::styled(
if self.patch_harness { "Enabled" } else { "Disabled" },
status_style,
),
]));
];
if self.selected_row == 0 {
header_spans.push(Span::styled(" ", Style::default().bg(crate::colors::selection())));
header_spans.push(Span::styled(
if self.patch_harness { "(press Enter to disable)" } else { "(press Enter to enable)" },
Style::default().fg(crate::colors::text_dim()).bg(crate::colors::selection()),
));
}
lines.push(Line::from(header_spans));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled("Tools:", Style::default().fg(crate::colors::text_dim()))));
@@ -160,18 +189,49 @@ impl<'a> BottomPaneView<'a> for ValidationSettingsView {
if selected {
name_style = name_style.bg(crate::colors::selection()).add_modifier(Modifier::BOLD);
}
let extra = if status.installed {
""
} else {
" • missing (Enter: copy install command)"
};
lines.push(Line::from(vec![
let mut state_style = Style::default().fg(crate::colors::dim());
if selected {
state_style = state_style.bg(crate::colors::selection()).add_modifier(Modifier::BOLD);
}
let mut missing_style = Style::default().fg(crate::colors::warning());
if selected {
missing_style = missing_style.bg(crate::colors::selection()).add_modifier(Modifier::BOLD);
}
let mut main_spans = vec![
Span::raw("• "),
Span::styled(status.name, name_style),
Span::raw(" — "),
Span::styled(if *enabled { "on" } else { "off" }, Style::default().fg(crate::colors::dim())),
Span::raw(extra),
Span::styled(if *enabled { "enabled" } else { "disabled" }, state_style),
];
if !status.installed {
main_spans.push(Span::raw(" "));
main_spans.push(Span::styled("not installed", missing_style));
}
if selected {
main_spans.push(Span::styled(" ", Style::default().bg(crate::colors::selection())));
let hint = if !status.installed {
"(press Enter to install)"
} else if *enabled {
"(press Enter to disable)"
} else {
"(press Enter to enable)"
};
main_spans.push(Span::styled(
hint,
Style::default().fg(crate::colors::text_dim()).bg(crate::colors::selection()),
));
}
lines.push(Line::from(main_spans));
let mut desc_style = Style::default().fg(crate::colors::text_dim());
if selected {
desc_style = desc_style.bg(crate::colors::selection());
}
lines.push(Line::from(vec![
Span::styled(" ", desc_style),
Span::styled(status.description, desc_style),
]));
lines.push(Line::from(""));
}
lines.push(Line::from(""));
@@ -193,7 +253,7 @@ impl<'a> BottomPaneView<'a> for ValidationSettingsView {
Span::styled("←→/Space", Style::default().fg(crate::colors::success())),
Span::raw(" Toggle "),
Span::styled("Enter", Style::default().fg(crate::colors::success())),
Span::raw(" Toggle / Copy install "),
Span::raw(" Toggle / Install "),
Span::styled("Esc", Style::default().fg(crate::colors::error())),
Span::raw(" Close"),
]));
@@ -214,26 +274,56 @@ impl<'a> BottomPaneView<'a> for ValidationSettingsView {
}
pub(crate) fn detect_tools() -> Vec<ToolStatus> {
let mut result = Vec::new();
let tools = [
("actionlint", actionlint_hint()),
("shellcheck", shellcheck_hint()),
("markdownlint", markdownlint_hint()),
("hadolint", hadolint_hint()),
("yamllint", yamllint_hint()),
("cargo-check", cargo_check_hint()),
("shfmt", shfmt_hint()),
("prettier", prettier_hint()),
];
for (name, hint) in tools.into_iter() {
let installed = if name == "cargo-check" {
has("cargo")
} else {
which(name).is_some()
};
result.push(ToolStatus { name, installed, install_hint: hint });
}
result
vec![
ToolStatus {
name: "actionlint",
description: "Lint GitHub Actions workflows for syntax and logic issues.",
installed: has("actionlint"),
install_hint: actionlint_hint(),
},
ToolStatus {
name: "shellcheck",
description: "Analyze shell scripts for bugs and common pitfalls.",
installed: has("shellcheck"),
install_hint: shellcheck_hint(),
},
ToolStatus {
name: "markdownlint",
description: "Lint Markdown content for style and formatting problems.",
installed: has("markdownlint"),
install_hint: markdownlint_hint(),
},
ToolStatus {
name: "hadolint",
description: "Lint Dockerfiles for best practices and mistakes.",
installed: has("hadolint"),
install_hint: hadolint_hint(),
},
ToolStatus {
name: "yamllint",
description: "Validate YAML files for syntax and style issues.",
installed: has("yamllint"),
install_hint: yamllint_hint(),
},
ToolStatus {
name: "cargo-check",
description: "Run `cargo check` to catch Rust compilation errors quickly.",
installed: has("cargo"),
install_hint: cargo_check_hint(),
},
ToolStatus {
name: "shfmt",
description: "Format shell scripts consistently with shfmt.",
installed: has("shfmt"),
install_hint: shfmt_hint(),
},
ToolStatus {
name: "prettier",
description: "Format web assets (JS/TS/JSON/MD) with Prettier.",
installed: has("prettier"),
install_hint: prettier_hint(),
},
]
}
fn which(exe: &str) -> Option<std::path::PathBuf> {
+55 -15
View File
@@ -5448,6 +5448,46 @@ impl ChatWidget<'_> {
})
}
pub(crate) fn launch_validation_tool_install(
&mut self,
tool_name: &str,
install_hint: &str,
) -> Option<TerminalLaunch> {
let trimmed = install_hint.trim();
if trimmed.is_empty() {
self.history_push(history_cell::new_error_event(format!(
"No install command available for validation tool '{tool_name}'."
)));
self.request_redraw();
return None;
}
let wrapped = wrap_command(trimmed);
if wrapped.is_empty() {
self.history_push(history_cell::new_error_event(format!(
"Unable to build install command for validation tool '{tool_name}'."
)));
self.request_redraw();
return None;
}
let id = self.terminal.alloc_id();
let display = Self::truncate_with_ellipsis(trimmed, 128);
let launch = TerminalLaunch {
id,
title: format!("Install {tool_name}"),
command: wrapped,
command_display: display,
controller: None,
auto_close_on_success: false,
};
self.history_push(history_cell::new_background_event(format!(
"Installing validation tool '{tool_name}' with `{trimmed}`"
)));
Some(launch)
}
fn try_handle_terminal_shortcut(&mut self, raw_text: &str) -> bool {
let trimmed = raw_text.trim_start();
if let Some(rest) = trimmed.strip_prefix("$$") {
@@ -9649,12 +9689,12 @@ impl ChatWidget<'_> {
Ok(()) => self.history_push(history_cell::new_background_event(format!(
"✅ {}: {}",
name,
if enable { "on" } else { "off" }
if enable { "enabled" } else { "disabled" }
))),
Err(err) => self.history_push(history_cell::new_background_event(format!(
"⚠️ {}: {} (persist failed: {err})",
name,
if enable { "on" } else { "off" }
if enable { "enabled" } else { "disabled" }
))),
}
return;
@@ -9671,7 +9711,7 @@ impl ChatWidget<'_> {
self.history_push(history_cell::new_background_event(format!(
"ℹ️ {} already {}",
name,
if enable { "on" } else { "off" }
if enable { "enabled" } else { "disabled" }
)));
return;
}
@@ -9689,16 +9729,16 @@ impl ChatWidget<'_> {
Err(err) => Err(err.to_string()),
};
match persist_result {
Ok(()) => self.history_push(history_cell::new_background_event(format!(
"✅ {}: {}",
name,
if enable { "on" } else { "off" }
))),
Err(err) => self.history_push(history_cell::new_background_event(format!(
"⚠️ {}: {} (persist failed: {err})",
name,
if enable { "on" } else { "off" }
))),
Ok(()) => self.history_push(history_cell::new_background_event(format!(
"✅ {}: {}",
name,
if enable { "enabled" } else { "disabled" }
))),
Err(err) => self.history_push(history_cell::new_background_event(format!(
"⚠️ {}: {} (persist failed: {err})",
name,
if enable { "enabled" } else { "disabled" }
))),
}
}
@@ -9712,11 +9752,11 @@ impl ChatWidget<'_> {
lines.push("Tools:".to_string());
for status in validation_settings_view::detect_tools() {
let enabled = self.validation_tool_enabled(status.name);
let suffix = if status.installed { "" } else { " (missing)" };
let suffix = if status.installed { "" } else { " (not installed)" };
lines.push(format!(
"• {} — {}{}",
status.name,
if enabled { "on" } else { "off" },
if enabled { "enabled" } else { "disabled" },
suffix
));
}
+3 -2
View File
@@ -381,11 +381,12 @@ startup_timeout_ms = 20_000
Controls the quick validation harness that runs before applying patches. The
master toggle lives under `[validation]` with per-tool overrides in the nested
`[validation.tools]` table:
`[validation.tools]` table. The harness is disabled by default—enable it via the
config file or `/validation` when needed:
```toml
[validation]
patch_harness = true
patch_harness = false
[validation.tools]
shellcheck = true