# 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<string | null>(null)
const [closeOutcome, setCloseOutcome] = useState<SessionOutcome | ''>('')
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<HTMLDivElement>(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<string, string> = {
    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 */}
<div className="relative flex gap-2">
  <button
    onClick={() => navigate(`/sessions/${session.id}`)}
    className={cn(
      'rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground',
      'hover:bg-accent hover:text-foreground'
    )}
  >
    View Details
  </button>
  {!session.completed_at && session.started_at && (
    <>
      <button
        onClick={() => {
          setClosingSessionId(closingSessionId === session.id ? null : session.id)
          setCloseOutcome('')
          setCloseNotes('')
        }}
        className={cn(
          'rounded-md border border-border px-3 py-2 text-sm font-medium text-muted-foreground',
          'hover:bg-accent hover:text-foreground',
          closingSessionId === session.id && 'bg-accent text-foreground'
        )}
      >
        Close
      </button>
      <button
        onClick={() => navigate(getSessionResumePath(session.tree_id, session.tree_snapshot?.tree_type), { state: { sessionId: session.id } })}
        className={cn(
          'rounded-md bg-gradient-brand px-3 py-2 text-sm font-medium text-white shadow-lg shadow-primary/20',
          'hover:opacity-90'
        )}
      >
        Resume
      </button>
    </>
  )}

  {/* Close Session Popover */}
  {closingSessionId === session.id && (
    <div
      ref={closePopoverRef}
      className="absolute right-0 top-full z-20 mt-2 w-72 rounded-xl border border-border bg-card p-4 shadow-xl"
    >
      <p className="text-sm font-heading font-medium text-foreground mb-3">Close Session</p>

      <label className="block text-xs font-label text-muted-foreground mb-1">Outcome</label>
      <select
        value={closeOutcome}
        onChange={(e) => setCloseOutcome(e.target.value as SessionOutcome)}
        className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none mb-3"
      >
        <option value="">Select outcome...</option>
        <option value="resolved">Resolved</option>
        <option value="escalated">Escalated</option>
        <option value="workaround">Workaround</option>
        <option value="unresolved">Unresolved</option>
        <option value="cancelled">Cancelled</option>
        <option value="resolved_externally">Resolved Externally</option>
      </select>

      <label className="block text-xs font-label text-muted-foreground mb-1">Notes (optional)</label>
      <textarea
        value={closeNotes}
        onChange={(e) => setCloseNotes(e.target.value)}
        rows={2}
        placeholder="Add closure notes..."
        className="w-full rounded-lg border border-border bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-[rgba(6,182,212,0.3)] focus:outline-none resize-none mb-3"
      />

      <div className="flex items-center justify-end gap-2">
        <button
          onClick={() => {
            setClosingSessionId(null)
            setCloseOutcome('')
            setCloseNotes('')
          }}
          className="rounded-lg px-3 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-foreground"
        >
          Cancel
        </button>
        <button
          onClick={handleCloseSession}
          disabled={!closeOutcome || closeLoading}
          className={cn(
            'rounded-lg px-4 py-1.5 text-sm font-medium shadow-lg shadow-primary/20 transition-opacity',
            closeOutcome
              ? 'bg-gradient-brand text-[#101114] hover:opacity-90'
              : 'bg-gradient-brand text-[#101114] opacity-50 cursor-not-allowed'
          )}
        >
          {closeLoading ? 'Closing...' : 'Confirm'}
        </button>
      </div>
    </div>
  )}
</div>
```

**Step 5: Verify build**

Run: `cd frontend && npm run build`

Expected: Clean build, no errors.

**Step 6: Commit**

```bash
git add frontend/src/pages/SessionHistoryPage.tsx
git commit -m "feat: add close session button with inline popover on history page"
```

---

### Task 4: Verify Full Stack

**Step 1: Run backend tests**

Run: `docker exec resolutionflow_backend pytest tests/test_sessions.py -v`

Expected: All tests PASS including the 2 new ones.

**Step 2: Run frontend build**

Run: `cd frontend && npm run build`

Expected: Clean build.

**Step 3: Manual smoke test**

1. Open http://localhost:5173/sessions
2. Find an active session (yellow dot, no completed_at)
3. Verify "Close" button appears between "View Details" and "Resume"
4. Click "Close" — popover appears below
5. Select "Cancelled" outcome, type a note
6. Click "Confirm" — session card updates with completed status + "Cancelled" badge
7. Verify "Close" and "Resume" buttons disappear (session is now completed)
8. Verify completed sessions do NOT show a "Close" button
9. Verify prepared sessions (not yet started) do NOT show a "Close" button

**Step 4: Final commit (if any adjustments needed)**

```bash
git add -A
git commit -m "fix: session closure adjustments from smoke test"
```
