feat: am tip cheat sheet + readable man pages + REPL completion refresh
This commit is contained in:
+115
-12
@@ -33,7 +33,7 @@ use reedline::{
|
||||
Reedline, ReedlineEvent, ReedlineMenu, Signal, Span, Suggestion,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -103,9 +103,10 @@ const COMMAND_DESCRIPTIONS: &[(&str, &str)] = &[
|
||||
("untag", "remove a personal tag"),
|
||||
("tags", "list personal tags"),
|
||||
("profile", "manage environment profiles"),
|
||||
("man", "generate man pages"),
|
||||
("man", "readable man page in the terminal"),
|
||||
("shell", "show or switch the system shell"),
|
||||
("theme", "show or switch the color theme"),
|
||||
("tip", "cheat sheet of the most useful commands"),
|
||||
("ls", "list files and directories (Nushell-style)"),
|
||||
("dir", "list files and directories (same as ls)"),
|
||||
("cd", "change directory — Tab completes folders"),
|
||||
@@ -143,6 +144,30 @@ impl AmCompleter {
|
||||
}
|
||||
completer.agents.sort();
|
||||
completer.agents.dedup();
|
||||
|
||||
// Issue #41 follow-up: only propose aliases and groups whose target is
|
||||
// actually launchable (installed managed or external on PATH).
|
||||
let launchable: HashSet<String> = completer.agents.iter().cloned().collect();
|
||||
completer
|
||||
.aliases
|
||||
.retain(|alias| match app.catalog.resolve(alias) {
|
||||
Some(agent) => launchable.contains(&agent.name),
|
||||
None => {
|
||||
// Fallback: alias target is not a known agent name, but
|
||||
// points to a launchable binary recorded in the catalog.
|
||||
app.catalog
|
||||
.alias_target(alias)
|
||||
.map(|target| launchable.contains(target))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
});
|
||||
completer.groups.retain(|group| {
|
||||
let name = group.strip_prefix("group:").unwrap_or(group);
|
||||
app.catalog
|
||||
.group_members(name)
|
||||
.iter()
|
||||
.any(|agent| launchable.contains(&agent.name))
|
||||
});
|
||||
completer
|
||||
}
|
||||
|
||||
@@ -191,7 +216,7 @@ impl AmCompleter {
|
||||
"list", "status", "sessions", "stats", "top", "report", "projects", "timeline", "log", "logs", "history", "init", "alias", "secret", "open", "watch", "search", "info", "install", "uninstall", "update",
|
||||
"start", "stop", "restart", "run", "doctor", "config", "completion",
|
||||
"self-update", "self-uninstall", "export", "import", "shell", "theme",
|
||||
"dashboard", "favorite", "unfavorite", "note", "tag", "untag", "tags",
|
||||
"tip", "dashboard", "favorite", "unfavorite", "note", "tag", "untag", "tags",
|
||||
"profile", "man",
|
||||
"ls", "dir", "cd", "ps", "where", "get", "help", "version", "exit",
|
||||
],
|
||||
@@ -206,7 +231,7 @@ impl AmCompleter {
|
||||
"--background", "--force", "--purge", "--method", "--all",
|
||||
"--category", "--args", "--env", "--notify", "--timeout", "--yes",
|
||||
"--verbose", "--quiet", "--json", "--dry-run", "--no-color",
|
||||
"--profile", "--installed", "--tag", "--output",
|
||||
"--profile", "--installed", "--tag", "--output", "--random",
|
||||
],
|
||||
agents,
|
||||
catalog_agents,
|
||||
@@ -711,23 +736,35 @@ pub fn banner_box(
|
||||
color,
|
||||
)));
|
||||
|
||||
// am command groups.
|
||||
// am command groups: every command the REPL recognizes, grouped by
|
||||
// purpose. Keep this in sync with the completer (COMMAND_DESCRIPTIONS).
|
||||
rows.push(blank());
|
||||
rows.push(inner(if color {
|
||||
theme.hdr("Commands")
|
||||
} else {
|
||||
"Commands".to_string()
|
||||
}));
|
||||
rows.push(inner(" catalog list · status · search · info".to_string()));
|
||||
rows.push(inner(" catalog list · status · search · info · init".to_string()));
|
||||
rows.push(inner(
|
||||
" activity sessions · stats · top · report · projects · timeline · log · logs · history"
|
||||
.to_string(),
|
||||
));
|
||||
rows.push(inner(
|
||||
" lifecycle install · uninstall · update · start · stop · restart · run".to_string(),
|
||||
));
|
||||
rows.push(inner(
|
||||
" system doctor · config · completion · self-update · self-uninstall · export · import"
|
||||
" manage open · watch · dashboard · favorite · unfavorite · note · tag · untag · tags"
|
||||
.to_string(),
|
||||
));
|
||||
rows.push(inner(
|
||||
" shell ls · cd · ps · where · get · shell · theme · help · version · exit".to_string(),
|
||||
" config alias · secret · profile · config · doctor · completion · man · tip".to_string(),
|
||||
));
|
||||
rows.push(inner(
|
||||
" system self-update · self-uninstall · export · import".to_string(),
|
||||
));
|
||||
rows.push(inner(
|
||||
" shell ls · dir · cd · ps · where · get · shell · theme · help · version · exit"
|
||||
.to_string(),
|
||||
));
|
||||
|
||||
// Stats footer.
|
||||
@@ -1078,10 +1115,15 @@ fn run_with_editor(app: &App, session: &mut ShellSession, sid: &str) -> Result<i
|
||||
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: rebuild the editor
|
||||
// so the completion menu follows the new palette (the history is
|
||||
// file-backed and re-read from disk).
|
||||
if !std::ptr::eq(theme, app.theme()) {
|
||||
// Rebuild the editor when the completion pool or the palette may
|
||||
// have changed: 'theme <name>' switches the menu colors, and the
|
||||
// install/uninstall/update/alias/config/import commands mutate the
|
||||
// installed agents, aliases or groups that Tab proposes (the
|
||||
// completer snapshots them at startup, so a fresh install would
|
||||
// stay hidden until an exit/restart otherwise). The history is
|
||||
// file-backed and re-read from disk.
|
||||
let first_word = trimmed.split_whitespace().next().unwrap_or("");
|
||||
if !std::ptr::eq(theme, app.theme()) || completion_pool_mutating(first_word) {
|
||||
theme = app.theme();
|
||||
let _ = rl.sync_history();
|
||||
match build_line_editor(app, &history_path) {
|
||||
@@ -1568,6 +1610,9 @@ fn handle_line(
|
||||
"import" => Command::Import {
|
||||
file: need("file")?.into(),
|
||||
},
|
||||
"tip" => Command::Tip {
|
||||
random: flag("--random"),
|
||||
},
|
||||
other => {
|
||||
app.log
|
||||
.error(&format!("unknown command '{other}' — type 'help'"));
|
||||
@@ -1589,6 +1634,16 @@ fn is_shell_command(word: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Commands whose execution can change the agent completion pool
|
||||
/// (installed agents, aliases, groups): the Tab completer snapshots that
|
||||
/// pool at startup, so the line editor is rebuilt after one of these runs.
|
||||
fn completion_pool_mutating(first: &str) -> bool {
|
||||
matches!(
|
||||
first,
|
||||
"install" | "uninstall" | "update" | "alias" | "config" | "import" | "self-update"
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether a word is one of the am commands (everything else is executed by
|
||||
/// the system shell).
|
||||
fn is_am_command(word: &str) -> bool {
|
||||
@@ -1635,6 +1690,7 @@ fn is_am_command(word: &str) -> bool {
|
||||
| "tags"
|
||||
| "profile"
|
||||
| "man"
|
||||
| "tip"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1990,6 +2046,27 @@ mod tests {
|
||||
assert!(cands.contains(&"--dry-run".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_mutating_commands_are_detected() {
|
||||
// These commands change the agents/aliases/groups that Tab proposes:
|
||||
// the line editor must be rebuilt after them.
|
||||
for cmd in [
|
||||
"install", "uninstall", "update", "alias", "config", "import", "self-update",
|
||||
] {
|
||||
assert!(
|
||||
completion_pool_mutating(cmd),
|
||||
"{cmd} must refresh the completion pool"
|
||||
);
|
||||
}
|
||||
// Read-only and system commands must not trigger a rebuild.
|
||||
for cmd in ["start", "stop", "list", "status", "ls", "cd", "theme", "help", "exit"] {
|
||||
assert!(
|
||||
!completion_pool_mutating(cmd),
|
||||
"{cmd} must not refresh the completion pool"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completes_config_subcommands() {
|
||||
let c = completer();
|
||||
@@ -2055,6 +2132,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_commands_section_lists_every_command() {
|
||||
let session = test_session();
|
||||
let text = banner_box(
|
||||
"9.9.9",
|
||||
65,
|
||||
3,
|
||||
12,
|
||||
&session,
|
||||
"20260815_123456_abcdef",
|
||||
"/home/alice/dev/agent-manager",
|
||||
crate::theme::default_theme(),
|
||||
false,
|
||||
);
|
||||
// The Commands section starts at the "Commands" heading and runs to
|
||||
// the stats footer. Every command the REPL knows (am + shell) must
|
||||
// appear there.
|
||||
let section = text.split_once("Commands").map(|(_, r)| r).unwrap_or_default();
|
||||
for (cmd, _) in COMMAND_DESCRIPTIONS {
|
||||
assert!(
|
||||
section.contains(&format!(" {cmd} ")),
|
||||
"banner Commands section misses {cmd}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_box_is_themed_and_still_aligned() {
|
||||
let session = test_session();
|
||||
|
||||
Reference in New Issue
Block a user