fix(frontend,harness): phase-stepper wiring, central tail, activity-panel markdown

- WorkflowBar: center the phase pills (add justify-center).
- tht-gate.js: the gate was sending the descriptive phase name (e.g.
  "chiarimento") instead of the workflow.yaml id ("F1") in the widget's
  `phase` field, so the frontend's F1..F8 match never hit and pills never
  lit up. Rename phaseName -> phaseId, return p.id.
- CentralStatus: expand the live "working" tail from a single truncated
  line to up to 5 lines (line-clamp-5 safety net for unbroken paragraphs).
- ModelActivityPanel: render the streamed tail through react-markdown +
  remark-gfm instead of raw per-line <p> tags, so emphasis/headings render
  and blank-line paragraph breaks (previously stripped) are preserved;
  tail-cut now operates on paragraphs instead of physical lines.

Updated tests accordingly (WorkflowBar/CentralStatus/ModelActivityPanel/
AppShell.session-mgmt); full frontend suite (98/98) + harness gate node
tests (34/34) + tsc -b pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 15:53:31 +02:00
co-authored by Claude Sonnet 5
parent 7c417d4cf1
commit 023c69d752
6 changed files with 86 additions and 48 deletions
+21 -15
View File
@@ -1,25 +1,33 @@
import { useState } from "react";
import { ChevronDown, ChevronUp, X } from "lucide-react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { useSessionStore } from "../store/sessionStore";
import { Button } from "../components/ui/button";
const TAIL_LINES = 5;
const TAIL_PARAGRAPHS = 5;
/** Left drawer showing "what's happening": the tail of the model's streamed output.
* Collapsed it shows the last few lines (refreshing as the stream grows); the expand
* toggle reveals the full stream. Opened on demand from the rotating activity icon. */
/** Left drawer showing "what's happening": the tail of the model's streamed markdown
* output, rendered as markdown (not raw text) so headings/emphasis/lists come through
* and paragraphs stay visually separated. Collapsed it shows the last few paragraphs
* (refreshing as the stream grows); the expand toggle reveals the full stream. Opened
* on demand from the rotating activity icon. */
export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
const transcript = useSessionStore((s) => s.transcript);
const [expanded, setExpanded] = useState(false);
const lines = transcript
// Blank lines are markdown's paragraph separator — split on them (not on every "\n",
// which would also break single soft-wrapped lines apart) so each block below is a
// real paragraph and the tail cut lands on paragraph boundaries.
const paragraphs = transcript
.map((e) => e.text)
.join("\n")
.split("\n")
.map((l) => l.trimEnd())
.filter((l) => l.trim() !== "");
const shown = expanded ? lines : lines.slice(-TAIL_LINES);
const canExpand = lines.length > TAIL_LINES;
.join("\n\n")
.split(/\n{2,}/)
.map((p) => p.trim())
.filter((p) => p !== "");
const shown = expanded ? paragraphs : paragraphs.slice(-TAIL_PARAGRAPHS);
const canExpand = paragraphs.length > TAIL_PARAGRAPHS;
const source = shown.join("\n\n");
return (
<aside className="flex w-[380px] shrink-0 flex-col border-r border-border bg-sidebar">
@@ -46,10 +54,8 @@ export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
{shown.length === 0 ? (
<p className="text-sm text-muted-foreground">No activity yet.</p>
) : (
<div className="flex flex-col gap-1 font-mono text-xs leading-relaxed text-foreground/90">
{shown.map((l, i) => (
<p key={i} className="whitespace-pre-wrap break-words">{l}</p>
))}
<div className="prose prose-sm dark:prose-invert max-w-none text-foreground/90">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{source}</ReactMarkdown>
</div>
)}
</div>