feat: close sessions from history page with inline popover

Add ability to close active sessions directly from the Session History
page via an inline popover with outcome selection and optional notes.
Adds two new outcomes: cancelled and resolved_externally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-11 01:59:12 -04:00
parent 91d2bc6df3
commit 416bb230e3
6 changed files with 659 additions and 17 deletions

View File

@@ -3,7 +3,7 @@ from typing import Optional, Any, Literal
from uuid import UUID
from pydantic import BaseModel, Field, validator
SessionOutcome = Literal["resolved", "escalated", "workaround", "unresolved"]
SessionOutcome = Literal["resolved", "escalated", "workaround", "unresolved", "cancelled", "resolved_externally"]
class CustomStepSchema(BaseModel):

View File

@@ -163,6 +163,53 @@ class TestSessions:
assert data["outcome"] == "resolved"
assert data["outcome_notes"] == "Issue fixed after restarting service"
@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
@pytest.mark.asyncio
async def test_complete_session_requires_outcome(
self, client: AsyncClient, auth_headers: dict, test_tree: dict

View File

@@ -0,0 +1,60 @@
# Session Closure from History Page — Design
> **Date:** 2026-03-11
## Problem
Active sessions on the Session History page only have "View Details" and "Resume" buttons. Engineers have no way to close out sessions that were abandoned, resolved externally, or otherwise no longer needed — without resuming the entire flow.
## Design Decisions
- **Outcome model:** Hybrid — reuse existing 4 outcomes (resolved, escalated, workaround, unresolved) + add 2 early-closure outcomes (cancelled, resolved_externally)
- **UX:** Inline popover anchored to a "Close" button on the session card — no modal, no slide panel
- **Scope:** Active sessions only (started but not completed). No bulk close. No AI summary generation.
- **Backend:** No new endpoints or migrations. Expand `SessionOutcome` literal type; existing `POST /sessions/{id}/complete` handles everything.
## Data Model
No new columns. Expand `SessionOutcome` in `backend/app/schemas/session.py`:
```python
SessionOutcome = Literal["resolved", "escalated", "workaround", "unresolved", "cancelled", "resolved_externally"]
```
`VARCHAR(20)` on `session.outcome` fits both new values (max 19 chars for `resolved_externally`).
## UI
### Close Button
Appears on active session cards (`started_at` is set, `completed_at` is null), between "View Details" and "Resume":
```
[View Details] [Close] [Resume]
```
Secondary button styling (border, muted text). Not shown on prepared or completed sessions.
### Close Popover
Anchored below the "Close" button:
- **Outcome selector:** `<select>` with 6 options — Resolved, Escalated, Workaround, Unresolved, Cancelled, Resolved Externally
- **Notes:** Optional textarea (2 rows)
- **Confirm:** `bg-gradient-brand`, disabled until outcome selected
- **Cancel / click outside:** Closes popover
- Glass card styling (`glass-card-static` pattern)
On confirm: calls `POST /sessions/{id}/complete` with `{ outcome, outcome_notes }`, updates local state, shows toast.
## Implementation Scope
### Backend (2 files)
1. `backend/app/schemas/session.py` — add new outcome values to `SessionOutcome`
2. Update frontend outcome type to match
### Frontend (2-3 files)
1. `frontend/src/types/` — update `SessionOutcome` TypeScript type
2. `frontend/src/pages/SessionHistoryPage.tsx` — add Close button, popover, outcome label formatting for new values
No new components, endpoints, or migrations.

View File

@@ -0,0 +1,404 @@
# 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"
```

View File

@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useRef, useCallback } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { PageMeta } from '@/components/common/PageMeta'
import { StaggerList } from '@/components/common/StaggerList'
import { sessionsApi } from '@/api/sessions'
import { treesApi } from '@/api/trees'
import type { Session, TreeListItem } from '@/types'
import type { Session, TreeListItem, SessionOutcome } from '@/types'
import type { DateRange } from 'react-day-picker'
import { SessionFilters } from '@/components/session/SessionFilters'
import type { SessionFilterState } from '@/components/session/SessionFilters'
@@ -24,6 +24,13 @@ export function SessionHistoryPage() {
const [isLoading, setIsLoading] = useState(true)
const [filter, setFilter] = useState<'all' | 'completed' | 'active' | 'prepared'>('all')
// Close session popover state
const [closingSessionId, setClosingSessionId] = useState<string | null>(null)
const [closeOutcome, setCloseOutcome] = useState<SessionOutcome | ''>('')
const [closeNotes, setCloseNotes] = useState('')
const [closeLoading, setCloseLoading] = useState(false)
const closePopoverRef = useRef<HTMLDivElement>(null)
// Initialize filters from URL params
const [filters, setFilters] = useState<SessionFilterState>(() => {
const ticketNumber = searchParams.get('ticket') || ''
@@ -147,6 +154,46 @@ export function SessionHistoryPage() {
})
}
const handleCloseSession = useCallback(async () => {
if (!closingSessionId || !closeOutcome) return
setCloseLoading(true)
try {
await sessionsApi.complete(closingSessionId, {
outcome: closeOutcome,
outcome_notes: closeNotes || undefined,
})
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])
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString()
}
@@ -157,9 +204,15 @@ export function SessionHistoryPage() {
const formatOutcomeLabel = (outcome: Session['outcome']): string => {
if (!outcome) return 'Not set'
return outcome === 'workaround'
? 'Workaround'
: outcome.charAt(0).toUpperCase() + outcome.slice(1)
const labels: Record<string, string> = {
resolved: 'Resolved',
escalated: 'Escalated',
workaround: 'Workaround',
unresolved: 'Unresolved',
cancelled: 'Cancelled',
resolved_externally: 'Resolved Externally',
}
return labels[outcome] ?? outcome
}
return (
@@ -254,6 +307,8 @@ export function SessionHistoryPage() {
session.outcome === 'workaround' && 'bg-amber-500/20 text-amber-300',
session.outcome === 'escalated' && 'bg-blue-500/20 text-blue-300',
session.outcome === 'unresolved' && 'bg-rose-500/20 text-rose-300',
session.outcome === 'cancelled' && 'bg-zinc-500/20 text-zinc-300',
session.outcome === 'resolved_externally' && 'bg-cyan-500/20 text-cyan-300',
!session.outcome && 'bg-accent text-muted-foreground'
)}
>
@@ -285,7 +340,7 @@ export function SessionHistoryPage() {
</div>
{/* Actions */}
<div className="flex gap-2">
<div className="relative flex gap-2">
<button
onClick={() => navigate(`/sessions/${session.id}`)}
className={cn(
@@ -295,16 +350,92 @@ export function SessionHistoryPage() {
>
View Details
</button>
{!session.completed_at && (
<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'
)}
{!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"
>
Resume
</button>
<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)}
title="Session outcome"
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>
</div>

View File

@@ -1,7 +1,7 @@
import type { TreeStructure } from './tree'
import type { Step, StepContent } from './step'
export type SessionOutcome = 'resolved' | 'escalated' | 'workaround' | 'unresolved'
export type SessionOutcome = 'resolved' | 'escalated' | 'workaround' | 'unresolved' | 'cancelled' | 'resolved_externally'
export interface DecisionRecord {
node_id: string