mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
feat(validation): polish settings UI and disable defaults
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user