feat: task lane persistence + sidebar cleanup #121

Merged
chihlasm merged 49 commits from feat/task-lane-persistence into main 2026-03-29 16:59:41 +00:00
5 changed files with 69 additions and 3 deletions
Showing only changes of commit 80af408f2d - Show all commits

View File

@@ -48,6 +48,7 @@ from app.schemas.ai_session import (
ChatSessionCreateResponse,
ChatMessageRequest,
ChatMessageResponse,
SaveTaskLaneRequest,
)
from app.services import flowpilot_engine
from app.services import unified_chat_service
@@ -497,6 +498,33 @@ async def pause_session(
await db.commit()
# ── Save Task Lane ──
@router.put("/{session_id}/task-lane", status_code=204)
@limiter.limit("30/minute")
async def save_task_lane(
request: Request,
session_id: UUID,
body: SaveTaskLaneRequest,
current_user: Annotated[User, Depends(get_current_active_user)],
db: Annotated[AsyncSession, Depends(get_db)],
_: None = Depends(require_engineer_or_admin),
):
"""Save the current task lane state including user's in-progress responses."""
session = await db.get(AISession, session_id)
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 your session")
session.pending_task_lane = {
"questions": [q.model_dump() for q in body.questions],
"actions": [a.model_dump() for a in body.actions],
"responses": body.responses,
}
await db.commit()
# ── Resume ──
@router.post("/{session_id}/resume", status_code=204)

View File

@@ -287,6 +287,16 @@ class ChatMessageResponse(BaseModel):
questions: list[QuestionItem] | None = None
class SaveTaskLaneRequest(BaseModel):
"""Save the full task lane state (AI items + user responses)."""
questions: list[QuestionItem] = []
actions: list[ActionItem] = []
responses: list[dict[str, Any]] = Field(
default_factory=list,
description="User's in-progress task responses with state/value",
)
class AISessionSearchResult(BaseModel):
"""Lightweight session result for Command Palette / autocomplete."""
id: UUID

View File

@@ -110,6 +110,14 @@ export const aiSessionsApi = {
return response.data
},
async saveTaskLane(sessionId: string, data: {
questions: Array<{ text: string; context?: string }>;
actions: Array<{ label: string; command?: string | null; description?: string }>;
responses: Array<Record<string, unknown>>;
}): Promise<void> {
await apiClient.put(`/ai-sessions/${sessionId}/task-lane`, data)
},
async pauseSession(sessionId: string): Promise<void> {
await apiClient.post(`/ai-sessions/${sessionId}/pause`)
},

View File

@@ -5,6 +5,7 @@ import {
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { toast } from '@/lib/toast'
import { aiSessionsApi } from '@/api/aiSessions'
import type { ActionItem, QuestionItem } from '@/types/ai-session'
// ── Types ──
@@ -129,10 +130,22 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
}
}, [handleMouseMove, handleMouseUp])
// Save task state to sessionStorage on every change
// Save task state to sessionStorage on every change + debounce to backend
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
if (sessionId) saveTaskState(sessionId, tasks)
}, [sessionId, tasks])
if (!sessionId) return
saveTaskState(sessionId, tasks)
// Debounce save to backend (2s after last change)
if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
saveTimerRef.current = setTimeout(() => {
aiSessionsApi.saveTaskLane(sessionId, {
questions: questions.map(q => ({ text: q.text, context: q.context })),
actions: actions.map(a => ({ label: a.label, command: a.command, description: a.description })),
responses: tasks as unknown as Array<Record<string, unknown>>,
}).catch(() => { /* silent — best-effort save */ })
}, 2000)
return () => { if (saveTimerRef.current) clearTimeout(saveTimerRef.current) }
}, [sessionId, tasks]) // eslint-disable-line react-hooks/exhaustive-deps
// Reset when new tasks come in from AI response
useEffect(() => {

View File

@@ -228,6 +228,13 @@ export default function AssistantChatPage() {
setActiveQuestions(q)
setActiveActions(a)
setShowTaskLane(true)
// Pre-load user's saved responses into sessionStorage so TaskLane restores them
const responses = (detail.pending_task_lane as Record<string, unknown>).responses as unknown[] | undefined
if (responses && responses.length > 0) {
try {
sessionStorage.setItem(`rf-tasklane-state:${chatId}`, JSON.stringify(responses))
} catch { /* ignore */ }
}
}
}
} catch {