supergravity-mcp 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -70,6 +70,38 @@ npm run build
70
70
  (see `AVAILABLE_MODELS` in `src/antigravity-client.ts`).
71
71
  - `timeoutMs` — optional, default 120000 (2 minutes).
72
72
 
73
+ If the task generates files (images, documents, etc.), the result also
74
+ includes a real filesystem path — Antigravity stores each conversation's
75
+ files at `~/.gemini/antigravity/brain/<conversation-id>/` on disk, confirmed
76
+ directly (not documented anywhere by Google). No need to fetch anything
77
+ through Antigravity's local server; just read the file.
78
+
79
+ ### `delegate_to_antigravity_batch(tasks)`
80
+
81
+ Runs several independent tasks at once, each in its own Antigravity window —
82
+ genuinely concurrent, not queued. Confirmed with two simultaneous
83
+ ~200-word generations that both completed correctly in the same ~16s window.
84
+ Opens extra windows automatically as needed (macOS's "New Window" menu
85
+ command via System Events, with a fallback for when zero windows are open).
86
+
87
+ Requires Accessibility permission for whatever process runs this (System
88
+ Settings > Privacy & Security > Accessibility) — macOS only. Don't run this
89
+ at the same time as a separate `delegate_to_antigravity` call; window
90
+ allocation between the two isn't coordinated.
91
+
92
+ ```ts
93
+ delegateToAntigravityBatch([
94
+ { task: "..." },
95
+ { task: "...", model: "Claude Sonnet 4.6 (Thinking)" },
96
+ ]);
97
+ ```
98
+
99
+ ### `list_antigravity_files(limit?)`
100
+
101
+ Every file Antigravity has generated, across every conversation, most recent
102
+ first — not scoped to a single delegate call. Useful for finding something
103
+ generated earlier in this session or a past one.
104
+
73
105
  ### `get_antigravity_quota()`
74
106
 
75
107
  Reads Antigravity's Settings > Models panel and returns remaining quota —
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { execFileSync, execSync, spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -32,34 +32,187 @@ GPT-OSS 120B (~$0.03-0.09/$0.10-0.40 — far cheaper than the rest) — open-wei
32
32
 
33
33
  Gemini models and Claude+GPT models draw from SEPARATE Antigravity quota pools — call getAntigravityQuota() first if unsure which pool has room.
34
34
  `.trim();
35
+ // Antigravity is a single window shared by every call. Two delegations
36
+ // running at once would type into and read from the same conversation,
37
+ // corrupting both. Every entry point below is serialized through this lock
38
+ // so calls queue up instead of racing each other.
39
+ let lock = Promise.resolve();
40
+ function withLock(fn) {
41
+ const run = lock.then(fn, fn);
42
+ lock = run.then(() => undefined, () => undefined);
43
+ return run;
44
+ }
35
45
  export async function delegateToAntigravity(opts) {
36
- const { task, model, timeoutMs = 120_000 } = opts;
46
+ return withLock(async () => {
47
+ await ensureAntigravityReady();
48
+ const target = await getMainPageTarget();
49
+ return runOnTarget(target, opts);
50
+ });
51
+ }
52
+ /**
53
+ * Run several tasks at once, each in its own Antigravity window - confirmed
54
+ * these genuinely run concurrently (not just dispatched together then
55
+ * queued), verified with two simultaneous ~200-word generations that both
56
+ * completed correctly in the same ~16s window.
57
+ *
58
+ * macOS only: opening additional windows uses the "New Window" menu command
59
+ * via System Events, which requires Accessibility permission (System
60
+ * Settings > Privacy & Security > Accessibility) for whatever process is
61
+ * running this. Don't mix this with a concurrent single delegateToAntigravity
62
+ * call - window allocation between the two isn't coordinated, so they could
63
+ * both grab the same window.
64
+ */
65
+ export async function delegateToAntigravityBatch(tasks) {
66
+ if (tasks.length === 0)
67
+ return [];
37
68
  await ensureAntigravityReady();
38
- const target = await getMainPageTarget();
69
+ const targets = await ensureWindowCount(tasks.length);
70
+ return Promise.all(tasks.map((opts, i) => runOnTarget(targets[i], opts)));
71
+ }
72
+ async function runOnTarget(target, opts) {
73
+ const { task, model, timeoutMs = 120_000 } = opts;
39
74
  const session = new CdpSession(target.webSocketDebuggerUrl);
40
75
  try {
76
+ // The CDP debug port opens before Antigravity's own UI finishes mounting
77
+ // (its window loads content from a local server that itself needs a
78
+ // moment to boot). On a cold start this can leave the chat input
79
+ // missing for several seconds after we've already connected.
80
+ await waitForChatReady(session);
41
81
  await startNewConversation(session);
42
82
  if (model)
43
83
  await selectModel(session, model);
44
84
  await pasteAndSend(session, task);
45
- return await waitForReply(session, task, timeoutMs);
85
+ const reply = await waitForReply(session, task, timeoutMs);
86
+ const conversationId = await getConversationId(session);
87
+ const localFilesDir = conversationId ? resolveBrainDir(conversationId) : null;
88
+ return { reply, conversationId, localFilesDir };
46
89
  }
47
90
  finally {
48
91
  session.close();
49
92
  }
50
93
  }
51
- export async function getAntigravityQuota() {
52
- await ensureAntigravityReady();
53
- const target = await getMainPageTarget();
54
- const session = new CdpSession(target.webSocketDebuggerUrl);
94
+ function openNewAntigravityWindow() {
55
95
  try {
56
- const raw = await openSettingsModelsTab(session);
57
- return parseQuotaText(raw);
96
+ execFileSync("osascript", [
97
+ "-e",
98
+ 'tell application "System Events" to tell process "Antigravity" to click menu item "New Window" of menu "File" of menu bar 1',
99
+ ]);
100
+ return;
58
101
  }
59
- finally {
60
- await closeSettings(session);
61
- session.close();
102
+ catch (err) {
103
+ // With zero windows open, System Events can't find "Antigravity" as an
104
+ // addressable process at all (confirmed directly - error -600, "app is
105
+ // not open" - even though the underlying process is genuinely running).
106
+ // macOS's normal app-reopen convention handles this: relaunching an
107
+ // already-running, windowless app creates a fresh window instead of
108
+ // just focusing an existing one.
109
+ try {
110
+ execFileSync("open", ["-a", resolveAppPath()]);
111
+ return;
112
+ }
113
+ catch {
114
+ throw new Error(`Could not open a new Antigravity window (needed to run tasks in parallel): ${err.message}. ` +
115
+ "If Antigravity has at least one window open, this needs Accessibility permission - grant it in " +
116
+ "System Settings > Privacy & Security > Accessibility for whatever is running this process. macOS only.");
117
+ }
118
+ }
119
+ }
120
+ async function ensureWindowCount(n) {
121
+ // No initial wait-for-a-window here: unlike getMainPageTarget() (used right
122
+ // after launching the app, where a window is guaranteed to appear on its
123
+ // own), this can run against an already-running app with zero windows open
124
+ // (user closed them all) - nothing creates that first window except us.
125
+ let pageTargets = (await fetchTargets()).filter(isMainPage);
126
+ while (pageTargets.length < n) {
127
+ const beforeCount = pageTargets.length;
128
+ openNewAntigravityWindow();
129
+ // wait for a genuinely new target to register, not a fixed guess at timing
130
+ await waitFor(async () => (await fetchTargets()).filter(isMainPage).length > beforeCount, 15_000);
131
+ pageTargets = (await fetchTargets()).filter(isMainPage);
132
+ }
133
+ const targets = pageTargets.slice(0, n);
134
+ // A freshly opened window can take longer to mount its chat UI than any
135
+ // fixed delay reliably covers (this is exactly what broke on the first
136
+ // real test - the 3rd window still wasn't ready 45s in). Confirm every
137
+ // window individually before handing any of them out.
138
+ await Promise.all(targets.map(async (target) => {
139
+ const session = new CdpSession(target.webSocketDebuggerUrl);
140
+ try {
141
+ await waitForChatReady(session);
142
+ }
143
+ finally {
144
+ session.close();
145
+ }
146
+ }));
147
+ return targets;
148
+ }
149
+ async function getConversationId(session) {
150
+ const href = await session.evaluate("location.href");
151
+ const match = href.match(/\/c\/([a-f0-9-]{36})/i);
152
+ return match ? match[1] : null;
153
+ }
154
+ // Antigravity (built on the Gemini CLI/Windsurf lineage) stores every
155
+ // conversation's generated files under ~/.gemini/antigravity/brain/<id>/ on
156
+ // disk - confirmed directly, not documented anywhere. This is real
157
+ // filesystem storage, separate from the local HTTPS server that serves it to
158
+ // the UI, so it's readable without going through that server at all.
159
+ // os.homedir() resolves per-user at runtime - not specific to any one machine.
160
+ export function getAntigravityBrainDir() {
161
+ return path.join(os.homedir(), ".gemini", "antigravity", "brain");
162
+ }
163
+ function resolveBrainDir(conversationId) {
164
+ const dir = path.join(getAntigravityBrainDir(), conversationId);
165
+ return fs.existsSync(dir) ? dir : null;
166
+ }
167
+ /**
168
+ * Every file Antigravity has ever generated, across every conversation, most
169
+ * recent first - not scoped to any single delegate_to_antigravity call.
170
+ */
171
+ export function listAntigravityFiles(limit = 20) {
172
+ const brainDir = getAntigravityBrainDir();
173
+ if (!fs.existsSync(brainDir))
174
+ return [];
175
+ const results = [];
176
+ for (const conversationId of fs.readdirSync(brainDir)) {
177
+ const convDir = path.join(brainDir, conversationId);
178
+ let entries;
179
+ try {
180
+ entries = fs.readdirSync(convDir, { withFileTypes: true });
181
+ }
182
+ catch {
183
+ continue;
184
+ }
185
+ for (const entry of entries) {
186
+ // skip Antigravity's own internal bookkeeping (.system_generated, etc.)
187
+ if (entry.name.startsWith(".") || !entry.isFile())
188
+ continue;
189
+ const filePath = path.join(convDir, entry.name);
190
+ const mtimeMs = fs.statSync(filePath).mtimeMs;
191
+ results.push({
192
+ path: filePath,
193
+ conversationId,
194
+ filename: entry.name,
195
+ modifiedAt: new Date(mtimeMs).toISOString(),
196
+ });
197
+ }
62
198
  }
199
+ results.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
200
+ return results.slice(0, limit);
201
+ }
202
+ export async function getAntigravityQuota() {
203
+ return withLock(async () => {
204
+ await ensureAntigravityReady();
205
+ const target = await getMainPageTarget();
206
+ const session = new CdpSession(target.webSocketDebuggerUrl);
207
+ try {
208
+ const raw = await openSettingsModelsTab(session);
209
+ return parseQuotaText(raw);
210
+ }
211
+ finally {
212
+ await closeSettings(session);
213
+ session.close();
214
+ }
215
+ });
63
216
  }
64
217
  async function openSettingsModelsTab(session) {
65
218
  await session.evaluate(`
@@ -258,6 +411,26 @@ class CdpSession {
258
411
  }
259
412
  }
260
413
  // ---------- UI actions (selectors verified against Antigravity 2.2.1) ----------
414
+ async function waitForChatReady(session, timeoutMs = 45_000) {
415
+ const start = Date.now();
416
+ while (Date.now() - start < timeoutMs) {
417
+ const state = await session.evaluate(`
418
+ (() => {
419
+ if (document.querySelector('[contenteditable="true"][aria-label="Message input"]')) return 'ready';
420
+ const text = document.body.innerText || '';
421
+ if (text.includes('Continue with Google') || text.includes('Welcome to Antigravity')) return 'needs-signin';
422
+ return 'loading';
423
+ })()
424
+ `);
425
+ if (state === "ready")
426
+ return;
427
+ if (state === "needs-signin") {
428
+ throw new Error("Antigravity needs you to sign in. Open the app and click \"Continue with Google\", then try again — this can't be automated (it's your Google login).");
429
+ }
430
+ await sleep(500);
431
+ }
432
+ throw new Error(`Antigravity's chat UI didn't finish loading within ${timeoutMs}ms of launch. It may still be starting up — try again in a few seconds.`);
433
+ }
261
434
  async function startNewConversation(session) {
262
435
  await session.evaluate(`
263
436
  (() => {
@@ -377,10 +550,15 @@ async function waitForReply(session, taskText, timeoutMs) {
377
550
  continue;
378
551
  if (threadText === lastText) {
379
552
  stableCount++;
380
- // Require the "Thought for Ns" marker so we don't return early on the
381
- // brief window right after sending, before Antigravity starts replying.
382
- if (stableCount >= 2 && /Thought for/.test(threadText)) {
383
- return extractReply(threadText, taskText);
553
+ // Two consecutive stable reads plus actual reply content (not just our
554
+ // own sent message sitting there) means it's done. "Thought for Ns" is
555
+ // NOT a reliable signal Antigravity skips it entirely for fast/simple
556
+ // answers, so requiring it caused real replies to be missed until
557
+ // timeout even though the answer was already on screen.
558
+ if (stableCount >= 2) {
559
+ const candidate = extractReply(threadText, taskText);
560
+ if (candidate)
561
+ return candidate;
384
562
  }
385
563
  }
386
564
  else {
@@ -391,13 +569,26 @@ async function waitForReply(session, taskText, timeoutMs) {
391
569
  throw new Error(`Timed out after ${timeoutMs}ms waiting for Antigravity's reply.`);
392
570
  }
393
571
  function extractReply(threadText, taskText) {
572
+ // We only search on the first 200 chars (waitForReply's DOM lookup does the
573
+ // same, to keep every poll's evaluate() call cheap on huge tasks) - but
574
+ // slicing by that same truncated length left a tail of our own message
575
+ // behind for anything longer than 200 chars. Instead, find where OUR
576
+ // message actually ends by locating the timestamp Antigravity renders
577
+ // immediately after it, which appears regardless of message length.
394
578
  const needle = taskText.slice(0, 200);
395
579
  const idx = threadText.lastIndexOf(needle);
396
- const after = idx >= 0 ? threadText.slice(idx) : threadText;
397
- const thoughtMatch = after.match(/Thought for[^\n]*\n+/);
398
- let rest = thoughtMatch ? after.slice(after.indexOf(thoughtMatch[0]) + thoughtMatch[0].length) : after;
399
- rest = rest.replace(/\n+\d{1,2}:\d{2}\s*$/, "").trim();
400
- return rest;
580
+ const searchFrom = idx >= 0 ? idx + needle.length : 0;
581
+ const closingTimestamp = threadText.slice(searchFrom).match(/\d{1,2}:\d{2}/);
582
+ let after = closingTimestamp
583
+ ? threadText.slice(searchFrom + closingTimestamp.index + closingTimestamp[0].length)
584
+ : threadText.slice(searchFrom);
585
+ // strip the "Thought for Ns" line when present - not always there
586
+ const thoughtMatch = after.match(/^\n*Thought for[^\n]*\n+/);
587
+ if (thoughtMatch)
588
+ after = after.slice(thoughtMatch[0].length);
589
+ // strip the trailing timestamp under the reply itself
590
+ after = after.replace(/\n+\d{1,2}:\d{2}\s*$/, "");
591
+ return after.trim();
401
592
  }
402
593
  function sleep(ms) {
403
594
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -2,7 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { AVAILABLE_MODELS, MODEL_GUIDE, delegateToAntigravity, getAntigravityQuota } from "./antigravity-client.js";
5
+ import { AVAILABLE_MODELS, MODEL_GUIDE, delegateToAntigravity, delegateToAntigravityBatch, getAntigravityQuota, listAntigravityFiles, } from "./antigravity-client.js";
6
6
  const server = new McpServer({
7
7
  name: "supergravity-mcp",
8
8
  version: "0.1.0",
@@ -12,7 +12,9 @@ server.registerTool("delegate_to_antigravity", {
12
12
  description: "Send a task to the Google Antigravity desktop app and return its reply. " +
13
13
  "Drives the real, already-logged-in app UI (types into its chat box and reads the response) — " +
14
14
  "does not touch Antigravity's internals or credentials. Requires Antigravity.app to be installed " +
15
- "and signed in on this machine.",
15
+ "and signed in on this machine. If the task produces files (generated images, edited code, etc.), " +
16
+ "the response includes a local filesystem path where they actually live — check that path directly " +
17
+ "instead of trying to fetch anything through Antigravity's UI or local server.",
16
18
  inputSchema: {
17
19
  task: z.string().describe("The task, question, or instruction to send to Antigravity."),
18
20
  model: z
@@ -29,8 +31,11 @@ server.registerTool("delegate_to_antigravity", {
29
31
  },
30
32
  }, async ({ task, model, timeoutMs }) => {
31
33
  try {
32
- const reply = await delegateToAntigravity({ task, model, timeoutMs });
33
- return { content: [{ type: "text", text: reply }] };
34
+ const result = await delegateToAntigravity({ task, model, timeoutMs });
35
+ const text = result.localFilesDir
36
+ ? `${result.reply}\n\n(Any files this produced are on disk at: ${result.localFilesDir})`
37
+ : result.reply;
38
+ return { content: [{ type: "text", text }] };
34
39
  }
35
40
  catch (err) {
36
41
  return {
@@ -39,6 +44,46 @@ server.registerTool("delegate_to_antigravity", {
39
44
  };
40
45
  }
41
46
  });
47
+ server.registerTool("delegate_to_antigravity_batch", {
48
+ title: "Delegate multiple tasks to Antigravity in parallel",
49
+ description: "Run several tasks at once, each in its own Antigravity window - genuinely concurrent, not queued " +
50
+ "one after another (verified: two simultaneous ~200-word generations both completed correctly in the " +
51
+ "same ~16s window). Use this instead of multiple delegate_to_antigravity calls when tasks are " +
52
+ "independent of each other (e.g. separate branches, unrelated questions). macOS only: opening extra " +
53
+ "windows needs Accessibility permission (System Settings > Privacy & Security > Accessibility) for " +
54
+ "whatever is running this. Don't call this at the same time as a separate delegate_to_antigravity call - " +
55
+ "window allocation between them isn't coordinated.",
56
+ inputSchema: {
57
+ tasks: z
58
+ .array(z.object({
59
+ task: z.string().describe("The task, question, or instruction to send to Antigravity."),
60
+ model: z
61
+ .enum(AVAILABLE_MODELS)
62
+ .optional()
63
+ .describe("Which model this specific task should use. If omitted, uses whatever is currently selected."),
64
+ timeoutMs: z.number().int().positive().optional().describe("Max time to wait for this task's reply."),
65
+ }))
66
+ .min(1)
67
+ .describe("One entry per task, each running in its own window at the same time."),
68
+ },
69
+ }, async ({ tasks }) => {
70
+ try {
71
+ const results = await delegateToAntigravityBatch(tasks);
72
+ const text = results
73
+ .map((r, i) => {
74
+ const files = r.localFilesDir ? `\n(Files on disk at: ${r.localFilesDir})` : "";
75
+ return `--- Task ${i + 1} ---\n${r.reply}${files}`;
76
+ })
77
+ .join("\n\n");
78
+ return { content: [{ type: "text", text }] };
79
+ }
80
+ catch (err) {
81
+ return {
82
+ content: [{ type: "text", text: `Batch delegation failed: ${err.message}` }],
83
+ isError: true,
84
+ };
85
+ }
86
+ });
42
87
  server.registerTool("get_antigravity_quota", {
43
88
  title: "Get Antigravity quota",
44
89
  description: "Read Antigravity's remaining model quota from its Settings > Models panel. " +
@@ -58,5 +103,25 @@ server.registerTool("get_antigravity_quota", {
58
103
  };
59
104
  }
60
105
  });
106
+ server.registerTool("list_antigravity_files", {
107
+ title: "List Antigravity-generated files",
108
+ description: "List files Antigravity has generated (images, documents, etc.), most recent first, across ALL " +
109
+ "conversations - not just the most recent delegate_to_antigravity call. Use this to find something " +
110
+ "generated earlier in this session or in a past one. Returns real filesystem paths, readable directly.",
111
+ inputSchema: {
112
+ limit: z.number().int().positive().optional().describe("Max number of files to return. Default 20."),
113
+ },
114
+ }, async ({ limit }) => {
115
+ try {
116
+ const files = listAntigravityFiles(limit);
117
+ return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
118
+ }
119
+ catch (err) {
120
+ return {
121
+ content: [{ type: "text", text: `Could not list Antigravity files: ${err.message}` }],
122
+ isError: true,
123
+ };
124
+ }
125
+ });
61
126
  const transport = new StdioServerTransport();
62
127
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supergravity-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "🚀 MCP server that lets Claude (or any MCP client) delegate tasks to Google's Antigravity desktop app 🌌. Built for Intel Macs 💻 that Antigravity's own CLI doesn't support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: delegate
3
- description: Use when the user wants to delegate a task to Google's Antigravity desktop app — phrases like "use gemini", "ask antigravity", "have claude in antigravity do this", "offload this", or "check antigravity's quota". Explains which model to pick and how to handle a disabled-send-button error. Requires the supergravity MCP server (delegate_to_antigravity, get_antigravity_quota tools) to be connected.
3
+ description: Use when the user wants to delegate a task to Google's Antigravity desktop app — phrases like "use gemini", "ask antigravity", "have claude in antigravity do this", "offload this", "run N agents/tasks in parallel", or "check antigravity's quota". Explains which model to pick, when to use the batch/parallel tool, and how to handle a disabled-send-button error. Requires the supergravity MCP server (delegate_to_antigravity, delegate_to_antigravity_batch, get_antigravity_quota, list_antigravity_files tools) to be connected.
4
4
  ---
5
5
 
6
6
  # delegate
7
7
 
8
8
  Antigravity is a separate AI desktop app (Google's). This skill runs tasks
9
- through it via the `supergravity-mcp` MCP server, instead of answering
9
+ through it via the `supergravity` MCP server's tools, instead of answering
10
10
  yourself — useful when the user explicitly asks to use it, wants a second
11
11
  opinion from a different model family, or wants to offload work.
12
12
 
@@ -18,6 +18,21 @@ Any explicit ask to use Antigravity or one of the models it hosts:
18
18
  generic requests that don't name Antigravity or one of its models — just
19
19
  answer normally.
20
20
 
21
+ Load this skill *before* calling either tool the first time in a
22
+ conversation, not after something already went wrong — the model-picking and
23
+ quota-pool guidance below changes which tool call you should even make.
24
+
25
+ ## You don't need to check if Antigravity is running
26
+
27
+ `delegate_to_antigravity` launches Antigravity itself if it isn't already
28
+ open, and waits for its chat UI to finish loading before typing anything —
29
+ you don't need to check first or tell the user to open it. Just call the
30
+ tool. The only implication: the very first call in a session can take
31
+ 15-30+ seconds longer than normal (cold app launch) before anything visible
32
+ happens. If a call fails specifically because the app couldn't be found at
33
+ all, the error names the exact env var (`ANTIGRAVITY_APP_PATH`) to fix it —
34
+ that's a real install problem, not something to retry.
35
+
21
36
  ## Picking a model
22
37
 
23
38
  If the user names a specific model ("use claude opus", "use gemini flash"),
@@ -44,6 +59,54 @@ until it refreshes — don't retry the same model. Instead:
44
59
  3. Otherwise, tell the user which pool is out and when it refreshes — don't
45
60
  silently fall back without saying so if they asked for a specific model.
46
61
 
62
+ ## Code-editing tasks: verify with the filesystem, not the reply text
63
+
64
+ If the task involves editing code, Antigravity works inside a real project
65
+ folder on disk (its "Projects" sidebar is literal directories, same
66
+ filesystem you already have access to) — it actually writes the files, it
67
+ doesn't just describe changes in chat. Its reply text is a narrative summary
68
+ written by the model, not the deliverable, and can be vague or incomplete
69
+ ("Updated the function as requested").
70
+
71
+ So after a code-editing delegation, don't parse the reply for specifics —
72
+ go check the real files yourself: `git diff`, `git status`, or just read the
73
+ file. You have direct filesystem access on this machine; use it instead of
74
+ trusting prose. Only fall back to the reply text for context Antigravity
75
+ wouldn't have written to disk (its reasoning, or a plain Q&A task with no
76
+ files involved).
77
+
78
+ ## Generated files (images, artifacts) - same idea
79
+
80
+ If the reply mentions it made an image or other file, the response also
81
+ includes a real filesystem path (`~/.gemini/antigravity/brain/<conversation
82
+ id>/`) — read the file directly from there rather than trying to fetch it
83
+ through Antigravity's UI or local server. That directory can occasionally
84
+ hold files from an earlier conversation too (Antigravity sometimes reuses a
85
+ conversation ID) — if more than one file is present, each filename ends in
86
+ a millisecond timestamp, so sort by that (or file mtime) to find the one
87
+ this task actually produced.
88
+
89
+ ## Running multiple tasks at once
90
+
91
+ If the user wants several independent things done at the same time ("spin up
92
+ 3 agents to work on different branches", "ask it these 5 questions"), use
93
+ `delegate_to_antigravity_batch` with one entry per task instead of calling
94
+ `delegate_to_antigravity` several times. This genuinely runs them
95
+ concurrently, each in its own Antigravity window - confirmed with real
96
+ simultaneous multi-paragraph generations that both completed correctly in
97
+ the same ~16s window, not queued one after another.
98
+
99
+ Only use batch when the tasks are actually independent of each other (they
100
+ can't see or depend on each other's output, since they're separate
101
+ conversations). For a multi-step task where step 2 needs step 1's result,
102
+ use sequential `delegate_to_antigravity` calls instead.
103
+
104
+ macOS only - opening extra windows needs Accessibility permission for
105
+ whatever process is running this (System Settings > Privacy & Security >
106
+ Accessibility). If that's missing, the error says so directly. Don't run a
107
+ batch call and a single `delegate_to_antigravity` call at the same time -
108
+ they don't coordinate which window they're using.
109
+
47
110
  ## Other failure modes
48
111
 
49
112
  - Antigravity not installed / not found: the error will say so — tell the