Synced from monorepo

Changes:
- Gate session-lifecycle heap steady state with a dhat soak
- Unbreak merge lifecycle e2e after default model → grok-4.5
- Scan home-scope rules dirs at <root>/rules
- Complete text-input paste and terminal parity
- Gate project roles and personas
- Use canonical editing in dialogs
- Use canonical editing in search bars
- Reject ambiguous MCP tool IDs
- Harden Git operands for plugins
- Simplify queue drain API
- Pass RFC 9207 iss through MCP OAuth token exchange
- Show leader roster when local agents map is empty
- Use canonical editing in Persona views
- Remove marketplace default-skills auto-install and purge old installs
- Use canonical editing in extension forms
- Add canonical dashboard text editing
- Use canonical editing in settings
- Add /summarize as a /recap alias
- Restore previous agent when exiting dashboard
- Use tool_choice auto for compaction
- Settings toggle for snap-prompt-to-top on send
- Update default models to grok-4.5
- Source login shell once for local bash (env + alias/function snapshot)
- Template hardcoded param names in server-native tool descriptions
- Fix System-Reminder XML tag injection in CLAUDE.md via agents_md
- Fix remote workspace-server hardcoding LSP trust (repo code execution risk)
- Clear orphaned tool-call updates at turn end
- Suppress task wake after cancel
- Send x-grok-client-identifier on direct API tool calls
- Harden dashboard peek lease transitions
- Host /btw side panel in live region (minimal mode)
- Bound scroll presentation latency
- Highlight multi-line constructs correctly in diffs and the file viewer
- Block web_fetch non-public IPs; local opt-in is explicit-host only
- Seed coding_data_retention_opt_out=false for OAuth e2es in pty-harness
- Follow up clipboard delivery feedback
- Use canonical editing in pickers
- Route TextArea through canonical editor
- Persistent "watching" status row; quieter turn markers
- Gate sensitive edit targets
- Expose agent registry counts and gate session churn on them
- Default coding data sharing to opt-out until server preference applies
- Wire chat attachment ids through gateway prompts
- On auth refresh failure, issue retry
- Forward preview provenance and computer lifecycle state
- Document independent privacy controls and scope /privacy output
- Strip SamplingError Display prefix on rate-limit UI copy
- Stop dumping Cloudflare HTML into Retry failed
- Disable in-place prompt edit (scroll jank on enter)
- Strip forced ANSI color from gh pr view JSON
- Plumb bash tool description onto ToolUsageCard wire
This commit is contained in:
grokkybara[bot] 2026-07-18 19:48:28 +01:00
commit 7cfcb20d2b
292 changed files with 23315 additions and 9209 deletions

View file

@ -1,6 +1,38 @@
/// Default auto-compact threshold (% of context window) when no source sets it.
pub const DEFAULT_AUTO_COMPACT_THRESHOLD_PERCENT: u8 = 85;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CompactionToolChoice {
#[default]
Auto,
None,
}
impl std::str::FromStr for CompactionToolChoice {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"auto" => Ok(Self::Auto),
"none" => Ok(Self::None),
_ => Err(()),
}
}
}
pub(crate) const ENV_COMPACTION_TOOL_CHOICE: &str = "GROK_COMPACTION_TOOL_CHOICE";
pub fn resolve_compaction_tool_choice_from(
env: Option<&str>,
config: Option<&str>,
remote: Option<&str>,
) -> CompactionToolChoice {
env.and_then(|s| s.parse().ok())
.or_else(|| config.and_then(|s| s.parse().ok()))
.or_else(|| remote.and_then(|s| s.parse().ok()))
.unwrap_or_default()
}
/// Env-var override for `auto_compact_threshold_percent`. Parsed as `u8`;
/// out-of-range or unparseable values are ignored.
pub(crate) const ENV_AUTO_COMPACT_THRESHOLD_PERCENT: &str = "GROK_AUTO_COMPACT_THRESHOLD_PERCENT";
@ -140,3 +172,48 @@ mod compaction_wall_clock_budget_tests {
assert_eq!(resolve(Some(5)), 5); // low values pass through (warned, not clamped)
}
}
#[cfg(test)]
mod compaction_tool_choice_tests {
use super::{CompactionToolChoice, resolve_compaction_tool_choice_from as resolve};
#[test]
fn default_is_auto() {
assert_eq!(resolve(None, None, None), CompactionToolChoice::Auto);
}
#[test]
fn precedence_env_over_config_over_remote() {
assert_eq!(
resolve(Some("none"), Some("auto"), Some("auto")),
CompactionToolChoice::None
);
assert_eq!(
resolve(None, Some("none"), Some("auto")),
CompactionToolChoice::None
);
assert_eq!(
resolve(None, None, Some("none")),
CompactionToolChoice::None
);
}
#[test]
fn garbage_falls_through() {
assert_eq!(
resolve(Some("garbage"), None, Some("none")),
CompactionToolChoice::None
);
assert_eq!(
resolve(Some("garbage"), Some("also-bad"), None),
CompactionToolChoice::Auto
);
}
#[test]
fn from_str_case_insensitive() {
assert_eq!("AUTO".parse(), Ok(CompactionToolChoice::Auto));
assert_eq!(" None ".parse(), Ok(CompactionToolChoice::None));
assert!("required".parse::<CompactionToolChoice>().is_err());
}
}

View file

@ -62,6 +62,137 @@ fn resolve_search_tool_enabled(
env.or(config).or(managed).unwrap_or(true)
}
const ENV_LOGIN_SHELL_CAPTURE: &str = "GROK_LOGIN_ENV";
fn login_shell_capture_from_toml(v: Option<&TomlValue>) -> Option<bool> {
v?.get("toolset")?
.get("bash")?
.get("login_shell_capture")?
.as_bool()
}
pub fn resolve_login_shell_capture(remote: Option<bool>) -> bool {
let requirements = crate::config::load_merged_requirements();
let layers = match crate::config::ConfigLayers::load() {
Ok(l) => Some(l),
Err(e) => {
tracing::warn!(error = %e, "login_shell_capture: failed to load config layers");
None
}
};
resolve_login_shell_capture_tiers(
requirements.as_ref(),
layers.as_ref().map(|l| &l.user),
layers.as_ref().map(|l| &l.managed),
layers.as_ref().map(|l| &l.system_managed),
remote,
)
}
fn resolve_login_shell_capture_tiers(
requirements: Option<&TomlValue>,
user: Option<&TomlValue>,
managed: Option<&TomlValue>,
system_managed: Option<&TomlValue>,
remote: Option<bool>,
) -> bool {
use crate::agent::config::BoolFlag;
BoolFlag::env(ENV_LOGIN_SHELL_CAPTURE)
.requirement(login_shell_capture_from_toml(requirements))
.config(login_shell_capture_from_toml(user))
.managed(
login_shell_capture_from_toml(managed)
.or_else(|| login_shell_capture_from_toml(system_managed)),
)
.feature_flag(remote)
.default(true)
.resolve()
.value
}
#[cfg(test)]
mod login_shell_capture_tests {
use super::{ENV_LOGIN_SHELL_CAPTURE, resolve_login_shell_capture_tiers};
use toml::Value as TomlValue;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn guard() -> std::sync::MutexGuard<'static, ()> {
let g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
unsafe { std::env::remove_var(ENV_LOGIN_SHELL_CAPTURE) };
g
}
fn cfg(enabled: bool) -> TomlValue {
toml::from_str(&format!(
"[toolset.bash]\nlogin_shell_capture = {enabled}\n"
))
.unwrap()
}
#[test]
fn defaults_on() {
let _g = guard();
assert!(resolve_login_shell_capture_tiers(
None, None, None, None, None
));
}
#[test]
fn remote_flag_can_disable() {
let _g = guard();
assert!(!resolve_login_shell_capture_tiers(
None,
None,
None,
None,
Some(false)
));
}
#[test]
fn user_config_beats_remote() {
let _g = guard();
assert!(resolve_login_shell_capture_tiers(
None,
Some(&cfg(true)),
None,
None,
Some(false)
));
assert!(!resolve_login_shell_capture_tiers(
None,
Some(&cfg(false)),
None,
None,
Some(true)
));
}
#[test]
fn env_beats_config_and_remote() {
let _g = guard();
unsafe { std::env::set_var(ENV_LOGIN_SHELL_CAPTURE, "0") };
let off = resolve_login_shell_capture_tiers(None, Some(&cfg(true)), None, None, Some(true));
unsafe { std::env::remove_var(ENV_LOGIN_SHELL_CAPTURE) };
assert!(!off);
}
#[test]
fn requirements_win_outright() {
let _g = guard();
unsafe { std::env::set_var(ENV_LOGIN_SHELL_CAPTURE, "1") };
let off = resolve_login_shell_capture_tiers(
Some(&cfg(false)),
Some(&cfg(true)),
None,
None,
Some(true),
);
unsafe { std::env::remove_var(ENV_LOGIN_SHELL_CAPTURE) };
assert!(!off);
}
}
/// Env override for `[toolset.ask_user_question] timeout_enabled` (parsed by
/// the shared [`xai_grok_config::env_bool`] via `BoolFlag`). The secs env var
/// lives in the tools crate (`RESPONSE_TIMEOUT_ENV`), parsed once there.

View file

@ -23,6 +23,10 @@ pub async fn set_show_timeline(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.show_timeline = Some(value)).await
}
pub async fn set_page_flip_on_send(value: bool) -> Result<()> {
update_config(|cfg| cfg.ui.page_flip_on_send = Some(value)).await
}
/// Persist `[ui].simple_mode` via `update_config`. Same `Option<bool>`
/// shape as `show_timestamps`.
pub async fn set_simple_mode(value: bool) -> Result<()> {

View file

@ -8,6 +8,33 @@ pub mod hooks;
// unchanged.
pub use xai_grok_shell_base::util::*;
pub(crate) fn is_user_instruction_path(
path: &std::path::Path,
grok_home: &std::path::Path,
vendor_homes: &[(std::path::PathBuf, bool)],
workspace_root: Option<&std::path::Path>,
) -> bool {
let parent = path.parent();
let grok_rules = grok_home.join("rules");
let is_exact_home_surface = parent
.is_some_and(|parent| parent == grok_home || parent == grok_rules)
|| vendor_homes.iter().any(|(vendor_home, named_enabled)| {
parent.is_some_and(|parent| {
(*named_enabled && parent == vendor_home) || parent == vendor_home.join("rules")
})
});
if is_exact_home_surface {
return true;
}
if workspace_root.is_some_and(|root| path.starts_with(root)) {
return false;
}
path.starts_with(grok_home)
|| vendor_homes
.iter()
.any(|(vendor_home, _)| path.starts_with(vendor_home))
}
/// Aborts the wrapped tokio task when dropped.
///
/// Use to tie a spawned helper task's lifetime to an async scope so that
@ -22,3 +49,35 @@ impl Drop for AbortOnDrop {
self.0.abort();
}
}
#[cfg(test)]
mod is_user_instruction_path_tests {
use super::is_user_instruction_path;
use std::path::Path;
#[test]
fn grok_home_named_file_nested_in_workspace_is_user_scoped() {
assert!(is_user_instruction_path(
Path::new("/repo/config/AGENTS.md"),
Path::new("/repo/config"),
&[],
Some(Path::new("/repo")),
));
assert!(!is_user_instruction_path(
Path::new("/repo/config/src/AGENTS.md"),
Path::new("/repo/config"),
&[],
Some(Path::new("/repo")),
));
}
#[test]
fn workspace_descendants_under_grok_home_stay_project_scoped() {
assert!(!is_user_instruction_path(
Path::new("/custom/grok/worktrees/repo/src/AGENTS.md"),
Path::new("/custom/grok"),
&[],
Some(Path::new("/custom/grok/worktrees/repo")),
));
}
}