diff --git a/README.md b/README.md index 805667f..1ff5f41 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,7 @@ OWLEN uses a modal, vim-inspired interface. Press `F1` (available from any mode) - **Normal Mode**: Navigate with `h/j/k/l`, `w/b`, `gg/G`. - **Editing Mode**: Enter with `i` or `a`. Send messages with `Enter`. - **Command Mode**: Enter with `:`. Access commands like `:quit`, `:save`, `:theme`. +- **Tutorial Command**: Type `:tutorial` any time for a quick summary of the most important keybindings. ## Documentation diff --git a/crates/owlen-core/src/config.rs b/crates/owlen-core/src/config.rs index 3146a63..e1271d0 100644 --- a/crates/owlen-core/src/config.rs +++ b/crates/owlen-core/src/config.rs @@ -625,6 +625,8 @@ pub struct UiSettings { pub show_role_labels: bool, #[serde(default = "UiSettings::default_wrap_column")] pub wrap_column: u16, + #[serde(default = "UiSettings::default_show_onboarding")] + pub show_onboarding: bool, } impl UiSettings { @@ -647,6 +649,10 @@ impl UiSettings { fn default_wrap_column() -> u16 { 100 } + + const fn default_show_onboarding() -> bool { + true + } } impl Default for UiSettings { @@ -657,6 +663,7 @@ impl Default for UiSettings { max_history_lines: Self::default_max_history_lines(), show_role_labels: Self::default_show_role_labels(), wrap_column: Self::default_wrap_column(), + show_onboarding: Self::default_show_onboarding(), } } } diff --git a/crates/owlen-tui/src/chat_app.rs b/crates/owlen-tui/src/chat_app.rs index 14fb871..1a0e3c5 100644 --- a/crates/owlen-tui/src/chat_app.rs +++ b/crates/owlen-tui/src/chat_app.rs @@ -20,6 +20,13 @@ use crate::events::Event; use std::collections::{BTreeSet, HashSet}; use std::sync::Arc; +const ONBOARDING_STATUS_LINE: &str = + "Welcome to Owlen! Press F1 for help or type :tutorial for keybinding tips."; +const ONBOARDING_SYSTEM_STATUS: &str = "Normal ▸ h/j/k/l • Insert ▸ i,a • Visual ▸ v • Command ▸ :"; +const TUTORIAL_STATUS: &str = "Tutorial loaded. Review quick tips in the footer."; +const TUTORIAL_SYSTEM_STATUS: &str = + "Normal ▸ h/j/k/l • Insert ▸ i,a • Visual ▸ v • Command ▸ : • Send ▸ Enter"; + #[derive(Clone, Debug)] pub(crate) struct ModelSelectorItem { kind: ModelSelectorItemKind, @@ -202,6 +209,7 @@ impl ChatApp { let config_guard = controller.config_async().await; let theme_name = config_guard.ui.theme.clone(); let current_provider = config_guard.general.default_provider.clone(); + let show_onboarding = config_guard.ui.show_onboarding; drop(config_guard); let theme = owlen_core::theme::get_theme(&theme_name).unwrap_or_else(|| { eprintln!("Warning: Theme '{}' not found, using default", theme_name); @@ -211,7 +219,11 @@ impl ChatApp { let app = Self { controller, mode: InputMode::Normal, - status: "Normal mode • Press F1 for help".to_string(), + status: if show_onboarding { + ONBOARDING_STATUS_LINE.to_string() + } else { + "Normal mode • Press F1 for help".to_string() + }, error: None, models: Vec::new(), available_providers: Vec::new(), @@ -252,13 +264,27 @@ impl ChatApp { available_themes: Vec::new(), selected_theme_index: 0, pending_consent: None, - system_status: String::new(), + system_status: if show_onboarding { + ONBOARDING_SYSTEM_STATUS.to_string() + } else { + String::new() + }, _execution_budget: 50, agent_mode: false, agent_running: false, operating_mode: owlen_core::mode::Mode::default(), }; + if show_onboarding { + let mut cfg = app.controller.config_mut(); + if cfg.ui.show_onboarding { + cfg.ui.show_onboarding = false; + if let Err(err) = config::save_config(&cfg) { + eprintln!("Warning: Failed to persist onboarding preference: {err}"); + } + } + } + Ok((app, session_rx)) } @@ -402,6 +428,24 @@ impl ChatApp { self.system_status.clear(); } + pub fn show_tutorial(&mut self) { + self.error = None; + self.status = TUTORIAL_STATUS.to_string(); + self.system_status = TUTORIAL_SYSTEM_STATUS.to_string(); + let tutorial_body = concat!( + "Keybindings overview:\n", + " • Movement: h/j/k/l, gg/G, w/b\n", + " • Insert text: i or a (Esc to exit)\n", + " • Visual select: v (Esc to exit)\n", + " • Command mode: : (press Enter to run, Esc to cancel)\n", + " • Send message: Enter in Insert mode\n", + " • Help overlay: F1 or ?\n" + ); + self.controller + .conversation_mut() + .push_system_message(tutorial_body.to_string()); + } + pub fn command_buffer(&self) -> &str { &self.command_buffer } @@ -439,6 +483,7 @@ impl ChatApp { ("n", "Alias for new"), ("theme", "Switch theme"), ("themes", "List available themes"), + ("tutorial", "Show keybinding tutorial"), ("reload", "Reload configuration and themes"), ("e", "Edit a file"), ("edit", "Alias for edit"), @@ -1688,6 +1733,9 @@ impl ChatApp { } } } + "tutorial" => { + self.show_tutorial(); + } "themes" => { // Load all themes and enter browser mode let themes = owlen_core::theme::load_all_themes(); diff --git a/docs/migrations/README.md b/docs/migrations/README.md new file mode 100644 index 0000000..4a56d8c --- /dev/null +++ b/docs/migrations/README.md @@ -0,0 +1,9 @@ +## Migration Notes + +Owlen is still in alpha, so configuration and storage formats may change between releases. This directory collects short guides that explain how to update a local environment when breaking changes land. + +### Schema 1.1.0 (October 2025) + +Owlen `config.toml` files now carry a `schema_version`. On startup the loader upgrades any existing file and warns when deprecated keys are present. No manual changes are required, but if you track the file in version control you may notice `schema_version = "1.1.0"` added near the top. + +If you previously set `agent.max_tool_calls`, replace it with `agent.max_iterations`. The former is now ignored.