Synced from monorepo

Changes:
- Non-blocking coding-data sharing upsell banner
- Consolidate remediation in Doctor
- Auto mode defers fail-closed gate asks to the classifier
- Coalesce marketplace list fetches
- Allow removing a marketplace source by name
- Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal)
- Label failed workspace RPCs with error_kind
- Drop redundant explicit tonic/prost deps from xai-grok-shell
- Report real exit codes for completed background shells
- Narrow the date-rollover reminder to date-bearing templates
- Wire toolOverrides through the session and agent
- Security: Bash(git:*) allowlist matches whole command chain by prefix
- Split prompt-trigger telemetry and record classifier provenance
- Raise connectors-manager timeout to 60s
- Auto classifier honors recorded approvals for repeat actions
- Apply doctor fixes in the TUI
- Auto-mode classifier timeouts prompt instead of silently denying
- Scope subagent completion drains to the owning session
- Add the toolOverrides wire types
- Set client_identifier=grok-agent-sdk
- Accept both spellings of the workspace-teleport kill switch
- Persist one-shot occurrence journal
- Stop turns that poll the exact same tool call 16x in a row
- Copy compaction checkpoint files when forking sessions
- Auto-focus permission prompt from scrollback
- Esc cancels the running turn in non-vim and minimal modes
- List Ctrl+Z undo and redo in keyboard shortcuts
- Out-of-process macOS mic capture
- Show active auth mode on session-info
- Install the npm binary under $GROK_HOME
- Remove hover/click dead zones between dashboard items
- Route startup warnings to doctor
- Document [feedback.user] author identity config
- Extend bang command timeout
- Close combine-queued edit-hold race
- Integrate relocation recovery
- Expose privacy notice rollout flag
- Break harness discovery ref cycle so connections can idle-evict
- Shift/Alt+Enter inserts newline when editing a queued prompt
- Gate project Claude permissions on folder trust
- Echo response.create.event_id on response.created
- Toast when session creation fails from disk full
- Add shared test process lifecycle
- Enable dynamic workflows by default
- Add relocation transaction state machine
- Add shared test sandbox
- Surface auth failures on model-switch compact
- Persist durable scheduler expiry
- Confirm before removing extensions-modal items
- Re-run compact and prompt after login when compact hit expired auth
- Recap sends hosted tools under backend search
This commit is contained in:
grokkybara[bot] 2026-07-22 19:18:53 +01:00
commit a5727c5960
482 changed files with 37627 additions and 13402 deletions

View file

@ -28,6 +28,13 @@ pub struct ScheduledTaskPreview {
pub tag: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DoctorRequest {
Report,
ListFixes,
Fix(crate::diagnostics::DiagnosticId),
}
/// Result of running a slash command.
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
@ -38,6 +45,8 @@ pub enum CommandResult {
/// Command handled but was a no-op (e.g., model already selected).
/// Included for TUI parity. Dispatch treats it identically to Handled.
HandledNoOp,
/// Build or act on TUI doctor state from live app/session inputs.
Doctor(DoctorRequest),
/// Command failed with an error message.
Error(String),
/// Command produced a user-visible message.

View file

@ -3,10 +3,43 @@
//! Runs the shared TUI probe and diagnostics path, including live runtime
//! evidence that the standalone command cannot observe.
use crate::slash::command::{CommandExecCtx, CommandResult, SlashCommand};
use crate::slash::command::{
AppCtx, ArgItem, CommandExecCtx, CommandResult, DoctorRequest, SlashCommand,
};
const USAGE: &str = "Usage: /doctor [fix [ssh-wrap]]";
pub struct DoctorCommand;
impl DoctorCommand {
pub(crate) fn report(
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,
crate::diagnostics::probes::TuiProbeEvidence {
fullscreen_active: screen_mode.is_fullscreen(),
kitty_flags_pushed: crate::app::kitty_flags_pushed(),
xtversion: crate::terminal::xtversion::detected(),
},
&query,
);
let runtime_findings = crate::diagnostics::collect_tui_runtime_findings(
&snapshot.common,
runtime.notification_method,
runtime.notification_protocol,
runtime.notification_condition,
runtime.workspace,
);
let mut report = crate::diagnostics::view(snapshot.into());
crate::diagnostics::merge_tui_runtime_findings(&mut report, runtime_findings);
report
}
}
impl SlashCommand for DoctorCommand {
fn name(&self) -> &str {
"doctor"
@ -17,34 +50,138 @@ impl SlashCommand for DoctorCommand {
}
fn description(&self) -> &str {
"Check terminal, color, clipboard, and voice input"
"Check this session and show available fixes"
}
fn usage(&self) -> &str {
"/doctor"
"/doctor [fix [ssh-wrap]]"
}
fn takes_args(&self) -> bool {
true
}
fn arg_placeholder(&self) -> Option<&str> {
Some("[fix [ssh-wrap]]")
}
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") {
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(),
}
} else {
ArgItem {
display: "fix".into(),
match_text: "fix".into(),
insert_text: "fix".into(),
description: "Show automatic fixes available here".into(),
}
};
Some(vec![item])
}
fn session_scoped(&self) -> bool {
true
}
fn run(&self, ctx: &mut CommandExecCtx, _args: &str) -> CommandResult {
let terminal = crate::terminal::terminal_context();
let query = crate::diagnostics::probes::LiveTmuxProbe;
let snapshot = crate::diagnostics::probes::collect_doctor_tui(
terminal,
crate::diagnostics::probes::TuiProbeEvidence {
fullscreen_active: ctx.screen_mode.is_fullscreen(),
kitty_flags_pushed: crate::app::kitty_flags_pushed(),
xtversion: crate::terminal::xtversion::detected(),
fn run(&self, _ctx: &mut CommandExecCtx, args: &str) -> CommandResult {
let mut tokens = args.split_whitespace();
match (tokens.next(), tokens.next(), tokens.next()) {
(None, None, None) => CommandResult::Doctor(DoctorRequest::Report),
(Some("fix"), None, None) => CommandResult::Doctor(DoctorRequest::ListFixes),
(Some("fix"), Some(value), None) => match crate::diagnostics::resolve_fix_id(value) {
Ok(id) => CommandResult::Doctor(DoctorRequest::Fix(id)),
Err(error) => CommandResult::Error(format!("{error}\n{USAGE}")),
},
&query,
);
let mut report = crate::diagnostics::view(snapshot.into());
// Passive enumeration cannot detect a denied macOS grant; capture reports that separately.
if crate::app::voice_mode_enabled() {
crate::diagnostics::apply_voice_probe(&mut report, true);
_ => CommandResult::Error(USAGE.to_owned()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::acp::model_state::ModelState;
use crate::app::bundle::BundleState;
fn run(args: &str) -> CommandResult {
let models = ModelState::default();
let bundle = BundleState::default();
let mut context = CommandExecCtx {
models: &models,
session_id: None,
bundle_state: &bundle,
screen_mode: crate::app::ScreenMode::Inline,
billing_surface_visible: true,
pager_state: crate::settings::PagerLocalSnapshot::default(),
};
DoctorCommand.run(&mut context, args)
}
#[test]
fn parses_report_list_short_and_canonical_fix_forms() {
assert!(matches!(
run(""),
CommandResult::Doctor(DoctorRequest::Report)
));
assert!(matches!(
run("fix"),
CommandResult::Doctor(DoctorRequest::ListFixes)
));
for value in ["ssh-wrap", "terminal.ssh-wrap"] {
assert!(matches!(
run(&format!("fix {value}")),
CommandResult::Doctor(DoctorRequest::Fix(crate::diagnostics::SSH_WRAP_ID))
));
}
}
#[test]
fn rejects_unknown_and_extra_arguments() {
for value in ["unknown", "fix unknown", "fix ssh-wrap extra", "report now"] {
assert!(matches!(run(value), CommandResult::Error(message) if message.contains(USAGE)));
}
}
#[test]
fn completion_stays_closed_until_an_argument_starts() {
let models = ModelState::default();
let context = AppCtx {
models: &models,
cwd: std::path::Path::new("/tmp"),
has_session_announcements: false,
billing_surface_visible: true,
workflows_available: false,
screen_mode: crate::app::ScreenMode::Inline,
};
let command = DoctorCommand;
assert!(command.suggest_args(&context, "").is_none());
assert!(command.suggest_args(&context, " ").is_none());
assert_eq!(
command.suggest_args(&context, "f").unwrap()[0].insert_text,
"fix"
);
for query in ["fix", "fix ", "fix s", "fix ssh", "fix terminal."] {
assert_eq!(
command.suggest_args(&context, query).unwrap()[0].insert_text,
"fix ssh-wrap"
);
}
for query in [
"fix ssh-wrap",
" fix ssh-wrap ",
"fix terminal.ssh-wrap",
" fix terminal.ssh-wrap ",
] {
assert!(command.suggest_args(&context, query).is_none(), "{query:?}");
}
CommandResult::Message(crate::diagnostics::format_doctor(&report))
}
}

View file

@ -81,7 +81,6 @@ pub fn builtin_commands() -> Vec<Arc<dyn SlashCommand>> {
Arc::new(docs::DocsCommand),
Arc::new(home::HomeCommand),
Arc::new(new::NewCommand),
Arc::new(fork::ForkCommand),
Arc::new(compact::CompactCommand),
Arc::new(copy::CopyCommand),
@ -92,6 +91,7 @@ pub fn builtin_commands() -> Vec<Arc<dyn SlashCommand>> {
Arc::new(edit_prompt::EditPromptCommand),
Arc::new(expand::ExpandCommand),
Arc::new(context::ContextCommand),
// Screen-mode switchers: visible only in the opposite mode.
Arc::new(screen_mode_switch::ScreenModeSwitchCommand::minimal()),
Arc::new(screen_mode_switch::ScreenModeSwitchCommand::fullscreen()),
Arc::new(model::ModelCommand),
@ -121,7 +121,6 @@ pub fn builtin_commands() -> Vec<Arc<dyn SlashCommand>> {
Arc::new(workflows::WorkflowsCommand),
Arc::new(btw::BtwCommand),
Arc::new(recap::RecapCommand),
Arc::new(doctor::DoctorCommand),
Arc::new(voice::VoiceCommand),
Arc::new(loop_cmd::LoopCommand),
@ -143,8 +142,11 @@ pub fn builtin_commands() -> Vec<Arc<dyn SlashCommand>> {
Arc::new(release_notes::ReleaseNotesCommand),
Arc::new(config_agents::ConfigAgentsCommand),
Arc::new(personas::PersonasCommand),
// Hidden easter egg: never listed, runs on bare `/gboom`.
Arc::new(gboom::GboomCommand),
// Hidden diagnostic: never listed, toggles the scroll-debug HUD.
Arc::new(scroll_debug::ScrollDebugCommand),
// Debug toggles: always registered, listed only on debug binaries.
Arc::new(debug::DebugCommand),
]
}
@ -359,7 +361,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");
assert_eq!(doctor.usage(), "/doctor [fix [ssh-wrap]]");
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

@ -100,12 +100,38 @@ impl FuzzyMatcher {
pattern.indices(s.slice(..), &mut self.matcher, &mut indices);
indices
}
/// Match `query` against display text and return display-relative indices.
pub fn indices_for(&mut self, query: &str, text: &str) -> Option<Vec<u32>> {
let query = query.trim();
if query.is_empty() || text.is_empty() {
return None;
}
self.pattern
.reparse(0, query, CaseMatching::Smart, Normalization::Smart, false);
let text = Utf32String::from(text);
self.pattern
.score(std::slice::from_ref(&text), &mut self.matcher)?;
let mut indices = Vec::new();
self.pattern
.column_pattern(0)
.indices(text.slice(..), &mut self.matcher, &mut indices);
Some(indices)
}
}
#[cfg(test)]
mod tests {
use super::FuzzyMatcher;
#[test]
fn indices_for_are_relative_to_display() {
let mut matcher = FuzzyMatcher::new();
assert_eq!(matcher.indices_for("ssh", "ssh-wrap"), Some(vec![0, 1, 2]));
assert_eq!(matcher.indices_for("sw", "ssh-wrap"), Some(vec![0, 4]));
assert_eq!(matcher.indices_for("fix s", "ssh-wrap"), None);
}
#[test]
fn empty_query_yields_insertion_order() {
let mut matcher = FuzzyMatcher::new();

View file

@ -983,6 +983,19 @@ impl SlashController {
self.arg_suggestions(command.as_ref(), models, &input.args_query)
}
fn argument_highlight_indices(&mut self, query: &str, display: &str) -> Vec<u32> {
let token = query.split_whitespace().next_back().unwrap_or("");
let fragment = token.rsplit(['/', '\\']).next().unwrap_or(token);
self.matcher
.indices_for(fragment, display)
.or_else(|| {
fragment
.rsplit_once('.')
.and_then(|(_, suffix)| self.matcher.indices_for(suffix, display))
})
.unwrap_or_default()
}
/// Generate argument suggestions for a specific command.
fn arg_suggestions(
&mut self,
@ -1012,7 +1025,7 @@ impl SlashController {
hits.into_iter()
.map(|(idx, _)| {
let mut row = SuggestionRow::from_arg(&items[idx]);
row.indices = self.matcher.indices(row.display.as_str());
row.indices = self.argument_highlight_indices(trimmed, &row.display);
row
})
.collect()
@ -2684,6 +2697,11 @@ mod tests {
.collect();
assert_eq!(rows, vec![("first", true), ("second", false)]);
ctrl.refresh(&state, "/chain fir", 10, &models);
let snap = state.snapshot();
assert!(snap.open);
assert_eq!(snap.matches[0].indices, vec![0, 1, 2]);
// Typing "first " triggers the phase-2 sub-menu of terminal rows.
ctrl.refresh(&state, "/chain first ", 13, &models);
let snap = state.snapshot();
@ -2693,6 +2711,11 @@ mod tests {
.map(|r| (r.display.as_str(), r.insert_text.ends_with(' ')))
.collect();
assert_eq!(rows, vec![("alpha", false), ("beta", false)]);
ctrl.refresh(&state, "/chain first al", 15, &models);
let snap = state.snapshot();
assert!(snap.open);
assert_eq!(snap.matches[0].indices, vec![0, 1]);
}
#[test]
@ -2712,6 +2735,39 @@ mod tests {
assert!(displays.contains(&"/doctor"), "matches: {displays:?}");
assert!(!displays.contains(&"/terminal-setup"));
for text in ["/doctor ", "/terminal-setup "] {
ctrl.refresh(&state, text, text.len(), &models);
let snapshot = state.snapshot();
assert!(!snapshot.open, "bare args opened for {text:?}");
assert!(snapshot.matches.is_empty(), "matches for {text:?}");
}
for (text, inserted, indices) in [
("/doctor f", "fix", vec![0]),
("/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]),
("/terminal-setup f", "fix", vec![0]),
("/terminal-setup fix s", "fix ssh-wrap", vec![0]),
] {
ctrl.refresh(&state, text, text.len(), &models);
let snapshot = state.snapshot();
assert!(snapshot.open, "no matches for {text:?}");
assert_eq!(snapshot.matches[0].insert_text, inserted);
assert_eq!(snapshot.matches[0].indices, indices, "{text:?}");
}
for text in [
"/doctor fix ssh-wrap",
"/doctor fix terminal.ssh-wrap",
"/terminal-setup fix ssh-wrap",
"/terminal-setup fix terminal.ssh-wrap",
] {
ctrl.refresh(&state, text, text.len(), &models);
let snapshot = state.snapshot();
assert!(!snapshot.open, "exact form left picker open for {text:?}");
assert!(snapshot.matches.is_empty(), "matches for {text:?}");
}
let text = "/terminal-setup";
ctrl.refresh(&state, text, text.len(), &models);
let snapshot = state.snapshot();