# Session Closure from History Page — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Allow engineers to close active sessions directly from the Session History page via an inline popover with outcome selection and optional notes. **Architecture:** No new endpoints or migrations. Expand the existing `SessionOutcome` type with two new values (`cancelled`, `resolved_externally`). Add a "Close" button + popover to active session cards on the history page. The popover calls the existing `POST /sessions/{id}/complete` endpoint. **Tech Stack:** Python/FastAPI (backend schema only), React/TypeScript (frontend UI) --- ### Task 1: Backend — Expand SessionOutcome Type **Files:** - Modify: `backend/app/schemas/session.py:6` **Step 1: Write the failing test** Add to `backend/tests/test_sessions.py`, inside the existing `TestSessions` class, after the `test_complete_session` test: ```python @pytest.mark.asyncio async def test_complete_session_with_cancelled_outcome( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test completing a session with 'cancelled' outcome.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] response = await client.post( f"/api/v1/sessions/{session_id}/complete", json={"outcome": "cancelled", "outcome_notes": "Ticket withdrawn by client"}, headers=auth_headers ) assert response.status_code == 200 data = response.json() assert data["outcome"] == "cancelled" assert data["outcome_notes"] == "Ticket withdrawn by client" assert data["completed_at"] is not None @pytest.mark.asyncio async def test_complete_session_with_resolved_externally_outcome( self, client: AsyncClient, auth_headers: dict, test_tree: dict ): """Test completing a session with 'resolved_externally' outcome.""" create_response = await client.post( "/api/v1/sessions", json={"tree_id": test_tree["id"]}, headers=auth_headers ) session_id = create_response.json()["id"] response = await client.post( f"/api/v1/sessions/{session_id}/complete", json={"outcome": "resolved_externally"}, headers=auth_headers ) assert response.status_code == 200 data = response.json() assert data["outcome"] == "resolved_externally" assert data["completed_at"] is not None ``` **Step 2: Run tests to verify they fail** Run: `docker exec resolutionflow_backend pytest tests/test_sessions.py::TestSessions::test_complete_session_with_cancelled_outcome tests/test_sessions.py::TestSessions::test_complete_session_with_resolved_externally_outcome -v` Expected: FAIL — 422 validation error because `cancelled` and `resolved_externally` are not valid `SessionOutcome` values. **Step 3: Update SessionOutcome literal** In `backend/app/schemas/session.py`, line 6, change: ```python SessionOutcome = Literal["resolved", "escalated", "workaround", "unresolved"] ``` to: ```python SessionOutcome = Literal["resolved", "escalated", "workaround", "unresolved", "cancelled", "resolved_externally"] ``` No other backend changes needed — the `POST /sessions/{id}/complete` endpoint, the `Session` model (`VARCHAR(20)`), and the `SessionComplete` schema all work with the new values automatically. **Step 4: Run tests to verify they pass** Run: `docker exec resolutionflow_backend pytest tests/test_sessions.py::TestSessions::test_complete_session_with_cancelled_outcome tests/test_sessions.py::TestSessions::test_complete_session_with_resolved_externally_outcome -v` Expected: PASS **Step 5: Run full test suite to check for regressions** Run: `docker exec resolutionflow_backend pytest tests/test_sessions.py -v` Expected: All session tests PASS. **Step 6: Commit** ```bash git add backend/app/schemas/session.py backend/tests/test_sessions.py git commit -m "feat: add cancelled and resolved_externally session outcomes" ``` --- ### Task 2: Frontend — Update SessionOutcome Type **Files:** - Modify: `frontend/src/types/session.ts:4` **Step 1: Update the TypeScript type** In `frontend/src/types/session.ts`, line 4, change: ```typescript export type SessionOutcome = 'resolved' | 'escalated' | 'workaround' | 'unresolved' ``` to: ```typescript export type SessionOutcome = 'resolved' | 'escalated' | 'workaround' | 'unresolved' | 'cancelled' | 'resolved_externally' ``` **Step 2: Verify build** Run: `cd frontend && npm run build` Expected: Clean build, no errors. **Step 3: Commit** ```bash git add frontend/src/types/session.ts git commit -m "feat: add cancelled and resolved_externally to frontend SessionOutcome type" ``` --- ### Task 3: Frontend — Add Close Button and Popover to Session History **Files:** - Modify: `frontend/src/pages/SessionHistoryPage.tsx` This is the main UI task. Add a "Close" button to active session cards and an inline popover with outcome selection + notes. **Step 1: Add state and handler** At the top of the `SessionHistoryPage` component (after existing `useState` calls around line 25), add: ```tsx const [closingSessionId, setClosingSessionId] = useState(null) const [closeOutcome, setCloseOutcome] = useState('') const [closeNotes, setCloseNotes] = useState('') const [closeLoading, setCloseLoading] = useState(false) ``` Add the import for `SessionOutcome` to the existing type import on line 7: ```typescript import type { Session, TreeListItem, SessionOutcome } from '@/types' ``` Add `useRef` to the React import on line 1: ```typescript import { useEffect, useState, useRef, useCallback } from 'react' ``` Add the close handler function inside the component, after `handleClearFilters`: ```tsx const closePopoverRef = useRef(null) const handleCloseSession = useCallback(async () => { if (!closingSessionId || !closeOutcome) return setCloseLoading(true) try { await sessionsApi.complete(closingSessionId, { outcome: closeOutcome, outcome_notes: closeNotes || undefined, }) // Update local state — mark session as completed setSessions(prev => prev.map(s => s.id === closingSessionId ? { ...s, completed_at: new Date().toISOString(), outcome: closeOutcome, outcome_notes: closeNotes || null } : s ) ) toast.success('Session closed') setClosingSessionId(null) setCloseOutcome('') setCloseNotes('') } catch { toast.error('Failed to close session') } finally { setCloseLoading(false) } }, [closingSessionId, closeOutcome, closeNotes]) // Close popover on click outside useEffect(() => { if (!closingSessionId) return const handleClickOutside = (e: MouseEvent) => { if (closePopoverRef.current && !closePopoverRef.current.contains(e.target as Node)) { setClosingSessionId(null) setCloseOutcome('') setCloseNotes('') } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, [closingSessionId]) ``` **Step 2: Update formatOutcomeLabel** In `SessionHistoryPage.tsx`, update the `formatOutcomeLabel` function (around line 158) to handle new outcomes: ```tsx const formatOutcomeLabel = (outcome: Session['outcome']): string => { if (!outcome) return 'Not set' const labels: Record = { resolved: 'Resolved', escalated: 'Escalated', workaround: 'Workaround', unresolved: 'Unresolved', cancelled: 'Cancelled', resolved_externally: 'Resolved Externally', } return labels[outcome] ?? outcome } ``` **Step 3: Add outcome badge colors for new outcomes** In the session card JSX (around line 249-258), update the outcome badge to handle new outcomes. Add these two lines inside the `cn()` call, after the `!session.outcome` line: ```tsx session.outcome === 'cancelled' && 'bg-zinc-500/20 text-zinc-300', session.outcome === 'resolved_externally' && 'bg-cyan-500/20 text-cyan-300', ``` **Step 4: Add Close button and popover to the Actions div** In the session card's Actions div (around line 288-309), replace the entire `{/* Actions */}` block with: ```tsx {/* Actions */}
{!session.completed_at && session.started_at && ( <> )} {/* Close Session Popover */} {closingSessionId === session.id && (

Close Session