fix(pilot): persist AI-proposal rejection + clear on outcome write

Issue #3 from phase-8-review-issues.md. 'Not yet' on the AI-confirming
banner was a local-state hide; the proposal re-surfaced on the next
refreshSessionDerived call.

Two-part fix:
- PATCH /outcome now clears ai_outcome_proposal on any terminal action
  (engineer has taken a decision; stale AI proposal is moot).
- New DELETE /ai-sessions/:sid/suggested-fixes/:fid/ai-outcome-proposal
  endpoint for explicit 'Not yet' rejection. Does not touch status
  or state_version — pure UI state.

Frontend handleRejectAIProposal now calls the DELETE and setActiveFix
with the server response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-23 22:15:48 -04:00
parent de2bef3175
commit 70c5da0c75
4 changed files with 168 additions and 6 deletions

View File

@@ -196,6 +196,18 @@ export const sessionSuggestedFixesApi = {
)
return r.data
},
/**
* Explicitly dismiss the AI-proposed outcome banner ("Not yet").
* Clears ai_outcome_proposal on the server without touching status or
* state_version. Idempotent: returns 200 even when the field is already null.
*/
async clearAIProposal(sessionId: string, fixId: string): Promise<SessionSuggestedFix> {
const r = await apiClient.delete<SessionSuggestedFix>(
`/ai-sessions/${sessionId}/suggested-fixes/${fixId}/ai-outcome-proposal`,
)
return r.data
},
}
export default sessionSuggestedFixesApi

View File

@@ -572,12 +572,20 @@ export default function AssistantChatPage() {
await handleSetOutcome(fixOutcome, notes)
}, [activeFix, handleSetOutcome])
// Phase 8: reject the AI proposal — clear it locally (client-side only for v1;
// the proposal will re-surface on next server fetch but that's acceptable).
const handleRejectAIProposal = useCallback(() => {
if (!activeFix) return
setActiveFix({ ...activeFix, ai_outcome_proposal: null })
}, [activeFix])
// Phase 8: reject the AI proposal — persist the rejection to the server so
// the banner does not re-surface on the next refreshSessionDerived call.
// Falls back to a local-state clear on error (non-fatal: banner may re-arm
// on the next refetch, matching the previous behaviour).
const handleRejectAIProposal = useCallback(async () => {
if (!activeFix || !activeChatId) return
try {
const updated = await sessionSuggestedFixesApi.clearAIProposal(activeChatId, activeFix.id)
setActiveFix(updated)
} catch {
// Non-fatal fallback: clear locally so the banner disappears immediately.
setActiveFix({ ...activeFix, ai_outcome_proposal: null })
}
}, [activeFix, activeChatId])
// Phase 8: silence the nudge banner without recording an outcome.
const handleSilenceNudge = useCallback(() => {