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