use unicode_width::UnicodeWidthStr;
use crate::{
common::conversation::Inline, config::ViewMode, session::Command as SessionCommand,
ui::suggestion_popup::Suggestion,
};
use std::collections::HashMap;
#[derive(Clone, Debug)]
struct ModalCommand {
command: fn(&str) -> Option<SessionCommand>,
hint: Vec<Inline>,
name: Vec<Inline>,
}
#[derive(Clone, Debug)]
struct ViewModeParameter {
mode: ViewMode,
hint: Vec<Inline>,
name: Vec<Inline>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum CommandId {
Cancel,
Export,
Model,
Quit,
Reprompt,
Session,
Title,
ViewMode,
}
#[derive(Clone, Debug)]
pub struct ModalCommands {
commands: HashMap<CommandId, ModalCommand>,
long_key: HashMap<String, CommandId>,
models: Vec<Suggestion>,
sessions: Vec<Suggestion>,
short_key: HashMap<String, CommandId>,
}
impl ModalCommands {
pub fn new() -> Self {
let commands = vec![
(
CommandId::Cancel,
"cancel".to_string(),
"c".to_string(),
ModalCommand {
command: |_| Some(SessionCommand::CancelRequest),
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("c".to_string())]),
Inline::Text("ancel".to_string()),
],
hint: vec![Inline::Text("Cancel running request".to_string())],
},
),
(
CommandId::Export,
"export".to_string(),
"e".to_string(),
ModalCommand {
command: |name| Some(SessionCommand::Export(name.to_string())),
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("e".to_string())]),
Inline::Text("xport".to_string()),
],
hint: vec![Inline::Text("Export to md file".to_string())],
},
),
(
CommandId::Model,
"model".to_string(),
"m".to_string(),
ModalCommand {
command: |model| Some(SessionCommand::SelectModel(model.to_string())),
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("m".to_string())]),
Inline::Text("odel".to_string()),
],
hint: vec![Inline::Text("Select model to use".to_string())],
},
),
(
CommandId::Quit,
"quit".to_string(),
"q".to_string(),
ModalCommand {
command: |_| Some(SessionCommand::Quit),
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("q".to_string())]),
Inline::Text("uit".to_string()),
],
hint: vec![Inline::Text("Quit monothink".to_string())],
},
),
(
CommandId::Reprompt,
"reprompt".to_string(),
"r".to_string(),
ModalCommand {
command: |_| Some(SessionCommand::ResendPrompt),
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("r".to_string())]),
Inline::Text("eprompt".to_string()),
],
hint: vec![Inline::Text("Resend the last prompt".to_string())],
},
),
(
CommandId::Session,
"session".to_string(),
"s".to_string(),
ModalCommand {
command: |id| Some(SessionCommand::LoadSession(id.to_string())),
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("s".to_string())]),
Inline::Text("ession".to_string()),
],
hint: vec![Inline::Text("Load a saved session".to_string())],
},
),
(
CommandId::Title,
"title".to_string(),
"t".to_string(),
ModalCommand {
command: |name| Some(SessionCommand::Title(name.to_string())),
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("t".to_string())]),
Inline::Text("itle".to_string()),
],
hint: vec![Inline::Text("Set conversation title".to_string())],
},
),
(
CommandId::ViewMode,
"view-mode".to_string(),
"v".to_string(),
ModalCommand {
command: |param| {
viewmode_parameters()
.iter()
.find(|(key, _)| *key == param)
.map(|(_, param)| param.mode)
.map(SessionCommand::ViewMode)
},
name: vec![
Inline::StrongEmphasis(vec![Inline::Text("v".to_string())]),
Inline::Text("iew-".to_string()),
Inline::StrongEmphasis(vec![Inline::Text("m".to_string())]),
Inline::Text("ode".to_string()),
],
hint: vec![Inline::Text("Change conversation view mode".to_string())],
},
),
];
let mut mc = ModalCommands {
commands: HashMap::new(),
long_key: HashMap::new(),
models: Vec::new(),
sessions: Vec::new(),
short_key: HashMap::new(),
};
for (id, long, short, command) in commands.into_iter() {
mc.insert(command, long, short, id);
}
mc
}
pub fn session_command(&self, key: &str, params: &str) -> Option<SessionCommand> {
self.long_key
.get(key)
.and_then(|id| self.commands.get(id))
.or_else(|| self.short_key.get(key).and_then(|id| self.commands.get(id)))
.and_then(|cmd| (cmd.command)(params))
}
pub fn command_suggestions(&self) -> Vec<Suggestion> {
self.commands
.keys()
.map(|id| Suggestion {
raw: self
.long_key
.iter()
.find(|(_, cand)| *cand == id)
.map(|(name, _)| name)
.expect("For every id there should be a long name")
.to_owned(),
name: self.commands[id].name.clone(),
hint: self.commands[id].hint.clone(),
})
.collect()
}
pub fn parameter_filter<'a>(&self, command: &str, params: &'a str) -> &'a str {
match self.command_id(command) {
Some(id) => match id {
CommandId::Cancel => params,
CommandId::Export => params, CommandId::Model => params,
CommandId::Quit => params,
CommandId::Reprompt => params,
CommandId::Session => params,
CommandId::Title => params,
CommandId::ViewMode => params,
},
None => "",
}
}
pub fn parameter_suggestions(&self, command: &str, _params: &str) -> Option<Vec<Suggestion>> {
match self.command_id(command) {
Some(id) => match id {
CommandId::Cancel => None,
CommandId::Export => None, CommandId::Model => {
if self.models.is_empty() {
None
} else {
Some(self.models.clone())
}
}
CommandId::Quit => None,
CommandId::Reprompt => None,
CommandId::Session => {
if self.sessions.is_empty() {
None
} else {
Some(self.sessions.clone())
}
}
CommandId::Title => None,
CommandId::ViewMode => Some(
viewmode_parameters()
.iter()
.map(|(name, param)| Suggestion {
raw: name.clone(),
name: param.name.clone(),
hint: param.hint.clone(),
})
.collect(),
),
},
None => None,
}
}
pub fn model_suggestions(&mut self, models: Vec<Suggestion>) {
self.models = models.to_vec();
}
pub fn session_suggestions(&mut self, sessions: Vec<Suggestion>) {
self.sessions = sessions.to_vec();
}
fn command_id(&self, command: &str) -> Option<CommandId> {
self.long_key
.get(command)
.or_else(|| self.short_key.get(command))
.copied()
}
fn insert(&mut self, cmd: ModalCommand, long: String, short: String, id: CommandId) {
self.commands.insert(id, cmd);
self.long_key.insert(long, id);
self.short_key.insert(short, id);
}
}
fn viewmode_parameters() -> HashMap<String, ViewModeParameter> {
HashMap::from([
(
"compact".to_string(),
ViewModeParameter {
mode: ViewMode::Compact,
hint: vec![Inline::Text(
"Show user prompt, agent response, tool call summaries".to_string(),
)],
name: vec![Inline::Text("compact".to_string())],
},
),
(
"debug".to_string(),
ViewModeParameter {
mode: ViewMode::Debug,
hint: vec![Inline::Text(
"Show everything incl. debug messages".to_string(),
)],
name: vec![Inline::Text("debug".to_string())],
},
),
(
"detailed".to_string(),
ViewModeParameter {
mode: ViewMode::Detailed,
hint: vec![Inline::Text(
"Show user message, full assistant output, tool call summaries".to_string(),
)],
name: vec![Inline::Text("detailed".to_string())],
},
),
(
"full".to_string(),
ViewModeParameter {
mode: ViewMode::Full,
hint: vec![Inline::Text("Show everything uncompacted".to_string())],
name: vec![Inline::Text("full".to_string())],
},
),
(
"minimal".to_string(),
ViewModeParameter {
mode: ViewMode::Minimal,
hint: vec![Inline::Text(
"Only show user message and assistant responses".to_string(),
)],
name: vec![Inline::Text("minimal".to_string())],
},
),
])
}
#[derive(Debug)]
pub struct ModalCommandParser {
name: String,
params: String,
}
impl ModalCommandParser {
pub fn new(input: &[String]) -> Self {
let command = input.first().map_or(String::new(), |line| line.to_owned());
let (name, params) = command[0..]
.split_once(char::is_whitespace)
.unwrap_or((&command[0..], ""));
let params = params.trim();
Self {
name: name.to_string(),
params: params.to_string(),
}
}
pub fn cmd_params(&self) -> (String, String) {
(self.name.clone(), self.params.clone())
}
pub fn command(&self) -> String {
self.name.clone()
}
pub fn params(&self) -> String {
self.params.clone()
}
pub fn hovers_command(&self, pos: usize) -> bool {
pos <= self.name.width()
}
}
#[test]
fn test_whitespace() {
let mcp = ModalCommandParser::new(&["view-mode co".to_string()]);
assert_eq!(mcp.command(), "view-mode");
assert_eq!(mcp.params(), "co");
}