I Made My AI IDE Switch Its Own Brain

Git commit messages don’t need Claude Opus. Neither do file renames, linting, or dependency updates. But my AI IDE was burning premium quota on all of them.

So I made it stop.

TL;DR

I wrote a 380-line, zero-dependency Node.js script that controls Antigravity’s model selector via Chrome DevTools Protocol. It can list models, check the current one, and switch to any other with a single command. I wired it into my git workflow so it automatically drops to a cheap model for commit messages, then restores my expensive model when it’s done. Every mundane task now costs roughly 10-20x less quota.


The Problem (Quick Version)

If you haven’t read They Keep Moving the Finish Line, here’s the short version: AI coding IDEs have usage limits. Every request burns quota. And not all requests are equal. An architecture discussion with Claude Opus eats way more quota than asking Gemini Flash to write a commit message.

But Antigravity doesn’t care. Whatever model you have selected, that’s what it uses for everything. Ask it to rename a variable? Opus. Ask it to format a JSON file? Opus. Ask it to write git commit -m "fix typo"? Opus.

The waste bothered me. So I asked a question: can the IDE switch its own model?

The Discovery: CDP

Antigravity is built on Electron, which is essentially a Chromium browser running a web app. And Chromium supports the Chrome DevTools Protocol, a WebSocket-based interface that lets you control the browser programmatically. Inspect the DOM, evaluate JavaScript, click buttons, read element states.

Including the model selector dropdown.

To enable it, you add one line to ~/.antigravity/argv.json:

{
  "remote-debugging-port": 9222
}

Restart Antigravity, and now http://127.0.0.1:9222/json/list returns a list of every window and tab. From there, you can connect via WebSocket and start talking to the DOM.

This is not an official API. It’s a debug interface. It’s fragile, undocumented, and will break whenever Antigravity updates its UI. I’ll address that later. But it works.

The Script

The full script is ag-model-switch.mjs. Here’s how the key pieces work.

Target Discovery

First, we need to find the right Antigravity window. CDP exposes every Electron page, including internal ones we don’t want (the Launchpad, background agents). We filter for the actual workbench:

async function discoverTarget() {
  const res = await fetch(`http://127.0.0.1:9222/json/list`);
  const pages = await res.json();

  const target = pages.find(t =>
    t.type === 'page' &&
    t.webSocketDebuggerUrl &&
    !t.title?.includes('Launchpad') &&
    !t.url?.includes('workbench-jetski-agent') &&
    (t.url?.includes('workbench') || t.title?.includes('Antigravity'))
  );

  if (!target?.webSocketDebuggerUrl) {
    throw new Error('No Antigravity workbench page found.');
  }

  return target.webSocketDebuggerUrl;
}

Minimal CDP Client

Instead of pulling in a heavyweight library like Puppeteer, the script implements a minimal CDP client using Node 24’s native WebSocket. About 100 lines for connecting, sending commands, tracking execution contexts, and handling timeouts:

class CdpClient {
  #ws = null;
  #id = 1;
  #pending = new Map();
  #contexts = [];

  async connect(wsUrl) {
    this.#ws = new WebSocket(wsUrl);
    await new Promise((resolve, reject) => {
      this.#ws.onopen = resolve;
      this.#ws.onerror = () => reject(new Error('WebSocket failed'));
    });
    // ... message handling, context tracking
  }

  async evaluate(expression) {
    const contextId = this.getBestContextId();
    const res = await this.call('Runtime.evaluate', {
      expression,
      returnByValue: true,
      awaitPromise: true,
      contextId,
    });
    return res?.result?.value;
  }
}

The getBestContextId() method is important. Antigravity runs multiple execution contexts (the main window, extensions, panels). The model selector lives in the cascade-panel context, so we prefer that one:

getBestContextId() {
  const cascade = this.#contexts.find(c =>
    c.url?.includes('cascade-panel')
  );
  if (cascade) return cascade.id;

  // Fallback: Extension context, then first available
  const ext = this.#contexts.find(c =>
    c.name?.includes('Extension')
  );
  return ext?.id ?? this.#contexts[0]?.id;
}

Reading the Model List

The model selector in Antigravity’s UI is a list of <button> elements with specific Tailwind CSS classes. The script queries the DOM for these elements and extracts the model names:

const items = Array.from(document.querySelectorAll('button.cursor-pointer'))
  .filter(el => el.className.includes('px-2 py-1 flex w-full items-center justify-between'));

return items.map(el => ({
  text: el.textContent.trim().replace(/New$/, '').trim(),
  cls: el.className
}));

The active model has a bg-gray- class (without a hover: prefix). Inactive models only have hover:bg. That’s how we detect which one is currently selected.

Switching Models

The setModel() function finds the target button (exact match first, then fuzzy substring match), checks if it’s already active (making the operation idempotent), clicks it, waits 600ms for the UI to update, then verifies the switch worked:

// Already selected? Do nothing.
if (isActive(target)) {
  return { ok: true, model: name, alreadySelected: true };
}

// Click to select
target.click();
await new Promise(r => setTimeout(r, 600));

// Verify the switch
const updated = document.querySelectorAll(MODEL_SELECTOR)
  .filter(el => el.className.includes(CLASS_FP));
const selected = updated.find(el =>
  clean(el).toLowerCase() === name.toLowerCase() && isActive(el)
);

return { ok: true, model: name, verified: !!selected };

CLI Interface

The final script supports three modes:

# List all available models (marks the current one)
node ag-model-switch.mjs --list
#   Gemini 3 Flash
#   Gemini 3.1 Pro (Low)
#   Gemini 3.1 Pro ← current
#   Claude Sonnet 4.6 (Thinking)
#   Claude Opus 4.6 (Thinking)

# Show current model
node ag-model-switch.mjs --current
# Claude Opus 4.6 (Thinking)

# Switch (exact or fuzzy match)
node ag-model-switch.mjs "Gemini 3 Flash"
# OK: Switched to Gemini 3 Flash

node ag-model-switch.mjs "flash"
# OK: Switched to Gemini 3 Flash

Zero dependencies. 380 lines. Works with any model Antigravity supports.

The Real Power: Workflow Integration

The script alone is useful. But the real value comes when you wire it into automated workflows.

I have a workflow called /gd that handles git commits. It analyzes diffs, groups changes into atomic commits, runs a security audit, writes conventional commit messages, and pushes. It’s the kind of task that needs an AI model, but doesn’t need a good one. Writing commit messages is a Flash-tier job.

Here’s how the workflow starts:

# Step 0: Save current model and switch to cheap one
PATH="$HOME/.local/share/mise/installs/node/24.13.1/bin:$PATH" \
  node ag-model-switch.mjs --current > /tmp/.gd-original-model.txt

PATH="$HOME/.local/share/mise/installs/node/24.13.1/bin:$PATH" \
  node ag-model-switch.mjs "Gemini 3.1 Pro (Low)"

The workflow runs its steps (diff analysis, security scan, commit grouping, message writing) all on the cheap model. Then at the end:

# Step N: Restore original model
PATH="$HOME/.local/share/mise/installs/node/24.13.1/bin:$PATH" \
  node ag-model-switch.mjs "$(cat /tmp/.gd-original-model.txt)"

rm -f /tmp/.gd-original-model.txt

The pattern is simple: save, switch, work, restore.

Every /gd run now costs Flash-tier quota instead of Opus-tier. For a task that analyzes diffs and writes commit messages, there’s zero quality difference. The cheap model handles it perfectly.

The Fragile Truth

I want to be upfront about the limitations:

DOM selectors will break. The script targets Tailwind CSS classes like px-2 py-1 flex w-full items-center justify-between. When Antigravity updates its UI (and it will), these classes will change and the script will stop working. I’ve already had to update them once.

The model panel must be visible. The script reads DOM elements that exist in the sidebar. If the model selector panel isn’t rendered, there’s nothing to query.

CDP requires a restart. You need to add the remote-debugging-port config and restart Antigravity. It’s a one-time setup, but it’s not zero-friction.

This is a hack. There’s no official “switch model” API. I’m clicking invisible buttons through a debug protocol. It’s the kind of thing that works perfectly until it doesn’t.

When it breaks, here’s how you fix it:

  1. Connect to CDP and probe the DOM for elements containing model names
  2. Find the new class fingerprint on the model buttons
  3. Update four constants at the top of the script:
const MODEL_ITEM_SELECTOR = 'button.cursor-pointer';
const MODEL_ITEM_CLASS_FP = 'px-2 py-1 flex w-full items-center justify-between';
const ACTIVE_CLASS_TOKEN = 'bg-gray-';
const INACTIVE_PREFIX = 'hover:bg';

Takes about 10 minutes when it happens. Worth it.

What This Means

The broader principle here isn’t about Antigravity or CDP or model switching. It’s about a mindset.

AI coding tools have limits. You can’t control the pricing. You can’t stop the goalposts from moving. But you can control how you spend your quota.

Not every task needs the expensive model. Commit messages, linting, file renames, boilerplate generation, dependency updates: these are all Flash-tier tasks. Architecture decisions, complex debugging, multi-file refactors: those are where you want the frontier model.

The IDE doesn’t make this distinction for you. So you make it yourself.

The best developer tools aren’t always the ones you download. Sometimes they’re the ones you build on top of the tools you already have.


The script is available at github.com/chongivan/ag-model-switch. If you use Antigravity and you’re tired of burning premium credits on git commit -m "fix typo", give it a try.

Previously: They Keep Moving the Finish Line