Files
resolutionflow/docs/superpowers/plans/2026-03-26-tasklane-improvements.md
2026-03-26 19:53:23 +00:00

20 KiB

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:

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

  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
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 <noreply@anthropic.com>"

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

<div className="px-3 sm:px-6 py-3 shrink-0 border-t border-border">

Replace with:

<div className={cn("px-3 sm:px-6 py-3 shrink-0", !showTaskLane && "border-t border-border")}>

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
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 <noreply@anthropic.com>"

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:

const allHandled = tasks.every(t => t.state === 'done' || t.state === 'skipped')

Add a new derived value below it:

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 <button> for submit:

        <button
          onClick={handleSubmit}
          disabled={!anyHandled || loading || submitting}
          className={cn(
            'w-full flex items-center justify-center gap-1.5 rounded-lg px-4 py-2.5 text-[0.8125rem] font-semibold transition-colors',
            anyHandled && !submitting
              ? 'bg-accent text-white hover:bg-accent-hover'
              : 'bg-elevated text-muted-foreground border border-default cursor-not-allowed'
          )}
        >
          {submitting ? (
            <><Loader2 size={14} className="animate-spin" /> Sending...</>
          ) : (
            <><Send size={14} /> {allHandled ? 'Send All Responses' : `Send ${handledCount} of ${totalCount} Responses`}</>
          )}
        </button>
  • 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
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 <noreply@anthropic.com>"

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

              if (q.state === 'done') {
                return (
                  <div key={idx} className="rounded-lg border border-success/20 bg-success-dim/20 p-3 mb-2">
                    <div className="text-[0.8125rem] text-muted-foreground">{q.text}</div>
                    <div className="text-[0.75rem] text-muted-foreground mt-1 italic">"{q.value}"</div>
                  </div>
                )
              }

Replace with:

              if (q.state === 'done') {
                return (
                  <div
                    key={idx}
                    onClick={() => 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"
                  >
                    <div className="text-[0.8125rem] text-muted-foreground">{q.text}</div>
                    <div className="text-[0.75rem] text-muted-foreground mt-1 italic">"{q.value}"</div>
                  </div>
                )
              }
  • Step 2: Make action done cards clickable

Find the action done card (around line 257):

              if (a.state === 'done') {
                return (
                  <div key={idx} className="rounded-lg border border-success/20 bg-success-dim/20 p-3 mb-2">
                    <div className="flex justify-between">
                      <div className="text-[0.8125rem] font-medium text-heading">{a.label}</div>
                      <span className="text-[10px] font-semibold uppercase tracking-wider text-success"> Done</span>
                    </div>
                  </div>
                )
              }

Replace with:

              if (a.state === 'done') {
                return (
                  <div
                    key={idx}
                    onClick={() => 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"
                  >
                    <div className="flex justify-between">
                      <div className="text-[0.8125rem] font-medium text-heading">{a.label}</div>
                      <span className="text-[10px] font-semibold uppercase tracking-wider text-success"> Done</span>
                    </div>
                  </div>
                )
              }
  • 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
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 <noreply@anthropic.com>"

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:

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:

  const [showPreview, setShowPreview] = useState(false)
  • Step 2: Add buildPreviewText function

After the handleCopy function (around line 87), add:

  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 <div> (starts with {/* Footer */}). Insert the preview section between the progress bar and the submit button:

      {/* Footer */}
      <div className="p-3 border-t border-default shrink-0">
        {/* Progress bar */}
        <div className="flex gap-1 mb-2">
          {tasks.map((t, i) => (
            <div
              key={i}
              className={cn(
                'flex-1 h-[3px] rounded-full',
                t.state === 'done' ? 'bg-success' :
                t.state === 'skipped' ? 'bg-muted' :
                t.state === 'active' ? 'bg-accent' :
                'bg-elevated'
              )}
            />
          ))}
        </div>

        {/* Collapsible preview */}
        {anyHandled && (
          <div className="mb-2">
            <button
              onClick={() => setShowPreview(!showPreview)}
              className="flex items-center gap-1.5 text-[0.75rem] font-medium text-muted-foreground hover:text-heading transition-colors mb-1"
            >
              <Eye size={12} />
              Preview ({handledCount}/{totalCount} done)
              {showPreview ? <ChevronUp size={12} /> : <ChevronDown size={12} />}
            </button>
            {showPreview && (
              <div className="rounded-lg border border-default bg-code p-2.5 max-h-[150px] overflow-y-auto">
                <pre className="text-[0.6875rem] font-mono text-heading whitespace-pre-wrap">{buildPreviewText()}</pre>
              </div>
            )}
          </div>
        )}

        <button
          onClick={handleSubmit}
          disabled={!anyHandled || loading || submitting}
          className={cn(
            'w-full flex items-center justify-center gap-1.5 rounded-lg px-4 py-2.5 text-[0.8125rem] font-semibold transition-colors',
            anyHandled && !submitting
              ? 'bg-accent text-white hover:bg-accent-hover'
              : 'bg-elevated text-muted-foreground border border-default cursor-not-allowed'
          )}
        >
          {submitting ? (
            <><Loader2 size={14} className="animate-spin" /> Sending...</>
          ) : (
            <><Send size={14} /> {allHandled ? 'Send All Responses' : `Send ${handledCount} of ${totalCount} Responses`}</>
          )}
        </button>
      </div>
  • 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
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 <noreply@anthropic.com>"

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:

import { useState, useEffect, useRef, useCallback } from 'react'

Add the GripVertical icon to the Lucide import:

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:

  // ── Resize state ──
  const DEFAULT_WIDTH = 340
  const MIN_WIDTH = 280
  const MAX_WIDTH_RATIO = 0.5 // 50vw

  const [panelWidth, setPanelWidth] = useState<number>(() => {
    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 <div> of the component return (the line with w-[340px]):

    <div className="w-[340px] bg-sidebar border-l border-default flex flex-col shrink-0 animate-in slide-in-from-right-4 duration-200">

Replace with:

    <div
      className="relative bg-sidebar border-l border-default flex flex-col shrink-0 animate-in slide-in-from-right-4 duration-200"
      style={{ width: panelWidth }}
    >
      {/* Resize grip handle */}
      <div
        onMouseDown={handleMouseDown}
        className="absolute left-0 top-0 bottom-0 w-[6px] cursor-col-resize z-20 flex items-center justify-center group hover:bg-accent/10 transition-colors"
      >
        <div className="flex flex-col gap-[3px] opacity-0 group-hover:opacity-100 transition-opacity">
          <div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
          <div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
          <div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
          <div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
          <div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
          <div className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
        </div>
      </div>

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
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 <noreply@anthropic.com>"

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
cd frontend && npm run build

Expected: Clean build with no TypeScript errors.

  • Step 3: Final commit if any cleanup needed
git add -A
git status
# Only commit if there are changes