feat(agents): add Google Antigravity CLI (agy) support#83
feat(agents): add Google Antigravity CLI (agy) support#83darion-yaphet wants to merge 12 commits into
Conversation
When an agent binary is not installed (or any other agent-side error
occurs), the SSE stream emits an 'error' event then closes normally.
Previously the post-stream handler unconditionally called
setStatusFor('done'), masking the error — users saw the generate button
reset with no visible feedback, while the error was silently written
only to the log tab.
Track whether an error event was received (hadError) and propagate it
to the final status. Also guard commitBaseFor so a failed convert does
not update the diff-edit baseline with empty HTML.
agy --help reveals it uses -p/--print and --dangerously-skip-permissions, identical to Claude Code. The previous implementation incorrectly used Gemini-style flags (--output-format stream-json --yolo) and the Gemini parser branch. Both produced no output since agy does not recognise those flags. Switch buildArgv to Claude Code flags and move parser to the claude branch so stream-json NDJSON events are correctly parsed.
mrcfps
left a comment
There was a problem hiding this comment.
Thanks for shipping the agy integration and the convert error-state fix — I left two follow-ups to make the new adapter safer to ship.
🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.| { id: "gemini-2.5-pro", label: "gemini-2.5-pro" }, | ||
| { id: "gemini-2.5-flash", label: "gemini-2.5-flash" }, | ||
| { id: "gemini-2.5-flash-lite", label: "gemini-2.5-flash-lite" }, | ||
| { id: "openai/gpt-5", label: "openai/gpt-5" }, | ||
| { id: "anthropic/claude-sonnet-4-6", label: "anthropic/claude-sonnet-4-6" }, |
There was a problem hiding this comment.
The newly added openai/gpt-5 and anthropic/claude-sonnet-4-6 entries are still described as placeholders in the PR notes, but this picker list is live user input: useConvert forwards the selected value directly into agy --model .... If either placeholder string is wrong, users can select it and every convert run will fail at runtime. Please either remove the unverified options until the Antigravity docs confirm the exact IDs, or replace them now with documented values so every model shown here is actually runnable.
| describe("antigravity parser — stream_event text_delta", () => { | ||
| it("emits a delta for a text_delta stream event", () => { | ||
| const parse = makeParser("antigravity"); | ||
| const line = JSON.stringify({ | ||
| type: "stream_event", | ||
| event: { | ||
| type: "content_block_delta", | ||
| delta: { type: "text_delta", text: "hello" }, | ||
| }, | ||
| }); | ||
| expect(parse(line)).toEqual([{ kind: "delta", text: "hello" }]); | ||
| }); | ||
| }); | ||
|
|
||
| describe("antigravity parser — assistant body fallback", () => { | ||
| it("emits delta from assistant body when no stream_event preceded it", () => { | ||
| const parse = makeParser("antigravity"); | ||
| const line = JSON.stringify({ | ||
| type: "assistant", | ||
| message: { | ||
| content: [{ type: "text", text: "hello" }], | ||
| }, | ||
| }); | ||
| expect(parse(line)).toEqual([{ kind: "delta", text: "hello" }]); | ||
| }); | ||
|
|
||
| it("suppresses assistant body when stream_event already emitted text", () => { | ||
| const parse = makeParser("antigravity"); | ||
| parse( | ||
| JSON.stringify({ | ||
| type: "stream_event", | ||
| event: { | ||
| type: "content_block_delta", | ||
| delta: { type: "text_delta", text: "hello" }, | ||
| }, | ||
| }), | ||
| ); | ||
| const result = parse( | ||
| JSON.stringify({ | ||
| type: "assistant", | ||
| message: { | ||
| content: [{ type: "text", text: "hello" }], | ||
| }, | ||
| }), | ||
| ); | ||
| expect(result.filter((p) => p.kind === "delta")).toHaveLength(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe("antigravity parser — non-JSON input", () => { | ||
| it("returns noise and does not throw for a plain-text line", () => { | ||
| const parse = makeParser("antigravity"); | ||
| let result: ReturnType<typeof parse> | undefined; | ||
| expect(() => { result = parse("some plain text"); }).not.toThrow(); | ||
| expect(result).toEqual([{ kind: "noise" }]); | ||
| }); | ||
| }); | ||
|
|
||
| // ── AgentDef integrity ──────────────────────────────────────────────────────── | ||
|
|
||
| describe("antigravity AgentDef", () => { | ||
| const def = AGENTS.find((a) => a.id === "antigravity"); | ||
|
|
||
| it("exists in AGENTS", () => { | ||
| expect(def).toBeDefined(); | ||
| }); | ||
|
|
||
| it("has bin === 'agy'", () => { | ||
| expect(def?.bin).toBe("agy"); | ||
| }); | ||
|
|
||
| it("has DEFAULT_MODEL as first fallbackModel", () => { | ||
| expect(def?.fallbackModels[0]).toEqual(DEFAULT_MODEL); |
There was a problem hiding this comment.
These tests don't lock in the regression that commit 7 just fixed. The current fixtures only cover stream_event / assistant parsing plus AGENTS presence, and those same parser examples would also pass if antigravity were accidentally wired back to the Gemini branch. That leaves the Claude-compatible argv shape (-p, --verbose, --include-partial-messages, --dangerously-skip-permissions) completely unprotected. Please add at least one buildArgv("antigravity") assertion and one Claude-only parser fixture such as system/subtype:init or result, so a future refactor can't silently reintroduce the wrong adapter shape.
agy --help exposes no --model flag; the TUI confirms only Gemini 3.5 Flash is in use. The previous openai/gpt-5 and anthropic/claude-sonnet-4-6 entries were unverified placeholders that would cause every convert run to fail if selected. Strip to DEFAULT_MODEL only until Antigravity docs confirm accepted --model strings.
lefarcen
left a comment
There was a problem hiding this comment.
Hey @darion-yaphet! 👋 The use-convert.ts bug fix is genuinely well-targeted — tracking hadError in the SSE loop and guarding commitBaseFor behind it is exactly the right shape here. The silent-failure pattern (stream closes cleanly, UI resets as if nothing happened, error lands only in the log tab) is the worst kind to debug, and this plugs it properly.
The adapter shape also looks correct: routing agy through claude || antigravity in parseLineWithState, using the Claude Code-compatible argv (-p, --output-format stream-json, --verbose, --include-partial-messages, --dangerously-skip-permissions), and following the same AgentDef pattern as the other agents (envOverride, fallbackModels, etc.) — all consistent with the existing codebase conventions.
Looper's two inline notes already cover what needs to land before this merges: the unverified openai/gpt-5 / anthropic/claude-sonnet-4-6 model IDs in the live picker (users picking those will hit a runtime failure), and the missing buildArgv("antigravity") argv assertion so a future refactor can't silently reroute through the wrong branch.
One small nit from me: the UnsupportedAgentProtocolError constructor message still lists the pre-antigravity agents (claude / codex / cursor-agent / gemini / copilot / opencode / qwen / qoder / deepseek / aider) — worth adding antigravity there so the error string stays accurate.
Happy to do a full pass once the placeholder model IDs are resolved and the WIP tag comes off. ❤️
agy --print <prompt> takes the prompt as a positional argument and emits plain UTF-8 text, not Claude Code's stream-json format. The old argv passed -p --output-format stream-json --verbose --include-partial-messages, which caused agy to treat --output-format as the argument to -p and exit immediately (exit=0) with no output, leaving the UI with an empty result. - detect.ts: set protocol "argv" so invoke.ts appends the prompt after argv - argv.ts buildArgv: replace Claude Code flags with --dangerously-skip-permissions --print - argv.ts parser: move antigravity to the plain-text branch (aider/deepseek style) - invoke.ts: flush remaining stdout buffer as a delta on close for antigravity - test: rewrite tests to cover correct argv shape and plain-text parsing
mrcfps
left a comment
There was a problem hiding this comment.
Thanks for the follow-up here — the agy adapter is much closer, but I found two correctness issues in the current head that still look merge-blocking.
🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.| else if (part.kind === "meta") safeEnqueue({ type: "meta", key: part.key, value: part.value }); | ||
| } | ||
| if (opts.agent === "aider" || opts.agent === "deepseek") { | ||
| if (opts.agent === "aider" || opts.agent === "deepseek" || opts.agent === "antigravity") { |
There was a problem hiding this comment.
parse(stdoutBuf) already turns the remaining Antigravity buffer into a delta just above this line, because parseLineWithState() now treats antigravity as plain-text output. Adding antigravity to this extra safeEnqueue({ type: "delta", text: stdoutBuf }) path means any response that ends without a trailing newline gets its final chunk emitted twice (tail\n from parse, then tail again here). For a one-line agy --print HTML response, that duplicates the whole document tail and corrupts the generated HTML. Please keep only one flush path for Antigravity on close — for example, either rely on parse(stdoutBuf) for the leftover buffer or bypass that parser and enqueue the raw tail once, but not both.
| // record the just-finished (content, html) as the new diff-edit baseline | ||
| // so the user's next edit goes through diff mode instead of full regen | ||
| useStore.getState().commitBaseFor(taskId); | ||
| useStore.getState().setStatusFor(taskId, hadError ? "error" : "done"); |
There was a problem hiding this comment.
This still only flips hadError on an explicit SSE error event, but invokeAgent() always emits a done event from child.on("close") regardless of the exit code. So an agent that prints an auth/config failure to stderr and exits with code !== 0 will still leave hadError === false, and this branch will mark the run as done and commit the diff baseline even though the convert failed. That means the silent-error bug called out in the PR description is still reproducible for the common non-zero-exit path. Please treat a non-zero done.code as failure here (or emit an error event before done in invoke.ts) so unsuccessful agent exits always land in error status and skip commitBaseFor.
…ed list The error string enumerated supported agents but was missing antigravity, which was added in the previous commit. Keeping this list in sync avoids misleading users who encounter the error into thinking antigravity is unsupported.
mrcfps
left a comment
There was a problem hiding this comment.
@darion-yaphet Thanks for pushing the follow-up fixes here — I re-checked the latest head and there are still two correctness issues in the changed paths that look merge-blocking.
🔁 Powered by Looper · runner=reviewer · agent=opencode · An autonomous AI dev team for your GitHub repos.| else if (part.kind === "meta") safeEnqueue({ type: "meta", key: part.key, value: part.value }); | ||
| } | ||
| if (opts.agent === "aider" || opts.agent === "deepseek") { | ||
| if (opts.agent === "aider" || opts.agent === "deepseek" || opts.agent === "antigravity") { |
There was a problem hiding this comment.
parse(stdoutBuf) already routes Antigravity through the new plain-text parser branch just above this line, so adding antigravity here emits the same leftover buffer twice whenever stdout does not end with \n. For agy --print, a one-line HTML response or final tail chunk becomes tail\n from parse() and then tail again here, which can corrupt the generated document. Please keep Antigravity on only one close-flush path — either rely on parse(stdoutBuf) for the trailing buffer, or bypass that parser and enqueue the raw tail once, but not both.
| // record the just-finished (content, html) as the new diff-edit baseline | ||
| // so the user's next edit goes through diff mode instead of full regen | ||
| useStore.getState().commitBaseFor(taskId); | ||
| useStore.getState().setStatusFor(taskId, hadError ? "error" : "done"); |
There was a problem hiding this comment.
hadError only flips on an explicit SSE error event, but invokeAgent() still emits done for every child exit and never turns code !== 0 into an error event. That means an auth/config failure that writes to stderr and exits non-zero still leaves hadError === false, so this block marks the run as done and commits the diff baseline even though the convert failed. This is the same silent-failure path the PR description is trying to close. Please also treat a non-zero done.code as failure here (or emit an error before done in invoke.ts) so unsuccessful agent exits always end in error and skip commitBaseFor.
…lose parse(stdoutBuf) already emits the remaining buffer as a delta via the plain-text branch. The extra safeEnqueue below it caused the tail to be sent twice, corrupting single-line agy --print HTML responses by appending the document a second time.
hadError was only set on an explicit SSE error event, so an agent that
printed an auth/config failure to stderr and exited with code != 0 still
reached setStatusFor("done") and commitBaseFor — silently recording a
failed (empty) HTML as the diff-edit baseline.
Treat any done event whose code is not 0 (including null, i.e. signal
termination) as a failure so unsuccessful agent exits always land in
error status and skip the baseline commit.
mrcfps
left a comment
There was a problem hiding this comment.
@darion-yaphet Re-checked the latest head and the two earlier blockers look resolved: useConvert() now treats non-zero/null agent exits as failures before committing the diff baseline, and the duplicate Antigravity close-flush is gone. I also spot-checked the current protocol: "argv" / buildArgv("antigravity") wiring plus the updated adapter tests, and I do not see any new actionable issues in the changed ranges. I could not run the local pnpm checks in this prepared worktree because node_modules is not installed here, but the code changes themselves look ready to me. Thanks for working through the follow-ups on this one ❤️
|
Hey @darion-yaphet — yes, and I’m sorry this sat quiet after your fixes. Current state: @mrcfps has approved the latest head ( Thanks for sticking with the follow-ups here. ❤️ |
Description:
Adds agy (Google Antigravity CLI) as a supported agent in html-anything, alongside a bug fix for silent error handling in the convert pipeline.
Agent integration
Bug fix — silent convert errors
Previously, when an agent binary was not installed (or any other agent-side error occurred), the SSE stream emitted an error event and closed normally. The
post-stream handler unconditionally called setStatusFor("done"), masking the error — users saw the generate button reset with no feedback while the error was silently
written only to the log tab.
Fix: track hadError in the SSE loop and propagate it to the final status. Also guard commitBaseFor so a failed convert does not overwrite the diff-edit baseline with
empty HTML.
Test coverage
6 new unit tests: stream_event delta, assistant body fallback, dedup, non-JSON noise, AgentDef id/bin/model integrity.