history : historique structure par session + am history (issue #7) - migration one-shot de history.txt, filtres kind/cwd/search/failed/session, enregistrement REPL avec duree et exit

This commit is contained in:
2026-08-16 22:34:48 -04:00
parent 9f4a13eae2
commit c9e2543f50
8 changed files with 576 additions and 5 deletions
+40 -5
View File
@@ -63,6 +63,7 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
("sessions", "list agent sessions"),
("stats", "usage statistics"),
("log", "read the event journal"),
("history", "search command history"),
("search", "search the catalog"),
("info", "agent details"),
("install", "install an agent"),
@@ -139,7 +140,7 @@ impl AmCompleter {
Self {
theme: std::cell::Cell::new(crate::theme::default_theme()),
commands: vec![
"list", "status", "sessions", "stats", "log", "search", "info", "install", "uninstall", "update",
"list", "status", "sessions", "stats", "log", "history", "search", "info", "install", "uninstall", "update",
"start", "stop", "restart", "run", "doctor", "config", "completion",
"self-update", "self-uninstall", "export", "import", "shell", "theme",
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
@@ -894,21 +895,24 @@ pub fn run(app: &App) -> Result<i32> {
.with_cwd(Some(current_dir_string())),
);
let _ = crate::sessions::start_repl(app, &sid, Some(current_dir_string()));
if let Err(e) = crate::history::migrate(app) {
app.log.verbose(&format!("history migration skipped: {e:#}"));
}
print!("{}", banner(app, &session, &sid));
let result = match run_with_editor(app, &mut session) {
let result = match run_with_editor(app, &mut session, &sid) {
Ok(code) => Ok(code),
Err(e) => {
app.log.verbose(&format!(
"line editor unavailable ({e:#}); falling back to plain input"
));
run_plain(app, &mut session)
run_plain(app, &mut session, &sid)
}
};
let _ = crate::sessions::finish_repl(app, &sid);
result
}
fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
fn run_with_editor(app: &App, session: &mut ShellSession, sid: &str) -> Result<i32> {
let mut rl: rustyline::Editor<AmCompleter, rustyline::history::FileHistory> =
rustyline::Editor::new()?;
rl.set_helper(Some(AmCompleter::new(app)));
@@ -936,6 +940,7 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
let t = Instant::now();
let result = handle_input(app, session, &mut last, &trimmed);
last_duration = Some(t.elapsed());
record_history(app, sid, &trimmed, &result, t.elapsed());
// 'theme <name>' may have switched the app theme: repoint the
// completion menu at the new palette.
if let Some(h) = rl.helper_mut() {
@@ -959,8 +964,33 @@ fn run_with_editor(app: &App, session: &mut ShellSession) -> Result<i32> {
Ok(0)
}
/// Append one structured history entry for a REPL command.
fn record_history(
app: &App,
sid: &str,
line: &str,
result: &Result<bool>,
elapsed: std::time::Duration,
) {
let kind = if is_am_command(line.split_whitespace().next().unwrap_or("")) {
"am"
} else {
"shell"
};
let exit_code = if result.is_err() { Some(1) } else { None };
let _ = crate::history::record(
app,
sid,
&crate::history::HistoryEntry::now(line.to_string())
.with_kind(kind.to_string())
.with_cwd(Some(current_dir_string()))
.with_exit_code(exit_code)
.with_duration_ms(elapsed.as_millis() as u64),
);
}
/// Plain fallback when no terminal/line editor is available.
fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
fn run_plain(app: &App, session: &mut ShellSession, sid: &str) -> Result<i32> {
let started = Instant::now();
let mut last: Option<DataTable> = None;
let mut last_duration: Option<Duration> = None;
@@ -979,6 +1009,7 @@ fn run_plain(app: &App, session: &mut ShellSession) -> Result<i32> {
let t = Instant::now();
let result = handle_input(app, session, &mut last, &line);
last_duration = Some(t.elapsed());
record_history(app, sid, &line, &result, t.elapsed());
match result {
Ok(true) => break,
Ok(false) => {}
@@ -1273,6 +1304,10 @@ fn is_am_command(word: &str) -> bool {
word,
"list"
| "status"
| "sessions"
| "stats"
| "log"
| "history"
| "search"
| "info"
| "install"