Synced from monorepo
Synced from monorepo Changes: - Workspace server: report `/ready` as failed with dwell on hub connect failure - Refresh OIDC token for the Grok agent in the shell - ACP terminal output recorder - Cross-platform provider auth commands in the shell - Default `/resume` to Grok sessions with a hint for hidden external sessions - Resume sessions by title with `--resume` - Limit app-builder archive size - Data-driven tag labels for slash commands - Doctor fixes for tmux - Custom provider gateways and subprocess environment policy in the shell - `/tutorial` — opt-in onboarding tour of Grok Build - Soft and required CLI version checks in the shell - Privacy banner env overrides survive live settings updates - Add remote flag to override the image-edit model - Return profile fields from auth info even when the access token is expired - Add edit control on queued prompt rows - Keep fail-closed policy when clearing orphans with no team - Setting to disable the Ctrl+Space/F8 voice shortcut - Pass `--raw` to pw-record so Linux dictation works on older PipeWire - Validate git URLs when adding marketplace entries - Stop shipping stale tool-doc parameter and tool names - Re-point dashboard attach after `/fork` only when the parent was attached - Surface Grok Computer media-generation results as file-path chunks - Clear web background-task tray on kill and keep the task description - Show privacy upsell banner in agent view until acted on - Add tools-server client callback surface - Protect persistent global hook sources Source-Revision: 95d84f443eddcbed6cbfd6eed22e2eafe6b3939d
This commit is contained in:
parent
a5727c5960
commit
69f0ba880a
286 changed files with 22939 additions and 9624 deletions
|
|
@ -7,16 +7,17 @@ use crate::slash::command::{
|
|||
AppCtx, ArgItem, CommandExecCtx, CommandResult, DoctorRequest, SlashCommand,
|
||||
};
|
||||
|
||||
const USAGE: &str = "Usage: /doctor [fix [ssh-wrap]]";
|
||||
const USAGE: &str =
|
||||
"Usage: /doctor [fix [ssh-wrap|tmux-clipboard|dcs-passthrough|tmux-extended-keys]]";
|
||||
|
||||
pub struct DoctorCommand;
|
||||
|
||||
impl DoctorCommand {
|
||||
pub(crate) fn report(
|
||||
pub(crate) fn report_for_terminal(
|
||||
terminal: &crate::terminal::TerminalContext,
|
||||
screen_mode: crate::app::ScreenMode,
|
||||
runtime: crate::diagnostics::TuiRuntimeRequest<'_>,
|
||||
) -> crate::diagnostics::DiagnosticReport {
|
||||
let terminal = crate::terminal::terminal_context();
|
||||
let query = crate::diagnostics::probes::LiveTmuxProbe;
|
||||
let snapshot = crate::diagnostics::probes::collect_doctor_tui(
|
||||
terminal,
|
||||
|
|
@ -54,7 +55,7 @@ impl SlashCommand for DoctorCommand {
|
|||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"/doctor [fix [ssh-wrap]]"
|
||||
"/doctor [fix [FIX]]"
|
||||
}
|
||||
|
||||
fn takes_args(&self) -> bool {
|
||||
|
|
@ -62,30 +63,38 @@ impl SlashCommand for DoctorCommand {
|
|||
}
|
||||
|
||||
fn arg_placeholder(&self) -> Option<&str> {
|
||||
Some("[fix [ssh-wrap]]")
|
||||
Some("[fix [FIX]]")
|
||||
}
|
||||
|
||||
fn suggest_args(&self, _ctx: &AppCtx, args_query: &str) -> Option<Vec<ArgItem>> {
|
||||
let query = args_query.trim();
|
||||
if query.is_empty() || matches!(query, "fix ssh-wrap" | "fix terminal.ssh-wrap") {
|
||||
if query.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let item = if query == "fix" || query.starts_with("fix ") {
|
||||
ArgItem {
|
||||
display: "ssh-wrap".into(),
|
||||
match_text: "fix ssh-wrap terminal.ssh-wrap".into(),
|
||||
insert_text: "fix ssh-wrap".into(),
|
||||
description: "Set up SSH wrapping on this computer".into(),
|
||||
if query == "fix" || query.starts_with("fix ") {
|
||||
let value = query.strip_prefix("fix").unwrap_or_default().trim();
|
||||
if !value.is_empty() && crate::diagnostics::resolve_fix_id(value).is_ok() {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
ArgItem {
|
||||
display: "fix".into(),
|
||||
match_text: "fix".into(),
|
||||
insert_text: "fix".into(),
|
||||
description: "Show automatic fixes available here".into(),
|
||||
}
|
||||
};
|
||||
Some(vec![item])
|
||||
let items = crate::diagnostics::automatic_fix_choices()
|
||||
.filter(|(id, handle, _)| {
|
||||
value.is_empty() || handle.contains(value) || id.to_string().starts_with(value)
|
||||
})
|
||||
.map(|(id, handle, label)| ArgItem {
|
||||
display: handle.into(),
|
||||
match_text: format!("fix {handle} {id}"),
|
||||
insert_text: format!("fix {handle}"),
|
||||
description: label.into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
return (!items.is_empty()).then_some(items);
|
||||
}
|
||||
Some(vec![ArgItem {
|
||||
display: "fix".into(),
|
||||
match_text: "fix".into(),
|
||||
insert_text: "fix".into(),
|
||||
description: "Show automatic fixes available here".into(),
|
||||
}])
|
||||
}
|
||||
|
||||
fn session_scoped(&self) -> bool {
|
||||
|
|
@ -136,10 +145,31 @@ mod tests {
|
|||
run("fix"),
|
||||
CommandResult::Doctor(DoctorRequest::ListFixes)
|
||||
));
|
||||
for value in ["ssh-wrap", "terminal.ssh-wrap"] {
|
||||
for (value, id) in [
|
||||
("ssh-wrap", crate::diagnostics::SSH_WRAP_ID),
|
||||
("terminal.ssh-wrap", crate::diagnostics::SSH_WRAP_ID),
|
||||
("tmux-clipboard", crate::diagnostics::TMUX_CLIPBOARD_ID),
|
||||
(
|
||||
"terminal.tmux-clipboard",
|
||||
crate::diagnostics::TMUX_CLIPBOARD_ID,
|
||||
),
|
||||
("dcs-passthrough", crate::diagnostics::DCS_PASSTHROUGH_ID),
|
||||
(
|
||||
"terminal.dcs-passthrough",
|
||||
crate::diagnostics::DCS_PASSTHROUGH_ID,
|
||||
),
|
||||
(
|
||||
"tmux-extended-keys",
|
||||
crate::diagnostics::TMUX_EXTENDED_KEYS_ID,
|
||||
),
|
||||
(
|
||||
"terminal.tmux-extended-keys",
|
||||
crate::diagnostics::TMUX_EXTENDED_KEYS_ID,
|
||||
),
|
||||
] {
|
||||
assert!(matches!(
|
||||
run(&format!("fix {value}")),
|
||||
CommandResult::Doctor(DoctorRequest::Fix(crate::diagnostics::SSH_WRAP_ID))
|
||||
CommandResult::Doctor(DoctorRequest::Fix(parsed)) if parsed == id
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -180,6 +210,12 @@ mod tests {
|
|||
" fix ssh-wrap ",
|
||||
"fix terminal.ssh-wrap",
|
||||
" fix terminal.ssh-wrap ",
|
||||
"fix tmux-clipboard",
|
||||
"fix terminal.tmux-clipboard",
|
||||
"fix dcs-passthrough",
|
||||
"fix terminal.dcs-passthrough",
|
||||
"fix tmux-extended-keys",
|
||||
"fix terminal.tmux-extended-keys",
|
||||
] {
|
||||
assert!(command.suggest_args(&context, query).is_none(), "{query:?}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ pub mod timeline;
|
|||
pub mod timestamps;
|
||||
pub mod toggle_mouse_reporting;
|
||||
pub mod transcript;
|
||||
pub mod tutorial;
|
||||
pub mod usage;
|
||||
pub mod view_plan;
|
||||
pub mod vim_mode;
|
||||
|
|
@ -140,6 +141,7 @@ pub fn builtin_commands() -> Vec<Arc<dyn SlashCommand>> {
|
|||
Arc::new(queue::QueueCommand),
|
||||
Arc::new(tasks::TasksCommand),
|
||||
Arc::new(release_notes::ReleaseNotesCommand),
|
||||
Arc::new(tutorial::TutorialCommand),
|
||||
Arc::new(config_agents::ConfigAgentsCommand),
|
||||
Arc::new(personas::PersonasCommand),
|
||||
// Hidden easter egg: never listed, runs on bare `/gboom`.
|
||||
|
|
@ -296,6 +298,7 @@ mod tests {
|
|||
"model",
|
||||
"multiline",
|
||||
"new",
|
||||
"onboarding",
|
||||
"personas",
|
||||
"plan",
|
||||
"plan-view",
|
||||
|
|
@ -328,7 +331,9 @@ mod tests {
|
|||
"timestamps",
|
||||
"title",
|
||||
"toggle-mouse-reporting",
|
||||
"tour",
|
||||
"transcript",
|
||||
"tutorial",
|
||||
"t",
|
||||
"usage",
|
||||
"view-plan",
|
||||
|
|
@ -361,7 +366,7 @@ mod tests {
|
|||
let quit_cmd = reg.get("quit").unwrap();
|
||||
assert_eq!(exit_cmd.name(), quit_cmd.name());
|
||||
let doctor = reg.get("doctor").unwrap();
|
||||
assert_eq!(doctor.usage(), "/doctor [fix [ssh-wrap]]");
|
||||
assert_eq!(doctor.usage(), "/doctor [fix [FIX]]");
|
||||
for alias in ["terminal-setup", "terminal-check", "terminal-info"] {
|
||||
assert_eq!(reg.get(alias).unwrap().name(), doctor.name());
|
||||
assert_eq!(reg.get(alias).unwrap().usage(), doctor.usage());
|
||||
|
|
|
|||
81
crates/codegen/xai-grok-pager/src/slash/commands/tutorial.rs
Normal file
81
crates/codegen/xai-grok-pager/src/slash/commands/tutorial.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//! `/tutorial` -- open the onboarding tutorial overlay.
|
||||
//!
|
||||
//! Purely opt-in: this command (also listed in the command palette) is the
|
||||
//! only way the tutorial opens — it never auto-shows.
|
||||
|
||||
use crate::app::actions::Action;
|
||||
use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand};
|
||||
|
||||
/// Open the onboarding tutorial.
|
||||
pub struct TutorialCommand;
|
||||
|
||||
impl SlashCommand for TutorialCommand {
|
||||
fn name(&self) -> &str {
|
||||
"tutorial"
|
||||
}
|
||||
|
||||
fn aliases(&self) -> &[&str] {
|
||||
&["tour", "onboarding"]
|
||||
}
|
||||
|
||||
fn description(&self) -> &str {
|
||||
"Quick tips to get the most out of Grok Build"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"/tutorial"
|
||||
}
|
||||
|
||||
/// The tutorial overlay is full-TUI chrome; minimal mode has no modal
|
||||
/// host, so the overlay would consume input invisibly. Gated off.
|
||||
fn available_in_minimal(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &mut CommandExecCtx, _args: &str) -> CommandResult {
|
||||
CommandResult::Action(Action::OpenTutorial)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::acp::model_state::ModelState;
|
||||
use crate::app::bundle::BundleState;
|
||||
use crate::settings::PagerLocalSnapshot;
|
||||
|
||||
static DEFAULT_BUNDLE_STATE: BundleState = BundleState {
|
||||
has_cache: false,
|
||||
version: String::new(),
|
||||
personas: Vec::new(),
|
||||
roles: Vec::new(),
|
||||
agents: Vec::new(),
|
||||
skills: Vec::new(),
|
||||
persona_details: Vec::new(),
|
||||
role_details: Vec::new(),
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn not_available_in_minimal() {
|
||||
// Minimal mode can't render the overlay; the command must be gated
|
||||
// off or the input intercept would freeze the session invisibly.
|
||||
assert!(!TutorialCommand.available_in_minimal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatches_open_tutorial() {
|
||||
let models = ModelState::default();
|
||||
let mut ctx = CommandExecCtx {
|
||||
models: &models,
|
||||
session_id: None,
|
||||
bundle_state: &DEFAULT_BUNDLE_STATE,
|
||||
screen_mode: crate::app::ScreenMode::Fullscreen,
|
||||
billing_surface_visible: true,
|
||||
pager_state: PagerLocalSnapshot::default(),
|
||||
};
|
||||
assert!(matches!(
|
||||
TutorialCommand.run(&mut ctx, ""),
|
||||
CommandResult::Action(Action::OpenTutorial)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -46,6 +46,9 @@ pub struct SuggestionRow {
|
|||
pub insert_text: String,
|
||||
/// Character positions for fuzzy match highlighting.
|
||||
pub indices: Vec<u32>,
|
||||
/// Free-form bracketed tag (e.g. "new") from the resolved tag map. `None`
|
||||
/// for untagged command rows and always `None` for arg rows.
|
||||
pub tag: Option<String>,
|
||||
}
|
||||
|
||||
impl SuggestionRow {
|
||||
|
|
@ -59,6 +62,7 @@ impl SuggestionRow {
|
|||
description: trigger.description.clone(),
|
||||
insert_text,
|
||||
indices: Vec::new(),
|
||||
tag: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +72,7 @@ impl SuggestionRow {
|
|||
description: item.description.clone(),
|
||||
insert_text: item.insert_text.clone(),
|
||||
indices: Vec::new(),
|
||||
tag: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -272,6 +277,11 @@ pub struct SlashController {
|
|||
/// defaults to an isolated in-memory store (no disk I/O) for tests and any
|
||||
/// surface that has not been wired up.
|
||||
mru: std::rc::Rc<std::cell::RefCell<mru::SlashMru>>,
|
||||
/// Resolved per-command tag map (canonical name → free-form tag). Owned by
|
||||
/// `AppView` and injected via [`Self::set_command_tags`] so agent prompts
|
||||
/// and the dashboard share one map; defaults to empty for tests and any
|
||||
/// surface that has not been wired up.
|
||||
command_tags: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<String, String>>>,
|
||||
}
|
||||
|
||||
impl SlashController {
|
||||
|
|
@ -300,6 +310,9 @@ impl SlashController {
|
|||
workflows_available: false,
|
||||
screen_mode: crate::app::ScreenMode::Fullscreen,
|
||||
mru,
|
||||
command_tags: std::rc::Rc::new(std::cell::RefCell::new(
|
||||
std::collections::HashMap::new(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -309,6 +322,16 @@ impl SlashController {
|
|||
self.mru = mru;
|
||||
}
|
||||
|
||||
/// Replace the per-command tag map with a shared one. Used by `AppView` to
|
||||
/// inject the resolved (remote + local) tag map into agent prompts and the
|
||||
/// dashboard dispatch input.
|
||||
pub fn set_command_tags(
|
||||
&mut self,
|
||||
command_tags: std::rc::Rc<std::cell::RefCell<std::collections::HashMap<String, String>>>,
|
||||
) {
|
||||
self.command_tags = command_tags;
|
||||
}
|
||||
|
||||
/// Gate `/announcements` on presence of session announcements (critical or promo).
|
||||
pub fn set_has_session_announcements(&mut self, has: bool) {
|
||||
self.has_session_announcements = has;
|
||||
|
|
@ -837,6 +860,9 @@ impl SlashController {
|
|||
// No cap here -- the dropdown renderer handles scrolling.
|
||||
let mut seen = HashSet::new();
|
||||
let mut rows = Vec::new();
|
||||
// Retain canonicals so tags are set in a second pass, keeping the
|
||||
// `takes_args_now` command callback outside any tag-map borrow.
|
||||
let mut canonicals: Vec<&str> = Vec::new();
|
||||
for (i, trigger) in triggers.iter().enumerate() {
|
||||
if !visible_indices.contains(&i) {
|
||||
continue;
|
||||
|
|
@ -848,8 +874,20 @@ impl SlashController {
|
|||
.map(|cmd| cmd.takes_args_now(&ctx))
|
||||
.unwrap_or(false);
|
||||
rows.push(SuggestionRow::from_command(trigger, takes));
|
||||
canonicals.push(trigger.canonical.as_str());
|
||||
}
|
||||
}
|
||||
// Tag from the data map in one scoped borrow; key off canonical
|
||||
// (never the alias/display).
|
||||
{
|
||||
let command_tags = self.command_tags.borrow();
|
||||
for (row, canonical) in rows.iter_mut().zip(canonicals.iter()) {
|
||||
row.tag = command_tags.get(*canonical).cloned();
|
||||
}
|
||||
}
|
||||
// Surface tagged commands (curated new/beta) at the top of the bare "/" menu;
|
||||
// stable so registry order is preserved within the tagged and untagged groups.
|
||||
rows.sort_by_key(|r| r.tag.is_none());
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
|
@ -929,6 +967,14 @@ impl SlashController {
|
|||
.iter()
|
||||
.map(|t| (t.canonical.clone(), t.source))
|
||||
.collect();
|
||||
// Tag each candidate from the data map (canonical key); one shared
|
||||
// borrow, dropped before the scoring borrow below.
|
||||
{
|
||||
let command_tags = self.command_tags.borrow();
|
||||
for (row, (canonical, _)) in rows.iter_mut().zip(sort_meta.iter()) {
|
||||
row.tag = command_tags.get(canonical.as_str()).cloned();
|
||||
}
|
||||
}
|
||||
// Resolve all recency scores under a single borrow (one keystroke =
|
||||
// one borrow, not one per candidate).
|
||||
let mru_scores: Vec<u64> = {
|
||||
|
|
@ -2157,6 +2203,7 @@ mod tests {
|
|||
description: String::new(),
|
||||
insert_text: "/Privacy ".to_string(),
|
||||
indices: Vec::new(),
|
||||
tag: None,
|
||||
};
|
||||
// Without smart-case, starts_with("p") fails on "Privacy" and ghost disappears
|
||||
// while the dropdown still highlights the row via CaseMatching::Smart.
|
||||
|
|
@ -2398,6 +2445,116 @@ mod tests {
|
|||
assert_eq!(ctrl.mru_last_used("", "exit"), 0);
|
||||
}
|
||||
|
||||
/// Inject a per-command tag map into a controller (test seam).
|
||||
fn set_tags(ctrl: &mut SlashController, entries: &[(&str, &str)]) {
|
||||
let map: std::collections::HashMap<String, String> = entries
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
ctrl.set_command_tags(std::rc::Rc::new(std::cell::RefCell::new(map)));
|
||||
}
|
||||
|
||||
/// Tag of the row whose display equals `name` in the current snapshot.
|
||||
fn row_tag(snap: &SlashSnapshot, name: &str) -> Option<String> {
|
||||
snap.matches
|
||||
.iter()
|
||||
.find(|r| r.display == name)
|
||||
.unwrap_or_else(|| panic!("row {name} missing"))
|
||||
.tag
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// A command with a tag-map entry (keyed by canonical) carries that tag in
|
||||
/// both the empty-query and the typed-query branches; one without has `None`.
|
||||
#[test]
|
||||
fn command_row_tag_from_map_keyed_by_canonical() {
|
||||
let mut ctrl = tie_controller(&["alpha", "bravo"], &[]);
|
||||
set_tags(&mut ctrl, &[("alpha", "new")]);
|
||||
let state = SlashState::default();
|
||||
let models = ModelState::default();
|
||||
|
||||
ctrl.refresh(&state, "/", 1, &models);
|
||||
let snap = state.snapshot();
|
||||
assert_eq!(row_tag(&snap, "/alpha"), Some("new".to_string()));
|
||||
assert_eq!(row_tag(&snap, "/bravo"), None);
|
||||
|
||||
// Typed-query branch tags the same way.
|
||||
ctrl.refresh(&state, "/al", 3, &models);
|
||||
let typed = state.snapshot();
|
||||
assert_eq!(
|
||||
row_tag(&typed, "/alpha"),
|
||||
Some("new".to_string()),
|
||||
"typed-query rows carry tags too"
|
||||
);
|
||||
}
|
||||
|
||||
/// The bare "/" picker surfaces tagged commands first, preserving registry
|
||||
/// order within the tagged and untagged groups (stable; not alphabetized).
|
||||
#[test]
|
||||
fn empty_query_sorts_tagged_commands_first_stably() {
|
||||
// Registry order: alpha, bravo, charlie, delta. Tag the 2nd and 4th.
|
||||
let mut ctrl = tie_controller(&["alpha", "bravo", "charlie", "delta"], &[]);
|
||||
set_tags(&mut ctrl, &[("bravo", "new"), ("delta", "beta")]);
|
||||
let state = SlashState::default();
|
||||
let models = ModelState::default();
|
||||
|
||||
ctrl.refresh(&state, "/", 1, &models);
|
||||
let order: Vec<String> = state
|
||||
.snapshot()
|
||||
.matches
|
||||
.iter()
|
||||
.map(|r| r.display.clone())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
order,
|
||||
vec!["/bravo", "/delta", "/alpha", "/charlie"],
|
||||
"tagged-first, stable registry order within each group"
|
||||
);
|
||||
}
|
||||
|
||||
/// ACP commands — including bundled skills that arrive as skill-shaped ACP
|
||||
/// commands — tag from the map the same way as builtins.
|
||||
#[test]
|
||||
fn acp_and_skill_commands_tag_from_map() {
|
||||
let mut ctrl = SlashController::new(
|
||||
CommandRegistry::new(vec![
|
||||
Arc::new(TieCmd("builtin-cmd")) as Arc<dyn SlashCommand>
|
||||
]),
|
||||
std::path::PathBuf::from("."),
|
||||
);
|
||||
// A skill arrives as an ACP command carrying skill meta (scope + path).
|
||||
let skill_meta = serde_json::json!({
|
||||
"scope": "local",
|
||||
"path": "/home/user/.grok/skills/skill-cmd/SKILL.md",
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("skill meta is an object");
|
||||
let skill_cmd =
|
||||
agent_client_protocol::AvailableCommand::new("skill-cmd".to_string(), String::new())
|
||||
.meta(skill_meta);
|
||||
ctrl.registry_mut().set_acp_commands(&[
|
||||
agent_client_protocol::AvailableCommand::new("acp-command".to_string(), String::new()),
|
||||
skill_cmd,
|
||||
]);
|
||||
assert!(
|
||||
ctrl.registry()
|
||||
.get("skill-cmd")
|
||||
.expect("skill command present")
|
||||
.is_skill(),
|
||||
"skill-shaped ACP command must classify as a skill"
|
||||
);
|
||||
|
||||
set_tags(&mut ctrl, &[("acp-command", "beta"), ("skill-cmd", "new")]);
|
||||
let state = SlashState::default();
|
||||
let models = ModelState::default();
|
||||
ctrl.refresh(&state, "/", 1, &models);
|
||||
let snap = state.snapshot();
|
||||
assert_eq!(row_tag(&snap, "/acp-command"), Some("beta".to_string()));
|
||||
assert_eq!(row_tag(&snap, "/skill-cmd"), Some("new".to_string()));
|
||||
assert_eq!(row_tag(&snap, "/builtin-cmd"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flat_mru_boosts_recent_command_regardless_of_typed_prefix() {
|
||||
// Flat schema (hermetic): using `plan` recently boosts it even when
|
||||
|
|
@ -2746,6 +2903,17 @@ mod tests {
|
|||
("/doctor fix s", "fix ssh-wrap", vec![0]),
|
||||
("/doctor fix ssh", "fix ssh-wrap", vec![0, 1, 2]),
|
||||
("/doctor fix terminal.s", "fix ssh-wrap", vec![0]),
|
||||
(
|
||||
"/doctor fix tmux-c",
|
||||
"fix tmux-clipboard",
|
||||
vec![0, 1, 2, 3, 4, 5],
|
||||
),
|
||||
("/doctor fix dcs", "fix dcs-passthrough", vec![0, 1, 2]),
|
||||
(
|
||||
"/doctor fix tmux-e",
|
||||
"fix tmux-extended-keys",
|
||||
vec![0, 1, 2, 3, 4, 5],
|
||||
),
|
||||
("/terminal-setup f", "fix", vec![0]),
|
||||
("/terminal-setup fix s", "fix ssh-wrap", vec![0]),
|
||||
] {
|
||||
|
|
@ -2759,6 +2927,12 @@ mod tests {
|
|||
for text in [
|
||||
"/doctor fix ssh-wrap",
|
||||
"/doctor fix terminal.ssh-wrap",
|
||||
"/doctor fix tmux-clipboard",
|
||||
"/doctor fix terminal.tmux-clipboard",
|
||||
"/doctor fix dcs-passthrough",
|
||||
"/doctor fix terminal.dcs-passthrough",
|
||||
"/doctor fix tmux-extended-keys",
|
||||
"/doctor fix terminal.tmux-extended-keys",
|
||||
"/terminal-setup fix ssh-wrap",
|
||||
"/terminal-setup fix terminal.ssh-wrap",
|
||||
] {
|
||||
|
|
|
|||
Loading…
Reference in a new issue