docs: TaskLane improvements design spec and implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
594
docs/superpowers/plans/2026-03-26-tasklane-improvements.md
Normal file
594
docs/superpowers/plans/2026-03-26-tasklane-improvements.md
Normal file
@@ -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 <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):
|
||||
|
||||
```tsx
|
||||
<div className="px-3 sm:px-6 py-3 shrink-0 border-t border-border">
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```tsx
|
||||
<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**
|
||||
|
||||
```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 <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:
|
||||
|
||||
```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 `<button>` for submit:
|
||||
|
||||
```tsx
|
||||
<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**
|
||||
|
||||
```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 <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):
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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):
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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**
|
||||
|
||||
```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 <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`:
|
||||
|
||||
```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 `<div>` (starts with `{/* Footer */}`). Insert the preview section between the progress bar and the submit button:
|
||||
|
||||
```tsx
|
||||
{/* 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**
|
||||
|
||||
```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 <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`:
|
||||
|
||||
```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<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]`):
|
||||
|
||||
```tsx
|
||||
<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:
|
||||
|
||||
```tsx
|
||||
<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**
|
||||
|
||||
```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 <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**
|
||||
|
||||
```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
|
||||
```
|
||||
@@ -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
|
||||
<div className={cn("px-3 sm:px-6 py-3 shrink-0", !showTaskLane && "border-t border-border")}>
|
||||
```
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user