# TaskLane Improvements 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:** Fix four TaskLane UX issues (partial submit, reset on new chat, double border, resizability) and add a response preview. **Architecture:** All changes are in two frontend files. TaskLane.tsx gets the bulk of changes (submit logic, preview, resize handle). AssistantChatPage.tsx gets state-reset fixes and conditional border. No backend changes. **Tech Stack:** React, TypeScript, Tailwind CSS, Lucide icons, localStorage --- ### Task 1: TaskLane Reset on New Chat and Chat Switch **Files:** - Modify: `frontend/src/pages/AssistantChatPage.tsx:169-189` (handleNewChat) - Modify: `frontend/src/pages/AssistantChatPage.tsx:153-167` (selectChat) - [ ] **Step 1: Add TaskLane reset to `handleNewChat`** In `frontend/src/pages/AssistantChatPage.tsx`, find the `handleNewChat` function. After `setMessages([])` (inside the try block), add the TaskLane cleanup: ```typescript const handleNewChat = async () => { try { const session = await aiSessionsApi.createChatSession({ intake_type: 'free_text', intake_content: { text: '' }, }) const chatItem: ChatListItem = { id: session.session_id, title: session.title, message_count: 0, pinned: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), } setChats(prev => [chatItem, ...prev]) setActiveChatId(session.session_id) setMessages([]) // Clear TaskLane from previous session setShowTaskLane(false) setActiveQuestions([]) setActiveActions([]) } catch { toast.error('Failed to create chat') } } ``` - [ ] **Step 2: Add TaskLane reset to `selectChat`** In the same file, find the `selectChat` callback. Add TaskLane cleanup at the start of the function (before the try block): ```typescript const selectChat = useCallback(async (chatId: string) => { setActiveChatId(chatId) // Clear TaskLane when switching chats setShowTaskLane(false) setActiveQuestions([]) setActiveActions([]) try { const detail = await aiSessionsApi.getSession(chatId) setMessages( (detail.conversation_messages || []).map(m => ({ role: m.role as 'user' | 'assistant', content: m.content, })) ) } catch { setMessages([]) } }, []) ``` - [ ] **Step 3: Verify in browser** 1. Start a new chat session, send a message that triggers the TaskLane 2. Click "+ New Chat" — TaskLane should disappear 3. Start another session with TaskLane visible, click an older chat in the sidebar — TaskLane should disappear - [ ] **Step 4: Commit** ```bash git add frontend/src/pages/AssistantChatPage.tsx git commit -m "fix: clear TaskLane when switching chats or creating new chat Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 2: Conditional Chat Input Border **Files:** - Modify: `frontend/src/pages/AssistantChatPage.tsx:564` - [ ] **Step 1: Make the border conditional** Find the chat input wrapper div (around line 564): ```tsx
``` Replace with: ```tsx
``` The `cn` utility is already imported in this file. - [ ] **Step 2: Verify in browser** 1. Open a chat without TaskLane — input area should have top border 2. Send a message that triggers TaskLane — top border should disappear 3. Close the TaskLane via X — top border should reappear - [ ] **Step 3: Commit** ```bash git add frontend/src/pages/AssistantChatPage.tsx git commit -m "fix: remove chat input top border when TaskLane is open Prevents double-border clash between chat input and TaskLane footer. Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 3: Partial Submit + Dynamic Label **Files:** - Modify: `frontend/src/components/assistant/TaskLane.tsx:75` (allHandled), `89-94` (handleSubmit), `367-382` (footer) - [ ] **Step 1: Change submit enablement from "all handled" to "any handled"** In `frontend/src/components/assistant/TaskLane.tsx`, find: ```typescript const allHandled = tasks.every(t => t.state === 'done' || t.state === 'skipped') ``` Add a new derived value below it: ```typescript const allHandled = tasks.every(t => t.state === 'done' || t.state === 'skipped') const anyHandled = tasks.some(t => t.state === 'done' || t.state === 'skipped') const handledCount = tasks.filter(t => t.state === 'done' || t.state === 'skipped').length ``` - [ ] **Step 2: Update the footer submit button** Find the footer section (starts around line 350). Replace the entire ` ``` - [ ] **Step 3: Update the header badge to use `allHandled` for the checkmark** The header badge already uses `allHandled` for "Ready" — this stays correct since it still reflects whether everything is handled. No change needed. - [ ] **Step 4: Verify in browser** 1. Trigger TaskLane with questions + actions 2. Answer only 1 question — submit button should be enabled, label should say "Send 1 of N Responses" 3. Answer all items — label should say "Send All Responses" 4. Submit with partial answers — AI should receive only the answered items - [ ] **Step 5: Commit** ```bash git add frontend/src/components/assistant/TaskLane.tsx git commit -m "feat: enable partial TaskLane submission with dynamic label Engineers can submit responses as soon as at least one item is answered or skipped. Pending items are omitted from the message. Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 4: Done Card Click-to-Edit **Files:** - Modify: `frontend/src/components/assistant/TaskLane.tsx:138-145` (question done card), `257-265` (action done card) - [ ] **Step 1: Make question done cards clickable** Find the question done card (around line 138): ```tsx if (q.state === 'done') { return (
{q.text}
"{q.value}"
) } ``` Replace with: ```tsx if (q.state === 'done') { return (
updateTask(idx, { state: 'active' })} className="rounded-lg border border-success/20 bg-success-dim/20 p-3 mb-2 cursor-pointer hover:border-success/40 transition-colors" >
{q.text}
"{q.value}"
) } ``` - [ ] **Step 2: Make action done cards clickable** Find the action done card (around line 257): ```tsx if (a.state === 'done') { return (
{a.label}
✓ Done
) } ``` Replace with: ```tsx if (a.state === 'done') { return (
updateTask(idx, { state: 'active' })} className="rounded-lg border border-success/20 bg-success-dim/20 p-3 mb-2 cursor-pointer hover:border-success/40 transition-colors" >
{a.label}
✓ Done
) } ``` - [ ] **Step 3: Verify in browser** 1. Answer a question, confirm it (green card appears) 2. Click the green card — it should reopen with the previous value pre-filled in the textarea 3. Edit the value, re-confirm — card goes back to green with updated text 4. Same test for an action item - [ ] **Step 4: Commit** ```bash git add frontend/src/components/assistant/TaskLane.tsx git commit -m "feat: click done TaskLane cards to re-edit responses Completed question and action cards are now clickable. Clicking reopens them in active state with the previous value preserved. Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 5: Collapsible Preview Section **Files:** - Modify: `frontend/src/components/assistant/TaskLane.tsx` — add import for `Eye` icon, add `showPreview` state, add `buildPreviewText` function, add preview UI in footer - [ ] **Step 1: Add state and icon import** At the top of `TaskLane.tsx`, update the Lucide import to include `Eye`: ```typescript import { Copy, Check, SkipForward, Terminal, ChevronDown, ChevronUp, Send, Clipboard, Loader2, X, MessageCircleQuestion, Wrench, Eye, } from 'lucide-react' ``` Inside the component, after the `showRunAll` state, add: ```typescript const [showPreview, setShowPreview] = useState(false) ``` - [ ] **Step 2: Add `buildPreviewText` function** After the `handleCopy` function (around line 87), add: ```typescript const buildPreviewText = (): string => { const parts: string[] = [] for (const t of tasks) { if (t.type === 'question') { const q = t as QuestionResponse const name = `Q: ${q.text}` if (q.state === 'done' && q.value.trim()) { parts.push(`**${name}:**\n\`\`\`\n${q.value.trim()}\n\`\`\``) } else if (q.state === 'skipped') { parts.push(`**${name}:** _(skipped)_`) } } else { const a = t as ActionResponse const name = a.label || 'Check' if (a.state === 'done' && a.value.trim()) { parts.push(`**${name}:**\n\`\`\`\n${a.value.trim()}\n\`\`\``) } else if (a.state === 'skipped') { parts.push(`**${name}:** _(skipped)_`) } } } return parts.join('\n\n') || '(No responses yet)' } ``` This mirrors the formatting logic in `AssistantChatPage.tsx`'s `handleTaskSubmit`. - [ ] **Step 3: Add preview UI in footer** Find the footer `
` (starts with `{/* Footer */}`). Insert the preview section between the progress bar and the submit button: ```tsx {/* Footer */}
{/* Progress bar */}
{tasks.map((t, i) => (
))}
{/* Collapsible preview */} {anyHandled && (
{showPreview && (
{buildPreviewText()}
)}
)}
``` - [ ] **Step 4: Verify in browser** 1. Trigger TaskLane, answer 1 item — "Preview (1/N done)" toggle should appear above submit button 2. Click the toggle — preview expands showing the formatted message 3. Answer more items — preview updates in real-time 4. Click toggle again — preview collapses 5. With 0 items handled — preview toggle should not appear - [ ] **Step 5: Commit** ```bash git add frontend/src/components/assistant/TaskLane.tsx git commit -m "feat: add collapsible response preview to TaskLane footer Shows a real-time preview of the formatted message that will be sent to the AI. Collapsed by default, appears when at least one item is answered or skipped. Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 6: Resizable TaskLane with Grip Handle **Files:** - Modify: `frontend/src/components/assistant/TaskLane.tsx` — add `useRef`, `useCallback`, `useEffect` imports, add resize state/refs, add grip handle JSX, replace fixed width - [ ] **Step 1: Add resize state and refs** Update the React import at the top of `TaskLane.tsx`: ```typescript import { useState, useEffect, useRef, useCallback } from 'react' ``` Add the `GripVertical` icon to the Lucide import: ```typescript import { Copy, Check, SkipForward, Terminal, ChevronDown, ChevronUp, Send, Clipboard, Loader2, X, MessageCircleQuestion, Wrench, Eye, GripVertical, } from 'lucide-react' ``` Inside the component, after the existing state declarations (after `showPreview` state), add: ```typescript // ── Resize state ── const DEFAULT_WIDTH = 340 const MIN_WIDTH = 280 const MAX_WIDTH_RATIO = 0.5 // 50vw const [panelWidth, setPanelWidth] = useState(() => { const stored = localStorage.getItem('rf-tasklane-width') return stored ? Math.max(MIN_WIDTH, parseInt(stored, 10) || DEFAULT_WIDTH) : DEFAULT_WIDTH }) const isDragging = useRef(false) const startX = useRef(0) const startWidth = useRef(0) const handleMouseMove = useCallback((e: MouseEvent) => { if (!isDragging.current) return const maxWidth = window.innerWidth * MAX_WIDTH_RATIO // Dragging left (negative deltaX) should increase width since panel is on the right const deltaX = startX.current - e.clientX const newWidth = Math.min(maxWidth, Math.max(MIN_WIDTH, startWidth.current + deltaX)) setPanelWidth(newWidth) }, []) const handleMouseUp = useCallback(() => { if (!isDragging.current) return isDragging.current = false document.body.style.cursor = '' document.body.style.userSelect = '' localStorage.setItem('rf-tasklane-width', String(Math.round(panelWidth))) }, [panelWidth]) const handleMouseDown = useCallback((e: React.MouseEvent) => { e.preventDefault() isDragging.current = true startX.current = e.clientX startWidth.current = panelWidth document.body.style.cursor = 'col-resize' document.body.style.userSelect = 'none' }, [panelWidth]) useEffect(() => { document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) return () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } }, [handleMouseMove, handleMouseUp]) ``` - [ ] **Step 2: Replace the outer div's fixed width with dynamic width** Find the outer `
` of the component return (the line with `w-[340px]`): ```tsx
``` Replace with: ```tsx
{/* Resize grip handle */}
``` Keep the rest of the component unchanged — the header, body, and footer are all children of this outer div. - [ ] **Step 3: Verify in browser** 1. Trigger the TaskLane — should appear at stored width (or 340px default) 2. Hover over the left edge — faint grip dots should appear, cursor changes to `col-resize` 3. Drag left — panel widens. Drag right — panel narrows 4. Release — width persists 5. Reload the page, trigger TaskLane again — width should match the last drag position 6. Try to drag past 50vw — should cap. Try to drag below 280px — should cap. - [ ] **Step 4: Commit** ```bash git add frontend/src/components/assistant/TaskLane.tsx git commit -m "feat: resizable TaskLane with grip handle and localStorage persistence Left edge has a 6px drag zone with dot grip indicator on hover. Width clamped between 280px and 50vw. Persists to localStorage. Co-Authored-By: Claude Opus 4.6 " ``` --- ### Task 7: Final Verification - [ ] **Step 1: Full integration test** Run through the complete flow: 1. Start from the dashboard — type a troubleshooting issue in the main input 2. TaskLane should appear on the first response (prefill path fix from earlier session) 3. Answer 1 of 3 questions — submit button should say "Send 1 of N Responses" 4. Click "Preview" — should show formatted response 5. Click a done card — should reopen for editing 6. Resize the TaskLane by dragging the left edge 7. Submit partial responses — AI should respond acknowledging the partial info 8. Click "+ New Chat" — TaskLane should disappear 9. Switch to an older chat in sidebar — TaskLane should stay hidden 10. Verify no double border on the chat input with and without TaskLane - [ ] **Step 2: Build check** ```bash cd frontend && npm run build ``` Expected: Clean build with no TypeScript errors. - [ ] **Step 3: Final commit if any cleanup needed** ```bash git add -A git status # Only commit if there are changes ```