feat: implement model management commands and overlays for enhanced user interaction

This commit is contained in:
asepharyana
2026-07-12 02:06:46 +07:00
parent 9f272d5973
commit 662e269f2c
8 changed files with 403 additions and 26 deletions
+146
View File
@@ -558,10 +558,156 @@ fn render_overlay(frame: &mut Frame, area: Rect, overlay: crate::app::state::typ
.block(block);
frame.render_widget(paragraph, overlay_area);
}
crate::app::state::types::Overlay::ModelSelector => {
let block = block.title(" Model Selector ");
let mut lines: Vec<Line> = vec![
Line::from(Span::styled(
format!("Current: {} / {}", state.settings.provider, state.settings.model),
Style::default().fg(Theme::INFO).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled("", Style::default())),
Line::from(Span::styled("Providers:", Style::default().fg(Theme::DIM))),
];
let providers: Vec<(&String, &crate::model::app_config::ProviderConfig)> = state.app_config.providers.iter().collect();
for (i, (name, cfg)) in providers.iter().enumerate() {
let is_current = *name == &state.settings.provider;
let is_selected = i == state.misc.selected_index;
let prefix = if is_selected { "" } else { " " };
let model_str = cfg.default_model.as_deref().unwrap_or("(any)");
let label = format!("{}{} ({})", prefix, name, model_str);
let style = if is_current {
Style::default().fg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD)
} else if is_selected {
Style::default().fg(Theme::BG).bg(Theme::HIGHLIGHT)
} else {
Style::default().fg(Theme::TEXT)
};
lines.push(Line::from(Span::styled(label, style)));
}
lines.push(Line::from(Span::styled("", Style::default())));
lines.push(Line::from(Span::styled(
"↑↓ navigate · Enter select · Esc close · /model add <name> <url> to add",
Style::default().fg(Theme::DIM),
)));
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, overlay_area);
}
crate::app::state::types::Overlay::ClearConfirm => {
let block = block.title(" Clear Transcript ");
let lines = vec![
Line::from(Span::styled(
"Clear all messages from the transcript?",
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled("", Style::default())),
Line::from(Span::styled(
"Enter to confirm · Esc to cancel",
Style::default().fg(Theme::DIM),
)),
];
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, overlay_area);
}
crate::app::state::types::Overlay::ModePicker => {
let block = block.title(" Mode ");
let modes = [
("Auto", "auto-approve tools"),
("Normal", "ask before risky tools"),
("Plan", "plan-only mode"),
("Yolo", "full auto"),
];
let current = state.mode.name();
let mut lines: Vec<Line> = vec![
Line::from(Span::styled(
format!("Current: {}", current),
Style::default().fg(Theme::INFO).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled("", Style::default())),
];
for (i, (name, desc)) in modes.iter().enumerate() {
let is_selected = i == state.misc.selected_index;
let prefix = if is_selected { "" } else { " " };
let label = format!("{}{} ({})", prefix, name, desc);
let style = if *name == current {
Style::default().fg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD).add_modifier(Modifier::ITALIC)
} else if is_selected {
Style::default().fg(Theme::BG).bg(Theme::HIGHLIGHT)
} else {
Style::default().fg(Theme::TEXT)
};
lines.push(Line::from(Span::styled(label, style)));
}
lines.push(Line::from(Span::styled("", Style::default())));
lines.push(Line::from(Span::styled(
"↑↓ navigate · Enter select · Esc close",
Style::default().fg(Theme::DIM),
)));
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, overlay_area);
}
crate::app::state::types::Overlay::LoginPicker => {
let block = block.title(" Login ");
let providers = crate::app::mode::onboard_provider::PROVIDERS;
let mut lines: Vec<Line> = vec![
Line::from(Span::styled(
"Select a provider to log in:",
Style::default().fg(Theme::TEXT),
)),
Line::from(Span::styled("", Style::default())),
];
for (i, p) in providers.iter().enumerate() {
let is_selected = i == state.misc.selected_index;
let prefix = if is_selected { "" } else { " " };
let style = if is_selected {
Style::default().fg(Theme::BG).bg(Theme::HIGHLIGHT)
} else {
Style::default().fg(Theme::TEXT)
};
lines.push(Line::from(Span::styled(format!("{}{}", prefix, p), style)));
}
lines.push(Line::from(Span::styled("", Style::default())));
lines.push(Line::from(Span::styled(
"↑↓ navigate · Enter select · Esc close",
Style::default().fg(Theme::DIM),
)));
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, overlay_area);
}
}
}
fn render_input_bar(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
// Render autocomplete dropdown if visible
if state.input.autocomplete_visible && !state.input.autocomplete_candidates.is_empty() {
let n = state.input.autocomplete_candidates.len().min(10) as u16;
let dropdown_height = n + 2; // border + items
let dropdown_area = Rect {
x: area.x,
y: area.y.saturating_sub(dropdown_height),
width: area.width.min(40),
height: dropdown_height,
};
let dropdown_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Theme::BORDER))
.title(" Commands ");
let mut lines: Vec<Line> = Vec::new();
let selected = state.input.autocomplete_idx;
for (i, candidate) in state.input.autocomplete_candidates.iter().enumerate().take(10) {
let prefix = if i == selected { "" } else { " " };
let style = if i == selected {
Style::default().fg(Theme::BG).bg(Theme::HIGHLIGHT)
} else {
Style::default().fg(Theme::TEXT)
};
let label = format!("{}{}", prefix, candidate);
lines.push(Line::from(Span::styled(label, style)));
}
let dropdown = Paragraph::new(lines).block(dropdown_block);
frame.render_widget(dropdown, dropdown_area);
}
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(Theme::BORDER));