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:
grokkybara[bot] 2026-07-23 17:12:33 +00:00
commit 69f0ba880a
286 changed files with 22939 additions and 9624 deletions

View file

@ -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",
] {