feat: implement model management commands and overlays for enhanced user interaction
This commit is contained in:
@@ -60,6 +60,14 @@ pub enum Action {
|
||||
name: String,
|
||||
command: String,
|
||||
},
|
||||
ModelList,
|
||||
ModelUse {
|
||||
provider: String,
|
||||
},
|
||||
ModelAdd {
|
||||
name: String,
|
||||
base_url: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
@@ -184,6 +192,44 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Action::ModelList => {
|
||||
state.misc.selected_index = 0;
|
||||
state.misc.overlay = Overlay::ModelSelector;
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::ModelUse { provider } => {
|
||||
if state.app_config.providers.contains_key(&provider) {
|
||||
let default_model = state.app_config.providers[&provider]
|
||||
.default_model.clone()
|
||||
.unwrap_or_else(|| "claude-opus-4-8".to_string());
|
||||
state.settings.provider = provider.clone();
|
||||
state.settings.model = default_model;
|
||||
let _ = state.settings.save();
|
||||
state.push_toast(Toast::new(ToastKind::Success,
|
||||
format!("Switched to provider '{}' (model: {})", provider, state.settings.model)));
|
||||
} else {
|
||||
state.push_toast(Toast::new(ToastKind::Error,
|
||||
format!("Unknown provider '{}'. Use /model ls to see available ones.", provider)));
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::ModelAdd { name, base_url } => {
|
||||
if base_url.is_empty() {
|
||||
state.push_toast(Toast::new(ToastKind::Error, "Usage: /model add <name> <base_url>".to_string()));
|
||||
} else {
|
||||
let cfg = crate::model::app_config::ProviderConfig {
|
||||
api_base: base_url.clone(),
|
||||
api_key_env: None,
|
||||
default_model: Some("claude-opus-4-8".to_string()),
|
||||
};
|
||||
state.app_config.providers.insert(name.clone(), cfg);
|
||||
state.push_toast(Toast::new(ToastKind::Success,
|
||||
format!("Added provider '{}' at {}", name, base_url)));
|
||||
state.push_toast(Toast::new(ToastKind::Info,
|
||||
"Use /model use <name> to switch, or /edit app_config.json to configure further".to_string()));
|
||||
state.dirty = true;
|
||||
}
|
||||
}
|
||||
Action::CloseOverlay => {
|
||||
// If the overlay is the Editor, dismiss it properly first
|
||||
if state.misc.overlay == Overlay::Editor {
|
||||
|
||||
@@ -34,6 +34,9 @@ pub fn apply_command(command: Command) -> Vec<Action> {
|
||||
Command::Mode(mode) => {
|
||||
vec![Action::SwitchMode(mode)]
|
||||
}
|
||||
Command::ClearConfirm => {
|
||||
vec![Action::OpenOverlay(Overlay::ClearConfirm)]
|
||||
}
|
||||
Command::Clear => {
|
||||
vec![Action::SystemNote {
|
||||
kind: "clear".to_string(),
|
||||
@@ -43,12 +46,27 @@ pub fn apply_command(command: Command) -> Vec<Action> {
|
||||
Command::Login { provider } => {
|
||||
vec![Action::StartOAuth { provider }]
|
||||
}
|
||||
Command::Edit(path) if path == "." || path.is_empty() => {
|
||||
vec![Action::SystemNote {
|
||||
kind: "info".to_string(),
|
||||
message: "Usage: /edit <path>\nOpens a file for inline editing.\nExample: /edit src/main.rs".to_string(),
|
||||
}]
|
||||
}
|
||||
Command::Edit(path) => {
|
||||
vec![Action::OpenEditor { path }]
|
||||
}
|
||||
Command::McpAdd { name, command } => {
|
||||
vec![Action::McpAdd { name, command }]
|
||||
}
|
||||
Command::ModelList => {
|
||||
vec![Action::ModelList]
|
||||
}
|
||||
Command::ModelUse { provider } => {
|
||||
vec![Action::ModelUse { provider }]
|
||||
}
|
||||
Command::ModelAdd { name, base_url } => {
|
||||
vec![Action::ModelAdd { name, base_url }]
|
||||
}
|
||||
Command::Unknown(cmd) => {
|
||||
vec![Action::SystemNote {
|
||||
kind: "error".to_string(),
|
||||
|
||||
+46
-15
@@ -62,6 +62,7 @@ pub struct InputState {
|
||||
pub autocomplete_prefix: String,
|
||||
pub autocomplete_candidates: Vec<String>,
|
||||
pub autocomplete_idx: usize,
|
||||
pub autocomplete_visible: bool,
|
||||
}
|
||||
|
||||
const COMMANDS: &[&str] = &[
|
||||
@@ -87,6 +88,9 @@ const COMMANDS: &[&str] = &[
|
||||
"/login openai",
|
||||
"/edit",
|
||||
"/mcp add",
|
||||
"/model",
|
||||
"/model ls",
|
||||
"/model add",
|
||||
];
|
||||
|
||||
impl InputState {
|
||||
@@ -99,36 +103,63 @@ impl InputState {
|
||||
autocomplete_prefix: String::new(),
|
||||
autocomplete_candidates: Vec::new(),
|
||||
autocomplete_idx: 0,
|
||||
autocomplete_visible: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_complete(&mut self) {
|
||||
let trimmed = self.buffer.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
return;
|
||||
}
|
||||
pub fn close_autocomplete(&mut self) {
|
||||
self.autocomplete_visible = false;
|
||||
self.autocomplete_candidates.clear();
|
||||
self.autocomplete_prefix.clear();
|
||||
self.autocomplete_idx = 0;
|
||||
}
|
||||
|
||||
if !trimmed.starts_with('/') {
|
||||
pub fn open_autocomplete(&mut self) {
|
||||
let trimmed = self.buffer.trim().to_string();
|
||||
if trimmed.is_empty() || !trimmed.starts_with('/') {
|
||||
self.close_autocomplete();
|
||||
return;
|
||||
}
|
||||
|
||||
let prefix = trimmed.to_lowercase();
|
||||
self.autocomplete_candidates = COMMANDS
|
||||
.iter()
|
||||
.filter(|c| c.starts_with(&prefix))
|
||||
.map(|c| c.to_string())
|
||||
.collect();
|
||||
self.autocomplete_prefix = prefix;
|
||||
self.autocomplete_idx = 0;
|
||||
self.autocomplete_visible = !self.autocomplete_candidates.is_empty();
|
||||
}
|
||||
|
||||
if prefix != self.autocomplete_prefix || self.autocomplete_candidates.is_empty() {
|
||||
self.autocomplete_candidates = COMMANDS
|
||||
.iter()
|
||||
.filter(|c| c.starts_with(&prefix))
|
||||
.map(|c| c.to_string())
|
||||
.collect();
|
||||
self.autocomplete_prefix = prefix;
|
||||
self.autocomplete_idx = 0;
|
||||
pub fn cycle_autocomplete(&mut self, forward: bool) {
|
||||
let n = self.autocomplete_candidates.len();
|
||||
if n == 0 { return; }
|
||||
if forward {
|
||||
self.autocomplete_idx = (self.autocomplete_idx + 1) % n;
|
||||
} else {
|
||||
self.autocomplete_idx = (self.autocomplete_idx + 1) % self.autocomplete_candidates.len();
|
||||
self.autocomplete_idx = if self.autocomplete_idx == 0 { n - 1 } else { self.autocomplete_idx - 1 };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_autocomplete(&mut self) -> bool {
|
||||
if let Some(candidate) = self.autocomplete_candidates.get(self.autocomplete_idx) {
|
||||
self.buffer = candidate.clone();
|
||||
self.cursor = self.buffer.len();
|
||||
self.close_autocomplete();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tab_complete(&mut self) {
|
||||
// Legacy inline tab-complete — used as a fallback when the dropdown
|
||||
// isn't visible yet. Opens the dropdown on the first Tab press.
|
||||
if !self.autocomplete_visible {
|
||||
self.open_autocomplete();
|
||||
} else {
|
||||
self.cycle_autocomplete(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,10 @@ pub enum Overlay {
|
||||
Learning,
|
||||
Usage,
|
||||
Loading,
|
||||
ModelSelector,
|
||||
ClearConfirm,
|
||||
ModePicker,
|
||||
LoginPicker,
|
||||
}
|
||||
|
||||
impl Overlay {
|
||||
|
||||
Reference in New Issue
Block a user