aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWard Peeters <ward@coding-tech.com>2026-07-04 00:29:42 +0200
committerGitHub <noreply@github.com>2026-07-04 01:29:42 +0300
commitd0e333451bd794a419e96f28db925772950ea156 (patch)
tree1a6e5d3bd6ac40f8ef454058f069d30202327002
parentd5521e3d250498df4af552fff83fce3d5ef33810 (diff)
feat: add mastracode integration (#788)
* feat: add mastracode integration refs #337 Co-authored-by: Mastra Code (openai/gpt-5.5) <noreply@mastra.ai> Co-authored-by: Ogulcan Celik <ogulcancelik@gmail.com>
-rw-r--r--docs/next/api/herdr-api.schema.json6
-rw-r--r--src/agent_resume.rs35
-rw-r--r--src/api/schema/integrations.rs1
-rw-r--r--src/cli/integration.rs9
-rw-r--r--src/config/sound.rs1
-rw-r--r--src/detect/mod.rs19
-rw-r--r--src/integration/actions.rs49
-rw-r--r--src/integration/assets/mastracode/herdr-agent-state.sh96
-rw-r--r--src/integration/config_edit.rs53
-rw-r--r--src/integration/env.rs4
-rw-r--r--src/integration/mod.rs18
-rw-r--r--src/integration/registry.rs9
-rw-r--r--src/integration/targets.rs112
-rw-r--r--src/integration/tests.rs205
-rw-r--r--src/integration/types.rs14
-rw-r--r--src/terminal/state.rs110
16 files changed, 705 insertions, 36 deletions
diff --git a/docs/next/api/herdr-api.schema.json b/docs/next/api/herdr-api.schema.json
index 8eee84cb..7ee7bfd8 100644
--- a/docs/next/api/herdr-api.schema.json
+++ b/docs/next/api/herdr-api.schema.json
@@ -1512,7 +1512,8 @@
"kilo",
"hermes",
"qodercli",
- "cursor"
+ "cursor",
+ "mastracode"
],
"type": "string"
},
@@ -5927,7 +5928,8 @@
"kilo",
"hermes",
"qodercli",
- "cursor"
+ "cursor",
+ "mastracode"
],
"type": "string"
},
diff --git a/src/agent_resume.rs b/src/agent_resume.rs
index 5c1001cb..facb9cf1 100644
--- a/src/agent_resume.rs
+++ b/src/agent_resume.rs
@@ -140,6 +140,13 @@ pub fn plan(source: &str, agent: &str, session_ref: &AgentSessionRef) -> Option<
("herdr:kimi", "kimi", AgentSessionRefKind::Id) => {
vec!["kimi".into(), "--session".into(), session_ref.value.clone()]
}
+ ("herdr:mastracode", "mastracode", AgentSessionRefKind::Id) => {
+ vec![
+ "mastracode".into(),
+ "--thread".into(),
+ session_ref.value.clone(),
+ ]
+ }
("herdr:pi", "pi", AgentSessionRefKind::Path | AgentSessionRefKind::Id) => {
vec!["pi".into(), "--session".into(), session_ref.value.clone()]
}
@@ -206,6 +213,7 @@ fn is_official_agent_source(source: &str, agent: &str) -> bool {
| ("herdr:droid", "droid")
| ("herdr:kimi", "kimi")
| ("herdr:omp", "omp")
+ | ("herdr:mastracode", "mastracode")
| ("herdr:pi", "pi")
| ("herdr:hermes", "hermes")
| ("herdr:opencode", "opencode")
@@ -316,6 +324,16 @@ mod tests {
);
assert_eq!(
plan(
+ "herdr:mastracode",
+ "mastracode",
+ &AgentSessionRef::id("mastracode-session").unwrap()
+ )
+ .unwrap()
+ .argv,
+ vec!["mastracode", "--thread", "mastracode-session"]
+ );
+ assert_eq!(
+ plan(
"herdr:pi",
"pi",
&AgentSessionRef::path(&pi_session).unwrap()
@@ -490,6 +508,16 @@ mod tests {
assert_eq!(session_ref.kind, AgentSessionRefKind::Id);
assert_eq!(session_ref.value, "kimi-id");
+ let session_ref = session_ref_from_report(
+ "herdr:mastracode",
+ "mastracode",
+ Some("mastracode-id".into()),
+ None,
+ )
+ .unwrap();
+ assert_eq!(session_ref.kind, AgentSessionRefKind::Id);
+ assert_eq!(session_ref.value, "mastracode-id");
+
let session_ref =
session_ref_from_report("herdr:kilo", "kilo", Some("kilo-id".into()), None).unwrap();
assert_eq!(session_ref.kind, AgentSessionRefKind::Id);
@@ -592,6 +620,13 @@ mod tests {
)
.is_none());
assert!(session_ref_from_snapshot(
+ "herdr:mastracode",
+ "mastracode",
+ AgentSessionRefKind::Id,
+ "mastracode-session"
+ )
+ .is_some());
+ assert!(session_ref_from_snapshot(
"herdr:hermes",
"hermes",
AgentSessionRefKind::Id,
diff --git a/src/api/schema/integrations.rs b/src/api/schema/integrations.rs
index a6d808fc..f20b9095 100644
--- a/src/api/schema/integrations.rs
+++ b/src/api/schema/integrations.rs
@@ -26,6 +26,7 @@ pub enum IntegrationTarget {
Hermes,
Qodercli,
Cursor,
+ Mastracode,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
diff --git a/src/cli/integration.rs b/src/cli/integration.rs
index 8dc053f1..c407278e 100644
--- a/src/cli/integration.rs
+++ b/src/cli/integration.rs
@@ -103,13 +103,13 @@ fn parse_integration_target(
) -> std::io::Result<Option<IntegrationTarget>> {
let Some(target) = args.first().map(|arg| arg.as_str()) else {
eprintln!(
- "usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor>"
+ "usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor|mastracode>"
);
return Ok(None);
};
if args.len() != 1 {
eprintln!(
- "usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor>"
+ "usage: herdr integration {action} <pi|omp|claude|codex|copilot|devin|droid|kimi|opencode|kilo|hermes|qodercli|cursor|mastracode>"
);
return Ok(None);
}
@@ -128,10 +128,11 @@ fn parse_integration_target(
"hermes" => IntegrationTarget::Hermes,
"qodercli" => IntegrationTarget::Qodercli,
"cursor" => IntegrationTarget::Cursor,
+ "mastracode" => IntegrationTarget::Mastracode,
_ => {
eprintln!("unknown integration target: {target}");
eprintln!(
- "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, cursor"
+ "currently supported: pi, omp, claude, codex, copilot, devin, droid, kimi, opencode, kilo, hermes, qodercli, cursor, mastracode"
);
return Ok(None);
}
@@ -155,6 +156,7 @@ fn print_integration_help() {
eprintln!(" herdr integration install hermes");
eprintln!(" herdr integration install qodercli");
eprintln!(" herdr integration install cursor");
+ eprintln!(" herdr integration install mastracode");
eprintln!(" herdr integration uninstall pi");
eprintln!(" herdr integration uninstall omp");
eprintln!(" herdr integration uninstall claude");
@@ -168,5 +170,6 @@ fn print_integration_help() {
eprintln!(" herdr integration uninstall hermes");
eprintln!(" herdr integration uninstall qodercli");
eprintln!(" herdr integration uninstall cursor");
+ eprintln!(" herdr integration uninstall mastracode");
eprintln!(" herdr integration status [--outdated-only]");
}
diff --git a/src/config/sound.rs b/src/config/sound.rs
index 8624881a..8221b753 100644
--- a/src/config/sound.rs
+++ b/src/config/sound.rs
@@ -127,6 +127,7 @@ impl AgentSoundOverrides {
Some(Agent::Antigravity) => self.agy,
Some(Agent::Cline) => self.cline,
Some(Agent::Omp) => AgentSoundSetting::Default,
+ Some(Agent::Mastracode) => AgentSoundSetting::Default,
Some(Agent::OpenCode) => self.open_code,
Some(Agent::GithubCopilot) => self.github_copilot,
Some(Agent::Kimi) => self.kimi,
diff --git a/src/detect/mod.rs b/src/detect/mod.rs
index cd350331..3520f010 100644
--- a/src/detect/mod.rs
+++ b/src/detect/mod.rs
@@ -50,6 +50,7 @@ pub enum Agent {
Antigravity,
Cline,
Omp,
+ Mastracode,
OpenCode,
GithubCopilot,
Kimi,
@@ -96,6 +97,7 @@ pub fn agent_label(agent: Agent) -> &'static str {
Agent::Antigravity => "agy",
Agent::Cline => "cline",
Agent::Omp => "omp",
+ Agent::Mastracode => "mastracode",
Agent::OpenCode => "opencode",
Agent::GithubCopilot => "copilot",
Agent::Kimi => "kimi",
@@ -121,6 +123,7 @@ pub fn parse_agent_label(agent: &str) -> Option<Agent> {
"agy" | "antigravity" | "antigravity-cli" => Some(Agent::Antigravity),
"cline" => Some(Agent::Cline),
"omp" => Some(Agent::Omp),
+ "mastracode" | "mastra-code" | "mastra code" => Some(Agent::Mastracode),
"opencode" | "open-code" => Some(Agent::OpenCode),
"copilot" | "github-copilot" | "ghcs" => Some(Agent::GithubCopilot),
"kimi" | "kimi-code" | "kimi code" => Some(Agent::Kimi),
@@ -150,6 +153,7 @@ pub fn identify_agent(process_name: &str) -> Option<Agent> {
"agy" | "antigravity" | "antigravity-cli" => Some(Agent::Antigravity),
"cline" => Some(Agent::Cline),
"omp" => Some(Agent::Omp),
+ "mastracode" | "mastra-code" | "mastra code" => Some(Agent::Mastracode),
"opencode" | "open-code" => Some(Agent::OpenCode),
"copilot" | "github-copilot" | "ghcs" => Some(Agent::GithubCopilot),
"kimi" | "kimi-code" | "kimi code" => Some(Agent::Kimi),
@@ -242,6 +246,7 @@ pub(crate) fn full_lifecycle_hook_authority(source: &str, agent_label: &str) ->
(source, agent_label),
("herdr:pi", "pi")
| ("herdr:omp", "omp")
+ | ("herdr:mastracode", "mastracode")
| ("herdr:hermes", "hermes")
| ("herdr:opencode", "opencode")
| ("herdr:kilo", "kilo")
@@ -621,6 +626,8 @@ mod tests {
assert_eq!(identify_agent("antigravity-cli"), Some(Agent::Antigravity));
assert_eq!(identify_agent("cline"), Some(Agent::Cline));
assert_eq!(identify_agent("omp"), Some(Agent::Omp));
+ assert_eq!(identify_agent("mastracode"), Some(Agent::Mastracode));
+ assert_eq!(identify_agent("mastra-code"), Some(Agent::Mastracode));
assert_eq!(identify_agent("opencode"), Some(Agent::OpenCode));
assert_eq!(identify_agent("opencode.exe"), Some(Agent::OpenCode));
assert_eq!(identify_agent("kimi"), Some(Agent::Kimi));
@@ -646,6 +653,8 @@ mod tests {
assert_eq!(parse_agent_label("agy"), Some(Agent::Antigravity));
assert_eq!(parse_agent_label("antigravity"), Some(Agent::Antigravity));
assert_eq!(parse_agent_label("omp"), Some(Agent::Omp));
+ assert_eq!(parse_agent_label("mastracode"), Some(Agent::Mastracode));
+ assert_eq!(parse_agent_label("mastra code"), Some(Agent::Mastracode));
assert_eq!(parse_agent_label("opencode.exe"), Some(Agent::OpenCode));
assert_eq!(parse_agent_label("copilot"), Some(Agent::GithubCopilot));
assert_eq!(parse_agent_label("kimi-code"), Some(Agent::Kimi));
@@ -668,6 +677,7 @@ mod tests {
assert_eq!(agent_label(Agent::Devin), "devin");
assert_eq!(agent_label(Agent::Antigravity), "agy");
assert_eq!(agent_label(Agent::Omp), "omp");
+ assert_eq!(agent_label(Agent::Mastracode), "mastracode");
assert_eq!(agent_label(Agent::Kiro), "kiro");
assert_eq!(agent_label(Agent::Grok), "grok");
assert_eq!(agent_label(Agent::Hermes), "hermes");
@@ -675,6 +685,15 @@ mod tests {
}
#[test]
+ fn mastracode_is_hook_authority_without_screen_manifest() {
+ assert!(full_lifecycle_hook_authority(
+ "herdr:mastracode",
+ "mastracode"
+ ));
+ assert!(!Agent::SCREEN_MANIFEST_AGENTS.contains(&Agent::Mastracode));
+ }
+
+ #[test]
fn identify_unknown_processes() {
assert_eq!(identify_agent("bash"), None);
assert_eq!(identify_agent("zsh"), None);
diff --git a/src/integration/actions.rs b/src/integration/actions.rs
index ea732891..807ceeb6 100644
--- a/src/integration/actions.rs
+++ b/src/integration/actions.rs
@@ -3,10 +3,11 @@ 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_hermes, install_kilo, install_kimi, install_omp, install_opencode, install_pi,
- install_qodercli, uninstall_claude, uninstall_codex, uninstall_copilot, uninstall_cursor,
- uninstall_devin, uninstall_droid, uninstall_hermes, uninstall_kilo, uninstall_kimi,
- uninstall_omp, uninstall_opencode, uninstall_pi, uninstall_qodercli,
+ 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_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};
@@ -190,6 +191,19 @@ fn install_target_inner(target: crate::api::schema::IntegrationTarget) -> io::Re
format!("updated cursor hooks at {}", installed.hooks_path.display()),
]
}
+ crate::api::schema::IntegrationTarget::Mastracode => {
+ let installed = install_mastracode()?;
+ vec![
+ format!(
+ "installed mastracode integration hook to {}",
+ installed.hook_path.display()
+ ),
+ format!(
+ "ensured mastracode hooks at {}",
+ installed.hooks_path.display()
+ ),
+ ]
+ }
};
if let Some(warning) = version_warning {
@@ -517,6 +531,33 @@ pub(crate) fn uninstall_target(
}
messages
}
+ crate::api::schema::IntegrationTarget::Mastracode => {
+ let result = uninstall_mastracode()?;
+ let mut messages = Vec::new();
+ if result.removed_hook_file {
+ messages.push(format!(
+ "removed mastracode hook at {}",
+ result.hook_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no mastracode hook found at {}",
+ result.hook_path.display()
+ ));
+ }
+ if result.updated_hooks {
+ messages.push(format!(
+ "removed herdr mastracode hook entries from {}",
+ result.hooks_path.display()
+ ));
+ } else {
+ messages.push(format!(
+ "no herdr mastracode hook entries found in {}",
+ result.hooks_path.display()
+ ));
+ }
+ messages
+ }
};
crate::logging::integration_action("uninstall", integration_target_label(target), "ok");
diff --git a/src/integration/assets/mastracode/herdr-agent-state.sh b/src/integration/assets/mastracode/herdr-agent-state.sh
new file mode 100644
index 00000000..b8e0a26a
--- /dev/null
+++ b/src/integration/assets/mastracode/herdr-agent-state.sh
@@ -0,0 +1,96 @@
+#!/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=mastracode
+# HERDR_INTEGRATION_VERSION=1
+
+set -eu
+
+action="${1:-}"
+hook_input_file="$(mktemp "${TMPDIR:-/tmp}/herdr-mastracode-hook.XXXXXX")" || exit 0
+trap 'rm -f "$hook_input_file"' EXIT HUP INT TERM
+cat >"$hook_input_file" 2>/dev/null || true
+
+case "$action" in
+ working|idle|blocked|release) ;;
+ *) exit 0 ;;
+esac
+
+[ "${HERDR_ENV:-}" = "1" ] || exit 0
+[ -n "${HERDR_SOCKET_PATH:-}" ] || exit 0
+[ -n "${HERDR_PANE_ID:-}" ] || exit 0
+command -v python3 >/dev/null 2>&1 || exit 0
+
+HERDR_ACTION="$action" HERDR_HOOK_INPUT_FILE="$hook_input_file" python3 - <<'PY'
+import json
+import os
+import random
+import socket
+import time
+
+source = "herdr:mastracode"
+action = os.environ.get("HERDR_ACTION", "")
+pane_id = os.environ.get("HERDR_PANE_ID")
+socket_path = os.environ.get("HERDR_SOCKET_PATH")
+hook_input_file = os.environ.get("HERDR_HOOK_INPUT_FILE")
+
+if not pane_id or not socket_path:
+ raise SystemExit(0)
+
+hook_input = {}
+if hook_input_file:
+ try:
+ with open(hook_input_file, encoding="utf-8") as handle:
+ content = handle.read()
+ if content.strip():
+ hook_input = json.loads(content)
+ except Exception:
+ hook_input = {}
+
+request_id = f"{source}:{int(time.time() * 1000)}:{random.randrange(1_000_000):06d}"
+report_seq = time.time_ns()
+session_id = hook_input.get("session_id")
+if isinstance(session_id, str) and session_id:
+ agent_session_id = session_id
+else:
+ agent_session_id = None
+if action == "release":
+ request = {
+ "id": request_id,
+ "method": "pane.release_agent",
+ "params": {
+ "pane_id": pane_id,
+ "source": source,
+ "agent": "mastracode",
+ "seq": report_seq,
+ },
+ }
+else:
+ request = {
+ "id": request_id,
+ "method": "pane.report_agent",
+ "params": {
+ "pane_id": pane_id,
+ "source": source,
+ "agent": "mastracode",
+ "state": action,
+ "seq": report_seq,
+ },
+ }
+ if agent_session_id:
+ request["params"]["agent_session_id"] = agent_session_id
+
+try:
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ client.settimeout(0.5)
+ client.connect(socket_path)
+ client.sendall((json.dumps(request) + "\n").encode())
+ try:
+ client.recv(4096)
+ except Exception:
+ pass
+ client.close()
+except Exception:
+ pass
+PY
diff --git a/src/integration/config_edit.rs b/src/integration/config_edit.rs
index a1653395..63c18845 100644
--- a/src/integration/config_edit.rs
+++ b/src/integration/config_edit.rs
@@ -110,6 +110,34 @@ pub(crate) fn ensure_command_hook(
// { "type": "command", "matcher": "...", "bash": "...", ... }
// Keep the helpers separate so install/uninstall preserves unrelated hooks in
// each agent's native format instead of normalizing user configuration.
+pub(crate) fn ensure_flat_command_hook(
+ hooks: &mut Map<String, Value>,
+ event: &str,
+ command: String,
+ timeout_ms: u64,
+) -> io::Result<()> {
+ let entries = hooks
+ .entry(event.to_string())
+ .or_insert_with(|| Value::Array(Vec::new()))
+ .as_array_mut()
+ .ok_or_else(|| io::Error::other(format!("hook entries for {event} must be an array")))?;
+
+ if entries.iter().any(|entry| {
+ entry.get("type").and_then(Value::as_str) == Some("command")
+ && entry.get("command").and_then(Value::as_str) == Some(command.as_str())
+ }) {
+ return Ok(());
+ }
+
+ entries.push(json!({
+ "type": "command",
+ "command": command,
+ "timeout": timeout_ms,
+ "description": "Report MastraCode agent state to Herdr",
+ }));
+ Ok(())
+}
+
pub(crate) fn ensure_direct_command_hook(
hooks: &mut Map<String, Value>,
event: &str,
@@ -214,6 +242,31 @@ pub(crate) fn remove_command_hook(
Ok(removed)
}
+pub(crate) fn remove_flat_command_hook(
+ hooks: &mut Map<String, Value>,
+ event: &str,
+ command: &str,
+) -> io::Result<bool> {
+ let Some(entries_value) = hooks.get_mut(event) else {
+ return Ok(false);
+ };
+
+ let entries = entries_value
+ .as_array_mut()
+ .ok_or_else(|| io::Error::other(format!("hook entries for {event} must be an array")))?;
+
+ let before = entries.len();
+ entries.retain(|entry| {
+ !(entry.get("type").and_then(Value::as_str) == Some("command")
+ && entry.get("command").and_then(Value::as_str) == Some(command))
+ });
+ let removed = entries.len() != before;
+ if entries.is_empty() {
+ hooks.remove(event);
+ }
+ Ok(removed)
+}
+
pub(crate) fn remove_direct_command_hook(
hooks: &mut Map<String, Value>,
event: &str,
diff --git a/src/integration/env.rs b/src/integration/env.rs
index 575c4c9a..ef946729 100644
--- a/src/integration/env.rs
+++ b/src/integration/env.rs
@@ -124,6 +124,10 @@ pub(crate) fn cursor_dir() -> io::Result<PathBuf> {
config_dir_from_env_or_home(CURSOR_CONFIG_DIR_ENV_VAR, &[".cursor"])
}
+pub(crate) fn mastracode_dir() -> io::Result<PathBuf> {
+ Ok(home_dir()?.join(".mastracode"))
+}
+
pub(crate) fn home_dir() -> io::Result<PathBuf> {
if let Some(home) = std::env::var_os("HOME").filter(|value| !value.is_empty()) {
return Ok(PathBuf::from(home));
diff --git a/src/integration/mod.rs b/src/integration/mod.rs
index 9f3f2bc4..d356175c 100644
--- a/src/integration/mod.rs
+++ b/src/integration/mod.rs
@@ -179,6 +179,24 @@ 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;
+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 = 1;
+const MASTRACODE_HOOK_TIMEOUT_MS: u64 = 10_000;
+const MASTRACODE_HOOK_EVENTS: [(&str, &str); 12] = [
+ ("SessionStart", "idle"),
+ ("UserPromptSubmit", "working"),
+ ("AgentStart", "working"),
+ ("PreToolUse", "working"),
+ ("PermissionRequest", "blocked"),
+ ("PermissionResult", "working"),
+ ("SubagentStart", "working"),
+ ("SubagentEnd", "working"),
+ ("Interrupt", "idle"),
+ ("AgentEnd", "idle"),
+ ("Stop", "idle"),
+ ("SessionEnd", "release"),
+];
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 e68dce0c..84e42a25 100644
--- a/src/integration/registry.rs
+++ b/src/integration/registry.rs
@@ -21,6 +21,7 @@ pub(crate) fn integration_target_label(
crate::api::schema::IntegrationTarget::Hermes => "hermes",
crate::api::schema::IntegrationTarget::Qodercli => "qodercli",
crate::api::schema::IntegrationTarget::Cursor => "cursor",
+ crate::api::schema::IntegrationTarget::Mastracode => "mastracode",
}
}
@@ -47,6 +48,7 @@ pub(crate) fn integration_target_command_names(
crate::api::schema::IntegrationTarget::Hermes => &["hermes"],
crate::api::schema::IntegrationTarget::Qodercli => qodercli_command_names(),
crate::api::schema::IntegrationTarget::Cursor => cursor_command_names(),
+ crate::api::schema::IntegrationTarget::Mastracode => &["mastracode"],
}
}
@@ -251,7 +253,7 @@ fn integration_specs() -> [(
crate::api::schema::IntegrationTarget,
io::Result<PathBuf>,
u32,
-); 13] {
+); 14] {
[
(
crate::api::schema::IntegrationTarget::Pi,
@@ -321,6 +323,11 @@ fn integration_specs() -> [(
cursor_dir().map(|dir| dir.join(super::CURSOR_HOOK_INSTALL_NAME)),
super::CURSOR_INTEGRATION_VERSION,
),
+ (
+ crate::api::schema::IntegrationTarget::Mastracode,
+ mastracode_dir().map(|dir| dir.join("hooks").join(super::MASTRACODE_HOOK_INSTALL_NAME)),
+ super::MASTRACODE_INTEGRATION_VERSION,
+ ),
]
}
diff --git a/src/integration/targets.rs b/src/integration/targets.rs
index a47f0dae..2b4c5615 100644
--- a/src/integration/targets.rs
+++ b/src/integration/targets.rs
@@ -7,15 +7,15 @@ use serde_json::{json, Value};
use super::command::{hook_command, shell_single_quote};
use super::config_edit::{
build_codex_config_with_hooks, build_kimi_config_with_hooks, ensure_command_hook,
- ensure_direct_command_hook, ensure_hermes_plugin_enabled, ensure_hooks_object,
- ensure_simple_command_hook, hooks_object_if_present, remove_direct_hook_commands,
- remove_hermes_plugin_enabled, remove_hook_commands, remove_kimi_config_block,
- remove_simple_command_hook,
+ ensure_direct_command_hook, ensure_flat_command_hook, ensure_hermes_plugin_enabled,
+ ensure_hooks_object, ensure_simple_command_hook, hooks_object_if_present,
+ remove_direct_hook_commands, remove_flat_command_hook, remove_hermes_plugin_enabled,
+ 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, hermes_dir,
- hermes_plugin_dir, kilo_dir, kimi_dir, omp_extension_dir, opencode_dir, pi_extension_dir,
- qodercli_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,
@@ -25,9 +25,9 @@ use super::types::{
CopilotInstallPaths, CopilotUninstallResult, CursorInstallPaths, CursorUninstallResult,
DevinInstallPaths, DevinUninstallResult, DroidInstallPaths, DroidUninstallResult,
HermesInstallPaths, HermesUninstallResult, KiloInstallPaths, KiloUninstallResult,
- KimiInstallPaths, KimiUninstallResult, OmpInstallPaths, OmpUninstallResult,
- OpenCodeInstallPaths, OpenCodeUninstallResult, PiUninstallResult, QodercliInstallPaths,
- QodercliUninstallResult,
+ 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,
@@ -38,9 +38,10 @@ use super::{
DROID_HOOK_INSTALL_NAME, DROID_REMOVED_LIFECYCLE_HOOK_EVENTS, 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, OMP_EXTENSION_ASSET, OMP_EXTENSION_INSTALL_NAME,
- OPENCODE_PLUGIN_ASSET, OPENCODE_PLUGIN_INSTALL_NAME, PI_EXTENSION_ASSET,
- PI_EXTENSION_INSTALL_NAME, QODERCLI_HOOK_ASSET, QODERCLI_HOOK_EVENTS,
+ KIMI_HOOK_ASSET, KIMI_HOOK_INSTALL_NAME, MASTRACODE_HOOK_ASSET, MASTRACODE_HOOK_EVENTS,
+ MASTRACODE_HOOK_INSTALL_NAME, MASTRACODE_HOOK_TIMEOUT_MS, OMP_EXTENSION_ASSET,
+ OMP_EXTENSION_INSTALL_NAME, OPENCODE_PLUGIN_ASSET, OPENCODE_PLUGIN_INSTALL_NAME,
+ PI_EXTENSION_ASSET, PI_EXTENSION_INSTALL_NAME, QODERCLI_HOOK_ASSET, QODERCLI_HOOK_EVENTS,
QODERCLI_HOOK_INSTALL_NAME, QODERCLI_REMOVED_LIFECYCLE_HOOK_EVENTS,
};
@@ -1106,3 +1107,90 @@ pub(crate) fn uninstall_cursor() -> io::Result<CursorUninstallResult> {
updated_hooks,
})
}
+
+pub(crate) fn install_mastracode() -> io::Result<MastracodeInstallPaths> {
+ let mastracode_home = mastracode_dir()?;
+ let hook_dir = mastracode_home.join("hooks");
+ fs::create_dir_all(&hook_dir)?;
+
+ let hook_path = hook_dir.join(MASTRACODE_HOOK_INSTALL_NAME);
+ fs::write(&hook_path, MASTRACODE_HOOK_ASSET)?;
+ make_executable(&hook_path)?;
+
+ let hooks_path = mastracode_home.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!(
+ "mastracode hooks file at {} must be a JSON object",
+ hooks_path.display()
+ ))
+ })?;
+
+ let quoted_hook_path = shell_single_quote(&hook_path.display().to_string());
+ for (event, action) in MASTRACODE_HOOK_EVENTS {
+ ensure_flat_command_hook(
+ hooks,
+ event,
+ format!("bash {quoted_hook_path} {action}"),
+ MASTRACODE_HOOK_TIMEOUT_MS,
+ )?;
+ }
+
+ fs::write(&hooks_path, serde_json::to_string_pretty(&hooks_file)?)?;
+
+ Ok(MastracodeInstallPaths {
+ hook_path,
+ hooks_path,
+ })
+}
+
+pub(crate) fn uninstall_mastracode() -> io::Result<MastracodeUninstallResult> {
+ let mastracode_home = mastracode_dir()?;
+ let hook_path = mastracode_home
+ .join("hooks")
+ .join(MASTRACODE_HOOK_INSTALL_NAME);
+ let hooks_path = mastracode_home.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!(
+ "mastracode hooks file at {} must be a JSON object",
+ hooks_path.display()
+ ))
+ })?;
+
+ let quoted_hook_path = shell_single_quote(&hook_path.display().to_string());
+ for (event, action) in MASTRACODE_HOOK_EVENTS {
+ updated_hooks |= remove_flat_command_hook(
+ hooks,
+ event,
+ &format!("bash {quoted_hook_path} {action}"),
+ )?;
+ }
+
+ 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(MastracodeUninstallResult {
+ hook_path,
+ hooks_path,
+ removed_hook_file,
+ updated_hooks,
+ })
+}
diff --git a/src/integration/tests.rs b/src/integration/tests.rs
index 2a8a7be3..b09e3c23 100644
--- a/src/integration/tests.rs
+++ b/src/integration/tests.rs
@@ -175,6 +175,7 @@ fn windows_supports_only_cli_hook_integrations() {
assert!(!integration_target_supported(IntegrationTarget::Hermes));
assert!(!integration_target_supported(IntegrationTarget::Cursor));
assert!(!integration_target_supported(IntegrationTarget::Devin));
+ assert!(!integration_target_supported(IntegrationTarget::Mastracode));
assert!(integration_target_supported(IntegrationTarget::Claude));
assert!(integration_target_supported(IntegrationTarget::Codex));
@@ -203,6 +204,7 @@ fn windows_does_not_offer_unsupported_integrations_even_when_commands_exist() {
fs::write(bin.join("hermes.exe"), "").unwrap();
fs::write(bin.join("cursor-agent.cmd"), "@echo off\r\n").unwrap();
fs::write(bin.join("devin.cmd"), "@echo off\r\n").unwrap();
+ fs::write(bin.join("mastracode.cmd"), "@echo off\r\n").unwrap();
assert!(!integration_target_available(IntegrationTarget::Pi));
assert!(!integration_target_available(IntegrationTarget::Omp));
@@ -211,6 +213,7 @@ fn windows_does_not_offer_unsupported_integrations_even_when_commands_exist() {
assert!(!integration_target_available(IntegrationTarget::Hermes));
assert!(!integration_target_available(IntegrationTarget::Cursor));
assert!(!integration_target_available(IntegrationTarget::Devin));
+ assert!(!integration_target_available(IntegrationTarget::Mastracode));
if let Some(path) = original_path {
std::env::set_var("PATH", path);
@@ -2587,6 +2590,13 @@ fn bundled_integration_assets_report_session_refs() {
assert!(CURSOR_HOOK_ASSET.contains("sessionStart"));
assert!(!CURSOR_HOOK_ASSET.contains("\"state\":"));
assert!(!CURSOR_HOOK_ASSET.contains("pane.release_agent"));
+ assert!(MASTRACODE_HOOK_ASSET.contains("HERDR_INTEGRATION_ID=mastracode"));
+ assert!(MASTRACODE_HOOK_ASSET.contains("HERDR_INTEGRATION_VERSION=1"));
+ assert!(MASTRACODE_HOOK_ASSET.contains("session_id"));
+ assert!(!MASTRACODE_HOOK_ASSET.contains("run_id"));
+ assert!(MASTRACODE_HOOK_ASSET.contains("agent_session_id"));
+ assert!(MASTRACODE_HOOK_ASSET.contains("pane.report_agent"));
+ assert!(MASTRACODE_HOOK_ASSET.contains("pane.release_agent"));
}
#[test]
@@ -3102,3 +3112,198 @@ fn install_cursor_errors_when_config_dir_missing() {
std::env::remove_var(CURSOR_CONFIG_DIR_ENV_VAR);
let _ = fs::remove_dir_all(base);
}
+
+#[test]
+fn install_mastracode_writes_hook_and_updates_hooks_json() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let original_home = std::env::var_os("HOME");
+ let mastracode_dir = base.join(".mastracode");
+ fs::create_dir_all(&mastracode_dir).unwrap();
+ fs::write(
+ mastracode_dir.join("hooks.json"),
+ r#"{"PostToolUse":[{"type":"command","command":"echo keep-me"}]}"#,
+ )
+ .unwrap();
+ std::env::set_var("HOME", &base);
+
+ let installed = install_mastracode().unwrap();
+
+ assert_eq!(
+ installed.hook_path,
+ mastracode_dir
+ .join("hooks")
+ .join(MASTRACODE_HOOK_INSTALL_NAME)
+ );
+ assert_eq!(installed.hooks_path, mastracode_dir.join("hooks.json"));
+ assert_eq!(
+ fs::read_to_string(&installed.hook_path).unwrap(),
+ MASTRACODE_HOOK_ASSET
+ );
+
+ let hooks_file: Value =
+ serde_json::from_str(&fs::read_to_string(mastracode_dir.join("hooks.json")).unwrap())
+ .unwrap();
+ let hooks = hooks_file.as_object().unwrap();
+ for (event, action) in MASTRACODE_HOOK_EVENTS {
+ let entries = hooks.get(event).and_then(Value::as_array).unwrap();
+ assert_eq!(entries.len(), 1, "{event} should have one Herdr hook");
+ let command = entries[0].get("command").and_then(Value::as_str).unwrap();
+ assert!(command.starts_with("bash "));
+ assert!(command.contains(MASTRACODE_HOOK_INSTALL_NAME));
+ assert!(command.ends_with(action));
+ assert_eq!(
+ entries[0].get("type").and_then(Value::as_str),
+ Some("command")
+ );
+ assert_eq!(
+ entries[0].get("timeout").and_then(Value::as_u64),
+ Some(MASTRACODE_HOOK_TIMEOUT_MS)
+ );
+ }
+ assert_eq!(
+ hooks["PostToolUse"][0]
+ .get("command")
+ .and_then(Value::as_str),
+ Some("echo keep-me")
+ );
+
+ if let Some(home) = original_home {
+ std::env::set_var("HOME", home);
+ } else {
+ std::env::remove_var("HOME");
+ }
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn install_mastracode_is_idempotent_for_hook_entries() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let original_home = std::env::var_os("HOME");
+ std::env::set_var("HOME", &base);
+
+ install_mastracode().unwrap();
+ install_mastracode().unwrap();
+
+ let hooks_file: Value = serde_json::from_str(
+ &fs::read_to_string(base.join(".mastracode").join("hooks.json")).unwrap(),
+ )
+ .unwrap();
+ let hooks = hooks_file.as_object().unwrap();
+ for (event, _) in MASTRACODE_HOOK_EVENTS {
+ assert_eq!(hooks.get(event).and_then(Value::as_array).unwrap().len(), 1);
+ }
+
+ if let Some(home) = original_home {
+ std::env::set_var("HOME", home);
+ } else {
+ std::env::remove_var("HOME");
+ }
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn uninstall_mastracode_removes_herdr_hooks_and_preserves_others() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let original_home = std::env::var_os("HOME");
+ std::env::set_var("HOME", &base);
+
+ install_mastracode().unwrap();
+ let hooks_path = base.join(".mastracode").join("hooks.json");
+ let mut hooks_file: Value =
+ serde_json::from_str(&fs::read_to_string(&hooks_path).unwrap()).unwrap();
+ hooks_file["UserPromptSubmit"]
+ .as_array_mut()
+ .unwrap()
+ .push(json!({ "type": "command", "command": "echo user-defined" }));
+ fs::write(
+ &hooks_path,
+ serde_json::to_string_pretty(&hooks_file).unwrap(),
+ )
+ .unwrap();
+
+ let result = uninstall_mastracode().unwrap();
+ assert!(result.removed_hook_file);
+ assert!(result.updated_hooks);
+ assert!(!base
+ .join(".mastracode")
+ .join("hooks")
+ .join(MASTRACODE_HOOK_INSTALL_NAME)
+ .is_file());
+
+ let hooks_file: Value =
+ serde_json::from_str(&fs::read_to_string(&hooks_path).unwrap()).unwrap();
+ let hooks = hooks_file.as_object().unwrap();
+ for (event, _) in MASTRACODE_HOOK_EVENTS {
+ if event == "UserPromptSubmit" {
+ continue;
+ }
+ assert!(!hooks.contains_key(event), "{event} should be removed");
+ }
+ let user_prompt_submit = hooks
+ .get("UserPromptSubmit")
+ .and_then(Value::as_array)
+ .unwrap();
+ assert_eq!(user_prompt_submit.len(), 1);
+ assert_eq!(
+ user_prompt_submit[0].get("command").and_then(Value::as_str),
+ Some("echo user-defined")
+ );
+
+ if let Some(home) = original_home {
+ std::env::set_var("HOME", home);
+ } else {
+ std::env::remove_var("HOME");
+ }
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn install_mastracode_errors_when_event_value_not_array() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let original_home = std::env::var_os("HOME");
+ let mastracode_dir = base.join(".mastracode");
+ fs::create_dir_all(&mastracode_dir).unwrap();
+ fs::write(mastracode_dir.join("hooks.json"), r#"{"SessionStart":{}}"#).unwrap();
+ std::env::set_var("HOME", &base);
+
+ let err = install_mastracode().unwrap_err().to_string();
+ assert!(
+ err.contains("hook entries for SessionStart must be an array"),
+ "unexpected error: {err}"
+ );
+
+ if let Some(home) = original_home {
+ std::env::set_var("HOME", home);
+ } else {
+ std::env::remove_var("HOME");
+ }
+ let _ = fs::remove_dir_all(base);
+}
+
+#[test]
+fn uninstall_mastracode_errors_when_event_value_not_array() {
+ let _lock = integration_env_lock();
+ let base = unique_base();
+ let original_home = std::env::var_os("HOME");
+ let mastracode_dir = base.join(".mastracode");
+ fs::create_dir_all(&mastracode_dir).unwrap();
+ fs::write(mastracode_dir.join("hooks.json"), r#"{"SessionStart":{}}"#).unwrap();
+ std::env::set_var("HOME", &base);
+
+ let err = uninstall_mastracode().unwrap_err().to_string();
+ assert!(
+ err.contains("hook entries for SessionStart must be an array"),
+ "unexpected error: {err}"
+ );
+
+ if let Some(home) = original_home {
+ std::env::set_var("HOME", home);
+ } else {
+ std::env::remove_var("HOME");
+ }
+ let _ = fs::remove_dir_all(base);
+}
diff --git a/src/integration/types.rs b/src/integration/types.rs
index c6ab7a0b..44ec76f1 100644
--- a/src/integration/types.rs
+++ b/src/integration/types.rs
@@ -82,6 +82,20 @@ pub(crate) struct CursorUninstallResult {
}
#[derive(Debug)]
+pub(crate) struct MastracodeInstallPaths {
+ pub hook_path: PathBuf,
+ pub hooks_path: PathBuf,
+}
+
+#[derive(Debug)]
+pub(crate) struct MastracodeUninstallResult {
+ pub hook_path: PathBuf,
+ pub hooks_path: PathBuf,
+ pub removed_hook_file: bool,
+ pub updated_hooks: bool,
+}
+
+#[derive(Debug)]
pub(crate) struct QodercliUninstallResult {
pub hook_path: PathBuf,
pub settings_path: PathBuf,
diff --git a/src/terminal/state.rs b/src/terminal/state.rs
index 96580b7d..eebf0fb0 100644
--- a/src/terminal/state.rs
+++ b/src/terminal/state.rs
@@ -416,8 +416,16 @@ impl TerminalState {
return None;
}
let session_ref = session_ref.map(|session_ref| {
- self.conflicting_same_owner_session_ref(&source, &agent_label, &session_ref, None)
- .unwrap_or(session_ref)
+ if self.lifecycle_hook_report_replaces_persisted_session(
+ &source,
+ &agent_label,
+ &session_ref,
+ ) {
+ session_ref
+ } else {
+ self.conflicting_same_owner_session_ref(&source, &agent_label, &session_ref, None)
+ .unwrap_or(session_ref)
+ }
});
if self.live_full_lifecycle_hook_authority_conflicts_with_session(
&source,
@@ -855,6 +863,26 @@ impl TerminalState {
)
}
+ fn lifecycle_hook_report_replaces_persisted_session(
+ &self,
+ source: &str,
+ agent_label: &str,
+ session_ref: &crate::agent_resume::AgentSessionRef,
+ ) -> bool {
+ self.hook_authority.is_none()
+ && (source, agent_label) == ("herdr:mastracode", "mastracode")
+ && self
+ .persisted_agent_session
+ .as_ref()
+ .is_some_and(|session| {
+ session.source == source
+ && session.agent == agent_label
+ && session.session_ref.kind == crate::agent_resume::AgentSessionRefKind::Id
+ && session_ref.kind == crate::agent_resume::AgentSessionRefKind::Id
+ && session.session_ref.value != session_ref.value
+ })
+ }
+
fn session_start_source_allows_session_replacement(
source: &str,
agent_label: &str,
@@ -2307,40 +2335,94 @@ mod tests {
// Regression for #614: a same-pane restart must reacquire lifecycle authority.
#[test]
fn omp_reacquires_full_lifecycle_hook_after_release_with_fresh_session_ref() {
+ assert_full_lifecycle_hook_reacquires_after_release_with_fresh_session_ref(
+ "herdr:omp",
+ "omp",
+ );
+ }
+
+ #[test]
+ fn mastracode_reacquires_full_lifecycle_hook_after_release_with_fresh_session_ref() {
+ assert_full_lifecycle_hook_reacquires_after_release_with_fresh_session_ref(
+ "herdr:mastracode",
+ "mastracode",
+ );
+ }
+
+ #[test]
+ fn mastracode_lifecycle_report_replaces_restored_thread_ref() {
let mut terminal = test_terminal();
+ terminal.set_persisted_agent_session(crate::agent_resume::PersistedAgentSession {
+ source: "herdr:mastracode".into(),
+ agent: "mastracode".into(),
+ session_ref: crate::agent_resume::AgentSessionRef::id("mastracode-old").unwrap(),
+ });
+
+ let mutation = terminal
+ .set_hook_authority_with_session_ref(
+ "herdr:mastracode".into(),
+ "mastracode".into(),
+ AgentState::Working,
+ None,
+ None,
+ crate::agent_resume::AgentSessionRef::id("mastracode-new"),
+ Some(20),
+ )
+ .expect("fresh MastraCode thread should replace restored thread id");
+
+ assert!(mutation.session_ref_changed);
+ assert_eq!(
+ terminal.current_session_identity_for_persistence(),
+ Some((
+ "herdr:mastracode".into(),
+ "mastracode".into(),
+ crate::agent_resume::AgentSessionRefKind::Id,
+ "mastracode-new".into()
+ ))
+ );
+ }
+
+ fn assert_full_lifecycle_hook_reacquires_after_release_with_fresh_session_ref(
+ source: &str,
+ agent_label: &str,
+ ) {
+ let mut terminal = test_terminal();
+ let old_session = format!("{agent_label}-old");
+ let new_session = format!("{agent_label}-new");
+
terminal.set_hook_authority_with_session_ref(
- "herdr:omp".into(),
- "omp".into(),
+ source.into(),
+ agent_label.into(),
AgentState::Working,
None,
None,
- crate::agent_resume::AgentSessionRef::id("omp-old"),
+ crate::agent_resume::AgentSessionRef::id(&old_session),
Some(20),
);
- terminal.release_agent("herdr:omp", "omp", Some(21));
+ terminal.release_agent(source, agent_label, Some(21));
// A late report from the released run keeps its old session ref and stays
- // suppressed, so a just-exited omp cannot resurrect the pane.
+ // suppressed, so a just-exited agent cannot resurrect the pane.
let stale = terminal.set_hook_authority_with_session_ref(
- "herdr:omp".into(),
- "omp".into(),
+ source.into(),
+ agent_label.into(),
AgentState::Working,
None,
None,
- crate::agent_resume::AgentSessionRef::id("omp-old"),
+ crate::agent_resume::AgentSessionRef::id(&old_session),
Some(22),
);
assert!(stale.is_none());
assert!(terminal.hook_authority.is_none());
- // A fresh omp run carries a new session ref and reacquires authority.
+ // A fresh run carries a new session ref and reacquires authority.
let fresh = terminal.set_hook_authority_with_session_ref(
- "herdr:omp".into(),
- "omp".into(),
+ source.into(),
+ agent_label.into(),
AgentState::Working,
None,
None,
- crate::agent_resume::AgentSessionRef::id("omp-new"),
+ crate::agent_resume::AgentSessionRef::id(&new_session),
Some(23),
);
assert!(fresh.is_some());