Files
ThothII/frontend/src/shell/CentralStatus.tsx
T
marcopanandClaude Opus 4.6 6ee5bda7f0 feat: gate decision-type validation, force-advance, and frontend fixes
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>
2026-07-19 14:14:44 +02:00

72 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}