Let users reset saved Pi model choices on the current host

This commit is contained in:
Neil
2026-09-11 01:28:25 -07:00
parent 26233e19fb
commit d419e9d723
6 changed files with 223 additions and 2 deletions
@@ -1,3 +1,5 @@
import { PiConfiguredModelReset } from './PiConfiguredModelReset'
import { getConnectionIdFromState } from '@/lib/connection-context'
import { useRef } from 'react'
import type React from 'react'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
@@ -103,6 +105,11 @@ export function CommitMessageAiPane({
const storeSearchQuery = useAppStore((s) => s.settingsSearchQuery)
const searchQuery = settingsSearchQuery ?? storeSearchQuery
const config = readSettings(settings)
const piHostKey = useAppStore((s) =>
getCommitMessageModelDiscoveryHostKeyForScope(
getRuntimeGitScope(settings, getConnectionIdFromState(s, s.activeWorktreeId))
)
)
const ownership = getSettingOwnershipSummary('sourceControlAiDefaults')
const settingsWriteQueueRef = useRef<Promise<void>>(undefined!)
settingsWriteQueueRef.current ??= Promise.resolve()
@@ -214,6 +221,24 @@ export function CommitMessageAiPane({
)
}
if (
config.enabled &&
matchesSettingsSearch(searchQuery, {
title: translate('settings.piConfiguredModelReset.title', 'Saved Pi model choices'),
description: translate('settings.piConfiguredModelReset.action', 'Use Pi’s configured model'),
keywords: ['pi', 'model', 'copilot']
})
) {
sections.push(
<PiConfiguredModelReset
key="pi-model-reset"
config={config}
hostKey={piHostKey}
writeConfig={writeConfig}
/>
)
}
sections.push(
<SourceControlAiActionRecipeDefaults
key="action-recipes"
@@ -0,0 +1,72 @@
import { useState } from 'react'
import { toast } from 'sonner'
import type {
SourceControlAiSettings,
SourceControlAiSettingsPatch
} from '../../../../shared/source-control-ai-types'
import {
hasSavedPiSourceControlModel,
resetPiSourceControlModelsForHost
} from '../../../../shared/pi-source-control-model-reset'
import { Button } from '../ui/button'
import { Label } from '../ui/label'
import { translate } from '@/i18n/i18n'
export function PiConfiguredModelReset({
config,
hostKey,
writeConfig
}: {
config: SourceControlAiSettings
hostKey: string
writeConfig: (patch: SourceControlAiSettingsPatch) => Promise<void>
}): React.JSX.Element | null {
const [saving, setSaving] = useState(false)
if (!hasSavedPiSourceControlModel(config, hostKey)) {
return null
}
const reset = async (): Promise<void> => {
setSaving(true)
try {
await writeConfig((current) => resetPiSourceControlModelsForHost(current, hostKey))
toast.success(
translate(
'settings.piConfiguredModelReset.success',
'Pi now uses its configured model on this host.'
)
)
} catch {
toast.error(
translate(
'settings.piConfiguredModelReset.error',
'Could not reset saved Pi model choices.'
)
)
} finally {
setSaving(false)
}
}
return (
<div className="flex items-center justify-between gap-4 py-2">
<div className="min-w-0 space-y-1">
<Label>
{translate('settings.piConfiguredModelReset.title', 'Saved Pi model choices')}
</Label>
<p className="text-xs text-muted-foreground">
{translate(
'settings.piConfiguredModelReset.description',
'Reset saved Pi model choices for commit messages, pull requests, and branch names on this host. Models in recipe CLI arguments still apply.'
)}
</p>
<p className="text-xs text-muted-foreground">
{translate('settings.piConfiguredModelReset.host', 'Host: {{host}}', {
host: hostKey === 'local' ? 'This computer' : 'Current workspace host'
})}
</p>
</div>
<Button variant="outline" size="sm" disabled={saving} onClick={() => void reset()}>
{translate('settings.piConfiguredModelReset.action', 'Use Pi’s configured model')}
</Button>
</div>
)
}
@@ -3,6 +3,11 @@ import { translateSearchKeyword } from './settings-search-keywords'
import { createLocalizedCatalog } from '@/i18n/localized-catalog'
export const getCommitMessageAiPaneSearchEntries = createLocalizedCatalog(() => [
{
title: translate('settings.piConfiguredModelReset.title', 'Saved Pi model choices'),
description: translate('settings.piConfiguredModelReset.action', 'Use Pi’s configured model'),
keywords: ['pi', 'model', 'copilot', 'reset']
},
{
title: translate(
'auto.components.settings.commit.message.ai.search.24dbdfca78',
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest'
import { getDefaultSettings } from './constants'
import {
hasSavedPiSourceControlModel,
resetPiSourceControlModelsForHost
} from './pi-source-control-model-reset'
import {
resolveSourceControlAiForOperation,
projectSourceControlAiToLegacyCommitMessageAi,
mergeLegacyCommitMessageAiIntoSourceControlAi
} from './source-control-ai'
const copilot = 'github-copilot/gpt-5.4-mini'
describe('Pi model reset', () => {
it.each(['local', 'ssh:fixture', 'wsl:Ubuntu', 'runtime:fixture'])(
'resets only Pi choices for %s through save/reload',
(hostKey) => {
const settings = getDefaultSettings('/tmp')
const config = settings.sourceControlAi!
config.agentId = 'pi'
config.selectedModelByAgent = { pi: copilot, codex: 'gpt-5.5' }
config.selectedModelByAgentByHost = {
[hostKey]: { pi: copilot, codex: 'gpt-5.5' },
'ssh:other': { pi: 'other/model' }
}
config.modelOverridesByOperation = {
branchName: {
selectedModelByAgentByHost: {
[hostKey]: { pi: copilot },
'ssh:other': { pi: 'other/branch' }
}
}
}
config.modelOverridesByOperation.branchName!.selectedThinkingByModel = { 'gpt-5.5': 'high' }
config.actions = { branchName: { agentId: 'pi', agentArgs: '--thinking low' } }
expect(hasSavedPiSourceControlModel(config, hostKey)).toBe(true)
const reset = resetPiSourceControlModelsForHost(config, hostKey)
expect(hasSavedPiSourceControlModel(reset, hostKey)).toBe(false)
expect(reset.actions).toEqual(config.actions)
expect(reset.modelOverridesByOperation?.branchName?.selectedThinkingByModel).toEqual({
'gpt-5.5': 'high'
})
expect(reset.selectedModelByAgentByHost?.['ssh:other']).toEqual({ pi: 'other/model' })
expect(reset.selectedModelByAgentByHost?.[hostKey]?.codex).toBe('gpt-5.5')
expect(
reset.modelOverridesByOperation?.branchName?.selectedModelByAgentByHost?.['ssh:other']
).toEqual({ pi: 'other/branch' })
expect(config.selectedModelByAgentByHost?.[hostKey]?.pi).toBe(copilot)
if (hostKey !== 'local') {
expect(reset.selectedModelByAgent.pi).toBe(copilot)
}
settings.commitMessageAi = projectSourceControlAiToLegacyCommitMessageAi(reset)
settings.sourceControlAi = mergeLegacyCommitMessageAiIntoSourceControlAi(
reset,
settings.commitMessageAi
)
for (const operation of ['commitMessage', 'pullRequest', 'branchName'] as const) {
const resolved = resolveSourceControlAiForOperation({
settings,
operation,
discoveryHostKey: hostKey
})
expect(resolved.ok && resolved.value.params.useConfiguredDefaultModel).toBe(true)
}
}
)
it('does not reset an unresolved host', () => {
const config = getDefaultSettings('/tmp').sourceControlAi!
config.selectedModelByAgent.pi = copilot
expect(hasSavedPiSourceControlModel(config, 'unknown')).toBe(false)
expect(resetPiSourceControlModelsForHost(config, 'unknown')).toBe(config)
})
})
@@ -0,0 +1,44 @@
import {
clearSourceControlAiModelChoiceForHost,
readSourceControlAiModelChoiceForHost
} from './source-control-ai-model-selection'
import { PI_DEFAULT_MODEL_ID } from './pi-configured-default-model'
import { UNKNOWN_COMMIT_MESSAGE_HOST_KEY } from './commit-message-host-key'
import type { SourceControlAiSettings } from './source-control-ai-types'
export function hasSavedPiSourceControlModel(
settings: SourceControlAiSettings,
hostKey: string
): boolean {
if (hostKey === UNKNOWN_COMMIT_MESSAGE_HOST_KEY) {
return false
}
return [settings, ...Object.values(settings.modelOverridesByOperation ?? {})].some((choice) => {
const model = readSourceControlAiModelChoiceForHost(choice, hostKey, 'pi')
return model !== undefined && model !== PI_DEFAULT_MODEL_ID
})
}
export function resetPiSourceControlModelsForHost(
settings: SourceControlAiSettings,
hostKey: string
): SourceControlAiSettings {
if (hostKey === UNKNOWN_COMMIT_MESSAGE_HOST_KEY) {
return settings
}
const choice = clearSourceControlAiModelChoiceForHost(settings, hostKey, 'pi')
const overrides = { ...settings.modelOverridesByOperation }
for (const operation of Object.keys(overrides) as (keyof typeof overrides)[]) {
const previous = overrides[operation]
const cleared = clearSourceControlAiModelChoiceForHost(previous, hostKey, 'pi')
overrides[operation] = previous?.selectedThinkingByModel
? { ...cleared, selectedThinkingByModel: previous.selectedThinkingByModel }
: cleared
}
return {
...settings,
selectedModelByAgent: choice?.selectedModelByAgent ?? {},
selectedModelByAgentByHost: choice?.selectedModelByAgentByHost ?? {},
modelOverridesByOperation: overrides
}
}
+4 -2
View File
@@ -1,6 +1,6 @@
import assert from 'node:assert/strict'
import { once } from 'node:events'
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises'
import { createServer } from 'node:http'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
@@ -88,7 +88,9 @@ try {
join(dir, 'settings.json'),
JSON.stringify({ defaultProvider: 'orca-proof', defaultModel: 'local' })
)
const settings = getDefaultSettings(scratch)
const settings = process.argv[3]
? JSON.parse(await readFile(resolve(process.argv[3]), 'utf8')).settings
: getDefaultSettings(scratch)
settings.defaultTuiAgent = 'pi'
settings.sourceControlAi.agentId = 'pi'
settings.commitMessageAi.agentId = 'pi'