diff --git a/README.md b/README.md
index f3b04319..37452012 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
Live page: open-design.ai/html-anything/ — overview, surface modes, and showcase before you clone.
-> **Markdown is the draft. HTML is what humans read. Your local agent writes it.** The agentic HTML editor — in the agentic era, you don't hand-edit docs anymore, so the output format should be what the reader actually wants: HTML. Local-first, zero API key, reuses the CLI session you already have logged in — **9 coding-agent CLIs** auto-detected on your `PATH` (Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob), driven by **75 composable skill templates** across **9 deliverable surfaces** (magazine articles · keynote decks · résumés · posters · Xiaohongshu cards · tweet cards · web prototypes · data reports · Hyperframes videos). One-click export to WeChat / X / Zhihu, or download `.html` / `.png`.
+> **Markdown is the draft. HTML is what humans read. Your local agent writes it.** The agentic HTML editor — in the agentic era, you don't hand-edit docs anymore, so the output format should be what the reader actually wants: HTML. Local-first, zero API key, reuses the CLI session you already have logged in — **10 coding-agent CLIs** auto-detected on your `PATH` (Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob · Antigravity), driven by **75 composable skill templates** across **9 deliverable surfaces** (magazine articles · keynote decks · résumés · posters · Xiaohongshu cards · tweet cards · web prototypes · data reports · Hyperframes videos). One-click export to WeChat / X / Zhihu, or download `.html` / `.png`.
@@ -12,7 +12,7 @@
-
+
@@ -203,6 +203,7 @@ On startup we scan `PATH` (including `~/.local/bin`, `~/.bun/bin`, `/opt/homebre
| **Qwen Coder** | `qwen` | `qwen --yolo -` |
| **Aider** | `aider` | `aider --no-pretty --no-stream --yes-always --message-file -` |
| **IBM Bob** | `bob` | `bob --output-format stream-json --hide-intermediary-output` |
+| **Antigravity** | `agy` | `agy --output-format stream-json --dangerously-skip-permissions --disable-slash-commands --effort medium -p ` |
> The detection strategy and per-CLI adapter shape are borrowed directly from [`nexu-io/open-design`](https://github.com/nexu-io/open-design) and [`multica-ai/multica`](https://github.com/multica-ai/multica): one privileged process spawns CLIs, JSON-line is the wire protocol, every CLI gets a thin adapter in [`next/src/lib/agents/argv.ts`](next/src/lib/agents/argv.ts).
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 0f72f0a9..84bb1067 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -4,7 +4,7 @@
项目主页: open-design.ai/html-anything/ —— 不用 clone 也能先看看 HTML Anything 长什么样、能干啥。
-> **Markdown 是草稿, HTML 才是给人读的成品 —— 让本地 agent 直接写 HTML。** Agent 时代的 HTML 编辑器 —— 既然你已经不亲手改文档、全都让 Claude 改了, 那 agent 的输出就该是读者真正想看的 HTML, 而不是中间态的 markdown。本地优先、零 API Key、复用你已经登录好的 CLI session —— **9 个 coding-agent CLI** 在 `PATH` 上自动识别(Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob),驱动 **75 套 skill 模板** 和 **9 类可交付场景**(杂志文章 · Keynote PPT · 简历 · 海报 · 小红书 · 推特卡 · Web 原型 · 数据报告 · Hyperframes 视频)。一键复制到公众号 / 推特 / 知乎,或者下载 `.html` / `.png`。
+> **Markdown 是草稿, HTML 才是给人读的成品 —— 让本地 agent 直接写 HTML。** Agent 时代的 HTML 编辑器 —— 既然你已经不亲手改文档、全都让 Claude 改了, 那 agent 的输出就该是读者真正想看的 HTML, 而不是中间态的 markdown。本地优先、零 API Key、复用你已经登录好的 CLI session —— **10 个 coding-agent CLI** 在 `PATH` 上自动识别(Claude Code · Cursor Agent · Codex · Gemini CLI · GitHub Copilot CLI · OpenCode · Qwen Coder · Aider · IBM Bob · Antigravity),驱动 **75 套 skill 模板** 和 **9 类可交付场景**(杂志文章 · Keynote PPT · 简历 · 海报 · 小红书 · 推特卡 · Web 原型 · 数据报告 · Hyperframes 视频)。一键复制到公众号 / 推特 / 知乎,或者下载 `.html` / `.png`。
@@ -12,7 +12,7 @@
-
+
@@ -203,6 +203,7 @@ pnpm -F @html-anything/e2e test
| **Qwen Coder** | `qwen` | `qwen --yolo -` |
| **Aider** | `aider` | `aider --no-pretty --no-stream --yes-always --message-file -` |
| **IBM Bob** | `bob` | `bob --output-format stream-json --hide-intermediary-output` |
+| **Antigravity** | `agy` | `agy --output-format stream-json --dangerously-skip-permissions --disable-slash-commands --effort medium -p ` |
> 这一层的设计直接借鉴了 [`nexu-io/open-design`](https://github.com/nexu-io/open-design) 和 [`multica-ai/multica`](https://github.com/multica-ai/multica) 的 agent 检测策略:唯一被 spawn 子进程的进程是 server route,业务进程不直接 spawn;CLI 的 stdin / stdout 用 JSON-line 协议复用,每个 CLI 一个轻 adapter,统一在 [`next/src/lib/agents/argv.ts`](next/src/lib/agents/argv.ts)。
diff --git a/cli/src/__tests__/agents-invoke.test.ts b/cli/src/__tests__/agents-invoke.test.ts
index 3cfad7d2..13198a14 100644
--- a/cli/src/__tests__/agents-invoke.test.ts
+++ b/cli/src/__tests__/agents-invoke.test.ts
@@ -527,4 +527,28 @@ describe("invokeAgent", () => {
});
});
+
+ describe("antigravity", () => {
+ it("passes the prompt right after -p and parses agy step_update/result lines", async () => {
+ const lines = [
+ JSON.stringify({ event: "init", conversation_id: "conv-1", init: { cwd: "/tmp" } }),
+ JSON.stringify({ event: "step_update", step_update: { text_delta: "hi
" } }),
+ JSON.stringify({ event: "result", result: { status: "SUCCESS", response: "hi
" } }),
+ ].join("\n") + "\n";
+
+ const events = await driveInvoke(
+ { agent: "antigravity", prompt: "make html", binOverride: BIN_OVERRIDE },
+ lines,
+ 0,
+ );
+
+ const argv = mockSpawn.mock.calls.at(-1)![1] as string[];
+ expect(argv.slice(-2)).toEqual(["-p", "make html"]);
+ expect(argv).toContain("--disable-slash-commands");
+
+ // result.response must not duplicate the streamed delta
+ const deltas = events.filter((e) => e.type === "delta");
+ expect(deltas).toEqual([{ type: "delta", text: "hi
" }]);
+ });
+ });
});
diff --git a/cli/src/agents-detect.ts b/cli/src/agents-detect.ts
index f4813b43..0efcc21c 100644
--- a/cli/src/agents-detect.ts
+++ b/cli/src/agents-detect.ts
@@ -89,6 +89,16 @@ export const AGENTS: AgentDef[] = [
{ id: "gpt-5", label: "gpt-5" },
],
},
+ {
+ id: "antigravity",
+ label: "Antigravity",
+ bin: "agy",
+ envOverride: "ANTIGRAVITY_BIN",
+ vendor: "Google",
+ protocol: "argv",
+ // Model ids come from the user's agy config; no curated list yet.
+ fallbackModels: [DEFAULT_MODEL],
+ },
{
id: "gemini",
label: "Gemini CLI",
diff --git a/cli/src/agents-invoke.ts b/cli/src/agents-invoke.ts
index 2416e634..f8994631 100644
--- a/cli/src/agents-invoke.ts
+++ b/cli/src/agents-invoke.ts
@@ -120,6 +120,20 @@ function buildArgv(agent: string, opts: AgentArgvOpts = {}): string[] {
"--trust",
...(model ? ["--model", model] : []),
];
+ case "antigravity":
+ // `-p` must stay last: invoke appends the prompt right after it
+ // (`protocol: "argv"`). Effort is pinned so runs don't depend on the
+ // user's local agy default.
+ return [
+ "--output-format",
+ "stream-json",
+ "--dangerously-skip-permissions",
+ "--disable-slash-commands",
+ "--effort",
+ "medium",
+ ...(model ? ["--model", model] : []),
+ "-p",
+ ];
case "gemini":
return [
"--output-format",
@@ -209,6 +223,7 @@ function rescueHtmlFromToolUse(
name !== "createfile" &&
name !== "writefile" &&
name !== "write_file" &&
+ name !== "write_to_file" &&
name !== "filewrite"
)
continue;
@@ -229,6 +244,20 @@ function rescueHtmlFromToolUse(
return parts.join("");
}
+/** agy reports `cache_read_tokens`; the UI reads Claude-style usage keys. */
+function agyUsage(u: Record): AgentParse {
+ return {
+ kind: "meta",
+ key: "usage",
+ value: {
+ input_tokens: u.input_tokens,
+ output_tokens: u.output_tokens,
+ cache_read_input_tokens: u.cache_read_tokens ?? 0,
+ cache_creation_input_tokens: 0,
+ },
+ };
+}
+
function parseLineWithState(agent: string, line: string, state: ParseState): AgentParse[] {
const trimmed = line.trim();
if (!trimmed) return [];
@@ -246,6 +275,73 @@ function parseLineWithState(agent: string, line: string, state: ParseState): Age
if (!parsed || typeof parsed !== "object") return [];
const obj = parsed as Record;
const out: AgentParse[] = [];
+ if (agent === "antigravity") {
+ // agy stream-json: one envelope per line, keyed by `event`
+ // (init / step_update / result).
+ if (obj.event === "init") {
+ if (typeof obj.conversation_id === "string") {
+ out.push({ kind: "meta", key: "session", value: obj.conversation_id });
+ }
+ const initObj = obj.init as Record | undefined;
+ if (typeof initObj?.cwd === "string") {
+ out.push({ kind: "meta", key: "cwd", value: initObj.cwd });
+ }
+ }
+
+ const step = obj.step_update as Record | undefined;
+ if (step && typeof step === "object") {
+ // agy writes files via `write_to_file { TargetFile, CodeContent }`.
+ // Map it onto the shared tool_use shape so the rescue rules live in
+ // one place.
+ const toolInfo = step.tool_info as
+ | { name?: string; parameters?: Record }
+ | undefined;
+ const params = toolInfo?.parameters;
+ if (params && (toolInfo?.name ?? step.tool_name) === "write_to_file") {
+ const toolHtml = rescueHtmlFromToolUse([
+ {
+ type: "tool_use",
+ name: "write_to_file",
+ input: { file_path: params.TargetFile, content: params.CodeContent },
+ },
+ ]);
+ if (toolHtml) {
+ out.push({ kind: "html", text: toolHtml });
+ state.sawStreamEventText = true;
+ }
+ }
+ if (typeof step.text_delta === "string" && step.text_delta) {
+ state.sawStreamEventText = true;
+ out.push({ kind: "delta", text: step.text_delta });
+ }
+ if (typeof step.thinking_delta === "string" && step.thinking_delta) {
+ out.push({ kind: "meta", key: "thinking", value: step.thinking_delta });
+ }
+ if (step.usage && typeof step.usage === "object") {
+ out.push(agyUsage(step.usage as Record));
+ }
+ }
+
+ const result = obj.result as Record | undefined;
+ if (result && typeof result === "object") {
+ if (!state.sawStreamEventText && typeof result.response === "string" && result.response) {
+ out.push({ kind: "delta", text: result.response });
+ }
+ if (result.usage && typeof result.usage === "object") {
+ out.push(agyUsage(result.usage as Record));
+ }
+ if (typeof result.duration_seconds === "number") {
+ out.push({
+ kind: "meta",
+ key: "duration_ms",
+ value: Math.round(result.duration_seconds * 1000),
+ });
+ }
+ if (typeof result.status === "string") {
+ out.push({ kind: "meta", key: "result", value: result.status });
+ }
+ }
+ }
if (agent === "claude") {
if (obj.type === "system" && obj.subtype === "init") {
diff --git a/next/src/lib/agents/__tests__/argv.test.ts b/next/src/lib/agents/__tests__/argv.test.ts
index 449277d9..6e9c5e63 100644
--- a/next/src/lib/agents/__tests__/argv.test.ts
+++ b/next/src/lib/agents/__tests__/argv.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
-import { parseLine, makeParser } from "../argv";
+import { buildArgv, parseLine, makeParser } from "../argv";
describe("parseLine opencode", () => {
it("extracts text from nested part payload", () => {
@@ -219,3 +219,208 @@ describe("parseLine bob", () => {
});
});
+describe("parseLine antigravity", () => {
+ it("extracts session and cwd from init event", () => {
+ const line = JSON.stringify({
+ event: "init",
+ conversation_id: "conv-123",
+ init: {
+ cwd: "/Users/test/project",
+ tools: ["write_to_file"],
+ },
+ });
+
+ expect(parseLine("antigravity", line)).toEqual([
+ { kind: "meta", key: "session", value: "conv-123" },
+ { kind: "meta", key: "cwd", value: "/Users/test/project" },
+ ]);
+ });
+
+ it("extracts streamed text delta from step_update event", () => {
+ const line = JSON.stringify({
+ event: "step_update",
+ step_update: {
+ conversation_id: "conv-123",
+ step_index: 1,
+ state: "ACTIVE",
+ step_type: "agent_response",
+ text_delta: "Hello world
",
+ },
+ });
+
+ expect(parseLine("antigravity", line)).toEqual([
+ { kind: "delta", text: "Hello world
" },
+ ]);
+ });
+
+ it("extracts usage and text delta from step_update done event", () => {
+ const line = JSON.stringify({
+ event: "step_update",
+ step_update: {
+ conversation_id: "conv-123",
+ step_index: 1,
+ state: "DONE",
+ step_type: "agent_response",
+ text_delta: "\n