Gate (tht-gate.js): - Pre-validate decision types against workflow.yaml before showing reviewer widget - Reject decisions emitted by later phases (min-phase check) - Copy top-level `kind` into artifact when model forgets it (prevents loop) - Force-advance on reviewer_decide/schema_linking when advance:true — skip redundant reviewer_confirm gate Backend: - Emit agent_end on clean Pi exit (code 0 + bridge idle) instead of marking failed Frontend: - Strip <think> tags from transcript and activity panel - Fix mermaid render with offscreen container + cleanup - Graceful mermaid error: show source code instead of red error, fall back to table Workflow: - F2 now emits table_promoted and table_excluded (early schema linking decisions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
72 lines
2.3 KiB
TypeScript
72 lines
2.3 KiB
TypeScript
import { useLayoutEffect, useRef } from "react";
|
||
import { useSessionStore } from "../store/sessionStore";
|
||
import { isNearBottom } from "./activityScroll";
|
||
|
||
function transcriptLines(transcript: Array<{ text: string }>): string[] {
|
||
return transcript.flatMap(({ text }) =>
|
||
text
|
||
.replace(/<think>[\s\S]*?<\/think>/g, "")
|
||
.replace(/<\/?think>/g, "")
|
||
.split("\n")
|
||
.map((line) => line.trimEnd())
|
||
.filter((line) => line.trim() !== ""),
|
||
);
|
||
}
|
||
|
||
/** Compact central projection of the assistant stream while the model is working. */
|
||
export function CentralStatus({ working }: { working: boolean }) {
|
||
const transcript = useSessionStore((state) => state.transcript);
|
||
const rows = transcriptLines(transcript);
|
||
const rowCount = rows.length;
|
||
const tail = rows.at(-1);
|
||
const scrollRef = useRef<HTMLOListElement>(null);
|
||
const followRef = useRef(true);
|
||
|
||
useLayoutEffect(() => {
|
||
if (!working) {
|
||
followRef.current = true;
|
||
return;
|
||
}
|
||
const viewport = scrollRef.current;
|
||
if (viewport && followRef.current) viewport.scrollTop = viewport.scrollHeight;
|
||
}, [working, rowCount, tail]);
|
||
|
||
if (!working || rows.length === 0) return null;
|
||
|
||
return (
|
||
<ol
|
||
ref={scrollRef}
|
||
role="log"
|
||
aria-label="Live model activity"
|
||
aria-live="polite"
|
||
aria-relevant="additions text"
|
||
tabIndex={0}
|
||
className="max-h-28 overflow-y-auto rounded-lg border border-border/60 bg-muted/40 px-3 py-2 outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/40"
|
||
onScroll={(event) => {
|
||
followRef.current = isNearBottom(event.currentTarget);
|
||
}}
|
||
>
|
||
{rows.map((line, index) => {
|
||
const isLast = index === rows.length - 1;
|
||
return (
|
||
<li
|
||
key={index}
|
||
className={[
|
||
"flex items-baseline gap-2 font-mono text-[0.72rem] leading-relaxed",
|
||
isLast ? "text-foreground/80" : "text-muted-foreground",
|
||
].join(" ")}
|
||
>
|
||
<span
|
||
aria-hidden
|
||
className={isLast ? "shrink-0 select-none text-primary" : "shrink-0 select-none text-muted-foreground/35"}
|
||
>
|
||
›
|
||
</span>
|
||
<span className="min-w-0 flex-1 whitespace-pre-wrap break-words">{line}</span>
|
||
</li>
|
||
);
|
||
})}
|
||
</ol>
|
||
);
|
||
}
|