Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ pub fn root_help() -> String {
format!(
"\
{header}
EXAMPLES
{bin} command palette (TTY)
{bin} claude launch Claude Code
{bin} claude --model auto preset picks the model
{bin} auth login sign in
{bin} models use owner/model persist default

USAGE
{bin} Open the interactive TUI (TTY)
{bin} <command> [flags]
Expand Down Expand Up @@ -490,7 +497,7 @@ mod tests {
assert!(out.contains("ar <command>"), "{out}");
assert!(out.contains("ar auth login"), "{out}");
assert!(out.contains("Sign in if needed"), "{out}");
for heading in ["CORE COMMANDS", "LAUNCH"] {
for heading in ["EXAMPLES", "CORE COMMANDS", "LAUNCH"] {
assert!(out.contains(heading), "missing {heading} in:\n{out}");
}
assert!(!out.contains("npx @anyr/cli"), "{out}");
Expand Down
69 changes: 66 additions & 3 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,16 +287,52 @@ pub fn format_models_list(
let mut out_lines = Vec::new();
if let Some(auto) = &auto_id {
out_lines.push(format!(
"auto -> {auto} ({})",
"PINNED {auto} ← auto · {}",
pinned_preset.unwrap_or("preset")
));
}
out_lines.push(String::new());
}
let id_w = models
.iter()
.map(|m| m.id.chars().count())
.max()
.unwrap_or(2)
.max(2);
out_lines.push(format!(
"{:<id_w$} {:<12} {:<6} PIN",
"ID",
"OWNER",
"CTX",
id_w = id_w
));
for model in models {
out_lines.push(format!(" {}", model.id));
let owner = model.owned_by.as_deref().unwrap_or("—");
let ctx = fmt_context_length(model.context_length);
let pin = if pinned_ids.iter().any(|id| id == &model.id) {
"●"
} else {
""
};
out_lines.push(format!(
"{:<id_w$} {:<12} {:<6} {pin}",
model.id,
owner,
ctx,
id_w = id_w
));
}
(format!("{}\n", out_lines.join("\n")), String::new())
}

fn fmt_context_length(n: Option<i64>) -> String {
match n {
Some(n) if n >= 1_000_000 => format!("{}m", n / 1_000_000),
Some(n) if n >= 1_000 => format!("{}k", n / 1_000),
Some(n) => n.to_string(),
None => "—".into(),
}
}

pub fn fetch_credits(base_url: &str, api_key: &str) -> Result<serde_json::Value, String> {
let url = join_api(base_url, "/v1/credits");
let (status, body) = http_get(&url, Some(api_key))?;
Expand Down Expand Up @@ -615,6 +651,33 @@ mod tests {
assert_eq!(me.display_label(), "duyet · a@b.co");
}

#[test]
fn format_models_list_is_a_table() {
let models = vec![
CatalogModel {
id: "anthropic/claude-sonnet-4.6".into(),
name: Some("Claude Sonnet 4.6".into()),
owned_by: Some("anthropic".into()),
context_length: Some(200_000),
},
CatalogModel {
id: "openai/gpt-5.4-mini".into(),
name: None,
owned_by: Some("openai".into()),
context_length: Some(128_000),
},
];
let pinned = vec!["anthropic/claude-sonnet-4.6".into()];
let (out, _) = format_models_list(&models, &pinned, Some("@preset/coding-stack"), false);
assert!(out.contains("PINNED anthropic/claude-sonnet-4.6"), "{out}");
assert!(out.contains("ID"), "{out}");
assert!(out.contains("OWNER"), "{out}");
assert!(out.contains("200k"), "{out}");
assert!(out.contains("128k"), "{out}");
assert!(out.contains('●'), "{out}");
assert!(!out.contains("auto ->"), "{out}");
}

#[test]
fn parse_models_reads_context_and_most_used_is_first() {
let models = parse_models_body(
Expand Down
10 changes: 6 additions & 4 deletions src/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ pub const MAGENTA: &str = "\x1b[38;2;187;154;247m";
pub const BLUE: &str = "\x1b[38;2;122;162;247m";
pub const TEAL: &str = "\x1b[38;2;58;149;171m";
pub const SUCCESS: &str = "\x1b[38;2;158;206;106m";
pub const ORANGE: &str = "\x1b[38;2;255;158;100m";
/// Site orange #F6821F — brand accent (help, links, TUI).
pub const BRAND: &str = "\x1b[38;2;246;130;31m";
pub const ORANGE: &str = "\x1b[38;2;246;130;31m";
pub const YELLOW: &str = "\x1b[38;2;224;175;104m";
pub const DANGER: &str = "\x1b[38;2;247;118;142m";
pub const WHITE: &str = "\x1b[38;2;225;225;225m";
Expand Down Expand Up @@ -41,7 +43,7 @@ pub fn dim(text: &str) -> String {
}

pub fn accent(text: &str) -> String {
paint(MAGENTA, text)
paint(BRAND, text)
}

pub fn ok(text: &str) -> String {
Expand Down Expand Up @@ -301,7 +303,7 @@ pub fn link(url: &str) -> String {
if !color_enabled() {
return url.to_string();
}
format!("\x1b]8;;{url}\x1b\\{MAGENTA}{url}{RESET}\x1b]8;;\x1b\\")
format!("\x1b]8;;{url}\x1b\\{BRAND}{url}{RESET}\x1b]8;;\x1b\\")
}

pub fn tool_color(tool: &str) -> &'static str {
Expand All @@ -312,7 +314,7 @@ pub fn tool_color(tool: &str) -> &'static str {
"opencode" => BLUE,
"pi" => TEAL,
"pool" | "poolside" => MAGENTA,
_ => MAGENTA,
_ => BRAND,
}
}

Expand Down
87 changes: 27 additions & 60 deletions src/tui/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -670,42 +670,9 @@ fn render_header(frame: &mut Frame, area: Rect, title: &str, header: &[String])
frame.render_widget(Paragraph::new(lines), area);
}

/// Icon for a launcher/picker row, derived from its label.
pub fn item_icon(label: &str) -> &'static str {
let l = label.to_ascii_lowercase();
let stem = l.trim_end_matches('…').trim();
if l.contains("onboard") {
"📋 "
} else if label.starts_with("Launch")
|| matches!(
stem,
"claude" | "codex" | "grok" | "opencode" | "pi" | "pool" | "agent"
)
|| l.contains("claude")
|| l.contains("codex")
{
"⚡ "
} else if l.contains("config") || l.contains("settings") {
"⚙ "
} else if l.contains("switch") || l.contains("account") {
"⇄ "
} else if l.contains("credit") {
"¤ "
} else if l.contains("logout") || l.contains("log out") {
"🚪 "
} else if l.contains("login") || l.contains("sign in") || stem == "key" {
"🔑 "
} else if l.contains("quit") || l.contains("done") {
"✕ "
} else if l.contains("install") {
"⬇ "
} else if l.contains("model") {
"◆ "
} else if l.contains("exacto") || l.contains("1m") || stem == "tools" {
"◇ "
} else {
"· "
}
/// Row prefix. Spotlight chrome uses the › marker only — no emoji.
pub fn item_icon(_label: &str) -> &'static str {
""
}

fn mark_line_width() -> usize {
Expand Down Expand Up @@ -1103,29 +1070,29 @@ mod tests {

#[test]
fn icons_cover_launcher_actions() {
for (label, icon) in [
("Launch claude", "⚡"),
("Config", "⚙"),
("Settings", "⚙"),
("Switch model", "⇄"),
("Credits", "¤"),
("Login / sign in", "🔑"),
("Log out", "🚪"),
("Agent onboard prompt…", "📋"),
("Quit", "✕"),
("claude", "⚡"),
("grok", "⚡"),
("opencode", "⚡"),
("pi", "⚡"),
("model…", "◆"),
("key…", "🔑"),
("exacto", "◇"),
("tools", "◇"),
("1M ctx", "◇"),
("install…", "⬇"),
("agent…", "⚡"),
for label in [
"Launch claude",
"Config",
"Settings",
"Switch model",
"Credits",
"Login / sign in",
"Log out",
"Agent onboard prompt…",
"Quit",
"claude",
"grok",
"opencode",
"pi",
"model…",
"key…",
"exacto",
"tools",
"1M ctx",
"install…",
"agent…",
] {
assert_eq!(item_icon(label).trim(), icon, "icon for {label}");
assert_eq!(item_icon(label), "", "spotlight chrome has no emoji icons");
}
}

Expand Down Expand Up @@ -1166,8 +1133,8 @@ mod tests {
] {
assert!(frame.contains(line), "missing {line} in:\n{frame}");
}
assert!(frame.contains("⚡"), "row icons missing:\n{frame}");
assert!(frame.contains(""), "{frame}");
assert!(!frame.contains("⚡"), "emoji icons removed:\n{frame}");
assert!(frame.contains("claude"), "{frame}");
assert!(frame.contains("LAUNCH"), "{frame}");
assert!(frame.contains("CONFIGURE"), "{frame}");
assert!(frame.contains('❯'), "{frame}");
Expand Down
6 changes: 3 additions & 3 deletions tests/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ fn help_lists_login_claude_account_and_spawn_targets() {
!stdout.contains("npx @anyr/cli"),
"native anyr --help must not tell people to type npx, got:\n{stdout}"
);
for heading in ["CORE COMMANDS", "LAUNCH"] {
for heading in ["EXAMPLES", "CORE COMMANDS", "LAUNCH"] {
assert!(
stdout.contains(heading),
"help should group commands under {heading}, got:\n{stdout}"
Expand Down Expand Up @@ -1597,8 +1597,8 @@ agents:
"agent rows must show model · account · key:\n{stdout}"
);
assert!(
stdout.contains("⚡") || stdout.contains("◆"),
"row icons missing:\n{stdout}"
stdout.contains('◆') || stdout.contains('❯'),
"selection marker missing:\n{stdout}"
);
assert!(
stdout.contains('❯'),
Expand Down
Loading