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:
parent
3af4d5d398
commit
a5727c5960
482 changed files with 37627 additions and 13402 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue