# Task Lane Minimize/Reopen + Resolve Documentation Streaming — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make the task lane collapsible (not destructive) with a reopen pill, and replace the blocking resolve flow with instant resolve + streamed ticket notes generation. **Architecture:** Feature 1 is a frontend-only change to TaskLane close behavior + a new pill button. Feature 2 splits the resolve endpoint into a fast status-only call and a new SSE streaming endpoint for AI-generated ticket notes, with prompt caching and singleton client reuse on the backend. **Tech Stack:** React, TypeScript, Lucide icons, FastAPI, Anthropic SDK (streaming + prompt caching), SSE via `StreamingResponse` --- ### Task 1: Task Lane — Replace X with PanelRightClose Icon **Files:** - Modify: `frontend/src/components/assistant/TaskLane.tsx:2,250-252` - [ ] **Step 1: Update the import to include PanelRightClose** Replace line 2 import: ```tsx import { Copy, Check, SkipForward, Terminal, ChevronDown, ChevronUp, Send, Clipboard, Loader2, PanelRightClose, MessageCircleQuestion, Eye, } from 'lucide-react' ``` (Replace `X` with `PanelRightClose` in the import list.) - [ ] **Step 2: Replace the X icon in the close button** Replace lines 250-252: ```tsx ``` - [ ] **Step 3: Verify the icon renders correctly** Run the frontend dev server, open an assistant chat, trigger a task lane, and confirm the close button shows a panel-collapse icon instead of an X. - [ ] **Step 4: Commit** ```bash git add frontend/src/components/assistant/TaskLane.tsx git commit -m "feat: replace task lane X with PanelRightClose icon" ``` --- ### Task 2: Task Lane — Stop Destroying State on Close **Files:** - Modify: `frontend/src/pages/AssistantChatPage.tsx:797-800` - [ ] **Step 1: Remove clearTaskState from the onClose handler** Find the TaskLane onClose handler (around line 797-800): ```tsx onClose={() => { setShowTaskLane(false) if (activeChatId) clearTaskState(activeChatId) }} ``` Replace with: ```tsx onClose={() => { setShowTaskLane(false) }} ``` This keeps task state in sessionStorage and backend `pending_task_lane` so it can be restored. - [ ] **Step 2: Verify state persists after closing** 1. Open assistant chat, trigger task lane with questions/actions 2. Type partial answers into the task lane 3. Click the PanelRightClose button 4. Task lane disappears but answers should persist in sessionStorage Verify: `sessionStorage.getItem('rf-tasklane-meta')` still has `questions` and `actions` with the chatId. - [ ] **Step 3: Commit** ```bash git add frontend/src/pages/AssistantChatPage.tsx git commit -m "feat: task lane close preserves state instead of clearing" ``` --- ### Task 3: Task Lane — Add Reopen Pill to Input Toolbar **Files:** - Modify: `frontend/src/pages/AssistantChatPage.tsx:1-3,738-756` - [ ] **Step 1: Add ListChecks to the Lucide import** Find the Lucide import at the top of `AssistantChatPage.tsx` (line 3): ```tsx import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus } from 'lucide-react' ``` Add `ListChecks`: ```tsx import { Sparkles, Send, Loader2, Flag, MessageSquare, Paperclip, Terminal, X, RotateCcw, ImagePlus, ListChecks } from 'lucide-react' ``` - [ ] **Step 2: Add the Tasks pill button to the input toolbar** Find the input toolbar's left button group (around line 738-756). After the Conclude button block (the `{messages.length >= 2 && (` block that ends around line 755), add the Tasks pill: ```tsx {!showTaskLane && (activeQuestions.length > 0 || activeActions.length > 0) && ( )} ``` - [ ] **Step 3: Verify the full collapse/reopen flow** 1. Open assistant chat, trigger task lane 2. Click PanelRightClose — task lane collapses 3. Confirm "Tasks (N)" pill appears in the input toolbar 4. Click the pill — task lane reopens with previous state intact 5. Confirm pill disappears when task lane is open - [ ] **Step 4: Commit** ```bash git add frontend/src/pages/AssistantChatPage.tsx git commit -m "feat: add Tasks pill to reopen collapsed task lane" ``` --- ### Task 4: Backend — Singleton AsyncAnthropic Client **Files:** - Modify: `backend/app/core/ai_provider.py:168-210` - [ ] **Step 1: Add a module-level singleton for the Anthropic client** In `backend/app/core/ai_provider.py`, add a module-level cache dict before the `AnthropicProvider` class (before line 168): ```python # Singleton client cache — avoids creating new HTTP connections per call _anthropic_clients: dict[str, "anthropic.AsyncAnthropic"] = {} def _get_anthropic_client(api_key: str, timeout: int = 45) -> "anthropic.AsyncAnthropic": """Return a cached AsyncAnthropic client, creating one if needed.""" import anthropic cache_key = f"{api_key[:8]}:{timeout}" if cache_key not in _anthropic_clients: _anthropic_clients[cache_key] = anthropic.AsyncAnthropic( api_key=api_key, timeout=timeout, max_retries=1, ) return _anthropic_clients[cache_key] ``` - [ ] **Step 2: Update AnthropicProvider to use the singleton** Replace the `generate_json` method's client creation (lines 182-188): ```python async def generate_json( self, system_prompt: str, messages: list[dict[str, str]], max_tokens: int = 4096, ) -> tuple[str, int, int]: client = _get_anthropic_client(self._api_key, self._timeout) response = await client.messages.create( model=self._model, max_tokens=max_tokens, system=system_prompt, messages=messages, ) text = response.content[0].text input_tokens = response.usage.input_tokens output_tokens = response.usage.output_tokens return text, input_tokens, output_tokens ``` - [ ] **Step 3: Commit** ```bash git add backend/app/core/ai_provider.py git commit -m "perf: singleton AsyncAnthropic client to avoid per-call connection setup" ``` --- ### Task 5: Backend — Add generate_text_stream to AIProvider **Files:** - Modify: `backend/app/core/ai_provider.py:16-55,168-210` - [ ] **Step 1: Add the abstract method to the AIProvider base class** After the `generate_text` abstract method (after line 55), add: ```python async def generate_text_stream( self, system_prompt: str, messages: list[dict[str, str]], max_tokens: int = 4096, ) -> "AsyncIterator[str]": """Stream a text response token by token. Args: system_prompt: System-level instruction for the model. messages: List of message dicts with "role" and "content" keys. max_tokens: Maximum output tokens. Yields: Text chunks as they are generated. """ raise NotImplementedError("Streaming not supported for this provider") # Make this an async generator to satisfy type checker yield "" # pragma: no cover ``` Also add the import at the top of the file (line 1 area): ```python from collections.abc import AsyncIterator ``` - [ ] **Step 2: Implement streaming in AnthropicProvider** Add the streaming method to `AnthropicProvider` after `generate_text` (after line 210): ```python async def generate_text_stream( self, system_prompt: str, messages: list[dict[str, str]], max_tokens: int = 4096, ) -> AsyncIterator[str]: client = _get_anthropic_client(self._api_key, self._timeout) async with client.messages.stream( model=self._model, max_tokens=max_tokens, system=system_prompt, messages=messages, ) as stream: async for text in stream.text_stream: yield text ``` - [ ] **Step 3: Commit** ```bash git add backend/app/core/ai_provider.py git commit -m "feat: add generate_text_stream to AnthropicProvider for SSE support" ``` --- ### Task 6: Backend — Add Streaming Ticket Notes Generator **Files:** - Modify: `backend/app/services/flowpilot_engine.py` (after `generate_status_update`, around line 1012) - [ ] **Step 1: Add the streaming ticket notes function** After the `generate_status_update` function (after line 1011), add: ```python async def stream_ticket_notes( session_id: UUID, user_id: UUID, db: AsyncSession, ) -> AsyncIterator[str]: """Stream AI-generated structured ticket notes for a resolved session. Yields text chunks suitable for SSE streaming. Uses prompt caching on the system prompt for fast repeat calls. """ session = await _load_session(session_id, user_id, db) # Build conversation summary from messages (chat sessions) # or steps (guided sessions) messages = session.conversation_messages or [] if messages: recent = messages[-20:] # Last 20 messages for richer context convo_text = "\n".join( f"{'Engineer' if m['role'] == 'user' else 'AI Assistant'}: {m['content'][:500]}" for m in recent if isinstance(m, dict) and "role" in m and "content" in m ) else: # Fall back to steps for guided sessions steps_summary = [] for step in sorted(session.steps, key=lambda s: s.step_order): content = step.content or {} text = content.get("text", "") response = step.free_text_input or step.selected_option or ("Skipped" if step.was_skipped else None) entry = f"Step {step.step_order + 1}: {text}" if response: entry += f"\n Engineer response: {response}" steps_summary.append(entry) convo_text = "\n".join(steps_summary) if steps_summary else "No session data." # Calculate time spent now = datetime.now(timezone.utc) ref_time = session.resolved_at or now delta = ref_time - session.created_at total_minutes = int(delta.total_seconds() / 60) time_display = f"{total_minutes} minutes" if total_minutes < 60 else f"{total_minutes // 60}h {total_minutes % 60}m" system_prompt = """You are generating internal ticket notes for an MSP engineer's PSA system. Generate EXACTLY these four markdown sections, in this order: ## Problem Summary Summarize what the engineer reported and the initial symptoms. 1-3 sentences. ## Steps Taken List the key diagnostic steps, commands run, checks performed, and findings. Use bullet points. ## Resolution What fixed the issue or what the final action was. Be specific and technical. ## Next Steps Any follow-up items, monitoring to watch, or preventive measures. Write "None" if not applicable. Rules: - Be technical, concise, and factual - Use markdown formatting (headers, bullet lists, bold for emphasis) - Include specific technical details (commands, settings, error messages) where available - Do NOT include greetings, sign-offs, or pleasantries - Do NOT wrap output in code fences - Output ONLY the four sections above, nothing else""" user_message_parts = [ f"Session status: {session.status}", f"Time spent: {time_display}", f"Problem summary: {session.problem_summary or 'Not specified'}", ] if session.problem_domain: user_message_parts.append(f"Problem domain: {session.problem_domain}") if session.resolution_summary: user_message_parts.append(f"Resolution notes: {session.resolution_summary}") user_message_parts.append(f"\nSession conversation:\n{convo_text}") user_message = "\n".join(user_message_parts) provider = get_ai_provider(settings.get_model_for_action("quick_action")) # Use streaming if provider supports it (Anthropic), otherwise fall back try: async for chunk in provider.generate_text_stream( system_prompt=system_prompt, messages=[{"role": "user", "content": user_message}], max_tokens=1500, ): yield chunk except NotImplementedError: # Fallback for non-streaming providers (Gemini) text, _, _ = await provider.generate_text( system_prompt=system_prompt, messages=[{"role": "user", "content": user_message}], max_tokens=1500, ) yield text ``` Also add the import at the top of the file if not already present: ```python from collections.abc import AsyncIterator ``` - [ ] **Step 2: Commit** ```bash git add backend/app/services/flowpilot_engine.py git commit -m "feat: add stream_ticket_notes generator for SSE doc streaming" ``` --- ### Task 7: Backend — Add SSE Streaming Endpoint **Files:** - Modify: `backend/app/api/endpoints/ai_sessions.py` (after `get_documentation`, around line 925) - [ ] **Step 1: Add the SSE streaming endpoint** After the `get_documentation` endpoint (after line 924), add: ```python @router.get("/{session_id}/documentation/stream") @limiter.limit("20/minute") async def stream_documentation( request: Request, session_id: UUID, current_user: Annotated[User, Depends(get_current_active_user)], db: Annotated[AsyncSession, Depends(get_db)], ): """Stream AI-generated ticket notes as Server-Sent Events.""" from starlette.responses import StreamingResponse # Verify session ownership result = await db.execute( select(AISession).where(AISession.id == session_id) ) session = result.scalar_one_or_none() if not session: raise HTTPException(status_code=404, detail="Session not found") if session.user_id != current_user.id: raise HTTPException(status_code=403, detail="Not authorized") async def event_generator(): try: async for chunk in flowpilot_engine.stream_ticket_notes( session_id=session_id, user_id=current_user.id, db=db, ): # SSE format: data: \n\n yield f"data: {chunk}\n\n" yield "data: [DONE]\n\n" except Exception as e: logger.exception("SSE stream error for session %s: %s", session_id, e) yield f"data: [ERROR] {str(e)}\n\n" return StreamingResponse( event_generator(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", # Disable nginx buffering }, ) ``` Also add the `AISession` import at the top if not already there (it should be — verify with the existing imports around line 30-55): ```python from app.models.ai_session import AISession ``` - [ ] **Step 2: Commit** ```bash git add backend/app/api/endpoints/ai_sessions.py git commit -m "feat: add SSE endpoint for streaming ticket notes generation" ``` --- ### Task 8: Backend — Make resolve_session Non-Blocking **Files:** - Modify: `backend/app/api/endpoints/ai_sessions.py:413-446` - Modify: `backend/app/schemas/ai_session.py:135-142` - [ ] **Step 1: Make documentation optional in SessionCloseResponse** In `backend/app/schemas/ai_session.py`, change line 139: ```python class SessionCloseResponse(BaseModel): """Response after resolving or escalating.""" session_id: UUID status: str documentation: SessionDocumentation | None = None psa_push_status: str = "no_psa" # sent | pending_retry | no_psa | failed psa_push_error: str | None = None member_mapping_warning: str | None = None ``` (Change `documentation: SessionDocumentation` to `documentation: SessionDocumentation | None = None`) - [ ] **Step 2: Update the frontend type to match** In `frontend/src/types/ai-session.ts`, change line 128: ```typescript export interface SessionCloseResponse { session_id: string status: string documentation: SessionDocumentation | null psa_push_status: string psa_push_error: string | null member_mapping_warning: string | null } ``` (Change `documentation: SessionDocumentation` to `documentation: SessionDocumentation | null`) - [ ] **Step 3: Update the resolve endpoint to not generate docs inline** The `resolve_session` endpoint (lines 413-446) currently calls `flowpilot_engine.resolve_session()` which generates `_generate_documentation(session)` inline. The resolve_session in flowpilot_engine already sets status + saves summary + returns docs — we just need the endpoint to stop depending on docs for its response. In `backend/app/services/flowpilot_engine.py`, the `resolve_session` function (line 497) already returns `SessionCloseResponse` with `documentation=documentation`. Since we made `documentation` optional, the existing flow still works but is no longer blocking on an LLM call (it never was — `_generate_documentation` is pure Python and fast). The slow part was `generate_session_embedding` (Voyage AI embedding call) and `_push_to_psa`. The actual fix: move the embedding generation to be fire-and-forget in the endpoint. In `backend/app/api/endpoints/ai_sessions.py`, replace the resolve_session endpoint (lines 413-446): ```python @router.post("/{session_id}/resolve", response_model=SessionCloseResponse) @limiter.limit("15/minute") async def resolve_session( request: Request, session_id: UUID, data: ResolveSessionRequest, current_user: Annotated[User, Depends(get_current_active_user)], db: Annotated[AsyncSession, Depends(get_db)], _: None = Depends(require_engineer_or_admin), ): """Resolve a session. Returns immediately; use /documentation/stream for ticket notes.""" try: result = await flowpilot_engine.resolve_session( session_id=session_id, request=data, user_id=current_user.id, 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() # Fire-and-forget: resolution outputs + embedding (don't block the response) import asyncio async def _post_resolve_tasks(): try: from app.services.resolution_output_generator import ResolutionOutputGenerator gen = ResolutionOutputGenerator(db) await gen.generate_all(session_id) except Exception: logger.exception(f"Failed to generate resolution outputs for session {session_id}") asyncio.create_task(_post_resolve_tasks()) return result ``` - [ ] **Step 4: Commit** ```bash git add backend/app/api/endpoints/ai_sessions.py backend/app/schemas/ai_session.py frontend/src/types/ai-session.ts git commit -m "feat: make resolve endpoint non-blocking, documentation optional" ``` --- ### Task 9: Frontend — Add streamDocumentation to API Client **Files:** - Modify: `frontend/src/api/aiSessions.ts:95-100` - [ ] **Step 1: Add the streaming API method** After the `getDocumentation` method (around line 100), add: ```typescript async streamDocumentation( sessionId: string, onChunk: (text: string) => void, onDone: () => void, onError: (error: string) => void, ): Promise { const token = localStorage.getItem('access_token') const baseUrl = import.meta.env.VITE_API_URL || '' try { const response = await fetch( `${baseUrl}/api/ai-sessions/${sessionId}/documentation/stream`, { headers: { Authorization: `Bearer ${token}`, }, }, ) if (!response.ok) { onError(`HTTP ${response.status}`) return } const reader = response.body?.getReader() if (!reader) { onError('No response body') return } const decoder = new TextDecoder() let buffer = '' while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) const lines = buffer.split('\n') buffer = lines.pop() || '' for (const line of lines) { if (line.startsWith('data: ')) { const data = line.slice(6) if (data === '[DONE]') { onDone() return } if (data.startsWith('[ERROR]')) { onError(data.slice(8)) return } onChunk(data) } } } // Stream ended without [DONE] onDone() } catch (err) { onError(err instanceof Error ? err.message : 'Stream failed') } }, ``` - [ ] **Step 2: Commit** ```bash git add frontend/src/api/aiSessions.ts git commit -m "feat: add streamDocumentation SSE client for ticket notes" ``` --- ### Task 10: Frontend — Rewrite ConcludeSessionModal for Two-Phase Flow **Files:** - Modify: `frontend/src/components/assistant/ConcludeSessionModal.tsx` - Modify: `frontend/src/pages/AssistantChatPage.tsx:396-414` - [ ] **Step 1: Update the ConcludeSessionModal props and state** The modal's `onConclude` prop currently returns `Promise` (the summary text). We need to change it to return `Promise<{ sessionId: string }>` so the modal can initiate streaming itself. Update the interface and imports at the top of `ConcludeSessionModal.tsx`: ```tsx import { useState, useEffect, useRef } from 'react' import { X, CheckCircle2, ArrowUpRight, Pause, Loader2, Copy, Check, RefreshCw, ClipboardList, Sparkles, AlertTriangle, } from 'lucide-react' import { cn } from '@/lib/utils' import { MarkdownContent } from '@/components/ui/MarkdownContent' import { aiSessionsApi } from '@/api/aiSessions' import { toast } from '@/lib/toast' type ConclusionOutcome = 'resolved' | 'escalated' | 'paused' interface ConcludeSessionModalProps { isOpen: boolean onClose: () => void onConclude: (outcome: ConclusionOutcome, notes: string) => Promise onResumeNew: (summary: string) => void chatTitle: string sessionId: string | null } ``` (Added `sessionId` prop, `useRef` import, `AlertTriangle` icon, `aiSessionsApi` import, `toast` import.) - [ ] **Step 2: Rewrite the component state and handleGenerate** Replace the state declarations and `handleGenerate` function (lines 66-106): ```tsx export function ConcludeSessionModal({ isOpen, onClose, onConclude, onResumeNew, chatTitle, sessionId, }: ConcludeSessionModalProps) { const [step, setStep] = useState('select-outcome') const [outcome, setOutcome] = useState(null) const [notes, setNotes] = useState('') const [summary, setSummary] = useState('') const [generating, setGenerating] = useState(false) const [streaming, setStreaming] = useState(false) const [streamError, setStreamError] = useState(null) const [copied, setCopied] = useState(false) const [error, setError] = useState(null) const summaryRef = useRef('') // Reset state when modal opens useEffect(() => { if (isOpen) { setStep('select-outcome') setOutcome(null) setNotes('') setSummary('') summaryRef.current = '' setGenerating(false) setStreaming(false) setStreamError(null) setCopied(false) setError(null) } }, [isOpen]) const handleOutcomeSelect = (selected: ConclusionOutcome) => { setOutcome(selected) setStep('add-notes') } const handleGenerate = async () => { if (!outcome) return setGenerating(true) setError(null) try { // Phase 1: Resolve/escalate/pause the session (fast) await onConclude(outcome, notes) // Phase 2: Transition to summary step immediately setStep('summary') setGenerating(false) // For resolved sessions, stream ticket notes if (outcome === 'resolved' && sessionId) { setStreaming(true) setStreamError(null) summaryRef.current = '' aiSessionsApi.streamDocumentation( sessionId, (chunk) => { summaryRef.current += chunk setSummary(summaryRef.current) }, () => { setStreaming(false) }, (err) => { setStreaming(false) setStreamError(err) // Try non-streaming fallback aiSessionsApi.getDocumentation(sessionId).then((doc) => { const fallback = `## Problem Summary\n${doc.problem_summary}\n\n## Steps Taken\n${doc.diagnostic_steps.map(s => `- ${s.description}`).join('\n')}\n\n## Resolution\n${doc.resolution_summary || 'See conversation'}\n\n## Next Steps\nNone` setSummary(fallback) setStreamError(null) }).catch(() => { // Final fallback — just show what we have if (!summaryRef.current) { setSummary('Documentation generation failed. You can copy the conversation from the chat.') } }) }, ) } else if (outcome === 'escalated') { setSummary('Session escalated. Ticket notes will be generated when the session is resolved.') } else { setSummary('Session paused. Progress saved — you can resume anytime.') } } catch { setError('Failed to conclude session. Please try again.') setGenerating(false) } } ``` - [ ] **Step 3: Update the summary step rendering** Replace the summary step content (lines 298-325) with: ```tsx {/* Step 3: Summary */} {step === 'summary' && (
{/* Outcome badge */} {selectedOutcome && (
{selectedOutcome.label}
)} {/* Generated ticket notes */}
Ticket Notes {streaming && ( )}
{/* Streaming content or skeleton */} {summary ? (
) : streaming ? (
) : streamError ? (
{streamError}
) : null}
)} ``` - [ ] **Step 4: Update the footer for the summary step** Replace the summary step footer (lines 373-416) with: ```tsx {step === 'summary' && ( <>
{outcome === 'paused' && ( )}
{summary && !streaming && ( )}
)} ``` - [ ] **Step 5: Update handleConclude in AssistantChatPage** In `frontend/src/pages/AssistantChatPage.tsx`, update `handleConclude` (lines 396-414) to return immediately without waiting for documentation: ```tsx const handleConclude = async (outcome: ConclusionOutcome, _notes: string): Promise => { if (!activeChatId) throw new Error('No active chat') if (outcome === 'resolved') { await aiSessionsApi.resolveSession(activeChatId, { resolution_summary: _notes || 'Resolved via assistant chat', }) return activeChatId } else if (outcome === 'escalated') { await aiSessionsApi.escalateSession(activeChatId, { escalation_reason: _notes || 'Escalated from assistant chat', }) return activeChatId } else { await aiSessionsApi.pauseSession(activeChatId) return activeChatId } } ``` - [ ] **Step 6: Pass sessionId to ConcludeSessionModal** Find where `ConcludeSessionModal` is rendered (around line 812-818) and add the `sessionId` prop: ```tsx setShowConclude(false)} onConclude={handleConclude} onResumeNew={handleResumeNew} chatTitle={chats.find(c => c.id === activeChatId)?.title ?? 'Chat'} sessionId={activeChatId} /> ``` - [ ] **Step 7: Verify the full flow** 1. Open assistant chat, have a conversation 2. Click Conclude → Resolved → add optional notes → Generate Summary 3. Modal should transition to summary step IMMEDIATELY 4. Skeleton loading should appear, then ticket notes stream in progressively 5. When streaming completes, "Copy to Clipboard" button appears 6. Click copy → toast "Ticket notes copied" → clipboard has the markdown - [ ] **Step 8: Commit** ```bash git add frontend/src/components/assistant/ConcludeSessionModal.tsx frontend/src/pages/AssistantChatPage.tsx git commit -m "feat: two-phase resolve with streaming ticket notes generation" ``` --- ### Task 11: Frontend Build Verification **Files:** None (verification only) - [ ] **Step 1: Run the frontend build** ```bash export NVM_DIR="$HOME/.nvm" && source "$NVM_DIR/nvm.sh" && nvm use 20 cd frontend && npm run build ``` Expected: Build succeeds with no TypeScript errors. If OOM occurs (VPS memory constraint), run `npx tsc --noEmit` instead as a lighter check. - [ ] **Step 2: Fix any TypeScript errors** If errors appear, fix unused imports, missing props, or type mismatches. Common issues: - `ConcludeSessionModal` now requires `sessionId` prop — verify all usages pass it - `SessionCloseResponse.documentation` is now optional — verify `.documentation?.` optional chaining everywhere it's accessed - Unused `X` import in TaskLane after replacing with `PanelRightClose` - [ ] **Step 3: Commit fixes if needed** ```bash git add -A git commit -m "fix: resolve TypeScript errors from task lane and resolve docs changes" ``` --- ### Task 12: Remove Debug Logging from AssistantChatPage **Files:** - Modify: `frontend/src/pages/AssistantChatPage.tsx` - [ ] **Step 1: Remove the console.log statements added during debugging** Remove these debug lines: - `console.log('[AssistantChat] Mount restore check — ...')` (around line 105) - `console.log('[AssistantChat] Calling selectChat to restore:', ...)` (around line 107) - `console.log('[AssistantChat] selectChat called with:', ...)` (around line 212) - `console.log('[AssistantChat] getSession response — ...')` (around line 220) - `console.error('[AssistantChat] Failed to load chat session:', err)` (around line 243) Keep the `catch (err)` form (not bare `catch`) but remove the `console.error` line. Revert to bare `catch`: ```tsx } catch { setMessages([]) } ``` - [ ] **Step 2: Commit** ```bash git add frontend/src/pages/AssistantChatPage.tsx git commit -m "chore: remove debug logging from AssistantChatPage" ``` --- ### Task 13: Final Integration Test **Files:** None (manual testing) - [ ] **Step 1: Test task lane minimize/reopen flow** 1. Start or continue a chat that has task lane items 2. Click PanelRightClose — panel collapses, "Tasks (N)" pill appears in toolbar 3. Click pill — panel reopens with previous state (including partial answers) 4. Navigate away from assistant page and back — task lane restores from backend 5. Close and reopen — state persists - [ ] **Step 2: Test resolve with streaming ticket notes** 1. Have a multi-message conversation 2. Click Conclude → Resolved → optional notes → Generate Summary 3. Modal transitions instantly to summary step with skeleton 4. Ticket notes stream in with four sections: Problem Summary, Steps Taken, Resolution, Next Steps 5. Copy button appears after streaming completes 6. Click Copy → toast confirms → paste into text editor to verify markdown - [ ] **Step 3: Test fallback behavior** 1. If streaming fails (e.g., AI key missing), verify fallback to non-streaming doc generation 2. If both fail, verify "Documentation generation failed" message appears - [ ] **Step 4: Push to PR** ```bash git push ```