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
|
|
@ -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