From 6ecb5a9bbd11f7e4620fe4794a203b73d8044dc1 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 21 Mar 2026 18:20:34 -0400 Subject: [PATCH] fix(flowpilot): widen message bar, add close/abandon session support - Message bar now fixed-positioned above action bar with full-width layout (respects both app sidebar and session sidebar) - Added abandon_session endpoint (POST /ai-sessions/{id}/abandon) - Added "Close" button to FlowPilot action bar with confirmation dialog - Session can now be closed without resolving or escalating Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/endpoints/ai_sessions.py | 28 +++++++ backend/app/services/flowpilot_engine.py | 23 ++++++ frontend/src/api/aiSessions.ts | 6 ++ .../flowpilot/FlowPilotActionBar.tsx | 76 ++++++++++++++++--- .../flowpilot/FlowPilotMessageBar.tsx | 62 ++++++++------- .../components/flowpilot/FlowPilotSession.tsx | 3 + frontend/src/hooks/useFlowPilotSession.ts | 14 ++++ frontend/src/pages/FlowPilotSessionPage.tsx | 1 + 8 files changed, 170 insertions(+), 43 deletions(-) diff --git a/backend/app/api/endpoints/ai_sessions.py b/backend/app/api/endpoints/ai_sessions.py index 7fb7d519..c04ff591 100644 --- a/backend/app/api/endpoints/ai_sessions.py +++ b/backend/app/api/endpoints/ai_sessions.py @@ -396,6 +396,34 @@ async def resume_session( await db.commit() +# ── Abandon / Close ── + +@router.post("/{session_id}/abandon", status_code=204) +@limiter.limit("15/minute") +async def abandon_session( + request: Request, + session_id: UUID, + current_user: Annotated[User, Depends(get_current_active_user)], + db: Annotated[AsyncSession, Depends(get_db)], + _: None = Depends(require_engineer_or_admin), + reason: str | None = None, +): + """Close a session without resolving or escalating.""" + try: + await flowpilot_engine.abandon_session( + session_id=session_id, + user_id=current_user.id, + reason=reason, + db=db, + ) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + except PermissionError as e: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e)) + + await db.commit() + + # ── Escalation Queue ── @router.get("/escalation-queue", response_model=list[AISessionSummary]) diff --git a/backend/app/services/flowpilot_engine.py b/backend/app/services/flowpilot_engine.py index fa0487d9..6e553957 100644 --- a/backend/app/services/flowpilot_engine.py +++ b/backend/app/services/flowpilot_engine.py @@ -793,6 +793,29 @@ async def resume_session( await db.flush() +async def abandon_session( + session_id: UUID, + user_id: UUID, + reason: Optional[str], + db: AsyncSession, +) -> None: + """Close a session without resolving or escalating. + + Used when the engineer no longer needs help, figured it out on their own, + or the session is no longer relevant. + """ + session = await _load_session(session_id, user_id, db) + + if session.status not in ("active", "paused"): + raise ValueError(f"Cannot close session in status: {session.status}") + + session.status = "abandoned" + session.resolved_at = datetime.now(timezone.utc) + if reason: + session.resolution_notes = reason + await db.flush() + + async def rate_session( session_id: UUID, rating: int, diff --git a/frontend/src/api/aiSessions.ts b/frontend/src/api/aiSessions.ts index 728d4565..86764547 100644 --- a/frontend/src/api/aiSessions.ts +++ b/frontend/src/api/aiSessions.ts @@ -96,6 +96,12 @@ export const aiSessionsApi = { await apiClient.post(`/ai-sessions/${sessionId}/resume`) }, + async abandonSession(sessionId: string, reason?: string): Promise { + await apiClient.post(`/ai-sessions/${sessionId}/abandon`, null, { + params: reason ? { reason } : undefined, + }) + }, + async pickupSession(sessionId: string, data: PickupSessionRequest): Promise { const response = await apiClient.post( `/ai-sessions/${sessionId}/pickup`, diff --git a/frontend/src/components/flowpilot/FlowPilotActionBar.tsx b/frontend/src/components/flowpilot/FlowPilotActionBar.tsx index 0b8a2407..a909a03d 100644 --- a/frontend/src/components/flowpilot/FlowPilotActionBar.tsx +++ b/frontend/src/components/flowpilot/FlowPilotActionBar.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { CheckCircle2, ArrowUpRight, Pause } from 'lucide-react' +import { CheckCircle2, ArrowUpRight, Pause, X } from 'lucide-react' import { EscalateModal } from './EscalateModal' import type { ResolveSessionRequest, EscalateSessionRequest, SessionDocumentation } from '@/types/ai-session' @@ -12,6 +12,7 @@ interface FlowPilotActionBarProps { onResolve: (data: ResolveSessionRequest) => Promise onEscalate: (data: EscalateSessionRequest) => Promise onPause?: () => Promise + onAbandon?: () => Promise } export function FlowPilotActionBar({ @@ -23,9 +24,11 @@ export function FlowPilotActionBar({ onResolve, onEscalate, onPause, + onAbandon, }: FlowPilotActionBarProps) { const [showResolve, setShowResolve] = useState(false) const [showEscalate, setShowEscalate] = useState(false) + const [showAbandon, setShowAbandon] = useState(false) const [resolutionSummary, setResolutionSummary] = useState('') const [submitting, setSubmitting] = useState(false) @@ -51,6 +54,18 @@ export function FlowPilotActionBar({ } } + const handleAbandon = async () => { + if (onAbandon) { + setSubmitting(true) + try { + await onAbandon() + setShowAbandon(false) + } finally { + setSubmitting(false) + } + } + } + return ( <> {/* Bottom bar — fixed to viewport bottom, works regardless of height chain */} @@ -76,16 +91,28 @@ export function FlowPilotActionBar({ Escalate - {onPause && ( - - )} +
+ {onPause && ( + + )} + {onAbandon && ( + + )} +
{/* Resolve modal */} @@ -121,6 +148,33 @@ export function FlowPilotActionBar({ )} + {/* Close/Abandon confirmation */} + {showAbandon && ( +
+
+

Close Session

+

+ Are you sure you want to close this session? The session history will be kept but it won't count as resolved. +

+
+ + +
+
+
+ )} + {/* Escalate modal */} -
-
+