Remove repo source control AI draft sync effect

This commit is contained in:
Neil
2026-05-29 06:20:58 -07:00
parent 8e425069b7
commit 37cf284f2a
2 changed files with 138 additions and 36 deletions
@@ -0,0 +1,70 @@
import { describe, expect, it } from 'vitest'
import type { RepoSourceControlAiOverrides } from '../../../../shared/source-control-ai-types'
import { createRepoAiDraftState, resolveRepoAiDraftState } from './RepositorySourceControlAiSection'
describe('RepositorySourceControlAiSection draft state', () => {
it('refreshes clean drafts when persisted repo overrides change', () => {
const state = createRepoAiDraftState('repo-1', {
instructionsByOperation: {
commitMessage: 'old'
}
})
const persisted: RepoSourceControlAiOverrides = {
instructionsByOperation: {
commitMessage: 'new'
}
}
expect(resolveRepoAiDraftState(state, 'repo-1', persisted)).toEqual({
repoId: 'repo-1',
value: persisted,
baseSerialized: JSON.stringify(persisted)
})
})
it('preserves dirty drafts across external repo override changes', () => {
const state = createRepoAiDraftState('repo-1', {
instructionsByOperation: {
commitMessage: 'old'
}
})
state.value = {
instructionsByOperation: {
commitMessage: 'local edit'
}
}
const persisted: RepoSourceControlAiOverrides = {
instructionsByOperation: {
commitMessage: 'external edit'
}
}
expect(resolveRepoAiDraftState(state, 'repo-1', persisted)).toBe(state)
})
it('resets the draft when the selected repo changes', () => {
const state = createRepoAiDraftState('repo-1', {
prCreationDefaults: {
draft: true
}
})
state.value = {
prCreationDefaults: {
draft: false
}
}
const persisted: RepoSourceControlAiOverrides = {
prCreationDefaults: {
useTemplate: true
}
}
expect(resolveRepoAiDraftState(state, 'repo-2', persisted)).toEqual({
repoId: 'repo-2',
value: persisted,
baseSerialized: JSON.stringify(persisted)
})
})
})
@@ -1,6 +1,6 @@
/* eslint-disable max-lines -- Why: repo Source Control AI settings keep one
draft/save flow across model, instruction, and PR-default override groups. */
import { useEffect, useMemo, useState } from 'react'
import { useMemo, useState } from 'react'
import type { Repo } from '../../../../shared/types'
import type {
RepoSourceControlAiOverrides,
@@ -98,6 +98,39 @@ function serializeRepoAiDraft(value: RepoSourceControlAiOverrides): string {
return JSON.stringify(normalizeRepoAiDraft(value))
}
export function createRepoAiDraftState(
repoId: string,
value: RepoSourceControlAiOverrides
): RepoAiDraftState {
const normalized = normalizeRepoAiDraft(value)
return {
repoId,
value: normalized,
baseSerialized: serializeRepoAiDraft(normalized)
}
}
export function resolveRepoAiDraftState(
current: RepoAiDraftState,
repoId: string,
persistedRepoAi: RepoSourceControlAiOverrides,
persistedSerialized = serializeRepoAiDraft(persistedRepoAi)
): RepoAiDraftState {
const currentSerialized = serializeRepoAiDraft(current.value)
if (
current.repoId !== repoId ||
currentSerialized === current.baseSerialized ||
currentSerialized === persistedSerialized
) {
return {
repoId,
value: persistedRepoAi,
baseSerialized: persistedSerialized
}
}
return current
}
export function RepositorySourceControlAiSection({
repo,
updateRepo
@@ -137,44 +170,47 @@ export function RepositorySourceControlAiSection({
)
// Why: repo.sourceControlAi is saved as one nested value; a local draft keeps
// textarea keystrokes and sibling controls from racing over IPC/RPC.
const [draftState, setDraftState] = useState<RepoAiDraftState>(() => ({
repoId: repo.id,
value: persistedRepoAi,
baseSerialized: persistedSerialized
}))
const [draftState, setDraftState] = useState<RepoAiDraftState>(() =>
createRepoAiDraftState(repo.id, persistedRepoAi)
)
const [isSaving, setIsSaving] = useState(false)
const [saveError, setSaveError] = useState<string | null>(null)
useEffect(() => {
setDraftState((current) => {
const currentSerialized = serializeRepoAiDraft(current.value)
if (
current.repoId !== repo.id ||
currentSerialized === current.baseSerialized ||
currentSerialized === persistedSerialized
) {
return {
repoId: repo.id,
value: persistedRepoAi,
baseSerialized: persistedSerialized
}
}
return current
})
setSaveError(null)
}, [persistedRepoAi, persistedSerialized, repo.id])
const resolvedDraftState = resolveRepoAiDraftState(
draftState,
repo.id,
persistedRepoAi,
persistedSerialized
)
if (resolvedDraftState !== draftState) {
// Why: repo settings may be refreshed externally; clean drafts should
// follow that source before paint, while dirty edits stay in place.
setDraftState(resolvedDraftState)
if (saveError !== null) {
setSaveError(null)
}
}
const repoAi = draftState.value
const repoAi = resolvedDraftState.value
const draftSerialized = useMemo(() => serializeRepoAiDraft(repoAi), [repoAi])
const isDirty = draftState.repoId !== repo.id || draftSerialized !== draftState.baseSerialized
const isDirty =
resolvedDraftState.repoId !== repo.id || draftSerialized !== resolvedDraftState.baseSerialized
const updateDraftRepoAi = (
update: (current: RepoSourceControlAiOverrides) => RepoSourceControlAiOverrides
): void => {
setDraftState((current) => ({
...current,
value: normalizeRepoAiDraft(update(current.value))
}))
setDraftState((current) => {
const resolved = resolveRepoAiDraftState(
current,
repo.id,
persistedRepoAi,
persistedSerialized
)
return {
...resolved,
value: normalizeRepoAiDraft(update(resolved.value))
}
})
setSaveError(null)
}
@@ -182,7 +218,7 @@ export function RepositorySourceControlAiSection({
if (!isDirty || isSaving) {
return
}
const next = normalizeRepoAiDraft(draftState.value)
const next = normalizeRepoAiDraft(resolvedDraftState.value)
const nextSerialized = serializeRepoAiDraft(next)
setIsSaving(true)
setSaveError(null)
@@ -211,11 +247,7 @@ export function RepositorySourceControlAiSection({
}
const discardDraft = (): void => {
setDraftState({
repoId: repo.id,
value: persistedRepoAi,
baseSerialized: persistedSerialized
})
setDraftState(createRepoAiDraftState(repo.id, persistedRepoAi))
setSaveError(null)
}