feat(pilot): Phase 6 — post-resolve templatize prompt + draft accept/reject
All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
Closes the loop on the Phase 5 "Run now, templatize after resolve" path.
After a session resolves, drafts queued by the three-option dialog surface
as a modal that lets the engineer review the AI-proposed parameterization
and either save as a reusable team template or skip. A "don't ask again"
toggle writes to account_settings.preferences so the next resolve won't
pop the modal.
Backend:
- /api/v1/draft-templates:
* GET — list account drafts (pending_only default true; pass false for
audit view including accepted/rejected)
* GET /{id} — single draft
* POST /{id}/accept — promotes to a new script_templates row with
source_session_id / source_user_id / source_ticket_ref populated
(drives the Script Library "generated from CW #X · resolved by Y"
provenance chip). Draft flips to status=accepted,
promoted_template_id set, resolved_at stamped. 409 on re-accept /
already-rejected. 400 on unknown category_id.
* POST /{id}/reject — flips to status=rejected. 409 on re-reject.
- /api/v1/accounts/me/preferences (GET/PATCH) — thin wrapper over
AccountSettings.get_setting/set_setting. PATCH merges keys into the
JSONB column, preserving existing keys the client didn't touch.
Used by the "Don't ask again for this team" checkbox
(templatize_prompt_enabled=false) and, forward-looking, by
cw_resolved_status_id / cw_escalated_status_id from Phase 4.
- 13 tests: list filter, accept with/without edited_body, provenance
copy-through, reject, 409 on re-accept / re-reject, 400 on unknown
category, prefs round-trip with merge semantics.
Frontend:
- src/components/pilot/script/TemplatizePrompt.tsx — modal showing the
drafted script with proposed parameters in the Phase 5
ParameterizationPreview, editable name/category/description, an
individual-parameter remove button, and the "don't ask again" opt-out.
Accept posts to /draft-templates/{id}/accept + optionally PATCHes
preferences. Skip posts /reject.
- src/api/draftTemplates.ts — typed client plus accountPreferencesApi.
- AssistantChatPage: after a successful Resolve (external OR local),
fetches preferences + pending drafts for the session and queues the
modal one draft at a time. Escalate does not trigger this flow.
- Sidebar: Scripts nav shows the pending-draft count as a badge. Fetched
independently of the main sidebar stats so endpoint flakes don't
break the rest of the sidebar.
Verified live 2026-04-22: seed two drafts → GET sees both pending →
accept draft A (template created, provenance CW #99123 populated) →
reject draft B → pending count drops → PATCH opt-out → GET confirms
persistence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,12 @@ import { SuggestedFix } from '@/components/pilot/sections/SuggestedFix'
|
||||
import { ResolutionNotePreview as ResolutionNotePreviewPopover } from '@/components/pilot/ResolutionNotePreview'
|
||||
import { TemplateMatchPanel } from '@/components/pilot/script/TemplateMatchPanel'
|
||||
import { NoTemplateDialog } from '@/components/pilot/script/NoTemplateDialog'
|
||||
import { TemplatizePrompt } from '@/components/pilot/script/TemplatizePrompt'
|
||||
import {
|
||||
draftTemplatesApi,
|
||||
accountPreferencesApi,
|
||||
type DraftTemplate,
|
||||
} from '@/api/draftTemplates'
|
||||
import { PILOT_INLINE_SCRIPT_EVENT } from '@/lib/pilotEvents'
|
||||
import { sessionFactsApi, type SessionFact } from '@/api/sessionFacts'
|
||||
import {
|
||||
@@ -109,6 +115,11 @@ export default function AssistantChatPage() {
|
||||
// whether the active fix has a script_template_id.
|
||||
const [scriptPanelOpen, setScriptPanelOpen] = useState(false)
|
||||
const [scriptDecisionBusy, setScriptDecisionBusy] = useState(false)
|
||||
// Phase 6: post-resolve "save as template?" queue. After Resolve succeeds
|
||||
// we fetch pending drafts for this session and show the modal one at a
|
||||
// time; the user accepts, rejects, or toggles "don't ask again", and we
|
||||
// advance to the next pending draft.
|
||||
const [templatizeQueue, setTemplatizeQueue] = useState<DraftTemplate[]>([])
|
||||
const [showOverflow, setShowOverflow] = useState(false)
|
||||
const toggleSidebarCollapse = () => {
|
||||
const next = !sidebarCollapsed
|
||||
@@ -505,6 +516,27 @@ export default function AssistantChatPage() {
|
||||
toast.success('Session escalated locally (no PSA ticket linked)')
|
||||
}
|
||||
handleClosePreview()
|
||||
|
||||
// Phase 6: on a successful Resolve (either external or local), check
|
||||
// for pending draft_templates rows created by the Phase 5 three-option
|
||||
// dialog. Show the TemplatizePrompt modal iff:
|
||||
// - the account preference hasn't opted out
|
||||
// - the session has at least one pending draft
|
||||
// Escalate doesn't trigger this flow — only resolution.
|
||||
if (out.outcome === 'resolved' || out.outcome === 'resolved_local') {
|
||||
try {
|
||||
const prefs = await accountPreferencesApi.get()
|
||||
if (prefs.preferences.templatize_prompt_enabled === false) return
|
||||
const drafts = await draftTemplatesApi.list(true)
|
||||
const forThisSession = drafts.filter(
|
||||
(d) => d.source_session_id === activeChatId,
|
||||
)
|
||||
if (forThisSession.length > 0) setTemplatizeQueue(forThisSession)
|
||||
} catch {
|
||||
// Soft-fail: the Resolve itself succeeded. A missing preference
|
||||
// or list fetch is not worth blocking the success toast.
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error('[AssistantChat] confirm post failed:', err)
|
||||
const status = (err as { response?: { status?: number }; response?: { data?: { detail?: string } } })?.response?.status
|
||||
@@ -1431,6 +1463,16 @@ export default function AssistantChatPage() {
|
||||
context="status"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Phase 6: post-resolve "save as team template?" modal. Shown one draft
|
||||
at a time; onResolved advances the queue. */}
|
||||
{templatizeQueue.length > 0 && (
|
||||
<TemplatizePrompt
|
||||
draft={templatizeQueue[0]}
|
||||
sourceTicketRef={activePsaTicketId ? `CW #${activePsaTicketId}` : null}
|
||||
onResolved={() => setTemplatizeQueue((q) => q.slice(1))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user