feat(pilot): Phase 7 — polish (loading/empty states, shortcuts, responsive drawer)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s
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:
@@ -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>
|
||||
|
||||
78
frontend/src/components/pilot/ShortcutsHelpOverlay.tsx
Normal file
78
frontend/src/components/pilot/ShortcutsHelpOverlay.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ShortcutsHelpOverlay — Phase 7 keyboard-shortcut reference modal.
|
||||
*
|
||||
* Opened by `?` from anywhere inside /pilot (the global useKeyboardShortcuts
|
||||
* hook skips keypresses inside inputs, so `?` is safe from composer collisions).
|
||||
*/
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
interface ShortcutsHelpOverlayProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
interface Row {
|
||||
keys: string[]
|
||||
label: string
|
||||
}
|
||||
|
||||
const ROWS: Row[] = [
|
||||
{ keys: ['⌘', 'K'], label: 'Open command palette' },
|
||||
{ keys: ['⌘', '↵'], label: 'Send the current message' },
|
||||
{ keys: ['⌘', 'G'], label: 'Toggle Script Generator for the active fix' },
|
||||
{ keys: ['?'], label: 'Show this shortcut reference' },
|
||||
{ keys: ['Esc'], label: 'Close modal / cancel edit' },
|
||||
]
|
||||
|
||||
export function ShortcutsHelpOverlay({ open, onClose }: ShortcutsHelpOverlayProps) {
|
||||
if (!open) return null
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Keyboard shortcuts"
|
||||
>
|
||||
<div
|
||||
className="relative w-full max-w-md rounded-xl border border-default bg-bg-page shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-default">
|
||||
<h2 className="font-heading text-sm font-semibold text-heading">
|
||||
Keyboard shortcuts
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded text-muted-foreground hover:text-heading hover:bg-elevated/40 transition-colors"
|
||||
aria-label="Close shortcuts"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-4 py-3 space-y-2">
|
||||
{ROWS.map((row) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className="flex items-center justify-between gap-3 text-[0.8125rem]"
|
||||
>
|
||||
<span className="text-muted-foreground">{row.label}</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{row.keys.map((k, i) => (
|
||||
<kbd
|
||||
key={i}
|
||||
className="inline-flex items-center justify-center rounded border border-white/[0.06] bg-white/[0.08] px-1.5 py-0.5 text-[0.6875rem] font-mono text-heading min-w-[20px]"
|
||||
>
|
||||
{k}
|
||||
</kbd>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShortcutsHelpOverlay
|
||||
@@ -20,7 +20,7 @@
|
||||
* modal for the next one after save/skip.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Loader2, Check, X, Trash2, Sparkles } from 'lucide-react'
|
||||
import { Loader2, Check, Trash2, Sparkles } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Modal } from '@/components/common/Modal'
|
||||
import { toast } from '@/lib/toast'
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
* and renders the section. Loading/refresh logic lives in the parent
|
||||
* (AssistantChatPage) so it can coordinate with the chat send cycle.
|
||||
*/
|
||||
import { Loader2 } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { SessionFact } from '@/api/sessionFacts'
|
||||
import { WhatWeKnowItem } from './WhatWeKnowItem'
|
||||
@@ -48,9 +49,25 @@ export function WhatWeKnow({
|
||||
What we know
|
||||
<span className="text-muted-foreground">·</span>
|
||||
<span className="tabular-nums">{count}</span>
|
||||
{loading && (
|
||||
<span
|
||||
className="ml-auto flex items-center gap-1 text-[0.625rem] font-medium normal-case tracking-normal text-muted-foreground"
|
||||
title="AI may be synthesizing new facts from this turn"
|
||||
>
|
||||
<Loader2 size={10} className="animate-spin" />
|
||||
synthesizing
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{count === 0 && loading && (
|
||||
<div className="space-y-2 px-1 py-2">
|
||||
<div className="h-3 w-3/4 rounded bg-elevated/60 animate-pulse" />
|
||||
<div className="h-3 w-1/2 rounded bg-elevated/60 animate-pulse" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{count === 0 && !loading && (
|
||||
<div className="text-[0.75rem] text-muted-foreground italic px-1 py-2">
|
||||
Nothing confirmed yet — facts appear here as the engineer answers questions and runs checks.
|
||||
|
||||
Reference in New Issue
Block a user