feat: implement interactive lesson management and update command handling
This commit is contained in:
@@ -23,10 +23,7 @@ Slash commands:
|
||||
/help Show this help
|
||||
/quit Quit session
|
||||
/mode <name> Switch mode (chat, bash, workflow)
|
||||
/lesson <text> Create a lesson
|
||||
/lesson list List lessons
|
||||
/lesson export Export lessons
|
||||
/lesson import Import lessons
|
||||
/lesson Interactive lesson manager
|
||||
/clear Clear transcript";
|
||||
|
||||
/// Route an incoming action while the help overlay is open.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
use crate::app::state::rest::AppStateRest;
|
||||
|
||||
/// A unified representation of a lesson item for the interactive TUI overlay.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LearningItem {
|
||||
Pending {
|
||||
name: String,
|
||||
content: String,
|
||||
scope: String,
|
||||
confidence: String,
|
||||
},
|
||||
Stored {
|
||||
name: String,
|
||||
content: String,
|
||||
lifecycle: String,
|
||||
scope: String,
|
||||
description: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Dynamically read all pending and stored lessons.
|
||||
pub fn get_learning_items(state: &AppStateRest) -> Vec<LearningItem> {
|
||||
let mut items = Vec::new();
|
||||
|
||||
// 1. Load pending lessons from session directory
|
||||
let pending = if let Some(ref rt) = state.session_runtime {
|
||||
crate::app::review::load_pending_lessons(&rt.session_dir)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
for p in pending {
|
||||
let scope_str = match p.lesson.scope {
|
||||
crate::app::review::LessonScope::Project => "project",
|
||||
crate::app::review::LessonScope::Global => "global",
|
||||
}.to_string();
|
||||
|
||||
let conf_str = match p.lesson.confidence {
|
||||
crate::app::review::Confidence::Human => "human",
|
||||
crate::app::review::Confidence::Verified => "verified",
|
||||
crate::app::review::Confidence::Unverified => "unverified",
|
||||
crate::app::review::Confidence::Auto => "auto",
|
||||
}.to_string();
|
||||
|
||||
items.push(LearningItem::Pending {
|
||||
name: p.lesson.name,
|
||||
content: p.lesson.content,
|
||||
scope: scope_str,
|
||||
confidence: conf_str,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Load stored memory lessons from long-term memory directory
|
||||
let names = crate::model::memory::Memory::list(&state.memory_dir);
|
||||
for name in names {
|
||||
if let Ok(mem) = crate::model::memory::Memory::read(&state.memory_dir, &name) {
|
||||
if mem.kind == "lesson" {
|
||||
items.push(LearningItem::Stored {
|
||||
name: mem.name,
|
||||
content: mem.content,
|
||||
lifecycle: mem.lifecycle,
|
||||
scope: mem.scope.unwrap_or_else(|| "project".to_string()),
|
||||
description: mem.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
+1
-22
@@ -1,8 +1,6 @@
|
||||
//! TUI mode definitions and per-mode input/action handlers, one submodule
|
||||
//! per overlay/mode (bash, editor, effort, mcp, quit confirm, rewind, etc.).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod bash;
|
||||
pub mod editor;
|
||||
pub mod effort;
|
||||
@@ -14,23 +12,4 @@ pub mod quit_confirm;
|
||||
pub mod rewind;
|
||||
pub mod settings;
|
||||
pub mod todo;
|
||||
|
||||
/// Which input/overlay mode the TUI is currently in; drives both key
|
||||
/// routing (`controller/input.rs`) and rendering.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ModeKind {
|
||||
Chat,
|
||||
Bash,
|
||||
Workflow,
|
||||
Help,
|
||||
Settings,
|
||||
QuitConfirm,
|
||||
|
||||
KeyInput,
|
||||
Editor,
|
||||
Effort,
|
||||
Mcp,
|
||||
Todo,
|
||||
Rewind,
|
||||
Loading,
|
||||
}
|
||||
pub mod learning;
|
||||
|
||||
@@ -20,7 +20,6 @@ use std::collections::VecDeque;
|
||||
|
||||
use crate::app::harness::Verdict;
|
||||
use sha2::Digest;
|
||||
use crate::app::mode::ModeKind;
|
||||
use crate::app::review::{should_trigger_review, trigger_review};
|
||||
use crate::app::state::rest::{AppStateRest, ChatMessageDisplay};
|
||||
use crate::app::state::runtime::TurnEvent;
|
||||
@@ -38,7 +37,6 @@ use crate::dto::chat::message::{ChatMessage, Role};
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Action {
|
||||
ForceQuit,
|
||||
SwitchMode(ModeKind),
|
||||
SubmitInput(String),
|
||||
DeleteChar,
|
||||
DeleteCharRight,
|
||||
@@ -57,18 +55,16 @@ pub enum Action {
|
||||
QuitConfirm,
|
||||
Resize(u16, u16),
|
||||
Tick,
|
||||
LessonExport {
|
||||
path: String,
|
||||
},
|
||||
LessonImport {
|
||||
path: String,
|
||||
},
|
||||
|
||||
LessonAccept {
|
||||
name: String,
|
||||
},
|
||||
LessonReject {
|
||||
name: String,
|
||||
},
|
||||
LessonDelete {
|
||||
name: String,
|
||||
},
|
||||
StartOAuth {
|
||||
provider: String,
|
||||
},
|
||||
@@ -106,25 +102,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
state.shutdown_lsp();
|
||||
state.quit = true;
|
||||
}
|
||||
Action::SwitchMode(mode) => {
|
||||
state.misc.overlay = match mode {
|
||||
ModeKind::Chat
|
||||
| ModeKind::Bash => Overlay::None,
|
||||
ModeKind::Workflow => Overlay::Workflow,
|
||||
ModeKind::Help => Overlay::Help,
|
||||
ModeKind::Settings => Overlay::Settings,
|
||||
ModeKind::QuitConfirm => Overlay::QuitConfirm,
|
||||
|
||||
ModeKind::KeyInput => Overlay::KeyInput,
|
||||
ModeKind::Editor => Overlay::Editor,
|
||||
ModeKind::Effort => Overlay::Effort,
|
||||
ModeKind::Mcp => Overlay::Mcp,
|
||||
ModeKind::Todo => Overlay::Todo,
|
||||
ModeKind::Rewind => Overlay::Rewind,
|
||||
ModeKind::Loading => Overlay::Loading,
|
||||
};
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::SubmitInput(text) => {
|
||||
state.input.submit();
|
||||
let text = text.trim().to_string();
|
||||
@@ -175,6 +153,9 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
}
|
||||
Action::OpenOverlay(overlay) => {
|
||||
state.misc.overlay = overlay;
|
||||
if overlay == Overlay::Learning || overlay == Overlay::Rewind || overlay == Overlay::ModelSelector {
|
||||
state.misc.selected_index = 0;
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::OpenEditor { path } => {
|
||||
@@ -245,45 +226,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
state.scroll.set_max_visible(w as usize);
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::LessonExport { path } => {
|
||||
let dest = std::path::Path::new(&path);
|
||||
if let Some(parent) = dest.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
match crate::model::memory::export_lessons(&state.memory_dir, dest) {
|
||||
Ok(_) => {
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Success,
|
||||
format!("lessons exported to {}", path),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Error,
|
||||
format!("export failed: {}", e),
|
||||
));
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::LessonImport { path } => {
|
||||
let src = std::path::Path::new(&path);
|
||||
match crate::model::memory::import_lessons(&state.memory_dir, src) {
|
||||
Ok(count) => {
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Success,
|
||||
format!("imported {} lessons from {}", count, path),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
state.push_toast(crate::app::state::types::Toast::new(
|
||||
crate::app::state::types::ToastKind::Error,
|
||||
format!("import failed: {}", e),
|
||||
));
|
||||
}
|
||||
}
|
||||
state.dirty = true;
|
||||
}
|
||||
|
||||
Action::StartOAuth { provider } => {
|
||||
let turn_events = state.turn_events.clone();
|
||||
let provider_clone = provider.clone();
|
||||
@@ -583,6 +526,9 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
&rt.session_dir, &state.memory_dir, &name, true,
|
||||
);
|
||||
}
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
refresh_lesson_counters(&state.memory_dir, rt);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Success,
|
||||
format!("accepted lesson: {}", name)));
|
||||
state.dirty = true;
|
||||
@@ -593,10 +539,21 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
&rt.session_dir, &state.memory_dir, &name, false,
|
||||
);
|
||||
}
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
refresh_lesson_counters(&state.memory_dir, rt);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info,
|
||||
format!("rejected lesson: {}", name)));
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::LessonDelete { name } => {
|
||||
let _ = crate::model::memory::Memory::remove(&state.memory_dir, &name);
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
refresh_lesson_counters(&state.memory_dir, rt);
|
||||
}
|
||||
state.push_toast(Toast::new(ToastKind::Info, format!("deleted lesson: {}", name)));
|
||||
state.dirty = true;
|
||||
}
|
||||
Action::RunWorkflow { script } => {
|
||||
// Open the Workflow overlay so the user can see progress.
|
||||
state.misc.overlay = Overlay::Workflow;
|
||||
|
||||
@@ -21,29 +21,11 @@ pub fn apply_command(command: Command) -> Vec<Action> {
|
||||
Command::Quit => {
|
||||
vec![Action::QuitConfirm]
|
||||
}
|
||||
Command::LessonCreate(text) => {
|
||||
vec![Action::SystemNote {
|
||||
kind: "lesson".to_string(),
|
||||
message: format!("/lesson {}", text),
|
||||
}]
|
||||
}
|
||||
Command::LessonExport(path) => {
|
||||
vec![Action::LessonExport { path }]
|
||||
}
|
||||
Command::LessonImport(path) => {
|
||||
vec![Action::LessonImport { path }]
|
||||
}
|
||||
Command::LessonList => {
|
||||
Command::LessonInteractive => {
|
||||
vec![Action::OpenOverlay(Overlay::Learning)]
|
||||
}
|
||||
Command::LessonAccept(name) => {
|
||||
vec![Action::LessonAccept { name }]
|
||||
}
|
||||
Command::LessonReject(name) => {
|
||||
vec![Action::LessonReject { name }]
|
||||
}
|
||||
Command::Mode(mode) => {
|
||||
vec![Action::SwitchMode(mode)]
|
||||
Command::McpOpen => {
|
||||
vec![Action::OpenOverlay(Overlay::Mcp)]
|
||||
}
|
||||
Command::ClearConfirm => {
|
||||
vec![Action::OpenOverlay(Overlay::ClearConfirm)]
|
||||
|
||||
@@ -80,11 +80,6 @@ const COMMANDS: &[&str] = &[
|
||||
"/quit",
|
||||
"/clear",
|
||||
"/lesson",
|
||||
"/lesson ls",
|
||||
"/lesson export",
|
||||
"/lesson import",
|
||||
"/lesson accept",
|
||||
"/lesson reject",
|
||||
"/login",
|
||||
"/login zen",
|
||||
"/login openai",
|
||||
|
||||
Reference in New Issue
Block a user