Fix Antigravity source-control model discovery and retired defaults (#21606)

* fix(antigravity): discover current source-control models and use CLI defaults

* fix(antigravity): gate configured models on remote runtime support

* fix(runtime): forward default TUI agent for remote git generation

* test(runtime): cover inherited agent forwarding
This commit is contained in:
Neil
2026-09-20 22:12:02 -07:00
committed by GitHub
parent 00da5fd556
commit 253f0e3946
14 changed files with 317 additions and 15 deletions
@@ -13,6 +13,7 @@ type CommitMessageGenerationOverride = {
sourceControlAi?: GlobalSettings['sourceControlAi']
sourceControlAiResolvedParams?: ResolvedSourceControlAiGenerationParams
agentCmdOverrides?: GlobalSettings['agentCmdOverrides']
defaultTuiAgent?: GlobalSettings['defaultTuiAgent']
commitMessageDiscoveryHostKey?: string
}
@@ -23,6 +24,7 @@ function buildCommitMessageGenerationOverride(params: {
sourceControlAi?: unknown
sourceControlAiResolvedParams?: unknown
agentCmdOverrides?: unknown
defaultTuiAgent?: GlobalSettings['defaultTuiAgent']
commitMessageDiscoveryHostKey?: string
}): CommitMessageGenerationOverride | undefined {
if (
@@ -30,6 +32,7 @@ function buildCommitMessageGenerationOverride(params: {
params.sourceControlAi === undefined &&
params.sourceControlAiResolvedParams === undefined &&
params.agentCmdOverrides === undefined &&
params.defaultTuiAgent === undefined &&
params.commitMessageDiscoveryHostKey === undefined
) {
return undefined
@@ -52,6 +55,7 @@ function buildCommitMessageGenerationOverride(params: {
agentCmdOverrides: params.agentCmdOverrides as GlobalSettings['agentCmdOverrides']
}
: {}),
...(params.defaultTuiAgent !== undefined ? { defaultTuiAgent: params.defaultTuiAgent } : {}),
...(params.commitMessageDiscoveryHostKey !== undefined
? { commitMessageDiscoveryHostKey: params.commitMessageDiscoveryHostKey }
: {})
@@ -48,7 +48,10 @@ export function pullRequestDraftGitExec(
}
export type RuntimeCommitMessageSettingsOverride = Partial<
Pick<GlobalSettings, 'commitMessageAi' | 'sourceControlAi' | 'agentCmdOverrides'>
Pick<
GlobalSettings,
'commitMessageAi' | 'sourceControlAi' | 'agentCmdOverrides' | 'defaultTuiAgent'
>
> & {
commitMessageDiscoveryHostKey?: string
sourceControlAiResolvedParams?: ResolvedSourceControlAiGenerationParams
@@ -59,6 +59,7 @@ export const getActionDescriptions = createLocalizedCatalog(
const FALLBACK_AGENT_ARGS_PLACEHOLDER = '--model sonnet'
const AGENT_ARGS_PLACEHOLDER_OVERRIDES: Partial<Record<TuiAgent, string>> = {
antigravity: '--effort low',
// Why: Source Control AI action prompts are short, reviewable tasks; the
// mini Codex model is a better default hint than the frontier model.
codex: '--model gpt-5.4-mini',
@@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../shared/constants'
import { ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import {
generateRuntimeCommitMessage,
generateRuntimePullRequestFields
} from './runtime-git-generation-client'
const mocks = vi.hoisted(() => ({ supports: vi.fn(), rpc: vi.fn() }))
vi.mock('./runtime-rpc-client', () => ({
getActiveRuntimeTarget: () => ({ kind: 'environment', environmentId: 'remote-test' }),
runtimeEnvironmentSupportsCapability: mocks.supports,
callRuntimeRpc: mocks.rpc
}))
beforeEach(() => {
vi.clearAllMocks()
mocks.supports.mockResolvedValue(false)
mocks.rpc.mockResolvedValue({ success: true })
})
for (const operation of ['commitMessage', 'pullRequest'] as const) {
describe(operation, () => {
function generate(model: string, resolved = true, agentArgs?: string) {
const settings = getDefaultSettings('/tmp')
settings.activeRuntimeEnvironmentId = 'remote-test'
settings.sourceControlAi = { ...settings.sourceControlAi!, agentId: 'antigravity' }
const context = { settings, worktreeId: 'wt-1', worktreePath: '/remote/workspace' }
const overrides = resolved
? {
sourceControlAiResolvedParams: {
agentId: 'antigravity' as const,
model,
...(agentArgs ? { agentArgs } : {})
}
}
: undefined
return operation === 'commitMessage'
? generateRuntimeCommitMessage(context, overrides)
: generateRuntimePullRequestFields(
context,
{ base: 'main', title: '', body: '', draft: true },
overrides
)
}
it('does not send the new default sentinel to an older server', async () => {
expect(await generate('default')).toMatchObject({
success: false,
error: expect.stringContaining('Update the remote server')
})
expect(mocks.rpc).not.toHaveBeenCalled()
expect(mocks.supports).toHaveBeenCalledWith(
'remote-test',
ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY
)
})
it('guards settings-derived defaults as well as one-shot selections', async () => {
expect(await generate('default', false)).toMatchObject({ success: false })
expect(mocks.rpc).not.toHaveBeenCalled()
})
it('sends the unchanged selection to a server that supports the configured model', async () => {
mocks.supports.mockResolvedValue(true)
expect(await generate('default')).toMatchObject({ success: true })
expect(mocks.rpc).toHaveBeenCalledWith(
{ kind: 'environment', environmentId: 'remote-test' },
operation === 'commitMessage'
? 'git.generateCommitMessage'
: 'git.generatePullRequestFields',
expect.objectContaining({
sourceControlAiResolvedParams: { agentId: 'antigravity', model: 'default' }
}),
expect.anything()
)
})
it('keeps explicit models usable on older servers without requiring the new capability', async () => {
expect(await generate('gemini-3.8-flash-low')).toMatchObject({ success: true })
expect(mocks.supports).not.toHaveBeenCalled()
expect(mocks.rpc).toHaveBeenCalledTimes(1)
})
it('accepts an explicit model supplied through the recipe CLI arguments', async () => {
expect(await generate('default', true, '--model gemini-3.8-flash-low')).toMatchObject({
success: true
})
expect(mocks.supports).not.toHaveBeenCalled()
expect(mocks.rpc).toHaveBeenCalledTimes(1)
})
})
}
@@ -0,0 +1,52 @@
import { ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY } from '../../../shared/protocol-version'
import { hasFlag } from '../../../shared/agent-cli-flag-detection'
import { planCommitMessageGeneration } from '../../../shared/commit-message-plan'
import { resolveSourceControlAiForOperation } from '../../../shared/source-control-ai'
import {
getRuntimeCommitMessageSettings,
type RuntimeGitContext,
type RuntimeGenerateCommitMessageOverrides
} from './runtime-git-client-context'
import { runtimeEnvironmentSupportsCapability } from './runtime-rpc-client'
export async function antigravityGenerationCompatibilityError(
environmentId: string,
context: RuntimeGitContext,
operation: 'commitMessage' | 'pullRequest',
overrides?: RuntimeGenerateCommitMessageOverrides
): Promise<string | null> {
let params = overrides?.sourceControlAiResolvedParams
if (!params) {
const settings = getRuntimeCommitMessageSettings(context.settings, context.connectionId)
const resolved = resolveSourceControlAiForOperation({
settings: {
...settings,
defaultTuiAgent: context.settings?.defaultTuiAgent ?? null,
sourceControlAi: overrides?.sourceControlAi ?? settings.sourceControlAi,
agentCmdOverrides: overrides?.agentCmdOverrides ?? settings.agentCmdOverrides ?? {}
},
operation,
discoveryHostKey: settings.commitMessageDiscoveryHostKey
})
if (resolved.ok) {
params = resolved.value.params
}
}
if (params?.agentId !== 'antigravity' || params.model !== 'default') {
return null
}
const planned = planCommitMessageGeneration(params, '')
// A recipe or command override can already supply a model that older planners understand.
if (planned.ok && hasFlag(planned.plan.args, ['--model'])) {
return null
}
if (
await runtimeEnvironmentSupportsCapability(
environmentId,
ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY
)
) {
return null
}
return 'This remote Orca server does not support Antigravitys configured model. Update the remote server or select an explicit Antigravity model.'
}
@@ -32,7 +32,12 @@ export type RuntimePullRequestGenerationInput = {
}
export type RuntimeGitSettings = Pick<GlobalSettings, 'activeRuntimeEnvironmentId'> &
Partial<Pick<GlobalSettings, 'commitMessageAi' | 'sourceControlAi' | 'agentCmdOverrides'>>
Partial<
Pick<
GlobalSettings,
'commitMessageAi' | 'sourceControlAi' | 'agentCmdOverrides' | 'defaultTuiAgent'
>
>
export type RuntimeDiscoverCommitMessageModelsResult =
| {
@@ -77,7 +82,12 @@ export function getRuntimeGitScope(
export function getRuntimeCommitMessageSettings(
settings: RuntimeGitSettings | null | undefined,
connectionId?: string
): Partial<Pick<GlobalSettings, 'commitMessageAi' | 'sourceControlAi' | 'agentCmdOverrides'>> & {
): Partial<
Pick<
GlobalSettings,
'commitMessageAi' | 'sourceControlAi' | 'agentCmdOverrides' | 'defaultTuiAgent'
>
> & {
commitMessageDiscoveryHostKey?: string
} {
if (!settings) {
@@ -94,6 +104,9 @@ export function getRuntimeCommitMessageSettings(
...(settings.agentCmdOverrides !== undefined
? { agentCmdOverrides: settings.agentCmdOverrides }
: {}),
...(settings.defaultTuiAgent !== undefined
? { defaultTuiAgent: settings.defaultTuiAgent }
: {}),
commitMessageDiscoveryHostKey: getCommitMessageModelDiscoveryHostKeyForScope(scope)
}
}
@@ -721,6 +721,7 @@ describe('runtime git client', () => {
await generateRuntimeCommitMessage({
settings: {
activeRuntimeEnvironmentId: 'env-1',
defaultTuiAgent: 'codex',
commitMessageAi,
agentCmdOverrides
},
@@ -733,6 +734,7 @@ describe('runtime git client', () => {
method: 'git.generateCommitMessage',
params: {
worktree: 'id:wt-1',
defaultTuiAgent: 'codex',
commitMessageAi,
agentCmdOverrides,
commitMessageDiscoveryHostKey: 'runtime:env-1'
@@ -1,4 +1,5 @@
import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id'
import { antigravityGenerationCompatibilityError } from './antigravity-generation-compatibility'
import {
getRuntimeCommitMessageSettings,
resolveLocalWorktreePath,
@@ -32,6 +33,15 @@ export async function generateRuntimeCommitMessage(
...(overrides?.agentCmdOverrides ? { agentCmdOverrides: overrides.agentCmdOverrides } : {})
}) as Promise<RuntimeGenerateCommitMessageResult>
}
const compatibilityError = await antigravityGenerationCompatibilityError(
target.environmentId,
context,
'commitMessage',
overrides
)
if (compatibilityError) {
return { success: false, error: compatibilityError }
}
return callRuntimeRpc<RuntimeGenerateCommitMessageResult>(
target,
'git.generateCommitMessage',
@@ -114,6 +124,15 @@ export async function generateRuntimePullRequestFields(
...(overrides?.agentCmdOverrides ? { agentCmdOverrides: overrides.agentCmdOverrides } : {})
}) as Promise<RuntimeGeneratePullRequestFieldsResult>
}
const compatibilityError = await antigravityGenerationCompatibilityError(
target.environmentId,
context,
'pullRequest',
overrides
)
if (compatibilityError) {
return { success: false, error: compatibilityError }
}
return callRuntimeRpc<RuntimeGeneratePullRequestFieldsResult>(
target,
'git.generatePullRequestFields',
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from './constants'
import { resolveSourceControlAiForOperation } from './source-control-ai'
import { planCommitMessageGeneration } from './commit-message-plan'
describe('Antigravity source-control model selection', () => {
for (const host of ['local', 'ssh:verification-host']) {
for (const operation of ['commitMessage', 'branchName', 'pullRequest'] as const) {
it(`${operation} on ${host} falls back from a retired saved model to the CLI configuration`, () => {
const settings = getDefaultSettings('/tmp')
settings.sourceControlAi = {
...settings.sourceControlAi!,
enabled: true,
agentId: 'antigravity',
selectedModelByAgent: { antigravity: 'Gemini 3.5 Flash (Medium)' },
selectedModelByAgentByHost: {
[host]: { antigravity: 'Gemini 3.5 Flash (Medium)' }
},
discoveredModelsByAgent: {},
discoveredModelsByAgentByHost: {}
}
const result = resolveSourceControlAiForOperation({
settings,
repo: null,
operation,
discoveryHostKey: host
})
expect(result.ok).toBe(true)
if (!result.ok) {
throw new Error(result.error)
}
expect(result.value.params.model).toBe('default')
const plan = planCommitMessageGeneration(result.value.params, 'Write Git text')
expect(plan.ok).toBe(true)
if (!plan.ok) {
throw new Error(plan.error)
}
expect(plan.plan.args).toEqual(['--print=Write Git text', '--sandbox'])
expect(plan.plan.stdinPayload).toBeNull()
})
}
}
it('lets explicit recipe arguments override the discovered model and requested effort once', () => {
const result = planCommitMessageGeneration(
{
agentId: 'antigravity',
model: 'gemini-3.8-flash-medium',
thinkingLevel: 'medium',
agentArgs: '--model gemini-3.7-flash-low --effort low'
},
'Write Git text'
)
expect(result.ok).toBe(true)
if (!result.ok) {
throw new Error(result.error)
}
expect(result.plan.args.filter((arg) => arg === '--model')).toHaveLength(1)
expect(result.plan.args.filter((arg) => arg === '--effort')).toHaveLength(1)
expect(result.plan.args).not.toContain('gemini-3.8-flash-medium')
expect(result.plan.args).not.toContain('medium')
expect(result.plan.args).toContain('gemini-3.7-flash-low')
expect(result.plan.args).toContain('low')
})
})
+35 -2
View File
@@ -653,8 +653,41 @@ describe('buildArgs (Antigravity)', () => {
expect(spec.modelDiscovery?.args).toEqual(['models'])
})
it('uses Gemini 3.5 Flash (Medium) as default model', () => {
expect(COMMIT_MESSAGE_AGENT_SPECS.antigravity?.defaultModelId).toBe('Gemini 3.5 Flash (Medium)')
it('uses the configured CLI model instead of a bundled model that can retire', () => {
expect(spec.defaultModelId).toBe('default')
expect(
spec.buildArgs({ prompt: 'Generate a commit message', model: spec.defaultModelId })
).toEqual(['--print=Generate a commit message', '--sandbox'])
})
it('passes only a nonempty requested effort', () => {
expect(spec.buildArgs({ prompt: 'P', model: 'default', thinkingLevel: '' })).not.toContain(
'--effort'
)
expect(spec.buildArgs({ prompt: 'P', model: 'default', thinkingLevel: 'high' })).toEqual([
'--print=P',
'--sandbox',
'--effort',
'high'
])
})
it('parses current tab-separated IDs without treating progress text as a model', () => {
expect(
parseAntigravityModels(
[
'Fetching available models...',
'id\tLabel',
'gemini-3.8-flash-medium\tGemini 3.8 Flash (Medium)',
'claude-sonnet-4-6\tClaude Sonnet 4.6 (Thinking)',
'gemini-3.8-flash-medium\tGemini 3.8 Flash (Medium)',
''
].join('\r\n')
)
).toEqual([
{ id: 'gemini-3.8-flash-medium', label: 'Gemini 3.8 Flash (Medium)' },
{ id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6 (Thinking)' }
])
})
})
@@ -216,15 +216,17 @@ export function buildSecondaryCommitMessageAgentSpecs({
// using `--print=<value>` so a leading-dash prompt binds to the flag instead of
// being parsed as its own option, and --sandbox/--model stay separate options.
promptDelivery: 'argv',
buildArgs: ({ prompt, model }) => [`--print=${prompt}`, '--sandbox', '--model', model],
buildArgs: ({ prompt, model, thinkingLevel }) => [
`--print=${prompt}`,
'--sandbox',
...(model && model !== 'default' ? ['--model', model] : []),
...(thinkingLevel ? ['--effort', thinkingLevel] : [])
],
singletonOptions: [['--model'], ['--effort']],
modelSource: 'dynamic',
modelDiscovery: { binary: 'agy', args: ['models'], parse: parseAntigravityModels },
models: [
{ id: 'Gemini 3.5 Flash (Medium)', label: 'Gemini 3.5 Flash (Medium)' },
{ id: 'Gemini 3.5 Flash (High)', label: 'Gemini 3.5 Flash (High)' },
{ id: 'Gemini 3.5 Flash (Low)', label: 'Gemini 3.5 Flash (Low)' }
],
defaultModelId: 'Gemini 3.5 Flash (Medium)'
models: [{ id: 'default', label: 'Config default' }],
defaultModelId: 'default'
}
}
}
+12 -3
View File
@@ -235,13 +235,22 @@ export function parseCursorModels(stdout: string): CommitMessageModel[] {
export function parseAntigravityModels(stdout: string): CommitMessageModel[] {
const models: CommitMessageModel[] = []
for (const rawLine of iterateModelOutputLines(stdout)) {
const id = rawLine.trim()
if (id.length === 0) {
const line = rawLine.trim()
const separator = line.indexOf('\t')
const id = (separator === -1 ? line : line.slice(0, separator)).trim()
const label = separator === -1 ? id : line.slice(separator + 1).trim()
// Older agy versions list display names; current versions emit id<TAB>label.
if (
!id ||
!label ||
(separator === -1 && !/^.+ \((?:low|medium|high|thinking)\)$/i.test(id)) ||
(separator !== -1 && (/\s/.test(id) || /^(?:id|model)$/i.test(id)))
) {
continue
}
models.push({
id,
label: id
label
})
}
return uniqueModels(models)
+4
View File
@@ -292,7 +292,11 @@ export const ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES = [
SESSION_TABS_RETIREMENT_PROOF_DELTA_RUNTIME_CAPABILITY
] as const
export const ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY =
'git.antigravity-configured-model.v1' as const
export const RUNTIME_CAPABILITIES = [
ANTIGRAVITY_CONFIGURED_MODEL_RUNTIME_CAPABILITY,
'files.pathsExist',
'runtime.status.compat.v1',
'runtime.environments.v1',
+2
View File
@@ -1,5 +1,6 @@
import { z } from 'zod'
import { OptionalGitAdmissionTier } from './git-admission-tier-params'
import { OptionalTuiAgent } from './worktree-params'
export const WorktreeSelector = z.object({
worktree: z
@@ -183,6 +184,7 @@ export const GitGenerateCommitMessage = WorktreeSelector.extend({
sourceControlAi: SourceControlAiSettings.optional(),
sourceControlAiResolvedParams: ResolvedSourceControlAiGenerationParams.optional(),
agentCmdOverrides: z.record(z.string(), z.string()).optional(),
defaultTuiAgent: OptionalTuiAgent.nullable(),
commitMessageDiscoveryHostKey: z.string().optional()
})