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

@ -5,17 +5,17 @@ use crate::diagnostics::{
};
use crate::host::{DisplayServer, HostOs};
const LIVE_TUI_PROBE_CTA: &str = "Run /doctor inside Grok.";
const LIVE_TUI_PROBE_CTA: &str = "Some checks only run in Grok. Start Grok and run /doctor.";
pub(super) fn format(report: &DiagnosticReport) -> String {
let facts = &report.facts;
let mut out = String::from("Grok Doctor\n\nTerminal\n");
let mut out = String::from("Grok Doctor\n\nEnvironment\n");
fact(&mut out, "terminal", &facts.terminal.to_string());
match &facts.xtversion {
RuntimeFact::Available(value) => fact(&mut out, "xtversion", value),
RuntimeFact::NoReply => unavailable(&mut out, "xtversion", "no reply"),
RuntimeFact::Unavailable => unavailable(&mut out, "xtversion", "unavailable"),
RuntimeFact::Available(value) => fact(&mut out, "terminal version", value),
RuntimeFact::NoReply => unavailable(&mut out, "terminal version", "no reply"),
RuntimeFact::Unavailable => unavailable(&mut out, "terminal version", "unavailable"),
}
fact(&mut out, "multiplexer", &facts.multiplexer.to_string());
if let Some(byobu) = facts.byobu {
@ -95,7 +95,7 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
);
fact(
&mut out,
"wrap",
"SSH wrap",
if clipboard.wrap_sink { "on" } else { "off" },
);
if clipboard.display_server == DisplayServer::Wayland {
@ -125,9 +125,6 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
ClipboardDelivery::Failed => "unavailable",
};
fact(&mut out, "status", status);
if let Some(fix) = &clipboard.fix {
fact(&mut out, "fix", fix);
}
if let Some(voice) = &facts.voice {
out.push_str("\nVoice\n");
@ -154,7 +151,7 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
.filter(|note| !fact_already_shows_probe(note.probe));
let mut notes = visible_notes.peekable();
if notes.peek().is_some() {
out.push_str("\nProbe notes\n");
out.push_str("\nChecks not completed\n");
for note in notes {
let message = match &note.message {
Some(message) => format!("{}: {message}", probe_status(note.status)),
@ -169,7 +166,7 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
.iter()
.any(crate::diagnostics::probe_requires_live_tui)
{
out.push_str("\nLive TUI evidence\n");
out.push_str("\nNeeds a running session\n");
out.push_str(&format!(" {LIVE_TUI_PROBE_CTA}\n"));
}
@ -220,7 +217,7 @@ fn format_finding(out: &mut String, finding: &DiagnosticFinding) {
let instruction = match (&remediation.config_path, &finding.automatic_remediation) {
(Some(path), _) => format!("Add `{}` to {path}", remediation.fix),
(None, Some(_)) => format!("One-off: `{}`", remediation.fix),
(None, None) => format!("Run `{}`", remediation.fix),
(None, None) => format!("Run: `{}`", remediation.fix),
};
out.push_str(&format!("{instruction}\n"));
}

View file

@ -13,7 +13,7 @@ pub const SCHEMA_VERSION: &str = "1";
#[derive(Clone, Debug, Default, Eq, PartialEq, clap::Args)]
#[command(args_conflicts_with_subcommands = true)]
pub struct DoctorArgs {
/// Emit machine-readable JSON output.
/// Print the diagnostic report as JSON.
#[arg(long)]
pub json: bool,
#[command(subcommand)]
@ -22,16 +22,16 @@ pub struct DoctorArgs {
#[derive(Clone, Debug, Eq, PartialEq, clap::Subcommand)]
pub enum DoctorCommand {
/// Apply a named automatic remediation.
/// Apply an automatic fix.
Fix(FixArgs),
}
#[derive(Clone, Debug, Eq, PartialEq, clap::Args)]
pub struct FixArgs {
/// Short fix handle (`ssh-wrap`); canonical `terminal.ssh-wrap` is also accepted.
pub id: String,
/// Apply without prompting after printing the exact plan.
#[arg(long)]
/// Fix to apply. Use `ssh-wrap` or `terminal.ssh-wrap`. Omit it to list available automatic fixes.
pub id: Option<String>,
/// Apply the displayed changes without confirmation.
#[arg(long, requires = "id")]
pub yes: bool,
}
@ -50,7 +50,7 @@ pub fn run(args: DoctorArgs) -> Result<()> {
pub fn run_with_writer(args: DoctorArgs, writer: &mut impl Write) -> Result<()> {
match args.command {
None => run_report(args.json, writer),
Some(_) => anyhow::bail!("doctor fixes require interactive input/output"),
Some(_) => anyhow::bail!("Doctor fixes require interactive input and output."),
}
}
@ -69,25 +69,20 @@ fn configured_report_for_terminal(
report: DiagnosticReport,
terminal: &crate::terminal::TerminalContext,
) -> DiagnosticReport {
let configured = shell_home_and_kind()
.map(|(home, shell)| {
crate::diagnostics::managed_alias_configured(&shell.config_path(&home), shell)
})
.unwrap_or(false);
if terminal.is_ssh || terminal.is_official_vscode_remote {
report
} else {
crate::diagnostics::configured_report(report, configured)
return report;
}
let configured = shell_home_and_kind().is_some_and(|(home, shell)| {
crate::diagnostics::managed_alias_configured(&shell.config_path(&home), shell)
});
crate::diagnostics::configured_report(report, configured)
}
fn collect_report_with(
snapshot: crate::diagnostics::probes::StandaloneDiagnosticSnapshot<'_>,
) -> DiagnosticReport {
let mut report = crate::diagnostics::view(snapshot.into());
// Passive mic fact when audio is compiled in. No issue finding — headless
// hosts often have no input device; the Voice fact row is enough.
crate::diagnostics::apply_voice_probe(&mut report, false);
crate::diagnostics::apply_voice_probe(&mut report, true);
report
}
@ -110,12 +105,20 @@ fn run_fix(
input: &mut impl std::io::BufRead,
writer: &mut impl Write,
) -> Result<()> {
let id = crate::diagnostics::resolve_fix_id(&args.id)?;
let terminal = crate::terminal::standalone_terminal_context();
let report = configured_report_for_terminal(
collect_report_with(crate::diagnostics::probes::collect_standalone(&terminal)),
&terminal,
);
let Some(value) = args.id.as_deref() else {
write!(
writer,
"{}",
crate::diagnostics::format_applicable_automatic_fixes(&report, &terminal)
)?;
return Ok(());
};
let id = crate::diagnostics::resolve_fix_id(value)?;
let request = crate::diagnostics::FixRequest::from_environment(id)?;
let plan = crate::diagnostics::plan_fix(request, &report, &terminal)?;
apply_fix_plan(args, stdin_is_terminal, input, writer, &terminal, plan)
@ -129,21 +132,20 @@ fn apply_fix_plan(
terminal: &crate::terminal::TerminalContext,
plan: FixPlan,
) -> Result<()> {
let id = plan.id;
write_fix_preview(&plan, writer)?;
if !args.yes {
if !stdin_is_terminal {
anyhow::bail!(
"refusing to apply a doctor fix from non-interactive stdin without --yes"
"Cannot apply this fix without confirmation. Run it in an interactive terminal or add `--yes`."
);
}
write!(writer, "\nApply this change? [y/N] ")?;
write!(writer, "\nApply this fix? [y/N] ")?;
writer.flush()?;
let mut answer = String::new();
input.read_line(&mut answer)?;
if !matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
writeln!(writer, "Cancelled.")?;
writeln!(writer, "Fix cancelled.")?;
return Ok(());
}
}
@ -154,63 +156,38 @@ fn apply_fix_plan(
collect_report_with(crate::diagnostics::probes::collect_standalone(terminal)),
crate::diagnostics::managed_alias_configured(&outcome.changed_path, shell),
);
if post_report.findings.iter().any(|finding| finding.id == id) {
anyhow::bail!("fix applied, but `{id}` is still reported");
if post_report
.findings
.iter()
.any(|finding| finding.id == outcome.id)
{
anyhow::bail!(
"The change was applied, but Doctor still reports `{}`.",
outcome.id
);
}
match outcome.status {
FixStatus::Applied => writeln!(
writer,
"\nConfigured {id} in {}.",
"\nSet up SSH wrapping in {}.",
outcome.changed_path.display()
)?,
FixStatus::AlreadyConfigured => writeln!(
writer,
"\n{id} is already configured in {}.",
"\nSSH wrapping is already set up in {}.",
outcome.changed_path.display()
)?,
}
if let Some(backup) = outcome.backup_path {
writeln!(writer, "Backup: {}", backup.display())?;
}
writeln!(writer, "Open a new interactive shell to use the alias.")?;
writeln!(writer, "Start a new shell to use the alias.")?;
Ok(())
}
fn write_fix_preview(plan: &FixPlan, writer: &mut impl Write) -> std::io::Result<()> {
writeln!(writer, "Doctor fix: {}", plan.id)?;
writeln!(writer, "Shell: {}", plan.shell.name())?;
for change in &plan.changes {
writeln!(writer, "File: {}", change.requested_path.display())?;
if change.target_path != change.requested_path {
writeln!(writer, "Physical target: {}", change.target_path.display())?;
}
writeln!(writer, "\nManaged block:")?;
writeln!(writer, "{}", change.block)?;
match &change.backup_path_hint {
Some(path) => writeln!(
writer,
"\nProposed backup: {} (apply retries a nearby unique name on collision)",
path.display()
)?,
None => writeln!(writer, "\nBackup: none (new file or exact no-op)")?,
}
}
writeln!(writer, "\nBehavior:")?;
writeln!(
writer,
" New interactive shells run typed `ssh ...` as `grok wrap ssh ...`."
)?;
writeln!(
writer,
" One-off alternative without changing config: `{}`.",
crate::diagnostics::SSH_WRAP_ONE_OFF
)?;
writeln!(writer, "Caveats:")?;
for caveat in &plan.caveats {
writeln!(writer, " - {caveat}")?;
}
Ok(())
write!(writer, "{}", crate::diagnostics::format_fix_preview(plan))
}
fn shell_home_and_kind() -> Option<(std::path::PathBuf, ShellKind)> {

View file

@ -309,6 +309,19 @@ fn human_wayland_error_includes_detail_once() {
report.facts.clipboard.data_control = DataControlFact::Error;
report.facts.clipboard.delivery = ClipboardDelivery::Failed;
report.facts.clipboard.fix = Some("/minimal".to_owned());
report.findings.push(DiagnosticFinding {
id: crate::diagnostics::CLIPBOARD_DELIVERY_UNAVAILABLE_ID,
disposition: FindingDisposition::Issue,
message: "No configured clipboard route can reach the intended clipboard".to_owned(),
remediation: None,
automatic_remediation: None,
note: Some(
"Each in-app copy is also written to the backup path shown by the operation. Use \
`/copy <file>` for an explicit file or `/minimal` for terminal-native selection, \
then check the native clipboard tool reported above."
.to_owned(),
),
});
report.probe_notes = vec![ProbeNote {
probe: "wayland.data-control",
status: ProbeStatus::Error,
@ -319,9 +332,9 @@ fn human_wayland_error_includes_detail_once() {
concat!(
"Grok Doctor\n",
"\n",
"Terminal\n",
"Environment\n",
" · terminal Ghostty\n",
" ? xtversion no reply\n",
" ? terminal version no reply\n",
" · multiplexer None detected\n",
" · ssh no\n",
" · color truecolor\n",
@ -331,10 +344,13 @@ fn human_wayland_error_includes_detail_once() {
" · native unavailable\n",
" · tmux off\n",
" · osc 52 off\n",
" · wrap off\n",
" · SSH wrap off\n",
" ? data-control error: probe worker died\n",
" · status unavailable\n",
" · fix /minimal\n",
"\n",
"Findings\n",
" ! clipboard.delivery-unavailable No configured clipboard route can reach the intended clipboard\n",
" Each in-app copy is also written to the backup path shown by the operation. Use `/copy <file>` for an explicit file or `/minimal` for terminal-native selection, then check the native clipboard tool reported above.\n",
"\n",
"1 issue, 0 recommendations\n",
)
@ -424,9 +440,9 @@ fn human_healthy_fixture_is_exact() {
concat!(
"Grok Doctor\n",
"\n",
"Terminal\n",
"Environment\n",
" · terminal Ghostty\n",
" ? xtversion no reply\n",
" ? terminal version no reply\n",
" · multiplexer None detected\n",
" · ssh no\n",
" · color truecolor\n",
@ -436,7 +452,7 @@ fn human_healthy_fixture_is_exact() {
" · native local (pbcopy)\n",
" · tmux off\n",
" · osc 52 off\n",
" · wrap off\n",
" · SSH wrap off\n",
" · status confirmed\n",
"\n",
"0 issues, 0 recommendations\n",
@ -451,9 +467,9 @@ fn human_mixed_fixture_is_exact() {
concat!(
"Grok Doctor\n",
"\n",
"Terminal\n",
"Environment\n",
" · terminal Ghostty\n",
" · xtversion Ghostty 1.2.3\n",
" · terminal version Ghostty 1.2.3\n",
" · multiplexer tmux\n",
" · byobu tmux\n",
" · ssh yes\n",
@ -466,7 +482,7 @@ fn human_mixed_fixture_is_exact() {
" · native local (pbcopy)\n",
" · tmux on\n",
" · osc 52 supported\n",
" · wrap off\n",
" · SSH wrap off\n",
" · status confirmed\n",
"\n",
"Findings\n",
@ -477,15 +493,15 @@ fn human_mixed_fixture_is_exact() {
" → Automatic setup: `grok doctor fix ssh-wrap`\n",
" → One-off: `grok wrap ssh <host>`\n",
"\n",
"Probe notes\n",
"Checks not completed\n",
" ? tmux.version unavailable\n",
" ? tmux.extended-keys unavailable\n",
" ? tmux.allow-passthrough-support unsupported\n",
" ? runtime.fullscreen-active unavailable\n",
" ? tmux.control-mode error: server unavailable\n",
"\n",
"Live TUI evidence\n",
" Run /doctor inside Grok.\n",
"Needs a running session\n",
" Some checks only run in Grok. Start Grok and run /doctor.\n",
"\n",
"1 issue, 1 recommendation\n",
)
@ -505,15 +521,14 @@ fn fix_preview_contains_exact_change_and_caveats() {
let mut preview = Vec::new();
write_fix_preview(&plan, &mut preview).unwrap();
let preview = String::from_utf8(preview).unwrap();
assert_eq!(preview, crate::diagnostics::format_fix_preview(&plan));
assert!(preview.contains("File: "));
assert!(
preview.contains(
"# >>> grok doctor >>>\n# >>> terminal.ssh-wrap >>>\nalias ssh='grok wrap ssh'"
)
);
assert!(
preview.contains("One-off alternative without changing config: `grok wrap ssh <host>`")
);
assert!(preview.contains("To use once without changing config: `grok wrap ssh <host>`"));
assert!(preview.contains("Use `command ssh ...` to bypass the alias."));
assert!(preview.contains("ssh -f"));
assert!(preview.contains("ControlPersist"));
@ -534,7 +549,7 @@ fn decline_is_success_and_does_not_write() {
let mut output = Vec::new();
apply_fix_plan(
FixArgs {
id: "ssh-wrap".to_owned(),
id: Some("ssh-wrap".to_owned()),
yes: false,
},
true,
@ -544,7 +559,11 @@ fn decline_is_success_and_does_not_write() {
plan,
)
.unwrap();
assert!(String::from_utf8(output).unwrap().ends_with("Cancelled.\n"));
assert!(
String::from_utf8(output)
.unwrap()
.ends_with("Fix cancelled.\n")
);
assert!(!temp.path().join(".bashrc").exists());
}
@ -560,7 +579,7 @@ fn non_tty_without_yes_fails_safely_before_write() {
.unwrap();
let error = apply_fix_plan(
FixArgs {
id: "terminal.ssh-wrap".to_owned(),
id: Some("terminal.ssh-wrap".to_owned()),
yes: false,
},
false,
@ -573,7 +592,7 @@ fn non_tty_without_yes_fails_safely_before_write() {
assert!(
error
.to_string()
.contains("non-interactive stdin without --yes")
.contains("Cannot apply this fix without confirmation")
);
assert!(!temp.path().join(".bashrc").exists());
}
@ -607,9 +626,9 @@ fn human_incomplete_fixture_is_exact_without_duplicate_probe_rows() {
concat!(
"Grok Doctor\n",
"\n",
"Terminal\n",
"Environment\n",
" · terminal Ghostty\n",
" ? xtversion unavailable\n",
" ? terminal version unavailable\n",
" · multiplexer None detected\n",
" · ssh no\n",
" ? color unavailable\n",
@ -619,11 +638,11 @@ fn human_incomplete_fixture_is_exact_without_duplicate_probe_rows() {
" · native local (pbcopy)\n",
" · tmux off\n",
" · osc 52 off\n",
" · wrap off\n",
" · SSH wrap off\n",
" · status confirmed\n",
"\n",
"Live TUI evidence\n",
" Run /doctor inside Grok.\n",
"Needs a running session\n",
" Some checks only run in Grok. Start Grok and run /doctor.\n",
"\n",
"0 issues, 0 recommendations\n",
)
@ -950,6 +969,49 @@ fn newline_variant_and_field_mappings_are_stable() {
);
}
#[test]
fn clipboard_issue_count_preserves_legacy_reports_without_double_counting_named_findings() {
let mut report = healthy_report();
report.facts.clipboard.delivery = ClipboardDelivery::Failed;
assert_eq!(report.issue_count(), 1, "legacy fact-only report");
report.findings.push(DiagnosticFinding {
id: crate::diagnostics::CLIPBOARD_DELIVERY_UNAVAILABLE_ID,
disposition: FindingDisposition::Issue,
message: "clipboard unavailable".to_owned(),
remediation: None,
automatic_remediation: None,
note: Some("manual recovery".to_owned()),
});
assert_eq!(report.issue_count(), 1, "named finding replaces fact count");
}
#[test]
fn new_named_findings_extend_json_without_schema_changes() {
let mut report = healthy_report();
report.facts.clipboard.delivery = ClipboardDelivery::Unverified;
report.facts.clipboard.fix = Some("grok wrap <ssh command> or /minimal".to_owned());
report.findings.push(DiagnosticFinding {
id: crate::diagnostics::CLIPBOARD_DELIVERY_UNVERIFIED_ID,
disposition: FindingDisposition::Issue,
message: "Clipboard delivery could not be verified across this remote boundary".to_owned(),
remediation: None,
automatic_remediation: None,
note: Some("Run /doctor guidance".to_owned()),
});
let mut output = Vec::new();
write_report(&report, true, &mut output).unwrap();
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
assert_eq!(json["schemaVersion"], "1");
assert_eq!(json["facts"]["clipboard"]["delivery"], "unverified");
assert_eq!(
json["facts"]["clipboard"]["fix"],
"grok wrap <ssh command> or /minimal"
);
assert_eq!(json["findings"][0]["id"], "clipboard.delivery-unverified");
assert_eq!(json["counts"]["issues"], 1);
}
#[test]
fn output_writer_errors_propagate() {
struct BrokenWriter;