aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/agent_resume.rs38
-rw-r--r--src/api/schema/integrations.rs4
-rw-r--r--src/cli/integration.rs5
-rw-r--r--src/detect/mod.rs18
-rw-r--r--src/integration/actions.rs53
-rw-r--r--src/integration/assets/antigravity_cli/herdr-agent-state.ps157
-rw-r--r--src/integration/assets/antigravity_cli/herdr-agent-state.sh77
-rw-r--r--src/integration/env.rs8
-rw-r--r--src/integration/mod.rs27
-rw-r--r--src/integration/registry.rs13
-rw-r--r--src/integration/targets.rs141
-rw-r--r--src/integration/tests.rs184
-rw-r--r--src/integration/types.rs14
-rw-r--r--src/terminal/state.rs213
14 files changed, 723 insertions, 129 deletions
diff --git a/src/agent_resume.rs b/src/agent_resume.rs
index f1389b54..55b7d51f 100644
--- a/src/agent_resume.rs
+++ b/src/agent_resume.rs
@@ -187,6 +187,13 @@ pub fn plan(source: &str, agent: &str, session_ref: &AgentSessionRef) -> Option<
session_ref.value.clone(),
]
}
+ ("herdr:antigravity_cli", "agy", AgentSessionRefKind::Id) => {
+ vec![
+ "agy".into(),
+ "--conversation".into(),
+ session_ref.value.clone(),
+ ]
+ }
("herdr:grok", "grok", AgentSessionRefKind::Id) => {
vec!["grok".into(), "--resume".into(), session_ref.value.clone()]
}
@@ -224,6 +231,7 @@ pub(crate) fn is_official_agent_source(source: &str, agent: &str) -> bool {
| ("herdr:qodercli", "qodercli")
| ("herdr:kilo", "kilo")
| ("herdr:cursor", "cursor")
+ | ("herdr:antigravity_cli", "agy")
| ("herdr:grok", "grok")
)
}
@@ -409,6 +417,16 @@ mod tests {
);
assert_eq!(
plan(
+ "herdr:antigravity_cli",
+ "agy",
+ &AgentSessionRef::id("agy-session").unwrap()
+ )
+ .unwrap()
+ .argv,
+ vec!["agy", "--conversation", "agy-session"]
+ );
+ assert_eq!(
+ plan(
"herdr:grok",
"grok",
&AgentSessionRef::id("grok-session").unwrap()
@@ -543,6 +561,12 @@ mod tests {
.unwrap();
assert_eq!(session_ref.kind, AgentSessionRefKind::Id);
assert_eq!(session_ref.value, "qoder-id");
+
+ let session_ref =
+ session_ref_from_report("herdr:antigravity_cli", "agy", Some("agy-id".into()), None)
+ .unwrap();
+ assert_eq!(session_ref.kind, AgentSessionRefKind::Id);
+ assert_eq!(session_ref.value, "agy-id");
}
#[test]
@@ -676,5 +700,19 @@ mod tests {
"devin-session"
)
.is_some());
+ assert!(session_ref_from_snapshot(
+ "herdr:antigravity_cli",
+ "agy",
+ AgentSessionRefKind::Id,
+ "agy-session"
+ )
+ .is_some());
+ let agy_session = absolute_test_path("agy-session");
+ assert!(plan(
+ "herdr:antigravity_cli",
+ "agy",
+ &AgentSessionRef::path(&agy_session).unwrap()
+ )
+ .is_none());
}
}
diff --git a/src/api/schema/integrations.rs b/src/api/schema/integrations.rs
index 8681de5d..ef55f8e2 100644
--- a/src/api/schema/integrations.rs
+++ b/src/api/schema/integrations.rs
@@ -27,11 +27,12 @@ pub enum IntegrationTarget {
Qodercli,
Cursor,
Mastracode,
+ AntigravityCli,
Grok,
}
impl IntegrationTarget {
- pub(crate) const ALL: [Self; 15] = [
+ pub(crate) const ALL: [Self; 16] = [
Self::Pi,
Self::Omp,
Self::Claude,
@@ -46,6 +47,7 @@ impl IntegrationTarget {
Self::Qodercli,
Self::Cursor,
Self::Mastracode,
+ Self::AntigravityCli,
Self::Grok,
];
}
diff --git a/src/cli/integration.rs b/src/cli/integration.rs
index 265bbf95..236cebe7 100644
--- a/src/cli/integration.rs
+++ b/src/cli/integration.rs
@@ -129,11 +129,12 @@ fn parse_integration_target(
"qodercli" => IntegrationTarget::Qodercli,
"cursor" => IntegrationTarget::Cursor,
"mastracode" => IntegrationTarget::Mastracode,
+ "antigravity-cli" | "antigravity_cli" => IntegrationTarget::AntigravityCli,
"grok" => IntegrationTarget::Grok,
_ => {
eprintln!("unknown integration target: {target}");
eprintln!(
- "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, cursor, mastracode, grok"
+ "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, cursor, mastracode, antigravity-cli, grok"
);
return Ok(None);
}
@@ -158,6 +159,7 @@ fn print_integration_help() {
eprintln!(" herdr integration install qodercli");
eprintln!(" herdr integration install cursor");
eprintln!(" herdr integration install mastracode");
+ eprintln!(" herdr integration install antigravity-cli");
eprintln!(" herdr integration install grok");
eprintln!(" herdr integration uninstall pi");
eprintln!(" herdr integration uninstall omp");
@@ -173,6 +175,7 @@ fn print_integration_help() {
eprintln!(" herdr integration uninstall qodercli");
eprintln!(" herdr integration uninstall cursor");
eprintln!(" herdr integration uninstall mastracode");
+ eprintln!(" herdr integration uninstall antigravity-cli");
eprintln!(" herdr integration uninstall grok");
eprintln!(" herdr integration status [--outdated-only]");
}
diff --git a/src/detect/mod.rs b/src/detect/mod.rs
index 33c4361c..389407a2 100644
--- a/src/detect/mod.rs
+++ b/src/detect/mod.rs
@@ -293,7 +293,10 @@ pub(crate) fn full_lifecycle_hook_authority(source: &str, agent_label: &str) ->
}
pub(crate) fn session_identity_only_integration(source: &str, agent_label: &str) -> bool {
- (source, agent_label) == ("herdr:hermes", "hermes")
+ matches!(
+ (source, agent_label),
+ ("herdr:hermes", "hermes") | ("herdr:antigravity_cli", "agy")
+ )
}
// ---------------------------------------------------------------------------
@@ -771,10 +774,15 @@ mod tests {
}
#[test]
- fn hermes_session_integration_leaves_state_to_screen_detection() {
- assert!(!full_lifecycle_hook_authority("herdr:hermes", "hermes"));
- assert!(session_identity_only_integration("herdr:hermes", "hermes"));
- assert!(Agent::SCREEN_MANIFEST_AGENTS.contains(&Agent::Hermes));
+ fn session_identity_integrations_leave_state_to_screen_detection() {
+ for (source, label, agent) in [
+ ("herdr:hermes", "hermes", Agent::Hermes),
+ ("herdr:antigravity_cli", "agy", Agent::Antigravity),
+ ] {
+ assert!(!full_lifecycle_hook_authority(source, label));
+ assert!(session_identity_only_integration(source, label));
+ assert!(Agent::SCREEN_MANIFEST_AGENTS.contains(&agent));
+ }
}
#[test]
diff --git a/src/integration/actions.rs b/src/integration/actions.rs
index c00e10b7..3cb73774 100644
--- a/src/integration/actions.rs
+++ b/src/integration/actions.rs
@@ -2,12 +2,13 @@ use std::io;
use super::registry::{integration_target_label, integration_target_supported};
use super::targets::{
- install_claude, install_codex, install_copilot, install_cursor, install_devin, install_droid,
- install_grok, install_hermes, install_kilo, install_kimi, install_mastracode, install_omp,
- install_opencode, install_pi, install_qodercli, uninstall_claude, uninstall_codex,
- uninstall_copilot, uninstall_cursor, uninstall_devin, uninstall_droid, uninstall_grok,
- uninstall_hermes, uninstall_kilo, uninstall_kimi, uninstall_mastracode, uninstall_omp,
- uninstall_opencode, uninstall_pi, uninstall_qodercli,
+ install_antigravity_cli, install_claude, install_codex, install_copilot, install_cursor,
+ install_devin, install_droid, install_grok, install_hermes, install_kilo, install_kimi,
+ install_mastracode, install_omp, install_opencode, install_pi, install_qodercli,
+ uninstall_antigravity_cli, uninstall_claude, uninstall_codex, uninstall_copilot,
+ uninstall_cursor, uninstall_devin, uninstall_droid, uninstall_grok, uninstall_hermes,
+ uninstall_kilo, uninstall_kimi, uninstall_mastracode, uninstall_omp, uninstall_opencode,
+ uninstall_pi, uninstall_qodercli,
};
use super::version::{agent_version_requirement, enforce_agent_version};
use super::{KIMI_MIN_VERSION, PI_EXTENSION_INSTALL_NAME};
@@ -204,6 +205,19 @@ fn install_target_inner(target: crate::api::schema::IntegrationTarget) -> io::Re
),
]
}
+ crate::api::schema::IntegrationTarget::AntigravityCli => {
+ let installed = install_antigravity_cli()?;
+ vec![
+ format!(
+ "installed antigravity-cli integration hook to {}",
+ installed.hook_path.display()
+ ),
+ format!(
+ "ensured antigravity-cli hooks at {}",
+ installed.hooks_path.display()
+ ),
+ ]
+ }
crate::api::schema::IntegrationTarget::Grok => {
let installed = install_grok()?;
vec![
@@ -571,6 +585,33 @@ pub(crate) fn uninstall_target(
}
messages
}
+ crate::api::schema::IntegrationTarget::AntigravityCli => {
+ let result = uninstall_antigravity_cli()?;
+ let mut messages = Vec::new();
+ if result.removed_hook_file {
+ messages.push(format!(
+ "removed antigravity-cli hook at {}",
+ result.hook_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no antigravity-cli hook found at {}",
+ result.hook_path.display()
+ ));
+ }
+ if result.updated_hooks {
+ messages.push(format!(
+ "removed herdr antigravity-cli hook entries from {}",
+ result.hooks_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no herdr antigravity-cli hook entries found in {}",
+ result.hooks_path.display()
+ ));
+ }
+ messages
+ }
crate::api::schema::IntegrationTarget::Grok => {
let result = uninstall_grok()?;
let mut messages = Vec::new();
diff --git a/src/integration/assets/antigravity_cli/herdr-agent-state.ps1 b/src/integration/assets/antigravity_cli/herdr-agent-state.ps1
new file mode 100644
index 00000000..aecb42a9
--- /dev/null
+++ b/src/integration/assets/antigravity_cli/herdr-agent-state.ps1
@@ -0,0 +1,57 @@
+# installed by herdr
+# managed by herdr; reinstalling or updating the integration overwrites this file.
+# add custom hooks beside this file instead of editing it.
+# HERDR_INTEGRATION_ID=antigravity_cli
+# HERDR_INTEGRATION_VERSION=1
+
+# Session-only: this hook reports the Antigravity conversation so Herdr can
+# resume the pane. Lifecycle state comes from Herdr's screen detection.
+
+param([string]$Action = "")
+
+# Antigravity CLI expects a JSON object on stdout and this hook never injects
+# anything, so every exit path emits an empty object.
+function Exit-Hook {
+ Write-Output "{}"
+ exit 0
+}
+
+if ($Action -ne "session") { Exit-Hook }
+if ($env:HERDR_ENV -ne "1") { Exit-Hook }
+if ([string]::IsNullOrWhiteSpace($env:HERDR_PANE_ID)) { Exit-Hook }
+
+$inputText = [Console]::In.ReadToEnd()
+try {
+ $payload = if ([string]::IsNullOrWhiteSpace($inputText)) { $null } else { $inputText | ConvertFrom-Json }
+} catch {
+ Exit-Hook
+}
+
+if ($null -eq $payload) { Exit-Hook }
+
+$conversationId = if ($payload.conversationId -is [string]) { $payload.conversationId } else { $null }
+if ([string]::IsNullOrWhiteSpace($conversationId)) { Exit-Hook }
+
+$seq = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
+try {
+ $sessionArgs = @(
+ "pane",
+ "report-agent-session",
+ $env:HERDR_PANE_ID,
+ "--source",
+ "herdr:antigravity_cli",
+ "--agent",
+ "agy",
+ "--seq",
+ "$seq",
+ "--agent-session-id",
+ "$conversationId"
+ )
+ if ($payload.transcriptPath -is [string] -and -not [string]::IsNullOrWhiteSpace($payload.transcriptPath)) {
+ $sessionArgs += @("--agent-session-path", "$($payload.transcriptPath)")
+ }
+ & herdr @sessionArgs 2>$null | Out-Null
+} catch {
+}
+
+Exit-Hook
diff --git a/src/integration/assets/antigravity_cli/herdr-agent-state.sh b/src/integration/assets/antigravity_cli/herdr-agent-state.sh
new file mode 100644
index 00000000..dd3db028
--- /dev/null
+++ b/src/integration/assets/antigravity_cli/herdr-agent-state.sh
@@ -0,0 +1,77 @@
+#!/bin/sh
+# installed by herdr
+# managed by herdr; reinstalling or updating the integration overwrites this file.
+# add custom hooks beside this file instead of editing it.
+# HERDR_INTEGRATION_ID=antigravity_cli
+# HERDR_INTEGRATION_VERSION=1
+
+# Session-only: this hook reports the Antigravity conversation so Herdr can
+# resume the pane. Lifecycle state comes from Herdr's screen detection.
+
+set -eu
+
+# Antigravity CLI expects a JSON object on stdout and this hook never injects
+# anything, so every exit path emits an empty object.
+emit_and_exit() {
+ printf '{}\n'
+ exit 0
+}
+
+[ "${1:-}" = "session" ] || emit_and_exit
+[ "${HERDR_ENV:-}" = "1" ] || emit_and_exit
+[ -n "${HERDR_SOCKET_PATH:-}" ] || emit_and_exit
+[ -n "${HERDR_PANE_ID:-}" ] || emit_and_exit
+command -v python3 >/dev/null 2>&1 || emit_and_exit
+
+python3 -c '
+import json
+import os
+import socket
+import sys
+import time
+
+try:
+ payload = json.load(sys.stdin)
+except Exception:
+ raise SystemExit(0)
+
+if not isinstance(payload, dict):
+ raise SystemExit(0)
+
+def text(name):
+ value = payload.get(name)
+ return value if isinstance(value, str) and value else None
+
+session_id = text("conversationId")
+if session_id is None:
+ raise SystemExit(0)
+
+seq = time.time_ns()
+params = {
+ "pane_id": os.environ["HERDR_PANE_ID"],
+ "source": "herdr:antigravity_cli",
+ "agent": "agy",
+ "seq": seq,
+ "agent_session_id": session_id,
+}
+
+transcript_path = text("transcriptPath")
+if transcript_path is not None:
+ params["agent_session_path"] = transcript_path
+
+request = json.dumps({
+ "id": f"herdr:antigravity_cli:{seq}",
+ "method": "pane.report_agent_session",
+ "params": params,
+})
+try:
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client:
+ client.settimeout(0.5)
+ client.connect(os.environ["HERDR_SOCKET_PATH"])
+ client.sendall((request + "\n").encode())
+ client.recv(4096)
+except Exception:
+ pass
+' 2>/dev/null || true
+
+emit_and_exit
diff --git a/src/integration/env.rs b/src/integration/env.rs
index 2d0feeb0..d76ce66c 100644
--- a/src/integration/env.rs
+++ b/src/integration/env.rs
@@ -17,6 +17,7 @@ pub(crate) const KIMI_CODE_HOME_ENV_VAR: &str = "KIMI_CODE_HOME";
pub(crate) const COPILOT_HOME_ENV_VAR: &str = "COPILOT_HOME";
pub(crate) const QODERCLI_CONFIG_DIR_ENV_VAR: &str = "QODER_CONFIG_DIR";
pub(crate) const CURSOR_CONFIG_DIR_ENV_VAR: &str = "CURSOR_CONFIG_DIR";
+pub(crate) const ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR: &str = "ANTIGRAVITY_CLI_CONFIG_DIR";
pub(crate) const GROK_CONFIG_DIR_ENV_VAR: &str = "GROK_CONFIG_DIR";
/// The grok CLI's own config-home override (documented alongside
/// `$GROK_HOME/config.toml` and `$GROK_HOME/auth.json`).
@@ -142,6 +143,13 @@ pub(crate) fn mastracode_dir() -> io::Result<PathBuf> {
Ok(home_dir()?.join(".mastracode"))
}
+pub(crate) fn antigravity_cli_dir() -> io::Result<PathBuf> {
+ // Antigravity CLI discovers global customizations (hooks.json included)
+ // from ~/.gemini/config; ~/.gemini/antigravity-cli holds runtime data and
+ // is never read for hooks.
+ config_dir_from_env_or_home(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR, &[".gemini", "config"])
+}
+
pub(crate) fn grok_dir() -> io::Result<PathBuf> {
// GROK_CONFIG_DIR is a herdr-level override only (primarily a test
// seam); the grok CLI does not honor it, so it stays first and explicit.
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index b691dce9..045d1bce 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -196,6 +196,32 @@ const QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS: [(&str, &str); 12] = [
const CURSOR_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
const CURSOR_HOOK_ASSET: &str = include_str!("assets/cursor/herdr-agent-state.sh");
const CURSOR_INTEGRATION_VERSION: u32 = 1;
+#[cfg(windows)]
+const ANTIGRAVITY_CLI_HOOK_INSTALL_NAME: &str = "herdr-agent-state.ps1";
+#[cfg(not(windows))]
+const ANTIGRAVITY_CLI_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
+#[cfg(windows)]
+const ANTIGRAVITY_CLI_HOOK_ASSET: &str =
+ include_str!("assets/antigravity_cli/herdr-agent-state.ps1");
+#[cfg(not(windows))]
+const ANTIGRAVITY_CLI_HOOK_ASSET: &str =
+ include_str!("assets/antigravity_cli/herdr-agent-state.sh");
+const ANTIGRAVITY_CLI_INTEGRATION_VERSION: u32 = 1;
+/// Antigravity CLI keys `hooks.json` by hook name, so every Herdr entry lives
+/// under one Herdr-owned block that install rewrites and uninstall removes.
+const ANTIGRAVITY_CLI_HOOK_BLOCK_NAME: &str = "herdr";
+const ANTIGRAVITY_CLI_HOOK_TIMEOUT_SEC: u64 = 10;
+/// `(event, reported action)`. Session-only: `PreInvocation` is the only event
+/// we need because it carries `conversationId`. The others cannot express
+/// lifecycle safely — Antigravity CLI has no blocked event, `PostInvocation` is
+/// skipped on interruption, and `Stop` is end-of-turn rather than process exit.
+/// Screen detection owns agent state instead.
+///
+/// `PreInvocation` takes a flat handler list; only the `PreToolUse`/`PostToolUse`
+/// events accept a `matcher`/`hooks` wrapper, and sending one here would
+/// invalidate the whole file.
+const ANTIGRAVITY_CLI_HOOK_EVENTS: [(&str, &str); 1] = [("PreInvocation", "session")];
+const INTEGRATION_VERSION_MARKER: &str = "HERDR_INTEGRATION_VERSION=";
const MASTRACODE_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
const MASTRACODE_HOOK_ASSET: &str = include_str!("assets/mastracode/herdr-agent-state.sh");
const MASTRACODE_INTEGRATION_VERSION: u32 = 2;
@@ -219,7 +245,6 @@ const GROK_HOOK_INSTALL_NAME: &str = "herdr-agent-state.sh";
const GROK_HOOK_CONFIG_INSTALL_NAME: &str = "herdr.json";
const GROK_HOOK_ASSET: &str = include_str!("assets/grok/herdr-agent-state.sh");
const GROK_INTEGRATION_VERSION: u32 = 1;
-const INTEGRATION_VERSION_MARKER: &str = "HERDR_INTEGRATION_VERSION=";
pub(crate) const INSTALL_WARNING_PREFIX: &str = "warning:";
diff --git a/src/integration/registry.rs b/src/integration/registry.rs
index 10500df5..7041eb1b 100644
--- a/src/integration/registry.rs
+++ b/src/integration/registry.rs
@@ -22,6 +22,7 @@ pub(crate) fn integration_target_label(
crate::api::schema::IntegrationTarget::Qodercli => "qodercli",
crate::api::schema::IntegrationTarget::Cursor => "cursor",
crate::api::schema::IntegrationTarget::Mastracode => "mastracode",
+ crate::api::schema::IntegrationTarget::AntigravityCli => "antigravity-cli",
crate::api::schema::IntegrationTarget::Grok => "grok",
}
}
@@ -50,6 +51,7 @@ pub(crate) fn integration_target_command_names(
crate::api::schema::IntegrationTarget::Qodercli => qodercli_command_names(),
crate::api::schema::IntegrationTarget::Cursor => cursor_command_names(),
crate::api::schema::IntegrationTarget::Mastracode => &["mastracode"],
+ crate::api::schema::IntegrationTarget::AntigravityCli => &["agy"],
crate::api::schema::IntegrationTarget::Grok => &["grok"],
}
}
@@ -73,6 +75,7 @@ pub(crate) fn integration_target_supported(target: crate::api::schema::Integrati
| crate::api::schema::IntegrationTarget::Droid
| crate::api::schema::IntegrationTarget::Kimi
| crate::api::schema::IntegrationTarget::Qodercli
+ | crate::api::schema::IntegrationTarget::AntigravityCli
)
}
@@ -259,7 +262,7 @@ fn integration_specs() -> [(
crate::api::schema::IntegrationTarget,
io::Result<PathBuf>,
u32,
-); 15] {
+); 16] {
[
(
crate::api::schema::IntegrationTarget::Pi,
@@ -335,6 +338,14 @@ fn integration_specs() -> [(
super::MASTRACODE_INTEGRATION_VERSION,
),
(
+ crate::api::schema::IntegrationTarget::AntigravityCli,
+ antigravity_cli_dir().map(|dir| {
+ dir.join("hooks")
+ .join(super::ANTIGRAVITY_CLI_HOOK_INSTALL_NAME)
+ }),
+ super::ANTIGRAVITY_CLI_INTEGRATION_VERSION,
+ ),
+ (
crate::api::schema::IntegrationTarget::Grok,
grok_dir().map(|dir| dir.join("hooks").join(super::GROK_HOOK_INSTALL_NAME)),
super::GROK_INTEGRATION_VERSION,
diff --git a/src/integration/targets.rs b/src/integration/targets.rs
index 87000eae..575c7e46 100644
--- a/src/integration/targets.rs
+++ b/src/integration/targets.rs
@@ -2,7 +2,7 @@ use std::fs;
use std::io;
use std::path::{Path, PathBuf};
-use serde_json::{json, Value};
+use serde_json::{json, Map, Value};
use super::command::{hook_command, shell_single_quote};
use super::config_edit::{
@@ -13,32 +13,33 @@ use super::config_edit::{
remove_hook_commands, remove_kimi_config_block, remove_simple_command_hook,
};
use super::env::{
- claude_dir, codex_dir, copilot_dir, cursor_dir, devin_dir, droid_dir, grok_dir, hermes_dir,
- hermes_plugin_dir, kilo_dir, kimi_dir, mastracode_dir, omp_extension_dir, opencode_dir,
- pi_extension_dir, qodercli_dir,
+ antigravity_cli_dir, claude_dir, codex_dir, copilot_dir, cursor_dir, devin_dir, droid_dir,
+ grok_dir, hermes_dir, hermes_plugin_dir, kilo_dir, kimi_dir, mastracode_dir, omp_extension_dir,
+ opencode_dir, pi_extension_dir, qodercli_dir,
};
use super::file_ops::{
make_executable, remove_dir_all_if_exists, remove_file_if_exists, remove_legacy_bash_hook_file,
};
use super::types::{
- ClaudeInstallPaths, ClaudeUninstallResult, CodexInstallPaths, CodexUninstallResult,
- CopilotInstallPaths, CopilotUninstallResult, CursorInstallPaths, CursorUninstallResult,
- DevinInstallPaths, DevinUninstallResult, DroidInstallPaths, DroidUninstallResult,
- GrokInstallPaths, GrokUninstallResult, HermesInstallPaths, HermesUninstallResult,
- KiloInstallPaths, KiloUninstallResult, KimiInstallPaths, KimiUninstallResult,
- MastracodeInstallPaths, MastracodeUninstallResult, OmpInstallPaths, OmpUninstallResult,
- OpenCodeInstallPaths, OpenCodeUninstallResult, PiUninstallResult, QodercliInstallPaths,
- QodercliUninstallResult,
+ AntigravityCliInstallPaths, AntigravityCliUninstallResult, ClaudeInstallPaths,
+ ClaudeUninstallResult, CodexInstallPaths, CodexUninstallResult, CopilotInstallPaths,
+ CopilotUninstallResult, CursorInstallPaths, CursorUninstallResult, DevinInstallPaths,
+ DevinUninstallResult, DroidInstallPaths, DroidUninstallResult, GrokInstallPaths,
+ GrokUninstallResult, HermesInstallPaths, HermesUninstallResult, KiloInstallPaths,
+ KiloUninstallResult, KimiInstallPaths, KimiUninstallResult, MastracodeInstallPaths,
+ MastracodeUninstallResult, OmpInstallPaths, OmpUninstallResult, OpenCodeInstallPaths,
+ OpenCodeUninstallResult, PiUninstallResult, QodercliInstallPaths, QodercliUninstallResult,
};
use super::{
- CLAUDE_HOOK_ASSET, CLAUDE_HOOK_INSTALL_NAME, CODEX_HOOK_ASSET, CODEX_HOOK_INSTALL_NAME,
- COPILOT_HOOK_ASSET, COPILOT_HOOK_EVENTS, COPILOT_HOOK_INSTALL_NAME,
- COPILOT_REMOVED_LIFECYCLE_HOOK_EVENTS, CURSOR_HOOK_ASSET, CURSOR_HOOK_INSTALL_NAME,
- DEVIN_HOOK_ASSET, DEVIN_HOOK_EVENTS, DEVIN_HOOK_INSTALL_NAME,
- DEVIN_REMOVED_LIFECYCLE_HOOK_EVENTS, DROID_HOOK_ASSET, DROID_HOOK_EVENTS,
- DROID_HOOK_INSTALL_NAME, DROID_REMOVED_LIFECYCLE_HOOK_EVENTS, GROK_HOOK_ASSET,
- GROK_HOOK_CONFIG_INSTALL_NAME, GROK_HOOK_INSTALL_NAME, HERMES_PLUGIN_INIT_ASSET,
- HERMES_PLUGIN_INIT_INSTALL_NAME, HERMES_PLUGIN_MANIFEST_ASSET,
+ ANTIGRAVITY_CLI_HOOK_ASSET, ANTIGRAVITY_CLI_HOOK_BLOCK_NAME, ANTIGRAVITY_CLI_HOOK_EVENTS,
+ ANTIGRAVITY_CLI_HOOK_INSTALL_NAME, ANTIGRAVITY_CLI_HOOK_TIMEOUT_SEC, CLAUDE_HOOK_ASSET,
+ CLAUDE_HOOK_INSTALL_NAME, CODEX_HOOK_ASSET, CODEX_HOOK_INSTALL_NAME, COPILOT_HOOK_ASSET,
+ COPILOT_HOOK_EVENTS, COPILOT_HOOK_INSTALL_NAME, COPILOT_REMOVED_LIFECYCLE_HOOK_EVENTS,
+ CURSOR_HOOK_ASSET, CURSOR_HOOK_INSTALL_NAME, DEVIN_HOOK_ASSET, DEVIN_HOOK_EVENTS,
+ DEVIN_HOOK_INSTALL_NAME, DEVIN_REMOVED_LIFECYCLE_HOOK_EVENTS, DROID_HOOK_ASSET,
+ DROID_HOOK_EVENTS, DROID_HOOK_INSTALL_NAME, DROID_REMOVED_LIFECYCLE_HOOK_EVENTS,
+ GROK_HOOK_ASSET, GROK_HOOK_CONFIG_INSTALL_NAME, GROK_HOOK_INSTALL_NAME,
+ HERMES_PLUGIN_INIT_ASSET, HERMES_PLUGIN_INIT_INSTALL_NAME, HERMES_PLUGIN_MANIFEST_ASSET,
HERMES_PLUGIN_MANIFEST_INSTALL_NAME, KILO_PLUGIN_ASSET, KILO_PLUGIN_INSTALL_NAME,
KIMI_HOOK_ASSET, KIMI_HOOK_INSTALL_NAME, MASTRACODE_HOOK_ASSET, MASTRACODE_HOOK_EVENTS,
MASTRACODE_HOOK_INSTALL_NAME, MASTRACODE_HOOK_TIMEOUT_MS, MASTRACODE_REMOVED_HOOK_EVENTS,
@@ -1203,6 +1204,106 @@ pub(crate) fn uninstall_mastracode() -> io::Result<MastracodeUninstallResult> {
})
}
+pub(crate) fn install_antigravity_cli() -> io::Result<AntigravityCliInstallPaths> {
+ let dir = antigravity_cli_dir()?;
+ if !dir.is_dir() {
+ return Err(io::Error::other(format!(
+ "antigravity cli config directory not found at {}. install antigravity cli first",
+ dir.display()
+ )));
+ }
+
+ let hooks_dir = dir.join("hooks");
+ fs::create_dir_all(&hooks_dir)?;
+
+ let hook_path = hooks_dir.join(ANTIGRAVITY_CLI_HOOK_INSTALL_NAME);
+ fs::write(&hook_path, ANTIGRAVITY_CLI_HOOK_ASSET)?;
+ make_executable(&hook_path)?;
+
+ let hooks_path = dir.join("hooks.json");
+ let mut hooks_file = if hooks_path.is_file() {
+ serde_json::from_str::<Value>(&fs::read_to_string(&hooks_path)?).map_err(|err| {
+ io::Error::other(format!("failed to parse {}: {err}", hooks_path.display()))
+ })?
+ } else {
+ json!({})
+ };
+
+ let hooks = hooks_file.as_object_mut().ok_or_else(|| {
+ io::Error::other(format!(
+ "antigravity cli hooks file at {} must be a JSON object",
+ hooks_path.display()
+ ))
+ })?;
+
+ // The Herdr block is Herdr-owned, so rewrite it wholesale and leave every
+ // other named hook untouched.
+ hooks.insert(
+ ANTIGRAVITY_CLI_HOOK_BLOCK_NAME.to_string(),
+ antigravity_cli_hook_block(&hook_path),
+ );
+
+ fs::write(&hooks_path, serde_json::to_string_pretty(&hooks_file)?)?;
+
+ Ok(AntigravityCliInstallPaths {
+ hook_path,
+ hooks_path,
+ })
+}
+
+/// Builds the Herdr-owned `hooks.json` block for Antigravity CLI.
+///
+/// Every event Herdr registers takes a flat handler list; the `matcher`/`hooks`
+/// group is only valid for the tool events, which Herdr does not use.
+fn antigravity_cli_hook_block(hook_path: &Path) -> Value {
+ let mut block = Map::new();
+ for (event, action) in ANTIGRAVITY_CLI_HOOK_EVENTS {
+ let handler = json!({
+ "type": "command",
+ "command": hook_command(hook_path, Some(action)),
+ "timeout": ANTIGRAVITY_CLI_HOOK_TIMEOUT_SEC,
+ });
+ block.insert(event.to_string(), json!([handler]));
+ }
+ Value::Object(block)
+}
+
+pub(crate) fn uninstall_antigravity_cli() -> io::Result<AntigravityCliUninstallResult> {
+ let dir = antigravity_cli_dir()?;
+ let hook_path = dir.join("hooks").join(ANTIGRAVITY_CLI_HOOK_INSTALL_NAME);
+ let hooks_path = dir.join("hooks.json");
+ let mut updated_hooks = false;
+
+ if hooks_path.is_file() {
+ let mut hooks_file = serde_json::from_str::<Value>(&fs::read_to_string(&hooks_path)?)
+ .map_err(|err| {
+ io::Error::other(format!("failed to parse {}: {err}", hooks_path.display()))
+ })?;
+
+ let hooks = hooks_file.as_object_mut().ok_or_else(|| {
+ io::Error::other(format!(
+ "antigravity cli hooks file at {} must be a JSON object",
+ hooks_path.display()
+ ))
+ })?;
+
+ updated_hooks = hooks.remove(ANTIGRAVITY_CLI_HOOK_BLOCK_NAME).is_some();
+
+ if updated_hooks {
+ fs::write(&hooks_path, serde_json::to_string_pretty(&hooks_file)?)?;
+ }
+ }
+
+ let removed_hook_file = remove_file_if_exists(&hook_path)?;
+
+ Ok(AntigravityCliUninstallResult {
+ hook_path,
+ hooks_path,
+ removed_hook_file,
+ updated_hooks,
+ })
+}
+
/// The complete Herdr-owned Grok hook config. Installation and status share
/// this value so any config drift is reported as outdated.
pub(crate) fn grok_hook_config(hook_path: &Path) -> Value {
diff --git a/src/integration/tests.rs b/src/integration/tests.rs
index ff616264..7abf7be2 100644
--- a/src/integration/tests.rs
+++ b/src/integration/tests.rs
@@ -102,6 +102,7 @@ fn clear_integration_path_env() {
std::env::remove_var("XDG_CONFIG_HOME");
std::env::remove_var(QODERCLI_CONFIG_DIR_ENV_VAR);
std::env::remove_var(CURSOR_CONFIG_DIR_ENV_VAR);
+ std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR);
std::env::remove_var(GROK_CONFIG_DIR_ENV_VAR);
std::env::remove_var(GROK_HOME_ENV_VAR);
}
@@ -3645,6 +3646,154 @@ fn uninstall_mastracode_errors_when_event_value_not_array() {
}
#[test]
+fn install_antigravity_cli_writes_hook_and_updates_hooks_json() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let agy_dir = base.join(".gemini").join("config");
+ fs::create_dir_all(&agy_dir).unwrap();
+ fs::write(
+ agy_dir.join("hooks.json"),
+ r#"{"lint-checker":{"PreInvocation":[{"type":"command","command":"echo keep-me"}]}}"#,
+ )
+ .unwrap();
+ std::env::set_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR, &agy_dir);
+
+ let installed = install_antigravity_cli().unwrap();
+
+ assert_eq!(
+ installed.hook_path,
+ agy_dir
+ .join("hooks")
+ .join(ANTIGRAVITY_CLI_HOOK_INSTALL_NAME)
+ );
+ assert_eq!(installed.hooks_path, agy_dir.join("hooks.json"));
+ assert_eq!(
+ fs::read_to_string(&installed.hook_path).unwrap(),
+ ANTIGRAVITY_CLI_HOOK_ASSET
+ );
+
+ let hooks_file: Value =
+ serde_json::from_str(&fs::read_to_string(agy_dir.join("hooks.json")).unwrap()).unwrap();
+ let hooks = hooks_file.as_object().unwrap();
+
+ // Herdr entries live under a named hook block; Antigravity CLI rejects a
+ // file whose top level maps event names straight to arrays.
+ let block = hooks
+ .get(ANTIGRAVITY_CLI_HOOK_BLOCK_NAME)
+ .and_then(Value::as_object)
+ .unwrap();
+
+ for (event, action) in ANTIGRAVITY_CLI_HOOK_EVENTS {
+ let entries = block.get(event).and_then(Value::as_array).unwrap();
+ assert_eq!(entries.len(), 1, "{event} should hold one Herdr entry");
+ let handler = &entries[0];
+
+ // Handlers must be a flat list; the matcher/hooks wrapper is only
+ // valid for tool events and invalidates the whole file here.
+ assert!(
+ handler.get("matcher").is_none() && handler.get("hooks").is_none(),
+ "{event} must be a flat handler, got {handler}"
+ );
+
+ assert_eq!(handler.get("type").and_then(Value::as_str), Some("command"));
+ assert_eq!(
+ handler.get("timeout").and_then(Value::as_u64),
+ Some(ANTIGRAVITY_CLI_HOOK_TIMEOUT_SEC)
+ );
+ let command = handler.get("command").and_then(Value::as_str).unwrap();
+ assert!(command.contains("herdr-agent-state"));
+ assert!(command.ends_with(action));
+ }
+
+ // The integration is session-only. Antigravity CLI cannot express blocked
+ // state, skips PostInvocation on interruption, and fires Stop at end of
+ // turn rather than process exit, so Herdr never claims lifecycle authority
+ // here and screen detection owns agent state.
+ for event in ["PreToolUse", "PostToolUse", "PostInvocation", "Stop"] {
+ assert!(
+ block.get(event).is_none(),
+ "{event} must not be registered; lifecycle stays with screen detection"
+ );
+ }
+
+ // Other named hooks are left untouched.
+ assert_eq!(
+ hooks
+ .get("lint-checker")
+ .and_then(|block| block.get("PreInvocation"))
+ .and_then(Value::as_array)
+ .and_then(|entries| entries.first())
+ .and_then(|entry| entry.get("command"))
+ .and_then(Value::as_str),
+ Some("echo keep-me")
+ );
+
+ std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR);
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn install_antigravity_cli_rewrites_stale_herdr_block() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let agy_dir = base.join(".gemini").join("config");
+ fs::create_dir_all(&agy_dir).unwrap();
+ // An older Herdr install claimed lifecycle authority, wrapped events in
+ // matcher/hooks, and left entries Antigravity CLI now rejects.
+ fs::write(
+ agy_dir.join("hooks.json"),
+ r#"{"herdr":{"Stop":[{"matcher":"*","hooks":[{"type":"command","command":"stale"}]}],"PostInvocation":[{"type":"command","command":"stale idle"}],"Legacy":[]}}"#,
+ )
+ .unwrap();
+ std::env::set_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR, &agy_dir);
+
+ install_antigravity_cli().unwrap();
+
+ let hooks_file: Value =
+ serde_json::from_str(&fs::read_to_string(agy_dir.join("hooks.json")).unwrap()).unwrap();
+ let block = hooks_file
+ .get(ANTIGRAVITY_CLI_HOOK_BLOCK_NAME)
+ .and_then(Value::as_object)
+ .unwrap();
+
+ // The block is Herdr-owned and rewritten wholesale, so a stale lifecycle
+ // install is migrated to session-only rather than merged with.
+ assert_eq!(
+ block.keys().map(String::as_str).collect::<Vec<_>>(),
+ vec!["PreInvocation"],
+ "stale lifecycle events should be gone"
+ );
+ let entries = block
+ .get("PreInvocation")
+ .and_then(Value::as_array)
+ .unwrap();
+ assert_eq!(entries.len(), 1);
+ assert!(entries[0].get("hooks").is_none());
+ assert!(entries[0]
+ .get("command")
+ .and_then(Value::as_str)
+ .is_some_and(|command| command.contains("herdr-agent-state")));
+
+ std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR);
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn install_antigravity_cli_errors_when_config_dir_missing() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let agy_dir = base.join(".gemini").join("config");
+ std::env::set_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR, &agy_dir);
+
+ let err = install_antigravity_cli().unwrap_err();
+ assert!(err.to_string().contains("install antigravity cli first"));
+ assert!(!agy_dir.exists(), "install must not create the config dir");
+
+ std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR);
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
fn grok_v1_integration_status_is_current() {
let _lock = integration_env_lock();
let base = unique_base();
@@ -3756,6 +3905,41 @@ fn grok_status_reports_outdated_when_hook_config_missing_or_broken() {
}
#[test]
+fn uninstall_antigravity_cli_removes_hooks_json_entries_and_hook_file() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let agy_dir = base.join(".gemini").join("config");
+ fs::create_dir_all(&agy_dir).unwrap();
+ fs::write(
+ agy_dir.join("hooks.json"),
+ r#"{"lint-checker":{"PreInvocation":[{"type":"command","command":"echo keep-me"}]}}"#,
+ )
+ .unwrap();
+ std::env::set_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR, &agy_dir);
+
+ // Install first
+ let installed = install_antigravity_cli().unwrap();
+ assert!(installed.hook_path.is_file());
+
+ // Uninstall
+ let result = uninstall_antigravity_cli().unwrap();
+ assert!(result.removed_hook_file);
+ assert!(!installed.hook_path.is_file());
+ assert!(result.updated_hooks);
+
+ let hooks_file: Value =
+ serde_json::from_str(&fs::read_to_string(agy_dir.join("hooks.json")).unwrap()).unwrap();
+ let hooks = hooks_file.as_object().unwrap();
+
+ // The Herdr block is gone and unrelated named hooks survive.
+ assert!(hooks.get(ANTIGRAVITY_CLI_HOOK_BLOCK_NAME).is_none());
+ assert!(hooks.contains_key("lint-checker"));
+
+ std::env::remove_var(ANTIGRAVITY_CLI_CONFIG_DIR_ENV_VAR);
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
fn grok_dir_honors_grok_home_after_config_dir_seam() {
let _lock = integration_env_lock();
let base = unique_base();
diff --git a/src/integration/types.rs b/src/integration/types.rs
index 93138a3d..09a235b0 100644
--- a/src/integration/types.rs
+++ b/src/integration/types.rs
@@ -241,3 +241,17 @@ pub(crate) struct HermesUninstallResult {
pub removed_plugin_dir: bool,
pub updated_config: bool,
}
+
+#[derive(Debug)]
+pub(crate) struct AntigravityCliInstallPaths {
+ pub hook_path: PathBuf,
+ pub hooks_path: PathBuf,
+}
+
+#[derive(Debug)]
+pub(crate) struct AntigravityCliUninstallResult {
+ pub hook_path: PathBuf,
+ pub hooks_path: PathBuf,
+ pub removed_hook_file: bool,
+ pub updated_hooks: bool,
+}
diff --git a/src/terminal/state.rs b/src/terminal/state.rs
index e806967e..5ed5adc4 100644
--- a/src/terminal/state.rs
+++ b/src/terminal/state.rs
@@ -1234,7 +1234,7 @@ impl TerminalState {
&& current_kind == crate::agent_resume::AgentSessionRefKind::Id
&& session_ref.kind == crate::agent_resume::AgentSessionRefKind::Id
&& current_value != session_ref.value
- && !Self::session_start_source_allows_session_replacement(
+ && !Self::session_report_allows_session_replacement(
source,
agent_label,
session_start_source,
@@ -1267,7 +1267,7 @@ impl TerminalState {
})
}
- fn session_start_source_allows_session_replacement(
+ fn session_report_allows_session_replacement(
source: &str,
agent_label: &str,
session_start_source: Option<&str>,
@@ -1291,6 +1291,7 @@ impl TerminalState {
"omp",
Some("startup" | "new" | "resume" | "fork")
)
+ | ("herdr:antigravity_cli", "agy", None)
)
}
@@ -1413,12 +1414,12 @@ impl TerminalState {
if self.known_agent_label_conflicts_with_detected_agent(&agent_label) {
return None;
}
- let session_replacement_allowed = Self::session_start_source_allows_session_replacement(
+ let session_replacement_allowed = Self::session_report_allows_session_replacement(
&source,
&agent_label,
session_start_source.as_deref(),
);
- let replacing_hermes_session =
+ let replacing_identity_only_session =
crate::detect::session_identity_only_integration(&source, &agent_label)
&& session_replacement_allowed
&& self.current_session_identity_for_persistence().is_some_and(
@@ -1430,7 +1431,7 @@ impl TerminalState {
&& current_value != session_ref.value
},
);
- if replacing_hermes_session && !process_present {
+ if replacing_identity_only_session && !process_present {
return None;
}
let owner_conflicts = self.current_session_owner_conflicts(&source, &agent_label);
@@ -2309,101 +2310,125 @@ mod tests {
}
#[test]
- fn hermes_session_claim_leaves_state_to_detection() {
- let mut terminal = test_terminal();
- terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle);
- let session_ref = crate::agent_resume::AgentSessionRef::id("hermes-root").unwrap();
-
- let session = terminal.set_agent_session_ref_for_session_start(
- "herdr:hermes".into(),
- "hermes".into(),
- Some(session_ref.clone()),
- Some(10),
- Some("startup".into()),
- );
-
- assert!(session.is_some());
- assert!(terminal.hook_authority.is_none());
- assert_eq!(
- terminal
- .persisted_agent_session
- .as_ref()
- .map(|session| &session.session_ref),
- Some(&session_ref)
- );
-
- terminal.set_detected_state(Some(Agent::Hermes), AgentState::Working);
+ fn session_identity_claims_leave_state_to_detection() {
+ for (source, label, agent, start_source, replacement_source) in [
+ (
+ "herdr:hermes",
+ "hermes",
+ Agent::Hermes,
+ Some("startup"),
+ Some("resume"),
+ ),
+ (
+ "herdr:antigravity_cli",
+ "agy",
+ Agent::Antigravity,
+ None,
+ None,
+ ),
+ ] {
+ let mut terminal = test_terminal();
+ terminal.set_detected_state(Some(agent), AgentState::Idle);
+ let first_ref =
+ crate::agent_resume::AgentSessionRef::id(format!("{label}-root")).unwrap();
+ let first = terminal.set_agent_session_ref_for_session_start(
+ source.into(),
+ label.into(),
+ Some(first_ref.clone()),
+ Some(10),
+ start_source.map(str::to_string),
+ );
- assert_eq!(terminal.state, AgentState::Working);
- assert!(terminal.hook_authority.is_none());
+ assert!(first.is_some(), "{label} should accept its session");
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Idle);
+ assert_eq!(
+ terminal
+ .persisted_agent_session
+ .as_ref()
+ .map(|session| &session.session_ref),
+ Some(&first_ref)
+ );
- let replacement_ref =
- crate::agent_resume::AgentSessionRef::id("hermes-replacement").unwrap();
- let replacement = terminal.set_agent_session_ref_for_session_start(
- "herdr:hermes".into(),
- "hermes".into(),
- Some(replacement_ref.clone()),
- Some(11),
- Some("startup".into()),
- );
+ terminal.set_detected_state(Some(agent), AgentState::Working);
+ let replacement_ref =
+ crate::agent_resume::AgentSessionRef::id(format!("{label}-replacement")).unwrap();
+ let replacement = terminal.set_agent_session_ref_for_session_start(
+ source.into(),
+ label.into(),
+ Some(replacement_ref.clone()),
+ Some(11),
+ start_source.map(str::to_string),
+ );
- assert!(replacement.is_some());
- assert_eq!(terminal.state, AgentState::Working);
- assert!(terminal.hook_authority.is_none());
- assert_eq!(
- terminal
- .persisted_agent_session
- .as_ref()
- .map(|session| &session.session_ref),
- Some(&replacement_ref)
- );
+ assert!(
+ replacement.is_some_and(|mutation| mutation.session_ref_changed),
+ "{label} should replace its detected session"
+ );
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Working);
+ assert_eq!(
+ terminal
+ .persisted_agent_session
+ .as_ref()
+ .map(|session| &session.session_ref),
+ Some(&replacement_ref)
+ );
- let legacy_state = terminal.set_hook_authority_with_session_ref(
- "herdr:hermes".into(),
- "hermes".into(),
- AgentState::Blocked,
- None,
- Some(replacement_ref.clone()),
- Some(12),
- );
- assert!(legacy_state.is_none());
- assert_eq!(terminal.state, AgentState::Working);
- assert!(terminal.hook_authority.is_none());
+ let legacy_state = terminal.set_hook_authority_with_session_ref(
+ source.into(),
+ label.into(),
+ AgentState::Blocked,
+ None,
+ Some(replacement_ref.clone()),
+ Some(12),
+ );
+ assert!(legacy_state.is_none());
+ assert!(terminal.hook_authority.is_none());
+ assert_eq!(terminal.state, AgentState::Working);
- terminal.set_detected_state(None, AgentState::Unknown);
- let background_replacement = terminal.set_agent_session_ref_for_session_start(
- "herdr:hermes".into(),
- "hermes".into(),
- crate::agent_resume::AgentSessionRef::id("hermes-background"),
- Some(13),
- Some("resume".into()),
- );
- assert!(background_replacement.is_none());
- assert_eq!(
- terminal
- .persisted_agent_session
- .as_ref()
- .map(|session| &session.session_ref),
- Some(&replacement_ref)
- );
+ terminal.set_detected_state(None, AgentState::Unknown);
+ let background_ref =
+ crate::agent_resume::AgentSessionRef::id(format!("{label}-background")).unwrap();
+ let background_replacement = terminal.set_agent_session_ref_for_session_start(
+ source.into(),
+ label.into(),
+ Some(background_ref.clone()),
+ Some(13),
+ replacement_source.map(str::to_string),
+ );
+ assert!(
+ background_replacement.is_none(),
+ "{label} should reject a background replacement"
+ );
+ assert_eq!(
+ terminal
+ .persisted_agent_session
+ .as_ref()
+ .map(|session| &session.session_ref),
+ Some(&replacement_ref)
+ );
- terminal.set_detected_state(Some(Agent::Hermes), AgentState::Idle);
- let retried_ref = crate::agent_resume::AgentSessionRef::id("hermes-background").unwrap();
- let retried_replacement = terminal.set_agent_session_ref_for_session_start(
- "herdr:hermes".into(),
- "hermes".into(),
- Some(retried_ref.clone()),
- Some(14),
- Some("resume".into()),
- );
- assert!(retried_replacement.is_some());
- assert_eq!(
- terminal
- .persisted_agent_session
- .as_ref()
- .map(|session| &session.session_ref),
- Some(&retried_ref)
- );
+ terminal.set_detected_state(Some(agent), AgentState::Idle);
+ let retried_replacement = terminal.set_agent_session_ref_for_session_start(
+ source.into(),
+ label.into(),
+ Some(background_ref.clone()),
+ Some(14),
+ replacement_source.map(str::to_string),
+ );
+ assert!(
+ retried_replacement.is_some_and(|mutation| mutation.session_ref_changed),
+ "{label} should replace the session once detected"
+ );
+ assert_eq!(
+ terminal
+ .persisted_agent_session
+ .as_ref()
+ .map(|session| &session.session_ref),
+ Some(&background_ref)
+ );
+ }
}
#[test]