From ecd73936468587b169dbec1dfd67483211b2bb4d Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 27 Mar 2026 21:48:06 +0000 Subject: [PATCH] feat: persist task lane across submits and session reloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task lane questions/actions are now saved to a pending_task_lane JSONB column on ai_sessions, restoring them on session switch or page reload. Partial submit no longer force-clears the lane — the AI response controls what stays. Also removes redundant "New Session" button from the sidebar (dashboard already provides this). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../versions/fc01_add_pending_task_lane.py | 30 +++++++++++++++++++ backend/app/models/ai_session.py | 4 +++ backend/app/schemas/ai_session.py | 1 + backend/app/services/unified_chat_service.py | 18 +++++++++++ frontend/src/components/layout/Sidebar.tsx | 25 +--------------- frontend/src/hooks/useFlowPilotSession.ts | 1 + frontend/src/pages/AssistantChatPage.tsx | 17 +++++++---- frontend/src/types/ai-session.ts | 1 + 8 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 backend/alembic/versions/fc01_add_pending_task_lane.py diff --git a/backend/alembic/versions/fc01_add_pending_task_lane.py b/backend/alembic/versions/fc01_add_pending_task_lane.py new file mode 100644 index 00000000..4a579c38 --- /dev/null +++ b/backend/alembic/versions/fc01_add_pending_task_lane.py @@ -0,0 +1,30 @@ +"""add pending_task_lane to ai_sessions + +Revision ID: fc01a1b2c3d4 +Revises: fb1481317ff6 +Create Date: 2026-03-27 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSONB + +revision = "fc01a1b2c3d4" +down_revision = "fb1481317ff6" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "ai_sessions", + sa.Column( + "pending_task_lane", + JSONB, + nullable=True, + comment="Current task lane state: {questions: [...], actions: [...]}", + ), + ) + + +def downgrade() -> None: + op.drop_column("ai_sessions", "pending_task_lane") diff --git a/backend/app/models/ai_session.py b/backend/app/models/ai_session.py index 0c10d9db..8bf1684a 100644 --- a/backend/app/models/ai_session.py +++ b/backend/app/models/ai_session.py @@ -209,6 +209,10 @@ class AISession(Base): JSONB, nullable=False, default=list, comment="Full LLM message history for context continuity", ) + pending_task_lane: Mapped[Optional[dict[str, Any]]] = mapped_column( + JSONB, nullable=True, + comment="Current task lane state: {questions: [...], actions: [...]}", + ) # ── Branching ── is_branching: Mapped[bool] = mapped_column( diff --git a/backend/app/schemas/ai_session.py b/backend/app/schemas/ai_session.py index 4037f38a..0039653e 100644 --- a/backend/app/schemas/ai_session.py +++ b/backend/app/schemas/ai_session.py @@ -228,6 +228,7 @@ class AISessionDetail(AISessionSummary): ticket_data: dict[str, Any] | None = None steps: list[AISessionStepResponse] = [] conversation_messages: list[dict[str, Any]] = [] # Chat sessions store messages here + pending_task_lane: dict[str, Any] | None = None is_branching: bool = False active_branch_id: str | None = None diff --git a/backend/app/services/unified_chat_service.py b/backend/app/services/unified_chat_service.py index 494eb01f..44236e66 100644 --- a/backend/app/services/unified_chat_service.py +++ b/backend/app/services/unified_chat_service.py @@ -286,6 +286,15 @@ async def send_chat_message( except Exception: logger.exception("Failed to create fork within branch for session %s", session.id) + # Persist task lane state on session + if branch_questions_data or branch_actions_data: + session.pending_task_lane = { + "questions": branch_questions_data or [], + "actions": branch_actions_data or [], + } + else: + session.pending_task_lane = None + suggested_flows = extract_suggested_flows( await rag_search(query=message, account_id=account_id, db=db, limit=8) ) @@ -393,6 +402,15 @@ async def send_chat_message( logger.exception("Failed to create fork for session %s", session_id) # Fork failed but chat message still sent — don't break the response + # Persist task lane state on session + if questions_data or actions_data: + session.pending_task_lane = { + "questions": questions_data or [], + "actions": actions_data or [], + } + else: + session.pending_task_lane = None + suggested_flows = extract_suggested_flows(rag_results) return display_content, suggested_flows, session, fork_metadata, actions_data, questions_data diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 3c3b825a..2b2fa664 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -5,7 +5,7 @@ import { LayoutGrid, Clock, AlertTriangle, GitBranch, Code2, Wand2, ListChecks, Download, BarChart3, Settings, Pin, PinOff, - Plus, History, FileText, + History, FileText, } from 'lucide-react' import { cn } from '@/lib/utils' import { useUserPreferencesStore } from '@/store/userPreferencesStore' @@ -363,17 +363,6 @@ export function Sidebar() { style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)' }} onWheel={handleWheel} > - {/* New Session button */} -
- - - New Session - -
- {/* Pinned sidebar content */}
{sections.map((section, si) => ( @@ -420,18 +409,6 @@ export function Sidebar() { style={{ background: 'var(--color-bg-sidebar)', borderRight: '1px solid var(--color-border-default)', width: '72px' }} onWheel={handleWheel} > - {/* New Session button */} -
- - - New - -
- {/* Nav items */}
{railGroups.map((item, i) => renderRailItem(item, `rail-${i}`))} diff --git a/frontend/src/hooks/useFlowPilotSession.ts b/frontend/src/hooks/useFlowPilotSession.ts index 8b0f661c..6034cbb0 100644 --- a/frontend/src/hooks/useFlowPilotSession.ts +++ b/frontend/src/hooks/useFlowPilotSession.ts @@ -92,6 +92,7 @@ export function useFlowPilotSession(): UseFlowPilotSession { ticket_data: null, steps: [firstStep], conversation_messages: [], + pending_task_lane: null, is_branching: false, active_branch_id: null, }) diff --git a/frontend/src/pages/AssistantChatPage.tsx b/frontend/src/pages/AssistantChatPage.tsx index fad0b11d..db060d0d 100644 --- a/frontend/src/pages/AssistantChatPage.tsx +++ b/frontend/src/pages/AssistantChatPage.tsx @@ -160,7 +160,7 @@ export default function AssistantChatPage() { const selectChat = useCallback(async (chatId: string) => { setActiveChatId(chatId) - // Clear TaskLane when switching chats + // Clear TaskLane when switching chats — will restore from backend if available setShowTaskLane(false) setActiveQuestions([]) setActiveActions([]) @@ -172,6 +172,16 @@ export default function AssistantChatPage() { content: m.content, })) ) + // Restore task lane from persisted state + if (detail.pending_task_lane) { + const q = detail.pending_task_lane.questions || [] + const a = detail.pending_task_lane.actions || [] + if (q.length > 0 || a.length > 0) { + setActiveQuestions(q) + setActiveActions(a) + setShowTaskLane(true) + } + } } catch { setMessages([]) } @@ -288,11 +298,6 @@ export default function AssistantChatPage() { } const userMessage = parts.join('\n\n') - // Close the task lane - setShowTaskLane(false) - setActiveQuestions([]) - setActiveActions([]) - setMessages(prev => [...prev, { role: 'user', content: userMessage }]) setLoading(true) diff --git a/frontend/src/types/ai-session.ts b/frontend/src/types/ai-session.ts index 446f3149..e1abab7f 100644 --- a/frontend/src/types/ai-session.ts +++ b/frontend/src/types/ai-session.ts @@ -195,6 +195,7 @@ export interface AISessionDetail extends AISessionSummary { ticket_data: Record | null steps: AISessionStepResponse[] conversation_messages: Array<{ role: string; content: string }> + pending_task_lane: { questions: QuestionItem[]; actions: ActionItem[] } | null is_branching: boolean active_branch_id: string | null }