// Detail-column panes for the Finder-style stack. // // Each column takes its own subject id (termId, blockId, etc.) and renders // in the same `.pane` shell as the section panes, so resize / close / header // behavior is consistent across the stack. // // Click handlers inside these columns call `pushFrom(index, ...)` to drill // further (e.g. clicking a linked block on a term column pushes a block // column to its right). The `index` prop is the column's own position in // the stack — so child pushes can truncate everything to its right. "use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { PaneFrame } from "../PaneFrame"; import { StatusChip } from "../StatusChip"; import { useAnalysis, type ClientTerm } from "../../../lib/workspace/analysisStore"; import { useModelStore } from "../../../lib/sync/ModelStore"; import { useOpenPanes } from "../../../lib/workspace/openPanesStore"; import { ContextualSocratesThread } from "../../socrates/ContextualSocratesThread"; import { PromoteToolbar } from "../PromoteToolbar"; import { decideElement, removeBlock, removeAssociation, removeConstraint, removeRequirement, type ReviewableElementKind, } from "../../../lib/sync/ops"; import type { ReviewStatus, SysMLModel } from "../../../lib/sysml/model"; // ─── Term column ──────────────────────────────────────────────────────── interface TermColumnProps { termId: string; index: number; onClose: () => void; } export function TermColumn({ termId, index, onClose }: TermColumnProps) { const { getTerm, terms, requirements, findings } = useAnalysis(); const { model } = useModelStore(); const { pushFrom } = useOpenPanes(); const term = getTerm(termId); if (!term) { return (
This concept no longer exists.
); } const parents = buildParentChain(term, terms); const children = terms.filter(t => t.parentId === term.id); const linkedBlock = term.linkedBlockId ? model.blocks.find(b => b.id === term.linkedBlockId) ?? null : null; const linkedAssociations = model.associations.filter(a => a.linkedTermId === term.id); const linkedConstraints = model.constraints.filter(c => c.linkedTermId === term.id); const linkedReqsFromModel = model.requirements.filter(r => r.linkedTermId === term.id); const linkedReqsFromAnalyze = requirements.filter(r => r.linkedTermId === term.id); const relatedFindings = linkedBlock ? findings.filter(f => f.linkedElementIds.includes(linkedBlock.id)) : findings.filter(f => f.linkedElementIds.includes(`term:${term.id}`)); const hasFormalism = !!linkedBlock || linkedAssociations.length > 0 || linkedConstraints.length > 0 || linkedReqsFromModel.length > 0 || linkedReqsFromAnalyze.length > 0; return (
{(parents.length > 0 || children.length > 0 || term.synonyms.length > 0) && (
{parents.length > 0 && ( {parents.map((p, i) => ( {i > 0 ? " › " : null} ))} )} {children.length > 0 && ( {children.map(c => ( ))} )} {term.synonyms.length > 0 && ( {term.synonyms.map(s => ( {s} ))} )}
)}
{!hasFormalism ? (

Not yet formalized. Promote it above to make it a block, association, constraint, or requirement.

) : (
    {linkedBlock && (
  • )} {linkedAssociations.map(a => (
  • ))} {linkedConstraints.map(c => (
  • ))} {linkedReqsFromModel.map(r => (
  • ))} {linkedReqsFromAnalyze .filter(r => !linkedReqsFromModel.some(m => m.tag === r.tag)) .map(r => (
  • {r.tag} {r.text}
  • ))}
)}
{relatedFindings.length > 0 && (
    {relatedFindings.map(f => (
  • ))}
)}
); } // ─── Block column ─────────────────────────────────────────────────────── export function BlockColumn({ blockId, index, onClose, }: { blockId: string; index: number; onClose: () => void; }) { const { model, apply } = useModelStore(); const { findings } = useAnalysis(); const { pushFrom } = useOpenPanes(); const block = model.blocks.find(b => b.id === blockId); if (!block) { return (
This block no longer exists.
); } const associations = model.associations.filter( a => a.fromBlockId === block.id || a.toBlockId === block.id ); const requirements = model.requirements.filter(r => r.relations.some(rel => rel.kind === "satisfy" && rel.blockId === block.id) ); const relatedFindings = findings.filter(f => f.linkedElementIds.includes(block.id)); return ( apply([decideElement({ kind: "block", id: block.id })])} onDiscard={() => apply([removeBlock(block.id)])} /> } onClose={onClose} >
{block.linkedTermId && (
)} {block.properties.length > 0 && (
    {block.properties.map(p => (
  • {p.name} {p.type.kind}
  • ))}
)} {associations.length > 0 && (
    {associations.map(a => (
  • ))}
)} {requirements.length > 0 && (
    {requirements.map(r => (
  • ))}
)} {relatedFindings.length > 0 && (
    {relatedFindings.map(f => (
  • ))}
)}
); } // ─── Association column ───────────────────────────────────────────────── export function AssociationColumn({ associationId, index, onClose, }: { associationId: string; index: number; onClose: () => void; }) { const { model, apply } = useModelStore(); const { pushFrom } = useOpenPanes(); const a = model.associations.find(x => x.id === associationId); if (!a) { return (
This association no longer exists.
); } const from = model.blocks.find(b => b.id === a.fromBlockId); const to = model.blocks.find(b => b.id === a.toBlockId); return ( apply([decideElement({ kind: "association", id: a.id })])} onDiscard={() => apply([removeAssociation(a.id)])} /> } onClose={onClose} >
{a.linkedTermId && (
)}
); } // ─── Constraint column ────────────────────────────────────────────────── export function ConstraintColumn({ constraintId, index, onClose, }: { constraintId: string; index: number; onClose: () => void; }) { const { model, apply } = useModelStore(); const { pushFrom } = useOpenPanes(); const c = model.constraints.find(x => x.id === constraintId); if (!c) { return (
This constraint no longer exists.
); } return ( apply([decideElement({ kind: "constraint", id: c.id })])} onDiscard={() => apply([removeConstraint(c.id)])} /> } onClose={onClose} >
{c.expression && (
{c.expression}
)} {c.appliesTo.length > 0 && (
    {c.appliesTo.map(bid => { const b = model.blocks.find(x => x.id === bid); return (
  • ); })}
)} {c.linkedTermId && (
)}
); } // ─── Requirement column ───────────────────────────────────────────────── export function RequirementColumn({ requirementId, index, onClose, }: { requirementId: string; index: number; onClose: () => void; }) { const { model, apply } = useModelStore(); const { requirements: analyzedReqs, getTerm, decideRequirement } = useAnalysis(); const { pushFrom } = useOpenPanes(); // Match by id in either list. const modelReq = model.requirements.find(r => r.id === requirementId); const analyzedReq = analyzedReqs.find(r => r.id === requirementId); if (!modelReq && !analyzedReq) { return (
This requirement no longer exists.
); } const tag = modelReq?.tag ?? analyzedReq?.tag ?? requirementId; const text = modelReq?.text ?? analyzedReq?.text ?? ""; const linkedTermId = modelReq?.linkedTermId ?? analyzedReq?.linkedTermId ?? null; const tracedTo = modelReq?.relations .filter((r): r is { kind: "satisfy"; blockId: string } => r.kind === "satisfy") .map(r => r.blockId) ?? analyzedReq?.tracedToIds ?? []; const reviewStatus = modelReq?.reviewStatus; const analyzedStatus = analyzedReq?.status; return ( apply([decideElement({ kind: "requirement", id: modelReq.id })])} onDiscard={() => apply([removeRequirement(modelReq.id)])} /> ) : analyzedReq && analyzedStatus !== "accepted" ? (
) : null } onClose={onClose} >

{text}

{linkedTermId && (
)} {tracedTo.length > 0 && (
    {tracedTo.map(bid => { const b = model.blocks.find(x => x.id === bid); return (
  • ); })}
)}
); } // ─── Finding column ───────────────────────────────────────────────────── export function FindingColumn({ findingId, index, projectId, onClose, }: { findingId: string; index: number; projectId: string; onClose: () => void; }) { const { findings, decideFinding, refresh } = useAnalysis(); const { pushFrom } = useOpenPanes(); const f = findings.find(x => x.id === findingId); if (!f) { return (
This finding no longer exists.
); } const isPending = f.status === "suggested" || f.status === "deprecated"; const termAnchors = f.linkedElementIds .filter(s => s.startsWith("term:")) .map(s => s.slice("term:".length)); const elementAnchors = f.linkedElementIds.filter(s => !s.startsWith("term:")); return ( ) : null } onClose={onClose} >

{f.text}

{(termAnchors.length > 0 || elementAnchors.length > 0) && (
{termAnchors.map(tid => ( ))} {elementAnchors.map(eid => ( ))}
)}
{ void refresh(); onClose(); }} />
); } // ─── Shared helpers ───────────────────────────────────────────────────── /** * Definition section for TermColumn. Read state by default; click anywhere * on the definition (or the empty placeholder) to enter edit mode. Save * with Cmd/Ctrl-Enter or by clicking Save; cancel with Esc. When the user * has authored the definition (definitionPinned), shows a "pinned" chip * in the header and a "Reset to AI suggestion" link in edit mode that * clears the text + the pin. */ function DefinitionSection({ term }: { term: ClientTerm }) { const { setTermDefinition } = useAnalysis(); const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(term.definition ?? ""); const textareaRef = useRef(null); // When the user opens the editor, sync the draft with the latest text and // focus the textarea. Selecting nothing keeps the cursor at the end. useEffect(() => { if (!editing) return; setDraft(term.definition ?? ""); requestAnimationFrame(() => { const el = textareaRef.current; if (!el) return; el.focus(); el.setSelectionRange(el.value.length, el.value.length); }); // term.id intentionally excluded from deps — re-running on every term // change would clobber an in-flight edit. // eslint-disable-next-line react-hooks/exhaustive-deps }, [editing]); const commit = () => { setEditing(false); const next = draft.trim(); const cur = (term.definition ?? "").trim(); if (next === cur) return; void setTermDefinition(term.id, next.length > 0 ? next : null); }; const cancel = () => { setEditing(false); setDraft(term.definition ?? ""); }; const reset = () => { setEditing(false); setDraft(""); void setTermDefinition(term.id, null); }; const headerRight = term.definitionPinned ? ( ) : null; if (editing) { return (