From 253f0e3946e594357e41ffd9fd5cac52f16231bf Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:12:02 -0700 Subject: [PATCH] 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 --- .../git-commit-message-generation-methods.ts | 4 + .../runtime/runtime-git-generation-context.ts | 5 +- .../source-control-action-recipe-options.ts | 1 + ...tigravity-generation-compatibility.test.ts | 93 +++++++++++++++++++ .../antigravity-generation-compatibility.ts | 52 +++++++++++ .../src/runtime/runtime-git-client-context.ts | 17 +++- .../src/runtime/runtime-git-client.test.ts | 2 + .../runtime/runtime-git-generation-client.ts | 19 ++++ src/shared/antigravity-source-control.test.ts | 65 +++++++++++++ src/shared/commit-message-agent-spec.test.ts | 37 +++++++- .../commit-message-agent-specs-secondary.ts | 16 ++-- src/shared/commit-message-model-parsers.ts | 15 ++- src/shared/protocol-version.ts | 4 + src/shared/rpc-contract/git-params.ts | 2 + 14 files changed, 317 insertions(+), 15 deletions(-) create mode 100644 src/renderer/src/runtime/antigravity-generation-compatibility.test.ts create mode 100644 src/renderer/src/runtime/antigravity-generation-compatibility.ts create mode 100644 src/shared/antigravity-source-control.test.ts diff --git a/src/main/runtime/rpc/methods/git-commit-message-generation-methods.ts b/src/main/runtime/rpc/methods/git-commit-message-generation-methods.ts index 1dffcfb2211..dcc376decf4 100644 --- a/src/main/runtime/rpc/methods/git-commit-message-generation-methods.ts +++ b/src/main/runtime/rpc/methods/git-commit-message-generation-methods.ts @@ -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 } : {}) diff --git a/src/main/runtime/runtime-git-generation-context.ts b/src/main/runtime/runtime-git-generation-context.ts index 141b5f18220..74ad56a3818 100644 --- a/src/main/runtime/runtime-git-generation-context.ts +++ b/src/main/runtime/runtime-git-generation-context.ts @@ -48,7 +48,10 @@ export function pullRequestDraftGitExec( } export type RuntimeCommitMessageSettingsOverride = Partial< - Pick + Pick< + GlobalSettings, + 'commitMessageAi' | 'sourceControlAi' | 'agentCmdOverrides' | 'defaultTuiAgent' + > > & { commitMessageDiscoveryHostKey?: string sourceControlAiResolvedParams?: ResolvedSourceControlAiGenerationParams diff --git a/src/renderer/src/components/settings/source-control-action-recipe-options.ts b/src/renderer/src/components/settings/source-control-action-recipe-options.ts index 4a6f491c1ca..b6d8f622eef 100644 --- a/src/renderer/src/components/settings/source-control-action-recipe-options.ts +++ b/src/renderer/src/components/settings/source-control-action-recipe-options.ts @@ -59,6 +59,7 @@ export const getActionDescriptions = createLocalizedCatalog( const FALLBACK_AGENT_ARGS_PLACEHOLDER = '--model sonnet' const AGENT_ARGS_PLACEHOLDER_OVERRIDES: Partial> = { + 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', diff --git a/src/renderer/src/runtime/antigravity-generation-compatibility.test.ts b/src/renderer/src/runtime/antigravity-generation-compatibility.test.ts new file mode 100644 index 00000000000..f2b44417f8a --- /dev/null +++ b/src/renderer/src/runtime/antigravity-generation-compatibility.test.ts @@ -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) + }) + }) +} diff --git a/src/renderer/src/runtime/antigravity-generation-compatibility.ts b/src/renderer/src/runtime/antigravity-generation-compatibility.ts new file mode 100644 index 00000000000..6c7d101a0cf --- /dev/null +++ b/src/renderer/src/runtime/antigravity-generation-compatibility.ts @@ -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 { + 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 Antigravity’s configured model. Update the remote server or select an explicit Antigravity model.' +} diff --git a/src/renderer/src/runtime/runtime-git-client-context.ts b/src/renderer/src/runtime/runtime-git-client-context.ts index 53751a51246..076978357ea 100644 --- a/src/renderer/src/runtime/runtime-git-client-context.ts +++ b/src/renderer/src/runtime/runtime-git-client-context.ts @@ -32,7 +32,12 @@ export type RuntimePullRequestGenerationInput = { } export type RuntimeGitSettings = Pick & - Partial> + 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> & { +): 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) } } diff --git a/src/renderer/src/runtime/runtime-git-client.test.ts b/src/renderer/src/runtime/runtime-git-client.test.ts index 9b426035b60..62085575b64 100644 --- a/src/renderer/src/runtime/runtime-git-client.test.ts +++ b/src/renderer/src/runtime/runtime-git-client.test.ts @@ -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' diff --git a/src/renderer/src/runtime/runtime-git-generation-client.ts b/src/renderer/src/runtime/runtime-git-generation-client.ts index 2bf17b3bb3d..3d823778ff9 100644 --- a/src/renderer/src/runtime/runtime-git-generation-client.ts +++ b/src/renderer/src/runtime/runtime-git-generation-client.ts @@ -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 } + const compatibilityError = await antigravityGenerationCompatibilityError( + target.environmentId, + context, + 'commitMessage', + overrides + ) + if (compatibilityError) { + return { success: false, error: compatibilityError } + } return callRuntimeRpc( target, 'git.generateCommitMessage', @@ -114,6 +124,15 @@ export async function generateRuntimePullRequestFields( ...(overrides?.agentCmdOverrides ? { agentCmdOverrides: overrides.agentCmdOverrides } : {}) }) as Promise } + const compatibilityError = await antigravityGenerationCompatibilityError( + target.environmentId, + context, + 'pullRequest', + overrides + ) + if (compatibilityError) { + return { success: false, error: compatibilityError } + } return callRuntimeRpc( target, 'git.generatePullRequestFields', diff --git a/src/shared/antigravity-source-control.test.ts b/src/shared/antigravity-source-control.test.ts new file mode 100644 index 00000000000..ed35713df29 --- /dev/null +++ b/src/shared/antigravity-source-control.test.ts @@ -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') + }) +}) diff --git a/src/shared/commit-message-agent-spec.test.ts b/src/shared/commit-message-agent-spec.test.ts index a9073cb0057..c304e129331 100644 --- a/src/shared/commit-message-agent-spec.test.ts +++ b/src/shared/commit-message-agent-spec.test.ts @@ -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)' } + ]) }) }) diff --git a/src/shared/commit-message-agent-specs-secondary.ts b/src/shared/commit-message-agent-specs-secondary.ts index 2ad7691ac36..53992143d19 100644 --- a/src/shared/commit-message-agent-specs-secondary.ts +++ b/src/shared/commit-message-agent-specs-secondary.ts @@ -216,15 +216,17 @@ export function buildSecondaryCommitMessageAgentSpecs({ // using `--print=` 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' } } } diff --git a/src/shared/commit-message-model-parsers.ts b/src/shared/commit-message-model-parsers.ts index 036557d5b89..072033e305e 100644 --- a/src/shared/commit-message-model-parsers.ts +++ b/src/shared/commit-message-model-parsers.ts @@ -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 idlabel. + 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) diff --git a/src/shared/protocol-version.ts b/src/shared/protocol-version.ts index 5c4fc6fbb12..8227950bd0c 100644 --- a/src/shared/protocol-version.ts +++ b/src/shared/protocol-version.ts @@ -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', diff --git a/src/shared/rpc-contract/git-params.ts b/src/shared/rpc-contract/git-params.ts index 6e4cb35b8ef..c606c5df382 100644 --- a/src/shared/rpc-contract/git-params.ts +++ b/src/shared/rpc-contract/git-params.ts @@ -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() })