From 7a32e7dbbcc274d0692548cde1fc9cceec9a346b Mon Sep 17 00:00:00 2001 From: kalpakprod Date: Sun, 27 Sep 2026 06:30:11 +0500 Subject: [PATCH] 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 `, forks with `--fork `, `--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 `. 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 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> --- README.md | 8 +- README.zh-CN.md | 4 +- crates/tty7-core/src/core/agent_hooks.rs | 295 ++++++++++++++++++++++- crates/tty7-core/src/core/cli_agent.rs | 89 ++++++- docs/agents/overview.mdx | 6 +- docs/agents/sessions.mdx | 1 + docs/agents/status.mdx | 11 +- docs/getting-started/first-launch.mdx | 2 +- docs/index.mdx | 2 +- src/ui/i18n/en.rs | 8 + src/ui/i18n/ja.rs | 8 + src/ui/i18n/mod.rs | 6 + src/ui/i18n/zh.rs | 8 + src/ui/settings.rs | 10 + 14 files changed, 441 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 74d6f4ac..218771d3 100644 --- a/README.md +++ b/README.md @@ -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 · ⌃ R fuzzy history | | **Window** | tabs & splits · ⌘ P palette · ⌘ F scrollback search · ⌘ J 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.
-The full support matrix, all twenty-three +The full support matrix, all twenty-five | 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 | ✓ | | |
diff --git a/README.zh-CN.md b/README.zh-CN.md index 571937ec..ccb7aa1d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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 | ✓ | | | diff --git a/crates/tty7-core/src/core/agent_hooks.rs b/crates/tty7-core/src/core/agent_hooks.rs index 38c846c8..a4867638 100644 --- a/crates/tty7-core/src/core/agent_hooks.rs +++ b/crates/tty7-core/src/core/agent_hooks.rs @@ -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::(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 { 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 { 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 anyhow::Result { 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 — +/// `{"": {"": [...]}}` — 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> { + 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 { + 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 { 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 { | HookAgent::Crush | HookAgent::CodeBuddy | HookAgent::Cursor + | HookAgent::Antigravity | HookAgent::Kimi => None, } } @@ -1568,6 +1726,7 @@ fn pi_extension_ts(target: &HookTarget, agent: HookAgent) -> Option { 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")); diff --git a/crates/tty7-core/src/core/cli_agent.rs b/crates/tty7-core/src/core/cli_agent.rs index 42b44526..0620a50b 100644 --- a/crates/tty7-core/src/core/cli_agent.rs +++ b/crates/tty7-core/src/core/cli_agent.rs @@ -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 `; `--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 [ diff --git a/docs/agents/overview.mdx b/docs/agents/overview.mdx index 649e7648..4b0ad97b 100644 --- a/docs/agents/overview.mdx +++ b/docs/agents/overview.mdx @@ -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 diff --git a/docs/agents/sessions.mdx b/docs/agents/sessions.mdx index 90703839..e47dc680 100644 --- a/docs/agents/sessions.mdx +++ b/docs/agents/sessions.mdx @@ -43,6 +43,7 @@ or right-click the **tab or sidebar row** to open the fork in a new tab. | Grok | `grok --resume --fork-session` | | OpenCode | `opencode --session --fork` | | Oh My Pi | `omp --fork ` | +| Prime Agent | `prime-agent --fork ` | It is the agent's own fork command, run in a new pane — nothing is copied by tty7 itself. diff --git a/docs/agents/status.mdx b/docs/agents/status.mdx index 34dcebf8..27251cfc 100644 --- a/docs/agents/status.mdx +++ b/docs/agents/status.mdx @@ -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. + + 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. + + `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 diff --git a/docs/getting-started/first-launch.mdx b/docs/getting-started/first-launch.mdx index ae7893dc..3ab597c6 100644 --- a/docs/getting-started/first-launch.mdx +++ b/docs/getting-started/first-launch.mdx @@ -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. diff --git a/docs/index.mdx b/docs/index.mdx index f14d99aa..88a3b375 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -31,7 +31,7 @@ something floods the screen. syntax highlighting, click-to-place-caret, real multi-line editing. - 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. diff --git a/src/ui/i18n/en.rs b/src/ui/i18n/en.rs index 68b1e8c4..b2e339d7 100644 --- a/src/ui/i18n/en.rs +++ b/src/ui/i18n/en.rs @@ -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" diff --git a/src/ui/i18n/ja.rs b/src/ui/i18n/ja.rs index 0b0c822b..0e0ebc46 100644 --- a/src/ui/i18n/ja.rs +++ b/src/ui/i18n/ja.rs @@ -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" } diff --git a/src/ui/i18n/mod.rs b/src/ui/i18n/mod.rs index e0fc74c7..ce929130 100644 --- a/src/ui/i18n/mod.rs +++ b/src/ui/i18n/mod.rs @@ -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 diff --git a/src/ui/i18n/zh.rs b/src/ui/i18n/zh.rs index 61074840..7bfbb78f 100644 --- a/src/ui/i18n/zh.rs +++ b/src/ui/i18n/zh.rs @@ -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" } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index bd2ebc97..dac43544 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -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,