feat(pilot): Phase 7 — polish (loading/empty states, shortcuts, responsive drawer)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s

- WhatWeKnow shows a "synthesizing" indicator + skeleton pulse while the
  chat cycle is in-flight; task-lane header mirrors the signal with a
  "thinking" pip so engineers know the AI is still working.
- Quiet-state hint when the lane is open (facts exist) but no open
  questions, checks, or active fix — keeps the surface from looking
  "finished" when the AI is about to follow up.
- Keyboard shortcuts: ⌘↵/Ctrl+↵ send in the composer (plain Enter still
  sends), ⌘G toggles the Script Generator panel for the active fix,
  `?` opens a new ShortcutsHelpOverlay listing all bindings. ⌘K palette
  was already wired in TopBar.
- Responsive: below 1200px the task lane collapses to a bottom drawer
  with a backdrop + a floating "Tasks ●" toggle button. TaskLane now
  takes a `variant: 'side' | 'drawer'` prop; drawer variant drops the
  resize handle and uses the shared slide-in-bottom animation.
- Build hygiene: fixed a pre-existing TS error in confirm-post error
  handling (duplicate `response` type keys) and an unused-import warning
  in TemplatizePrompt.

Verified: `npx tsc -b` and `npm run build` both clean against the dev
stack; Vite HMR applied each change without errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 14:19:44 -04:00
parent 4aaf57adb5
commit 8a242f5db9
7 changed files with 339 additions and 19 deletions

View File

@@ -49,6 +49,10 @@ interface TaskLaneProps {
// (parent owns state). Renders inside the scrollable body so the popover
// stays anchored as the lane scrolls.
bottomSlot?: React.ReactNode
// Phase 7: `'drawer'` renders the lane as a full-width, bottom-anchored
// sheet (no resize handle, no left border) — used on viewports <1200px.
// Default `'side'` keeps the existing resizable right-side panel.
variant?: 'side' | 'drawer'
}
// ── Storage helpers ──
@@ -75,7 +79,8 @@ export function clearTaskState(sessionId: string) {
// ── Component ──
export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loading, whatWeKnowSlot, suggestedFixSlot, bottomSlot }: TaskLaneProps) {
export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loading, whatWeKnowSlot, suggestedFixSlot, bottomSlot, variant = 'side' }: TaskLaneProps) {
const isDrawer = variant === 'drawer'
const [tasks, setTasks] = useState<TaskResponse[]>(() => {
// Try to restore saved state for this session (preserves user's in-progress answers)
if (sessionId) {
@@ -245,20 +250,31 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
return (
<div
className="relative border-l border-default flex flex-col shrink-0 animate-slide-in-right"
style={{ background: 'var(--color-bg-page)', width: panelWidth }}
className={cn(
'relative flex flex-col',
isDrawer
? 'w-full h-full border-t border-default animate-slide-in-bottom'
: 'border-l border-default shrink-0 animate-slide-in-right',
)}
style={{
background: 'var(--color-bg-page)',
...(isDrawer ? {} : { 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">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
))}
{/* Resize grip handle — side variant only. Drawer variant has no
horizontal neighbor to resize against. */}
{!isDrawer && (
<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">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="w-[3px] h-[3px] rounded-full bg-muted-foreground" />
))}
</div>
</div>
</div>
)}
{/* Header */}
<div className="px-4 py-3 border-b border-default flex items-center justify-between shrink-0" style={{ borderTop: '2px solid var(--color-accent)' }}>
<h3 className="font-heading text-sm font-bold text-heading flex items-center gap-2">
@@ -271,6 +287,15 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
)}>
{allHandled ? '✓ Ready' : `${doneCount}/${totalCount}`}
</span>
{loading && (
<span
className="flex items-center gap-1 text-[10px] font-medium text-muted-foreground"
title="AI is thinking"
>
<Loader2 size={10} className="animate-spin" />
thinking
</span>
)}
</h3>
<button onClick={onClose} className="text-muted-foreground hover:text-heading transition-colors p-1" title="Collapse tasks">
<PanelRightClose size={16} />
@@ -513,6 +538,18 @@ export function TaskLane({ questions, actions, sessionId, onSubmit, onClose, loa
{/* ── Suggested fix (Phase 3) ── */}
{suggestedFixSlot}
{/* Quiet-state hint: lane is open (facts exist), but AI hasn't
proposed a next step yet. Keeps the lane from feeling "finished"
when the engineer still expects a question / fix to arrive. */}
{questionTasks.length === 0
&& actionTasks.length === 0
&& !suggestedFixSlot
&& !loading && (
<div className="text-[0.6875rem] italic text-muted-foreground px-1 py-2">
No open questions send a message or add a note; the AI will follow up.
</div>
)}
{/* ── Resolve action bar + preview popover (Phase 3) ── */}
{bottomSlot}
</div>