feat: persist task lane across submits and session reloads

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) <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-03-27 21:48:06 +00:00
parent 217e70cb81
commit ecd7393646
8 changed files with 67 additions and 30 deletions

View File

@@ -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")

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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 */}
<div className="px-3 pt-3 pb-1">
<Link
to="/"
className="flex items-center justify-center gap-2 w-full rounded-lg bg-amber-400/15 py-2.5 text-sm font-semibold text-amber-400 hover:bg-amber-400/25 transition-colors"
>
<Plus size={16} strokeWidth={2} />
New Session
</Link>
</div>
{/* Pinned sidebar content */}
<div className="px-3 py-2 space-y-0.5">
{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 */}
<div className="w-full px-2 pt-4 pb-2">
<Link
to="/"
className="flex flex-col items-center justify-center w-full rounded-lg bg-amber-400/15 py-2.5 text-amber-400 hover:bg-amber-400/25 transition-colors"
title="New Session"
>
<Plus size={20} strokeWidth={2} />
<span className="mt-1 text-[0.5625rem] font-sans font-semibold">New</span>
</Link>
</div>
{/* Nav items */}
<div className="flex flex-col items-center w-full px-1 space-y-1.5">
{railGroups.map((item, i) => renderRailItem(item, `rail-${i}`))}

View File

@@ -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,
})

View File

@@ -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)

View File

@@ -195,6 +195,7 @@ export interface AISessionDetail extends AISessionSummary {
ticket_data: Record<string, unknown> | 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
}