selfware 0.2.2

Your personal AI workshop — software you own, software that lasts
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
//! Modern Input System for Selfware
//!
//! Rich terminal input with autocomplete, history, and vim keybindings.
//! Built on reedline for a professional IDE-like experience.

pub mod command_registry;
mod completer;
mod highlighter;
mod prompt;

pub use completer::SelfwareCompleter;
pub use highlighter::SelfwareHighlighter;
pub use prompt::SelfwarePrompt;

use anyhow::Result;
use reedline::{
    default_emacs_keybindings, ColumnarMenu, DefaultHinter, DefaultValidator, EditCommand, Emacs,
    FileBackedHistory, KeyCode, KeyModifiers, Keybindings, MenuBuilder, Reedline, ReedlineEvent,
    ReedlineMenu, Signal, Vi,
};
use std::path::PathBuf;

/// Input mode for the editor
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum InputMode {
    #[default]
    Emacs,
    Vi,
}

/// Configuration for the input system
#[derive(Debug, Clone)]
pub struct InputConfig {
    /// Keybinding mode (emacs or vi)
    pub mode: InputMode,
    /// Path to history file
    pub history_path: Option<PathBuf>,
    /// Maximum history entries
    pub max_history: usize,
    /// Enable syntax highlighting
    pub syntax_highlight: bool,
    /// Show inline hints
    pub show_hints: bool,
    /// Available tool names for completion
    pub tool_names: Vec<String>,
    /// Available commands for completion
    pub commands: Vec<String>,
}

impl Default for InputConfig {
    fn default() -> Self {
        Self {
            mode: InputMode::Emacs,
            history_path: dirs_history_path(),
            max_history: 10000,
            syntax_highlight: true,
            show_hints: true,
            tool_names: vec![],
            commands: command_registry::command_names(),
        }
    }
}

/// Get the default history path
fn dirs_history_path() -> Option<PathBuf> {
    dirs::data_local_dir().map(|p| p.join("selfware").join("history.txt"))
}

/// Modern line editor with IDE-like features
pub struct SelfwareEditor {
    editor: Reedline,
    prompt: SelfwarePrompt,
    config: InputConfig,
}

impl SelfwareEditor {
    /// Create a new editor with configuration
    pub fn new(config: InputConfig) -> Result<Self> {
        // Set up history
        let history = if let Some(path) = &config.history_path {
            // Ensure parent directory exists
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent)?;
            }
            Box::new(FileBackedHistory::with_file(
                config.max_history,
                path.clone(),
            )?)
        } else {
            Box::new(FileBackedHistory::new(config.max_history)?)
        };

        // Set up completer
        let completer = Box::new(SelfwareCompleter::new(
            config.tool_names.clone(),
            config.commands.clone(),
        ));

        // Set up highlighter
        let highlighter = Box::new(SelfwareHighlighter::new());

        // Set up hinter
        let hinter = Box::new(DefaultHinter::default());

        // Set up completion menu - IDE style that cycles with Tab
        let completion_menu = Box::new(
            ColumnarMenu::default()
                .with_name("completion_menu")
                .with_columns(1) // Single column for clearer selection
                .with_column_padding(2)
                .with_marker(" > "), // Show selection marker
        );

        // Set up keybindings
        let keybindings = Self::build_keybindings(config.mode);

        // Build the editor
        let edit_mode: Box<dyn reedline::EditMode> = match config.mode {
            InputMode::Emacs => Box::new(Emacs::new(keybindings)),
            InputMode::Vi => Box::new(Vi::default()),
        };

        // Set up validator
        let validator = Box::new(DefaultValidator);

        // Configure external editor for Ctrl+X
        let editor_cmd = std::env::var("VISUAL")
            .or_else(|_| std::env::var("EDITOR"))
            .unwrap_or_else(|_| "vi".to_string());
        let temp_file =
            std::env::temp_dir().join(format!("selfware_edit_{}.tmp", std::process::id()));
        let buffer_editor = std::process::Command::new(editor_cmd);

        let mut editor = Reedline::create()
            .with_history(history)
            .with_completer(completer)
            .with_quick_completions(true)
            .with_partial_completions(true)
            .with_hinter(hinter)
            .with_highlighter(highlighter)
            .with_validator(validator)
            .with_menu(ReedlineMenu::EngineCompleter(completion_menu))
            .with_edit_mode(edit_mode)
            .with_buffer_editor(buffer_editor, temp_file);

        // Add Ctrl+R for history search
        editor = editor.with_history_exclusion_prefix(Some(" ".into()));

        let prompt = SelfwarePrompt::new();

        Ok(Self {
            editor,
            prompt,
            config,
        })
    }

    /// Build keybindings for the given mode
    fn build_keybindings(mode: InputMode) -> Keybindings {
        let mut keybindings = match mode {
            InputMode::Emacs => default_emacs_keybindings(),
            InputMode::Vi => Keybindings::default(),
        };

        // Tab for completion
        // - First Tab: complete if single match, otherwise open menu
        // - Subsequent Tabs: cycle through menu items
        keybindings.add_binding(
            KeyModifiers::NONE,
            KeyCode::Tab,
            ReedlineEvent::UntilFound(vec![
                ReedlineEvent::HistoryHintComplete, // Complete history hint first
                ReedlineEvent::Edit(vec![EditCommand::Complete]), // Try inline completion
                ReedlineEvent::Menu("completion_menu".to_string()), // Open menu for visibility
                ReedlineEvent::MenuNext,            // Then cycle entries
            ]),
        );

        // Typing "/" opens slash command menu (Qwen-style)
        keybindings.add_binding(
            KeyModifiers::NONE,
            KeyCode::Char('/'),
            ReedlineEvent::Multiple(vec![
                ReedlineEvent::Edit(vec![EditCommand::InsertChar('/')]),
                ReedlineEvent::Menu("completion_menu".to_string()),
            ]),
        );

        // Shift+Tab to cycle execution mode: normal → auto-edit → yolo → daemon → normal
        keybindings.add_binding(
            KeyModifiers::SHIFT,
            KeyCode::BackTab,
            ReedlineEvent::ExecuteHostCommand("__cycle_mode__".to_string()),
        );

        // Escape to close menu without selecting
        keybindings.add_binding(KeyModifiers::NONE, KeyCode::Esc, ReedlineEvent::Esc);

        // Right arrow accepts the current hint/suggestion
        keybindings.add_binding(
            KeyModifiers::NONE,
            KeyCode::Right,
            ReedlineEvent::UntilFound(vec![
                ReedlineEvent::HistoryHintComplete,
                ReedlineEvent::Edit(vec![EditCommand::MoveRight { select: false }]),
            ]),
        );

        // Ctrl+J to insert newline (multi-line input)
        keybindings.add_binding(
            KeyModifiers::CONTROL,
            KeyCode::Char('j'),
            ReedlineEvent::Edit(vec![EditCommand::InsertNewline]),
        );

        // Ctrl+Y to toggle YOLO mode (via host command)
        keybindings.add_binding(
            KeyModifiers::CONTROL,
            KeyCode::Char('y'),
            ReedlineEvent::ExecuteHostCommand("__toggle_yolo__".to_string()),
        );

        // Ctrl+X to open external editor
        keybindings.add_binding(
            KeyModifiers::CONTROL,
            KeyCode::Char('x'),
            ReedlineEvent::OpenEditor,
        );

        // Ctrl+Space for command palette (we'll handle this in the app)
        keybindings.add_binding(
            KeyModifiers::CONTROL,
            KeyCode::Char(' '),
            ReedlineEvent::Edit(vec![EditCommand::InsertString("".into())]),
        );

        keybindings
    }

    /// Read a line from the user
    pub fn read_line(&mut self) -> Result<ReadlineResult> {
        match self.editor.read_line(&self.prompt) {
            Ok(Signal::Success(line)) => {
                // Detect sentinel values from ExecuteHostCommand keybindings
                if line.starts_with("__") && line.ends_with("__") {
                    Ok(ReadlineResult::HostCommand(line))
                } else {
                    Ok(ReadlineResult::Line(line))
                }
            }
            Ok(Signal::CtrlC) => Ok(ReadlineResult::Interrupt),
            Ok(Signal::CtrlD) => Ok(ReadlineResult::Eof),
            Err(e) => Err(e.into()),
        }
    }

    /// Update the prompt context
    pub fn set_prompt_context(&mut self, model: &str, step: usize) {
        self.prompt = SelfwarePrompt::with_context(model, step);
    }

    /// Update the prompt with full context including token usage
    pub fn set_prompt_full_context(&mut self, model: &str, step: usize, context_pct: f64) {
        self.prompt = SelfwarePrompt::with_full_context(model, step, context_pct);
    }

    /// Add tool names for completion
    pub fn add_tools(&mut self, tools: Vec<String>) {
        // Note: We'd need to rebuild the completer for dynamic updates
        // For now, tools should be passed at construction time
        let _ = tools;
    }

    /// Toggle between Emacs and Vi mode, returns the new mode
    pub fn toggle_vim_mode(&mut self) -> Result<InputMode> {
        let new_mode = match self.config.mode {
            InputMode::Emacs => InputMode::Vi,
            InputMode::Vi => InputMode::Emacs,
        };
        self.config.mode = new_mode;

        // Rebuild the editor with new mode
        let new_editor = SelfwareEditor::new(self.config.clone())?;
        self.editor = new_editor.editor;
        Ok(new_mode)
    }
}

/// Result of reading a line
#[derive(Debug)]
pub enum ReadlineResult {
    /// A line was entered
    Line(String),
    /// Ctrl+C was pressed
    Interrupt,
    /// Ctrl+D was pressed (EOF)
    Eof,
    /// Host command triggered by keybinding (e.g., "__toggle_yolo__")
    HostCommand(String),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_input_config_default() {
        let config = InputConfig::default();
        assert_eq!(config.mode, InputMode::Emacs);
        assert_eq!(config.max_history, 10000);
        assert!(config.commands.contains(&"/help".into()));
    }

    #[test]
    fn test_input_config_default_commands() {
        let config = InputConfig::default();

        assert!(config.commands.contains(&"/help".into()));
        assert!(config.commands.contains(&"/status".into()));
        assert!(config.commands.contains(&"/stats".into()));
        assert!(config.commands.contains(&"/mode".into()));
        assert!(config.commands.contains(&"/ctx".into()));
        assert!(config.commands.contains(&"/compress".into()));
        assert!(config.commands.contains(&"/context".into()));
        assert!(config.commands.contains(&"/memory".into()));
        assert!(config.commands.contains(&"/clear".into()));
        assert!(config.commands.contains(&"/tools".into()));
        assert!(config.commands.contains(&"/analyze".into()));
        assert!(config.commands.contains(&"/review".into()));
        assert!(config.commands.contains(&"/plan".into()));
        assert!(config.commands.contains(&"/swarm".into()));
        assert!(config.commands.contains(&"/queue".into()));
        assert!(config.commands.contains(&"/diff".into()));
        assert!(config.commands.contains(&"/git".into()));
        assert!(config.commands.contains(&"/undo".into()));
        assert!(config.commands.contains(&"/cost".into()));
        assert!(config.commands.contains(&"/model".into()));
        assert!(config.commands.contains(&"/compact".into()));
        assert!(config.commands.contains(&"/verbose".into()));
        assert!(config.commands.contains(&"/last".into()));
        assert!(config.commands.contains(&"/debug".into()));
        assert!(config.commands.contains(&"/debug-log".into()));
        assert!(config.commands.contains(&"/config".into()));
        assert!(config.commands.contains(&"/garden".into()));
        assert!(config.commands.contains(&"/journal".into()));
        assert!(config.commands.contains(&"/palette".into()));
        assert!(config.commands.contains(&"exit".into()));
        assert!(config.commands.contains(&"quit".into()));
    }

    #[test]
    fn test_input_config_custom() {
        let config = InputConfig {
            mode: InputMode::Vi,
            max_history: 500,
            tool_names: vec!["my_tool".into()],
            ..Default::default()
        };

        assert_eq!(config.mode, InputMode::Vi);
        assert_eq!(config.max_history, 500);
        assert!(config.tool_names.contains(&"my_tool".into()));
    }

    #[test]
    fn test_input_mode_default() {
        assert_eq!(InputMode::default(), InputMode::Emacs);
    }

    #[test]
    fn test_input_mode_equality() {
        assert_eq!(InputMode::Emacs, InputMode::Emacs);
        assert_eq!(InputMode::Vi, InputMode::Vi);
        assert_ne!(InputMode::Emacs, InputMode::Vi);
    }

    #[test]
    fn test_input_config_syntax_highlight() {
        let config = InputConfig::default();
        assert!(config.syntax_highlight);
    }

    #[test]
    fn test_input_config_show_hints() {
        let config = InputConfig::default();
        assert!(config.show_hints);
    }

    #[test]
    fn test_dirs_history_path() {
        // Should return Some path or None depending on environment
        let path = dirs_history_path();
        if let Some(p) = path {
            assert!(p.to_string_lossy().contains("selfware"));
            assert!(p.to_string_lossy().contains("history"));
        }
    }

    #[test]
    fn test_readline_result_variants() {
        // Just verify the enum variants exist and can be constructed
        let _line = ReadlineResult::Line("test".into());
        let _interrupt = ReadlineResult::Interrupt;
        let _eof = ReadlineResult::Eof;
        let _host_cmd = ReadlineResult::HostCommand("__toggle_yolo__".into());
    }

    #[test]
    fn test_readline_result_debug() {
        let result = ReadlineResult::Line("test".into());
        let debug_str = format!("{:?}", result);
        assert!(debug_str.contains("Line"));
        assert!(debug_str.contains("test"));
    }

    #[test]
    fn test_readline_result_host_command_debug() {
        let result = ReadlineResult::HostCommand("__toggle_yolo__".into());
        let debug_str = format!("{:?}", result);
        assert!(debug_str.contains("HostCommand"));
        assert!(debug_str.contains("__toggle_yolo__"));
    }

    #[test]
    fn test_input_config_new_commands() {
        let config = InputConfig::default();
        assert!(config.commands.contains(&"/vim".into()));
        assert!(config.commands.contains(&"/copy".into()));
        assert!(config.commands.contains(&"/restore".into()));
        assert!(config.commands.contains(&"/chat".into()));
        assert!(config.commands.contains(&"/theme".into()));
    }
}