Synced from monorepo

Synced from monorepo

Changes:
- Temporarily disable session share link creation in the TUI
- Do not approve plan on empty Enter from the revise prompt
- Expose chat product Skills via ACP available_commands_update
- Return immediately from a blocking wait on an already-completed ACP task
- Split headless pager module for clearer structure
- Stop git worktree prune from removing user registrations on resume
- Use compaction sampler tokenizer for item token counts
- Opt-in extra root CAs via GROK_EXTRA_CA_BUNDLE
- Cancel all session subagents when the user stops
- Let the session persistence actor exit when its session ends
- Make fullscreen terminal resize much cheaper on long sessions
- Report honestly from kill_task when an ACP task does not exist
- Hide /usage for external-auth deployments
- Forward the history-load trailer’s computer_reason to the client
- Remove ineffective no-op tool reminder
- Declare slash-command screen-mode support in one place
- Keep settings enum picker on the committed value until Enter
- Reap a PTY’s full process tree
- Stream tool calls from headless mode over ACP
- Bridge gateway task lifecycle to ACP for chat session background tasks
- Don’t warn about truncated history on a suppressed replay
- Fit full-replace summarizer input and recover on context-length errors
- Stop dropping agents over an unrecognized frontmatter color
- Add /undo as a slash alias for /rewind
- Harden sleep/wake token-refresh paths against forced re-login
- Add session/list ACP method
- Give each sampling backend its own conversion module
- Treat an unenrolled child process as a lint error
- Suppress the cancelled marker on send-now wake turns
- Stop tearing down Roslyn on every edit, and read C# diagnostics

Source-Revision: 2a28b4a86cfc4a4c133c35b7fc2a6a9964387c39
This commit is contained in:
grokkybara[bot] 2026-07-30 19:07:40 +00:00
commit dd04f397b1
367 changed files with 29489 additions and 10051 deletions

View file

@ -793,7 +793,7 @@ pub struct AgentDefinition {
pub isolation: Option<IsolationMode>,
#[serde(default)]
pub background: Option<bool>,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_agent_color")]
pub color: Option<AgentColor>,
#[serde(default)]
pub initial_prompt: Option<String>,
@ -1060,11 +1060,13 @@ const _: () =
Eq,
Deserialize,
serde::Serialize,
AsRefStr,
EnumString,
IntoStaticStr,
strum::EnumCount,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
pub enum AgentColor {
Red,
Blue,
@ -1081,6 +1083,35 @@ impl AgentColor {
];
}
const _: () = assert!(AgentColor::VALID_VALUES.len() == <AgentColor as strum::EnumCount>::COUNT);
/// Never fails: `color` is decorative, but a rejected value fails the whole
/// frontmatter parse, and discovery skips agents that fail to parse — so a
/// typo'd or hex color would silently make the agent unspawnable.
///
/// Frontmatter is only ever decoded by `serde_yaml`, so the intermediate value
/// is captured as `serde_yaml::Value` (total for YAML — tagged scalars and
/// maps with non-string keys included, which have no `serde_json::Value`
/// form). Unrecognized values are dropped to `None` with a warning rather
/// than mapped to a stand-in color the author never wrote.
fn deserialize_agent_color<'de, D>(deserializer: D) -> Result<Option<AgentColor>, D::Error>
where
D: serde::Deserializer<'de>,
{
use std::str::FromStr;
let Some(value) = Option::<serde_yaml::Value>::deserialize(deserializer)? else {
return Ok(None);
};
let parsed = value
.as_str()
.and_then(|name| AgentColor::from_str(name.trim()).ok());
if parsed.is_none() {
tracing::warn!(
color = ?value,
valid = ?AgentColor::VALID_VALUES,
"unrecognized agent color, ignoring"
);
}
Ok(parsed)
}
/// Agent memory scope. Distinct from `storage::MemoryScope` (global-vs-workspace write target).
#[derive(
Debug,
@ -2096,8 +2127,10 @@ description: Minimal agent
}
for color in AgentColor::VALID_VALUES {
let c = format!("---\nname: t\ndescription: t\ncolor: {color}\n---\n");
assert!(
AgentDefinition::parse(&c).unwrap().color.is_some(),
let parsed = AgentDefinition::parse(&c).unwrap().color;
assert_eq!(
parsed.map(<&'static str>::from),
Some(*color),
"color: {color}"
);
}
@ -2110,6 +2143,33 @@ description: Minimal agent
}
}
#[test]
fn unparseable_color_is_dropped_instead_of_dropping_the_agent() {
for (declared, expected) in [
("Purple", Some(AgentColor::Purple)),
(" CYAN ", Some(AgentColor::Cyan)),
("teal", None),
("\"#ff0000\"", None),
("chartreuse", None),
("42", None),
("[red, blue]", None),
("!custom x", None),
("{1: 2}", None),
] {
let c = format!("---\nname: t\ndescription: t\ncolor: {declared}\n---\n");
let def = AgentDefinition::parse(&c)
.unwrap_or_else(|e| panic!("color {declared} must not fail the parse: {e}"));
assert_eq!(def.color, expected, "color: {declared}");
assert_eq!(def.name, "t");
}
}
#[test]
fn absent_or_null_color_stays_none() {
let def = AgentDefinition::parse("---\nname: t\ndescription: t\n---\n").unwrap();
assert!(def.color.is_none());
let def = AgentDefinition::parse("---\nname: t\ndescription: t\ncolor:\n---\n").unwrap();
assert!(def.color.is_none());
}
#[test]
fn test_parse_missing_name() {
let content = r#"---
description: No name

View file

@ -399,14 +399,9 @@ fn all_subagents_with_plugins_and_home(
if path.extension().and_then(|e| e.to_str()) != Some("md") {
continue;
}
// Use frontmatter-only parsing for untrusted plugins
let def = if plugin.trusted {
AgentDefinition::from_file(&path).ok()
} else {
AgentDefinition::from_file_frontmatter_only(&path).ok()
let Some(def) = load_plugin_agent_definition(plugin, &path) else {
continue;
};
let Some(mut def) = def else { continue };
def.plugin_name = Some(plugin.name.clone());
let qualified_name = format!("{}:{}", plugin.name, def.name);
@ -481,17 +476,11 @@ fn by_name_in_cwd_with_plugins_and_home(
{
for agent_dir in &plugin.agent_dirs {
let agent_file = agent_dir.join(format!("{agent_name}.md"));
if agent_file.is_file() {
let load_fn = if plugin.trusted {
AgentDefinition::from_file
} else {
AgentDefinition::from_file_frontmatter_only
};
if let Ok(mut def) = load_fn(&agent_file) {
def.plugin_name = Some(plugin_name.to_string());
substitute_plugin_vars(&mut def, plugin);
return Some(def);
}
if agent_file.is_file()
&& let Some(mut def) = load_plugin_agent_definition(plugin, &agent_file)
{
substitute_plugin_vars(&mut def, plugin);
return Some(def);
}
}
}
@ -510,13 +499,7 @@ fn by_name_in_cwd_with_plugins_and_home(
}
if matches.len() == 1 {
let (plugin, agent_file) = &matches[0];
let load_fn = if plugin.trusted {
AgentDefinition::from_file
} else {
AgentDefinition::from_file_frontmatter_only
};
if let Ok(mut def) = load_fn(agent_file) {
def.plugin_name = Some(plugin.name.clone());
if let Some(mut def) = load_plugin_agent_definition(plugin, agent_file) {
substitute_plugin_vars(&mut def, plugin);
return Some(def);
}
@ -533,6 +516,37 @@ fn by_name_in_cwd_with_plugins_and_home(
None
}
/// Load one plugin-provided agent file, tagged with its owning plugin.
///
/// Untrusted plugins are parsed frontmatter-only so their prompt body never
/// reaches the model before the plugin is trusted. A parse failure drops the
/// agent from discovery entirely, so it is logged rather than swallowed.
fn load_plugin_agent_definition(
plugin: &crate::plugins::LoadedPlugin,
path: &Path,
) -> Option<AgentDefinition> {
let loaded = if plugin.trusted {
AgentDefinition::from_file(path)
} else {
AgentDefinition::from_file_frontmatter_only(path)
};
match loaded {
Ok(mut def) => {
def.plugin_name = Some(plugin.name.clone());
Some(def)
}
Err(e) => {
tracing::warn!(
plugin = %plugin.name,
path = %path.display(),
error = %e,
"Failed to parse plugin agent definition, skipping"
);
None
}
}
}
/// Expand `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` (and the Grok
/// aliases) in a plugin agent's body so the model receives absolute paths,
/// matching the expected load-time resolution for these variables.
@ -1368,6 +1382,44 @@ mod tests {
assert!(entries.iter().any(|e| e.name == "plugin-one:reviewer"));
}
#[test]
fn plugin_agent_with_unrecognized_color_is_still_discovered() {
let tmp = tempfile::tempdir().unwrap();
let cwd = tmp.path().join("workspace");
let home = tmp.path().join("home");
fs::create_dir_all(&cwd).unwrap();
fs::create_dir_all(&home).unwrap();
let plugin_root = tempfile::tempdir().unwrap();
let plugin_agents = plugin_root.path().join("agents");
fs::create_dir_all(&plugin_agents).unwrap();
fs::write(
plugin_agents.join("painter.md"),
"---\nname: painter\ndescription: Plugin painter\ncolor: chartreuse\n---\nBody.\n",
)
.unwrap();
let registry = make_plugin_registry("plugin-one", PluginScope::User, vec![plugin_agents]);
let entries = all_subagents_with_plugins_and_home(
&cwd,
&HashMap::new(),
Some(&registry),
Some(&home),
Some(&home.join(".grok")),
);
assert!(entries.iter().any(|e| e.name == "plugin-one:painter"));
let def = by_name_in_cwd_with_plugins_and_home(
"plugin-one:painter",
&cwd,
Some(&registry),
Some(&home),
Some(&home.join(".grok")),
)
.expect("agent must resolve despite the unrecognized color");
assert_eq!(def.color, None, "unrecognized color must be dropped");
}
#[test]
fn test_by_name_in_cwd_with_plugins_prefers_native_over_plugin_bare_name() {
let tmp = tempfile::tempdir().unwrap();