- 1
//! Multi-line REPL input composer, history navigation, and slash command autocomplete. - 2
- 3
#[derive(Debug, Clone, Default)] - 4
pub struct ReplComposer { - 5
pub buffer: String, - 6
pub cursor_pos: usize, - 7
pub history: Vec<String>, - 8
pub history_idx: Option<usize>, - 9
pub slash_palette_open: bool, - 10
pub selected_slash_cmd: usize, - 11
} - 12
- 13
pub struct SlashCommand { - 14
pub name: &'static str, - 15
pub description: &'static str, - 16
} - 17
- 18
pub const AVAILABLE_SLASH_COMMANDS: &[SlashCommand] = &[ - 19
SlashCommand { - 20
name: "/theme", - 21
description: "Switch visual theme (Vakyartha Warm, Slate, Paper, Tokyo Night)", - 22
}, - 23
SlashCommand { - 24
name: "/diff", - 25
description: "Toggle multi-file git diff inspector", - 26
}, - 27
SlashCommand { - 28
name: "/preview", - 29
description: "Open live webpage/dev server preview", - 30
}, - 31
SlashCommand { - 32
name: "/clear", - 33
description: "Clear stream scrollback buffer", - 34
}, - 35
SlashCommand { - 36
name: "/model", - 37
description: "Inspect or switch active model route", - 38
}, - 39
SlashCommand { - 40
name: "/ops", - 41
description: "Jump to Observability & Operations Deck", - 42
}, - 43
SlashCommand { - 44
name: "/admin", - 45
description: "Jump to Remote Administration Cockpit", - 46
}, - 47
SlashCommand { - 48
name: "/inbox", - 49
description: "Jump to Attention Inbox & Memory", - 50
}, - 51
SlashCommand { - 52
name: "/hil", - 53
description: "Trigger Human-In-The-Loop approval modal", - 54
}, - 55
SlashCommand { - 56
name: "/detach", - 57
description: "Detach from session (leave running on server)", - 58
}, - 59
SlashCommand { - 60
name: "/help", - 61
description: "Show keyboard shortcuts & hotkeys", - 62
}, - 63
SlashCommand { - 64
name: "/quit", - 65
description: "Exit terminal", - 66
}, - 67
]; - 68
- 69
impl ReplComposer { - 70
pub fn new() -> Self { - 71
Self::default() - 72
} - 73
- 74
pub fn insert_char(&mut self, c: char) { - 75
if self.cursor_pos >= self.buffer.len() { - 76
self.buffer.push(c); - 77
} else { - 78
self.buffer.insert(self.cursor_pos, c); - 79
} - 80
self.cursor_pos += c.len_utf8(); - 81
self.update_slash_state(); - 82
} - 83
- 84
pub fn delete_backspace(&mut self) { - 85
if self.cursor_pos > 0 && !self.buffer.is_empty() { - 86
// Find char boundary - 87
let mut prev = self.cursor_pos - 1; - 88
while prev > 0 && !self.buffer.is_char_boundary(prev) { - 89
prev -= 1; - 90
} - 91
self.buffer.remove(prev); - 92
self.cursor_pos = prev; - 93
self.update_slash_state(); - 94
} - 95
} - 96
- 97
pub fn move_cursor_left(&mut self) { - 98
if self.cursor_pos > 0 { - 99
let mut prev = self.cursor_pos - 1; - 100
while prev > 0 && !self.buffer.is_char_boundary(prev) { - 101
prev -= 1; - 102
} - 103
self.cursor_pos = prev; - 104
} - 105
} - 106
- 107
pub fn move_cursor_right(&mut self) { - 108
if self.cursor_pos < self.buffer.len() { - 109
let mut next = self.cursor_pos + 1; - 110
while next < self.buffer.len() && !self.buffer.is_char_boundary(next) { - 111
next += 1; - 112
} - 113
self.cursor_pos = next; - 114
} - 115
} - 116
- 117
pub fn move_to_start(&mut self) { - 118
self.cursor_pos = 0; - 119
} - 120
- 121
pub fn move_to_end(&mut self) { - 122
self.cursor_pos = self.buffer.len(); - 123
} - 124
- 125
pub fn insert_newline(&mut self) { - 126
self.insert_char('\n'); - 127
} - 128
- 129
pub fn clear(&mut self) { - 130
self.buffer.clear(); - 131
self.cursor_pos = 0; - 132
self.slash_palette_open = false; - 133
self.history_idx = None; - 134
} - 135
- 136
pub fn submit(&mut self) -> Option<String> { - 137
let trimmed = self.buffer.trim().to_string(); - 138
if trimmed.is_empty() { - 139
return None; - 140
} - 141
self.history.push(trimmed.clone()); - 142
self.clear(); - 143
Some(trimmed) - 144
} - 145
- 146
pub fn history_up(&mut self) { - 147
if self.history.is_empty() { - 148
return; - 149
} - 150
let next_idx = match self.history_idx { - 151
None => self.history.len().saturating_sub(1), - 152
Some(i) if i > 0 => i - 1, - 153
Some(i) => i, - 154
}; - 155
self.history_idx = Some(next_idx); - 156
if let Some(entry) = self.history.get(next_idx) { - 157
self.buffer = entry.clone(); - 158
self.cursor_pos = self.buffer.len(); - 159
} - 160
} - 161
- 162
pub fn history_down(&mut self) { - 163
if let Some(i) = self.history_idx { - 164
if i + 1 < self.history.len() { - 165
self.history_idx = Some(i + 1); - 166
if let Some(entry) = self.history.get(i + 1) { - 167
self.buffer = entry.clone(); - 168
self.cursor_pos = self.buffer.len(); - 169
} - 170
} else { - 171
self.history_idx = None; - 172
self.clear(); - 173
} - 174
} - 175
} - 176
- 177
fn update_slash_state(&mut self) { - 178
self.slash_palette_open = self.buffer.starts_with('/'); - 179
if !self.slash_palette_open { - 180
self.selected_slash_cmd = 0; - 181
} - 182
} - 183
- 184
pub fn matching_slash_commands(&self) -> Vec<&'static SlashCommand> { - 185
if !self.slash_palette_open { - 186
return Vec::new(); - 187
} - 188
AVAILABLE_SLASH_COMMANDS - 189
.iter() - 190
.filter(|cmd| cmd.name.starts_with(&self.buffer)) - 191
.collect() - 192
} - 193
- 194
pub fn select_next_slash(&mut self) { - 195
let count = self.matching_slash_commands().len(); - 196
if count > 0 { - 197
self.selected_slash_cmd = (self.selected_slash_cmd + 1) % count; - 198
} - 199
} - 200
- 201
pub fn select_prev_slash(&mut self) { - 202
let count = self.matching_slash_commands().len(); - 203
if count > 0 { - 204
self.selected_slash_cmd = (self.selected_slash_cmd + count - 1) % count; - 205
} - 206
} - 207
- 208
pub fn complete_selected_slash(&mut self) { - 209
let matches = self.matching_slash_commands(); - 210
if let Some(cmd) = matches.get(self.selected_slash_cmd) { - 211
self.buffer = format!("{} ", cmd.name); - 212
self.cursor_pos = self.buffer.len(); - 213
self.slash_palette_open = false; - 214
} - 215
} - 216
} - 217
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.