feat(agents): recognise Empryo and Prime Agent, add Antigravity status hooks (#975)

* feat(agents): recognise Empryo and Prime Agent

Prime Agent (PrimeIntellect-ai/prime-agent) is a Pi fork: detected as
`prime-agent`, resumes with `--resume <id>`, forks with `--fork <id>`,
`--no-session` opts out, and reports status through the shared Pi
extension bridge at ~/.prime/agent/extensions/tty7/index.ts.

Empryo is detected as `empryo` and resumes with `empryo --session <id>`.
No hook installer yet: Empryo filters TTY7* variables out of hook
processes, so `tty7-app agent-hook` would stop at the missing marker.

* feat(agents): Antigravity status hooks

Antigravity CLI (`agy`) now installs a `tty7` hook set in
~/.gemini/config/hooks.json, next to the user's own sets:
PreInvocation -> prompt-submit, PostToolUse -> tool-complete,
Stop -> stop. Only the first PreInvocation of a turn (invocationNum 0)
counts as a new prompt, later ones keep the turn working; a Stop with
fullyIdle false is ignored. conversationId and workspacePaths feed the
session id and cwd. PreToolUse is left alone because it must answer
with a decision.

* fix(agents): index Prime Agent and Antigravity in settings, scope conversationId alias

- Add settings titles, search keywords (en/ja/zh) and Agents index entries
  for the two new HookAgents; agent_rows_are_in_the_search_index requires
  one per HookAgent::ALL.
- Read Antigravity's conversationId as the session id only for antigravity:
  the alias table is last-write-wins, so a global alias could replace
  another agent's session id.
- Drop the stray .empryo/ job records.

---------

Co-authored-by: kalpak <you@example.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
kalpakprod
2026-09-27 09:30:11 +08:00
committed by GitHub
co-authored by kalpak l0ng-ai
parent e6009636b5
commit 7a32e7dbbc
14 changed files with 441 additions and 17 deletions
+5 -3
View File
@@ -52,7 +52,7 @@ Native builds for macOS, Windows, and Linux on [**Releases**](https://github.com
| | |
|---|---|
| **Agent-aware** | per-pane detection (23 CLIs) · status dot · notifications · branch + diff · tray icon when input is needed · resume after reboot · tab sidebar grouped by repository, with pinned groups |
| **Agent-aware** | per-pane detection (25 CLIs) · status dot · notifications · branch + diff · tray icon when input is needed · resume after reboot · tab sidebar grouped by repository, with pinned groups |
| **CLI + Skills** | bundled `tty7` CLI · [agent skill](skills/tty7/SKILL.md) · `run` streams a command and exits with its code · `split` · `send` · `wait --until free` · `capture` |
| **Editor-grade input** | ghost suggestions from history · explained tab completion · syntax highlighting · multi-line editing · click places the caret · <kbd>⌃ R</kbd> fuzzy history |
| **Window** | tabs & splits · <kbd>⌘ P</kbd> palette · <kbd>⌘ F</kbd> scrollback search · <kbd>⌘ J</kbd> panel with process tree and listening ports · 13 themes, your own YAML, iTerm2 import · IME |
@@ -69,7 +69,7 @@ after a reboot. **Fork** needs both — the agent's own fork command, and the ho
that tells tty7 which session to fork.
<details>
<summary>The full support matrix, all twenty-three</summary>
<summary>The full support matrix, all twenty-five</summary>
| Agent | Detected | Status · resume | Fork |
|---|:-:|:-:|:-:|
@@ -79,6 +79,7 @@ that tells tty7 which session to fork.
| **Grok** | ✓ | ✓ | ✓ |
| **OpenCode** | ✓ | ✓ | ✓ |
| **Oh My Pi** | ✓ | ✓ | ✓ |
| **Prime Agent** | ✓ | ✓ | ✓ |
| **Droid** | ✓ | ✓ | ✓ |
| **Qwen Code** | ✓ | ✓ | ✓ |
| **Goose** | ✓ | ✓ | ✓ |
@@ -89,13 +90,14 @@ that tells tty7 which session to fork.
| **Kimi Code** | ✓ | ✓ | |
| **Pi** | ✓ | ✓ | |
| **Crush** | ✓ | ✓ | |
| **Antigravity** | ✓ | ✓ | |
| **Cursor** | ✓ | ✓ | |
| Aider | ✓ | | |
| Amp | ✓ | | |
| Auggie | ✓ | | |
| Hermes | ✓ | | |
| Vibe | ✓ | | |
| Antigravity | ✓ | | |
| Empryo | ✓ | | |
</details>
+3 -1
View File
@@ -77,6 +77,7 @@ macOS、Windows、Linux 的原生构建都在 [**Releases**](https://github.com/
| **Grok** | ✓ | ✓ | ✓ |
| **OpenCode** | ✓ | ✓ | ✓ |
| **Oh My Pi** | ✓ | ✓ | ✓ |
| **Prime Agent** | ✓ | ✓ | ✓ |
| **Droid** | ✓ | ✓ | ✓ |
| **Qwen Code** | ✓ | ✓ | ✓ |
| **Goose** | ✓ | ✓ | ✓ |
@@ -84,13 +85,14 @@ macOS、Windows、Linux 的原生构建都在 [**Releases**](https://github.com/
| **Copilot** | ✓ | ✓ | |
| **Kimi Code** | ✓ | ✓ | |
| **Pi** | ✓ | ✓ | |
| **Antigravity** | ✓ | ✓ | |
| Aider | ✓ | | |
| Amp | ✓ | | |
| Cursor | ✓ | | |
| Auggie | ✓ | | |
| Hermes | ✓ | | |
| Vibe | ✓ | | |
| Antigravity | ✓ | | |
| Empryo | ✓ | | |
</details>
+292 -3
View File
@@ -43,6 +43,26 @@ fn effective_agent(agent: &str, ran_by_grok: bool) -> &str {
}
fn effective_event<'a>(agent: &str, event: &'a str, stdin_json: &str) -> Option<&'a str> {
// Antigravity has no turn-start event, only `PreInvocation` before every
// model call. The first call of a turn starts it; later ones are the same
// turn still working, and must not count as new prompts. `Stop` with
// `fullyIdle: false` ends one execution loop while more are queued.
if agent == "antigravity"
&& let Ok(payload) = serde_json::from_str::<serde_json::Value>(stdin_json)
{
if event == "prompt-submit"
&& payload
.get("invocationNum")
.and_then(|n| n.as_u64())
.unwrap_or(0)
> 0
{
return Some("tool-complete");
}
if event == "stop" && payload.get("fullyIdle").and_then(|v| v.as_bool()) == Some(false) {
return None;
}
}
// Qoder and CodeBuddy also emit SessionStart after compacting the active
// turn. Preserve its status until a real turn or session boundary arrives.
if matches!(agent, "qodercli" | "codebuddy")
@@ -90,11 +110,25 @@ fn build_hook_sequence(agent: &str, event: &str, stdin_json: &str) -> Vec<u8> {
body[key] = serde_json::Value::String(v.to_string());
}
}
// Antigravity names the session after the conversation, in camelCase.
// Scoped to it: the alias table is last-write-wins, and another agent's
// unrelated `conversationId` must not replace its real session id.
if agent == "antigravity"
&& body.get("session_id").is_none()
&& let Some(id) = payload
.get("conversationId")
.and_then(|v| v.as_str())
.filter(|v| !v.is_empty())
{
body["session_id"] = serde_json::Value::String(id.to_string());
}
// Cursor puts `cwd` only on its tool events; the rest carry the workspace
// roots instead, and the first of those is where the session runs.
// Antigravity's `workspacePaths` is the same list under its own name.
if body.get("cwd").is_none()
&& let Some(root) = payload
.get("workspace_roots")
.or_else(|| payload.get("workspacePaths"))
.and_then(|r| r.as_array())
.and_then(|r| r.first())
.and_then(|r| r.as_str())
@@ -285,10 +319,12 @@ pub enum HookAgent {
Crush,
CodeBuddy,
Cursor,
PrimeAgent,
Antigravity,
}
impl HookAgent {
pub const ALL: [HookAgent; 17] = [
pub const ALL: [HookAgent; 19] = [
HookAgent::Claude,
HookAgent::Codex,
HookAgent::TraeCode,
@@ -306,6 +342,8 @@ impl HookAgent {
HookAgent::Crush,
HookAgent::CodeBuddy,
HookAgent::Cursor,
HookAgent::PrimeAgent,
HookAgent::Antigravity,
];
/// The hooks behind a detected agent process, if it has any.
@@ -323,6 +361,8 @@ impl HookAgent {
CLIAgent::Pi => Some(HookAgent::Pi),
CLIAgent::Grok => Some(HookAgent::Grok),
CLIAgent::OhMyPi => Some(HookAgent::OhMyPi),
CLIAgent::PrimeAgent => Some(HookAgent::PrimeAgent),
CLIAgent::Antigravity => Some(HookAgent::Antigravity),
CLIAgent::Gemini => Some(HookAgent::Gemini),
CLIAgent::Droid => Some(HookAgent::Droid),
CLIAgent::Qwen => Some(HookAgent::Qwen),
@@ -337,7 +377,7 @@ impl HookAgent {
| CLIAgent::Auggie
| CLIAgent::Hermes
| CLIAgent::Vibe
| CLIAgent::Antigravity => None,
| CLIAgent::Empryo => None,
}
}
@@ -361,6 +401,8 @@ impl HookAgent {
| HookAgent::Pi
| HookAgent::Grok
| HookAgent::OhMyPi
| HookAgent::PrimeAgent
| HookAgent::Antigravity
| HookAgent::Goose
| HookAgent::Kimi => None,
}
@@ -406,6 +448,8 @@ impl HookAgent {
HookAgent::Pi => "pi",
HookAgent::Grok => "grok",
HookAgent::OhMyPi => "omp",
HookAgent::PrimeAgent => "prime-agent",
HookAgent::Antigravity => "antigravity",
HookAgent::Gemini => "gemini",
HookAgent::Droid => "droid",
HookAgent::Qwen => "qwen",
@@ -428,6 +472,8 @@ impl HookAgent {
HookAgent::Pi => "Pi",
HookAgent::Grok => "Grok Build",
HookAgent::OhMyPi => "Oh My Pi",
HookAgent::PrimeAgent => "Prime Agent",
HookAgent::Antigravity => "Antigravity",
HookAgent::Gemini => "Gemini",
HookAgent::Droid => "Droid",
HookAgent::Qwen => "Qwen Code",
@@ -455,6 +501,10 @@ impl HookAgent {
&["opencode", "plugins", OWNED_FILE_STEM_JS],
),
HookAgent::Pi => target.under_home(&[".pi", "agent", "extensions", "tty7", "index.ts"]),
HookAgent::PrimeAgent => {
target.under_home(&[".prime", "agent", "extensions", "tty7", "index.ts"])
}
HookAgent::Antigravity => target.under_home(&[".gemini", "config", "hooks.json"]),
HookAgent::Grok => target.under_home(&[".grok", "hooks", OWNED_FILE_STEM_JSON]),
HookAgent::OhMyPi => {
target.under_home(&[".omp", "agent", "extensions", "tty7", "index.ts"])
@@ -683,6 +733,9 @@ pub enum HooksState {
pub fn hooks_state(target: &HookTarget, agent: HookAgent) -> HooksState {
let path = agent.target_path(target);
if agent == HookAgent::Antigravity {
return named_hook_set_state(target, &path, &antigravity_hook_set(target));
}
if let Some(events) = agent.toml_hook_events() {
return toml_hooks_state(target, &path, agent, events);
}
@@ -719,6 +772,10 @@ pub enum HookOutcome {
pub fn install_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<HookOutcome> {
let path = agent.target_path(target);
if agent == HookAgent::Antigravity {
named_hook_set_install(target, &path, antigravity_hook_set(target))?;
return Ok(HookOutcome::Installed);
}
if let Some(events) = agent.toml_hook_events() {
toml_hooks_install(target, &path, agent, events)?;
return Ok(HookOutcome::Installed);
@@ -744,6 +801,9 @@ pub fn install_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<Ho
pub fn uninstall_hooks(target: &HookTarget, agent: HookAgent) -> anyhow::Result<HookOutcome> {
let path = agent.target_path(target);
if agent == HookAgent::Antigravity {
return named_hook_set_uninstall(target, &path);
}
if agent.toml_hook_events().is_some() {
return toml_hooks_uninstall(target, &path, agent);
}
@@ -1324,11 +1384,108 @@ fn enable_codex_hooks_feature() -> Result<(), String> {
}
}
/// The key tty7's entry lives under in Antigravity's `hooks.json`.
///
/// Antigravity names every hook set at the root of the file —
/// `{"<name>": {"<Event>": [...]}}` — rather than grouping handlers under a
/// shared `hooks` object the way Claude does, so tty7 owns one named set and
/// never has to pick its commands out of the user's lists.
const ANTIGRAVITY_HOOK_SET: &str = "tty7";
/// Antigravity CLI's lifecycle events, mapped onto tty7's.
///
/// It has no session-start or session-end event. `PreInvocation` fires before
/// every model call of a turn, so `effective_event` keeps only the first one
/// as the turn start; `Stop` fires when the execution loop ends. The
/// `PreToolUse` gate is left alone: it must answer with a `decision`, and an
/// empty reply from a status hook has no business deciding anything.
fn antigravity_hook_set(target: &HookTarget) -> serde_json::Value {
let handler = |event: &str| {
serde_json::json!({
"type": "command",
"command": target.hook_command(HookAgent::Antigravity, event),
"timeout": 5,
})
};
serde_json::json!({
"PreInvocation": [handler("prompt-submit")],
"PostToolUse": [{ "matcher": "*", "hooks": [handler("tool-complete")] }],
"Stop": [handler("stop")],
})
}
fn read_json_object(target: &HookTarget, path: &Path) -> anyhow::Result<Option<serde_json::Value>> {
let text = match target.read(path) {
Ok(text) => text,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(anyhow::anyhow!("read {}: {e}", path.display())),
};
if text.trim().is_empty() {
return Ok(Some(serde_json::json!({})));
}
let root: serde_json::Value = serde_json::from_str(&text).map_err(|e| {
anyhow::anyhow!(
"{} is not valid JSON ({e}); not touching it",
path.display()
)
})?;
if !root.is_object() {
return Err(anyhow::anyhow!(
"{} is not a JSON object; not touching it",
path.display()
));
}
Ok(Some(root))
}
fn named_hook_set_state(
target: &HookTarget,
path: &Path,
expected: &serde_json::Value,
) -> HooksState {
match read_json_object(target, path) {
Ok(Some(root)) => match root.get(ANTIGRAVITY_HOOK_SET) {
None => HooksState::NotInstalled,
Some(set) if set == expected => HooksState::Installed,
Some(_) => HooksState::Outdated,
},
_ => HooksState::NotInstalled,
}
}
fn named_hook_set_install(
target: &HookTarget,
path: &Path,
set: serde_json::Value,
) -> anyhow::Result<()> {
let mut root = read_json_object(target, path)?.unwrap_or_else(|| serde_json::json!({}));
root.as_object_mut()
.unwrap()
.insert(ANTIGRAVITY_HOOK_SET.to_string(), set);
target.write(path, serde_json::to_string_pretty(&root)?.as_bytes())
}
fn named_hook_set_uninstall(target: &HookTarget, path: &Path) -> anyhow::Result<HookOutcome> {
let Some(mut root) = read_json_object(target, path)? else {
return Ok(HookOutcome::NothingInstalled);
};
if root
.as_object_mut()
.unwrap()
.remove(ANTIGRAVITY_HOOK_SET)
.is_none()
{
return Ok(HookOutcome::NoTty7Hooks);
}
target.write(path, serde_json::to_string_pretty(&root)?.as_bytes())?;
Ok(HookOutcome::Removed)
}
fn owned_file_content(target: &HookTarget, agent: HookAgent) -> Option<String> {
match agent {
HookAgent::Copilot => copilot_hooks_json(target),
HookAgent::OpenCode => opencode_plugin_js(target),
HookAgent::Pi | HookAgent::OhMyPi => pi_extension_ts(target, agent),
HookAgent::Pi | HookAgent::OhMyPi | HookAgent::PrimeAgent => pi_extension_ts(target, agent),
HookAgent::Grok => grok_hooks_json(target),
HookAgent::Goose => goose_hooks_json(target),
HookAgent::Claude
@@ -1341,6 +1498,7 @@ fn owned_file_content(target: &HookTarget, agent: HookAgent) -> Option<String> {
| HookAgent::Crush
| HookAgent::CodeBuddy
| HookAgent::Cursor
| HookAgent::Antigravity
| HookAgent::Kimi => None,
}
}
@@ -1568,6 +1726,7 @@ fn pi_extension_ts(target: &HookTarget, agent: HookAgent) -> Option<String> {
let (slug, package) = match agent {
HookAgent::Pi => ("pi", "@mariozechner/pi-coding-agent"),
HookAgent::OhMyPi => ("omp", "@oh-my-pi/pi-coding-agent"),
HookAgent::PrimeAgent => ("prime-agent", "@earendil-works/pi-coding-agent"),
_ => return None,
};
let exe = serde_json::to_string(&target.exe.display().to_string()).ok()?;
@@ -1838,6 +1997,10 @@ mod tests {
(HookAgent::Crush, "/home/me/.config/crush/crush.json"),
(HookAgent::CodeBuddy, "/home/me/.codebuddy/settings.json"),
(HookAgent::Cursor, "/home/me/.cursor/hooks.json"),
(
HookAgent::PrimeAgent,
"/home/me/.prime/agent/extensions/tty7/index.ts",
),
] {
assert_eq!(
agent.target_path(&t),
@@ -2582,6 +2745,127 @@ mod tests {
);
}
#[test]
fn antigravity_owns_a_named_hook_set_beside_the_users() {
let dir = std::env::temp_dir().join(format!("tty7-agy-hooks-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let host = FakeRemote::shared();
let target = HookTarget::remote(&*host, dir.clone());
let path = HookAgent::Antigravity.target_path(&target);
assert_eq!(path, dir.join(".gemini/config/hooks.json"));
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let theirs = serde_json::json!({
"my-linter-hook": {
"PostToolUse": [{ "matcher": "run_command", "hooks": [{ "command": "./lint.sh" }] }]
}
});
std::fs::write(&path, theirs.to_string()).unwrap();
assert_eq!(
hooks_state(&target, HookAgent::Antigravity),
HooksState::NotInstalled
);
install_hooks(&target, HookAgent::Antigravity).expect("install succeeds");
assert_eq!(
hooks_state(&target, HookAgent::Antigravity),
HooksState::Installed
);
let root: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
root["my-linter-hook"], theirs["my-linter-hook"],
"the user's set survives"
);
let set = &root[ANTIGRAVITY_HOOK_SET];
assert!(
set["PreInvocation"][0]["command"]
.as_str()
.unwrap()
.ends_with("agent-hook antigravity prompt-submit")
);
assert_eq!(set["PostToolUse"][0]["matcher"], "*");
assert!(
set["Stop"][0]["command"]
.as_str()
.unwrap()
.ends_with("agent-hook antigravity stop")
);
assert!(
set.get("PreToolUse").is_none(),
"a status hook must not gate tools"
);
root_set_to(&path, serde_json::json!({ "Stop": [] }), &root);
assert_eq!(
hooks_state(&target, HookAgent::Antigravity),
HooksState::Outdated
);
install_hooks(&target, HookAgent::Antigravity).expect("reinstall succeeds");
assert_eq!(
hooks_state(&target, HookAgent::Antigravity),
HooksState::Installed
);
assert_eq!(
uninstall_hooks(&target, HookAgent::Antigravity).expect("uninstall succeeds"),
HookOutcome::Removed
);
let root: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
root, theirs,
"uninstall leaves exactly what was there before"
);
assert_eq!(
uninstall_hooks(&target, HookAgent::Antigravity).expect("second uninstall"),
HookOutcome::NoTty7Hooks
);
let _ = std::fs::remove_dir_all(&dir);
}
fn root_set_to(path: &Path, set: serde_json::Value, root: &serde_json::Value) {
let mut root = root.clone();
root[ANTIGRAVITY_HOOK_SET] = set;
std::fs::write(path, root.to_string()).unwrap();
}
#[test]
fn antigravity_payloads_map_onto_turns() {
let first = r#"{"conversationId":"c-1","invocationNum":0,"workspacePaths":["/repo"]}"#;
let later = r#"{"conversationId":"c-1","invocationNum":2,"workspacePaths":["/repo"]}"#;
assert_eq!(
effective_event("antigravity", "prompt-submit", first),
Some("prompt-submit")
);
assert_eq!(
effective_event("antigravity", "prompt-submit", later),
Some("tool-complete")
);
assert_eq!(
effective_event(
"antigravity",
"stop",
r#"{"conversationId":"c-1","fullyIdle":true}"#
),
Some("stop")
);
assert_eq!(
effective_event(
"antigravity",
"stop",
r#"{"conversationId":"c-1","fullyIdle":false}"#
),
None
);
let ev = round_trip("antigravity", "prompt-submit", first);
assert_eq!(ev.session_id.as_deref(), Some("c-1"));
assert_eq!(ev.cwd.as_deref(), Some(Path::new("/repo")));
assert_eq!(
ev.agent,
Some(crate::core::cli_agent::CLIAgent::Antigravity)
);
}
#[test]
fn a_remote_install_round_trips_through_the_host() {
let dir = std::env::temp_dir().join(format!("tty7-remote-hooks-{}", std::process::id()));
@@ -2679,6 +2963,11 @@ mod tests {
for (agent, slug, package) in [
(HookAgent::Pi, "pi", "@mariozechner/pi-coding-agent"),
(HookAgent::OhMyPi, "omp", "@oh-my-pi/pi-coding-agent"),
(
HookAgent::PrimeAgent,
"prime-agent",
"@earendil-works/pi-coding-agent",
),
] {
let bridge =
pi_extension_ts(&target, agent).unwrap_or_else(|| panic!("{slug} content builds"));
+85 -4
View File
@@ -29,10 +29,12 @@ pub enum CLIAgent {
QoderCLI,
Crush,
CodeBuddy,
Empryo,
PrimeAgent,
}
impl CLIAgent {
pub const ALL: [CLIAgent; 23] = [
pub const ALL: [CLIAgent; 25] = [
CLIAgent::Claude,
CLIAgent::Codex,
CLIAgent::TraeCode,
@@ -56,6 +58,8 @@ impl CLIAgent {
CLIAgent::QoderCLI,
CLIAgent::Crush,
CLIAgent::CodeBuddy,
CLIAgent::Empryo,
CLIAgent::PrimeAgent,
];
fn aliases(self) -> &'static [&'static str] {
@@ -106,6 +110,8 @@ impl CLIAgent {
// `cbc` also names the COIN-OR solver; a solver run wearing the
// avatar until it exits is the cost of catching the short name.
CLIAgent::CodeBuddy => &["codebuddy", "codebuddy-code", "cbc"],
CLIAgent::Empryo => &["empryo"],
CLIAgent::PrimeAgent => &["prime-agent"],
}
}
@@ -134,6 +140,8 @@ impl CLIAgent {
CLIAgent::QoderCLI => "qodercli",
CLIAgent::Crush => "crush",
CLIAgent::CodeBuddy => "codebuddy",
CLIAgent::Empryo => "empryo",
CLIAgent::PrimeAgent => "prime-agent",
}
}
@@ -167,6 +175,8 @@ impl CLIAgent {
CLIAgent::QoderCLI => "Qoder CLI",
CLIAgent::Crush => "Crush",
CLIAgent::CodeBuddy => "CodeBuddy",
CLIAgent::Empryo => "Empryo",
CLIAgent::PrimeAgent => "Prime Agent",
}
}
@@ -204,13 +214,15 @@ impl CLIAgent {
CLIAgent::Kimi => Some(format!("kimi{flags} --session {session_id}")),
CLIAgent::Crush => Some(format!("crush{flags} --session {session_id}")),
CLIAgent::CodeBuddy => Some(format!("codebuddy{flags} --resume {session_id}")),
CLIAgent::Empryo => Some(format!("empryo{flags} --session {session_id}")),
CLIAgent::PrimeAgent => Some(format!("prime-agent{flags} --resume {session_id}")),
_ => None,
}
}
fn opts_out_of_sessions(self, argv: &[String]) -> bool {
let ephemeral: &[&str] = match self {
CLIAgent::Pi | CLIAgent::OhMyPi => &["--no-session"],
CLIAgent::Pi | CLIAgent::OhMyPi | CLIAgent::PrimeAgent => &["--no-session"],
// "Do not save conversation history" — nothing is persisted, so
// there is no session left to resume from.
CLIAgent::Auggie => &["--dont-save-session"],
@@ -245,6 +257,7 @@ impl CLIAgent {
)),
CLIAgent::OpenCode => Some(format!("opencode{flags} --session {session_id} --fork")),
CLIAgent::OhMyPi => Some(format!("omp{flags} --fork {session_id}")),
CLIAgent::PrimeAgent => Some(format!("prime-agent{flags} --fork {session_id}")),
// Droid forks with a standalone flag rather than resume-plus-a-switch.
CLIAgent::Droid => Some(format!("droid{flags} --fork {session_id}")),
// `fork` is missing from `amp threads --help`, but the subcommand is
@@ -267,6 +280,7 @@ impl CLIAgent {
| CLIAgent::Grok
| CLIAgent::OpenCode
| CLIAgent::OhMyPi
| CLIAgent::PrimeAgent
| CLIAgent::Droid
| CLIAgent::Amp
| CLIAgent::Qwen
@@ -433,6 +447,12 @@ impl CLIAgent {
// `--resume`, `-r` and `--session` are three spellings of one flag
// in Oh My Pi; `--session-dir` is a different one and survives.
CLIAgent::OhMyPi => &["--resume", "-r", "--session", "--fork", "--continue", "-c"],
// Prime Agent is a Pi fork with the same session switches;
// `--session-dir` is a different option and survives.
CLIAgent::PrimeAgent => &["--resume", "-r", "--fork", "--continue", "-c"],
// empryo resumes with `--session <id>`; `--save-session` only
// applies to headless runs and is not a session selector.
CLIAgent::Empryo => &["--session"],
// `--resume`/`-r` is Kimi's hidden alias for `--session`/`-S`.
// `--agent`/`--agent-file` bind the main agent at session creation
// and Kimi rejects either next to `--session` outright; resuming
@@ -548,6 +568,8 @@ impl CLIAgent {
CLIAgent::QoderCLI => 0xFFFFFF,
// The blue-violet field Charm ships the Crush heart on.
CLIAgent::Crush => 0x6B50FF,
CLIAgent::Empryo => 0xE8663D,
CLIAgent::PrimeAgent => 0x111111,
// The near-black field CodeBuddy's own app icon sits on.
CLIAgent::CodeBuddy => 0x1F1F1F,
}
@@ -597,7 +619,9 @@ impl CLIAgent {
| CLIAgent::Auggie
| CLIAgent::Hermes
| CLIAgent::Vibe
| CLIAgent::Antigravity => "icons/bot.svg",
| CLIAgent::Antigravity
| CLIAgent::Empryo
| CLIAgent::PrimeAgent => "icons/bot.svg",
}
}
@@ -1239,7 +1263,15 @@ mod tests {
.collect();
assert_eq!(
fallback,
["aider", "auggie", "hermes", "vibe", "antigravity"]
[
"aider",
"auggie",
"hermes",
"vibe",
"antigravity",
"empryo",
"prime-agent"
]
);
assert!(
!fallback.contains(&"omp"),
@@ -1255,6 +1287,55 @@ mod tests {
}
}
#[test]
fn empryo_and_prime_agent_are_detected_and_resume() {
assert_eq!(
CLIAgent::detect_from_argv(&argv(&[
"/home/me/.empryo/bin/empryo",
"--session",
"750adff9"
])),
Some(CLIAgent::Empryo)
);
assert_eq!(
CLIAgent::detect_from_argv(&argv(&["prime-agent", "--model", "x"])),
Some(CLIAgent::PrimeAgent)
);
assert_eq!(CLIAgent::from_slug("empryo"), Some(CLIAgent::Empryo));
assert_eq!(
CLIAgent::from_slug("prime-agent"),
Some(CLIAgent::PrimeAgent)
);
assert_eq!(
CLIAgent::Empryo
.resume_command(
"750adff9-f366",
Some(&argv(&["empryo", "--session", "old"]))
)
.as_deref(),
Some("empryo --session 750adff9-f366")
);
assert_eq!(
CLIAgent::PrimeAgent
.resume_command(
"s-1",
Some(&argv(&["prime-agent", "-r", "old", "--model", "m"]))
)
.as_deref(),
Some("prime-agent --model m --resume s-1")
);
assert_eq!(
CLIAgent::PrimeAgent.fork_command("s-1", None).as_deref(),
Some("prime-agent --fork s-1")
);
assert_eq!(
CLIAgent::PrimeAgent
.resume_command("s-1", Some(&argv(&["prime-agent", "--no-session"]))),
None
);
assert_eq!(CLIAgent::Empryo.fork_command("s-1", None), None);
}
#[test]
fn detects_newer_agents_by_command() {
for (cmd, agent) in [
+4 -2
View File
@@ -15,7 +15,7 @@ need, and what changed.
## Which agents
Twenty-three CLIs are recognised on sight, by the command running in the pane:
Twenty-five CLIs are recognised on sight, by the command running in the pane:
| Agent | Command |
|---|---|
@@ -42,6 +42,8 @@ Twenty-three CLIs are recognised on sight, by the command running in the pane:
| Antigravity | `agy`, `antigravity` |
| Pi | `pi` |
| Oh My Pi | `omp` |
| Prime Agent | `prime-agent` |
| Empryo | `empryo` |
Detection sees through the usual disguises: a full path, a `.cmd` or `.exe` on
Windows, leading environment assignments, and an interpreter in front
@@ -64,7 +66,7 @@ If you launch agents through a wrapper script, map its name to an agent in
The key is your command's name; the value is one of the slugs above (`claude`,
`codex`, `traecli`, `gemini`, `aider`, `amp`, `opencode`, `copilot`, `cursor`, `goose`,
`droid`, `pi`, `auggie`, `hermes`, `vibe`, `antigravity`, `grok`, `qwen`,
`omp`, `kimi`, `qodercli`, `crush`, `codebuddy`).
`omp`, `kimi`, `qodercli`, `crush`, `codebuddy`, `empryo`, `prime-agent`).
## Quick launch
+1
View File
@@ -43,6 +43,7 @@ or right-click the **tab or sidebar row** to open the fork in a new tab.
| Grok | `grok --resume <id> --fork-session` |
| OpenCode | `opencode --session <id> --fork` |
| Oh My Pi | `omp --fork <id>` |
| Prime Agent | `prime-agent --fork <id>` |
It is the agent's own fork command, run in a new pane — nothing is copied by
tty7 itself.
+9 -2
View File
@@ -14,8 +14,8 @@ the agent say which one it is.
| Agent | |
|---|---|
| Claude Code · Codex · TraeCode · Copilot CLI · OpenCode · Pi · Grok Build · Oh My Pi · Gemini · Droid · Qwen Code · Goose · Kimi Code · Qoder CLI · Crush · CodeBuddy · Cursor CLI | Hooks available |
| Aider · Amp · Auggie · Hermes · Vibe · Antigravity | Detected and labelled, but no status channel yet |
| Claude Code · Codex · TraeCode · Copilot CLI · OpenCode · Pi · Grok Build · Oh My Pi · Gemini · Droid · Qwen Code · Goose · Kimi Code · Qoder CLI · Crush · CodeBuddy · Cursor CLI · Prime Agent · Antigravity | Hooks available |
| Aider · Amp · Auggie · Hermes · Vibe · Empryo | Detected and labelled, but no status channel yet |
Installing writes into that agent's own configuration directory. Once installed
the row grows a second **Uninstall** button beside the first, which itself
@@ -28,6 +28,13 @@ ships a newer hook.
carries no status dot and `tty7 wait` times out rather than returning.
</Note>
<Note>
Antigravity has no session-start or permission event, so its pane goes
straight from no dot to working on the first model call, and a pending
permission prompt shows as working rather than amber. Its hooks live in a
`tty7` set inside `~/.gemini/config/hooks.json`, beside any sets of your own.
</Note>
<Note>
`cursor-agent -p` (print mode) sends neither of Cursor's turn hooks, so a
headless run records its session id — Copy Session ID and resume still
+1 -1
View File
@@ -54,7 +54,7 @@ leave it off if you type accented characters.
## 4. If you use coding agents, install the hooks
**Settings → Integrations.** tty7 detects 23 coding CLIs by process name on its own —
**Settings → Integrations.** tty7 detects 25 coding CLIs by process name on its own —
you get brand avatars and tab labels for free. The *status dots*, the "needs
your permission" notifications, and `tty7 wait` all need one more thing: a small
hook the agent calls to report what it is doing.
+1 -1
View File
@@ -31,7 +31,7 @@ something floods the screen.
syntax highlighting, click-to-place-caret, real multi-line editing.
</Card>
<Card title="Agent-aware" icon="robot" href="/agents/overview">
23 coding CLIs are recognised on sight. Per-pane status dots, notifications
25 coding CLIs are recognised on sight. Per-pane status dots, notifications
when one needs you, git context, and session resume after a reboot.
</Card>
<Card title="Remote work that feels local" icon="server" href="/remote/workspaces">
+8
View File
@@ -805,6 +805,8 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::SettingsAgentCrush => "Crush",
L10nKey::SettingsAgentCodeBuddy => "CodeBuddy",
L10nKey::SettingsAgentCursorCli => "Cursor CLI",
L10nKey::SettingsAgentPrimeAgent => "Prime Agent",
L10nKey::SettingsAgentAntigravity => "Antigravity",
L10nKey::SettingsSearchAboutKeywords => "version license credits build update check github",
L10nKey::SettingsSearchAppHttpProxyKeywords => {
"proxy http https socks socks5 clash v2ray network download update"
@@ -911,6 +913,12 @@ pub fn translate_en(key: L10nKey) -> &'static str {
L10nKey::SettingsSearchCursorCliKeywords => {
"agent integration hooks install cursor cursor-agent"
}
L10nKey::SettingsSearchPrimeAgentKeywords => {
"agent integration extension install prime prime-agent primeintellect"
}
L10nKey::SettingsSearchAntigravityKeywords => {
"agent integration hooks install antigravity agy google"
}
L10nKey::SettingsSearchPiKeywords => "agent integration extension install pi",
L10nKey::SettingsSearchPortForwardingKeywords => {
"ssh tunnel local remote dynamic socks forward rule"
+8
View File
@@ -815,6 +815,8 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsAgentCrush => "Crush",
L10nKey::SettingsAgentCodeBuddy => "CodeBuddy",
L10nKey::SettingsAgentCursorCli => "Cursor CLI",
L10nKey::SettingsAgentPrimeAgent => "Prime Agent",
L10nKey::SettingsAgentAntigravity => "Antigravity",
L10nKey::SettingsSearchAboutKeywords => {
"バージョン ライセンス クレジット ビルド 更新 確認 github about version license credits update check"
}
@@ -967,6 +969,12 @@ pub fn translate_ja(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsSearchCursorCliKeywords => {
"エージェント 統合 フック インストール cursor cursor-agent agent integration hooks install"
}
L10nKey::SettingsSearchPrimeAgentKeywords => {
"エージェント 統合 拡張 インストール prime prime-agent primeintellect agent integration extension install"
}
L10nKey::SettingsSearchAntigravityKeywords => {
"エージェント 統合 フック インストール antigravity agy google agent integration hooks install"
}
L10nKey::SettingsSearchPiKeywords => {
"エージェント 統合 拡張 インストール pi agent integration extension install"
}
+6
View File
@@ -620,6 +620,8 @@ l10n_keys! {
SettingsAgentCrush,
SettingsAgentCodeBuddy,
SettingsAgentCursorCli,
SettingsAgentPrimeAgent,
SettingsAgentAntigravity,
SettingsSearchAppHttpProxyKeywords,
SettingsSearchAboutKeywords,
SettingsSearchAutoDownloadKeywords,
@@ -679,6 +681,8 @@ l10n_keys! {
SettingsSearchCrushKeywords,
SettingsSearchCodeBuddyKeywords,
SettingsSearchCursorCliKeywords,
SettingsSearchPrimeAgentKeywords,
SettingsSearchAntigravityKeywords,
SettingsSearchRememberWindowSizeKeywords,
SettingsSearchReportMouseToAppsKeywords,
SettingsSearchRestoreLastLayoutKeywords,
@@ -1666,6 +1670,8 @@ mod tests {
L10nKey::SettingsAgentCrush,
L10nKey::SettingsAgentCodeBuddy,
L10nKey::SettingsAgentCursorCli,
L10nKey::SettingsAgentPrimeAgent,
L10nKey::SettingsAgentAntigravity,
// Windows names its backdrop materials, and Japanese Windows keeps
// those names in Latin script — so does this list. Chinese does
// translate them (云母 / 亚克力), which is what Microsoft's own
+8
View File
@@ -712,6 +712,8 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsAgentCrush => "Crush",
L10nKey::SettingsAgentCodeBuddy => "CodeBuddy",
L10nKey::SettingsAgentCursorCli => "Cursor CLI",
L10nKey::SettingsAgentPrimeAgent => "Prime Agent",
L10nKey::SettingsAgentAntigravity => "Antigravity",
L10nKey::SettingsSearchAboutKeywords => {
"关于 版本 许可证 致谢 构建 更新 检查 github about version license credits update"
}
@@ -858,6 +860,12 @@ pub fn translate_zh(key: L10nKey) -> Option<&'static str> {
L10nKey::SettingsSearchCursorCliKeywords => {
"Cursor CLI agent 集成 钩子 安装 cursor cursor-agent"
}
L10nKey::SettingsSearchPrimeAgentKeywords => {
"Prime Agent agent 集成 扩展 安装 prime prime-agent primeintellect"
}
L10nKey::SettingsSearchAntigravityKeywords => {
"Antigravity agent 集成 钩子 安装 antigravity agy google"
}
L10nKey::SettingsSearchPiKeywords => {
"Pi agent 集成 扩展 安装 pi agent integration extension install"
}
+10
View File
@@ -799,6 +799,16 @@ fn settings_search_entries() -> &'static [SearchEntry] {
title: SettingsAgentCursorCli,
keywords: SettingsSearchCursorCliKeywords,
},
SearchEntry {
section: Agents,
title: SettingsAgentPrimeAgent,
keywords: SettingsSearchPrimeAgentKeywords,
},
SearchEntry {
section: Agents,
title: SettingsAgentAntigravity,
keywords: SettingsSearchAntigravityKeywords,
},
SearchEntry {
section: General,
title: SettingsStartupWindow,