diff --git a/docs/superpowers/plans/2026-03-26-tasklane-improvements.md b/docs/superpowers/plans/2026-03-26-tasklane-improvements.md new file mode 100644 index 00000000..807e7250 --- /dev/null +++ b/docs/superpowers/plans/2026-03-26-tasklane-improvements.md @@ -0,0 +1,594 @@ +# 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 +``` diff --git a/docs/superpowers/specs/2026-03-26-tasklane-improvements-design.md b/docs/superpowers/specs/2026-03-26-tasklane-improvements-design.md new file mode 100644 index 00000000..d9c649fb --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-tasklane-improvements-design.md @@ -0,0 +1,95 @@ +# TaskLane Improvements Design + +> **Date:** 2026-03-26 +> **Status:** Approved +> **Scope:** `frontend/src/components/assistant/TaskLane.tsx`, `frontend/src/pages/AssistantChatPage.tsx` + +--- + +## Context + +The TaskLane is a right-side panel in the AI Assistant chat that renders structured questions and diagnostic actions from the AI. It launched with a working parse → render → submit pipeline, but has four UX issues and a missing submission flow design. + +## Changes + +### 1. Progressive Preview + Batch Submit + +**Problem:** The "Send All Responses" button is disabled until every item is answered or skipped. Engineers often want to submit partial results — answer 2 of 3 questions, paste output from 1 of 4 commands, and let the AI work with what they have. + +**Design:** + +- **Submit enabled when ≥1 item is done or skipped.** Remaining pending items are simply omitted from the message (not auto-skipped — they're just not addressed). +- **Dynamic submit label:** `Send 2 of 6 Responses` when partial, `Send All Responses` when all handled. +- **Collapsible preview section** above the submit button: + - Toggle: `▶ Preview (2/6 done)` — collapsed by default + - Expands to show the formatted markdown message that will be sent to the AI + - Updates in real-time as items are answered/skipped + - Uses the same formatting logic as `handleTaskSubmit` (question answers in blockquotes, command output in code fences, skipped items noted) + - Styled as a `bg-code` block with `font-mono text-xs`, max-height ~150px with overflow scroll +- **Done items are re-editable.** Clicking anywhere on a completed (green) card reopens it in `active` state for editing. Cards get `cursor-pointer` and a subtle hover state (`hover:border-success/40`) to signal editability. This uses the existing `updateTask(idx, { state: 'active' })` mechanism — no new logic needed, just making the done card itself a click target. +- **No change to the TaskLane header or pending/active card behavior.** Changes affect the footer area and done card interactivity. + +**Submission logic changes in `TaskLane.tsx`:** +- `allHandled` check on submit button changes from "all done/skipped" to "at least 1 done/skipped" +- `handleSubmit` sends only items that have state `done` or `skipped` +- Items still in `pending` or `active` state are excluded from the submission payload + +**Submission logic in `AssistantChatPage.tsx` (`handleTaskSubmit`):** +- No changes needed — it already formats based on `r.state === 'done'` and `r.state === 'skipped'`, ignoring pending items. + +### 2. TaskLane Reset on New Chat + +**Problem:** Starting a new chat via `handleNewChat` doesn't clear the TaskLane. The previous session's questions/actions persist visually. + +**Fix:** Add three lines to `handleNewChat` in `AssistantChatPage.tsx`: +```typescript +setShowTaskLane(false) +setActiveQuestions([]) +setActiveActions([]) +``` + +Same pattern already exists in `handleTaskSubmit`. Also add to `selectChat` for switching between chats. + +### 3. Conditional Chat Input Border + +**Problem:** The chat input area has `border-t border-border` which creates a double-border effect alongside the TaskLane footer's `border-t border-default`. + +**Fix:** Conditionally remove the chat input's top border when the TaskLane is open: +```tsx +
+``` + +The TaskLane's own left border and footer border provide sufficient visual separation when the panel is open. When closed, the chat input border returns. + +### 4. Resizable TaskLane with Grip Handle + +**Design:** + +- **Drag zone:** 6px wide hit target on the left edge of the TaskLane, absolutely positioned. +- **Grip indicator:** Centered vertically on the drag zone — a 2×3 grid of small dots (6 dots total, `w-1 h-1 rounded-full bg-current`). Subtle `text-muted/40` by default, `text-muted-foreground` on hover. +- **Resize behavior:** + - `onMouseDown` on the grip starts tracking + - `mousemove` on `document` updates the width + - `mouseUp` on `document` stops tracking + - Width clamped between **280px** min and **50vw** max + - `cursor: col-resize` applied to the grip and to `document.body` during drag (prevents cursor flicker) + - `user-select: none` on `document.body` during drag (prevents text selection) +- **Persistence:** Width saved to `localStorage` key `rf-tasklane-width`. Read on mount, written on drag end. Default: `340px`. +- **Implementation:** All in `TaskLane.tsx` — no external libraries. Uses `useRef` for drag state (avoids re-renders during drag), `useState` for the width value. + +**CSS change:** Replace `w-[340px]` with `style={{ width: panelWidth }}` on the outer div. Keep `shrink-0` so the chat area flexes. + +--- + +## Files Modified + +| File | Change | +|------|--------| +| `frontend/src/components/assistant/TaskLane.tsx` | Preview section, submit logic, resize handle, width persistence | +| `frontend/src/pages/AssistantChatPage.tsx` | TaskLane reset in `handleNewChat`/`selectChat`, conditional border | + +## Out of Scope + +- TaskLane rendering when loading historical sessions (markers are stripped from stored messages — no TaskLane on reload) +- Mobile-specific TaskLane layout (current: hidden on mobile, which is acceptable for now) +- Keyboard accessibility for resize handle (future enhancement)