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

@ -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:?}");
}

View file

@ -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());

View 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)
));
}
}

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