All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s
Each lane section (What we know, Questions, Diagnostic Checks, Suggested fix) had its own `position: sticky; top: 0` header. As the engineer scrolled past a section, that section's header would pin until the section's bottom edge cleared the viewport, producing an "orphaned" label floating over unrelated content below. Headers now scroll with their content — in a 340px-wide lane the affordance was negative value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.2 KiB
TypeScript
92 lines
3.2 KiB
TypeScript
/**
|
|
* What-we-know section — the load-bearing structural feature of the FlowPilot
|
|
* task lane (per FLOWPILOT-MIGRATION.md Section 0 architectural claim #2).
|
|
*
|
|
* Owns the list of confirmed facts for a session. Facts arrive via three paths:
|
|
* 1. AI [PROMOTE] markers parsed in unified_chat_service (most common)
|
|
* 2. Engineer "+ Add a note" (manual user_note)
|
|
* 3. Engineer-initiated promotion of a question/check (future affordance)
|
|
*
|
|
* This component is the parent's contract: it takes a fact list + handlers
|
|
* 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'
|
|
import { AddNoteButton } from './AddNoteButton'
|
|
|
|
interface WhatWeKnowProps {
|
|
facts: SessionFact[]
|
|
onAddNote: (text: string, summary: string | null) => Promise<void> | void
|
|
onUpdateFact: (factId: string, text: string, summary: string | null) => Promise<void> | void
|
|
onDeleteFact: (factId: string) => Promise<void> | void
|
|
loading?: boolean
|
|
}
|
|
|
|
export function WhatWeKnow({
|
|
facts,
|
|
onAddNote,
|
|
onUpdateFact,
|
|
onDeleteFact,
|
|
loading,
|
|
}: WhatWeKnowProps) {
|
|
const count = facts.length
|
|
|
|
return (
|
|
<section
|
|
className={cn(
|
|
'rounded-lg p-3 -mx-1 mb-1',
|
|
// Subtle green-to-transparent gradient distinguishes this section
|
|
// from the rest of the lane (mockup 01-session-primary.png).
|
|
'bg-gradient-to-b from-success/[0.05] to-transparent',
|
|
)}
|
|
>
|
|
<div className="pb-2">
|
|
<div className="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[1.2px] text-muted-foreground pl-0.5">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-success" />
|
|
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.
|
|
</div>
|
|
)}
|
|
|
|
{facts.map((fact) => (
|
|
<WhatWeKnowItem
|
|
key={fact.id}
|
|
fact={fact}
|
|
onSave={(text, summary) => onUpdateFact(fact.id, text, summary)}
|
|
onDelete={() => onDeleteFact(fact.id)}
|
|
/>
|
|
))}
|
|
|
|
<AddNoteButton onAdd={onAddNote} />
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default WhatWeKnow
|