- CentralStatus: replace the red pulsing dot at the start of the "working" line with the rotating WorkingSpinner, now a button that opens the model-activity panel (onOpenActivity). WorkingSpinner is extracted to its own module (was local to AppShell). - ModelActivityPanel: show the last 5 lines of the model-stream tail (refreshing as the stream grows) with an expand toggle to the full stream, replacing the unbounded full transcript. - AppShell: drop the separate spinner button (the inline icon is now the single trigger) and the local WorkingSpinner def. - Remove the now-orphaned Transcript.tsx (its only consumer was the panel). TDD: CentralStatus + ModelActivityPanel tests RED->GREEN; frontend suite 87/87; tsc clean. Live visual verification pending (to do with the B/C frontend pass). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
59 lines
2.3 KiB
TypeScript
59 lines
2.3 KiB
TypeScript
import { useState } from "react";
|
|
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
|
import { useSessionStore } from "../store/sessionStore";
|
|
import { Button } from "../components/ui/button";
|
|
|
|
const TAIL_LINES = 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. */
|
|
export function ModelActivityPanel({ onClose }: { onClose: () => void }) {
|
|
const transcript = useSessionStore((s) => s.transcript);
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const lines = 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;
|
|
|
|
return (
|
|
<aside className="flex w-[380px] shrink-0 flex-col border-r border-border bg-sidebar">
|
|
<div className="flex items-center justify-between border-b border-border/70 px-4 py-3">
|
|
<h2 className="font-heading text-sm font-semibold text-foreground">Model activity</h2>
|
|
<div className="flex items-center gap-1">
|
|
{canExpand && (
|
|
<Button
|
|
size="icon-sm"
|
|
variant="ghost"
|
|
aria-label={expanded ? "Show less" : "Show more"}
|
|
aria-expanded={expanded}
|
|
onClick={() => setExpanded((v) => !v)}
|
|
>
|
|
{expanded ? <ChevronUp className="size-4" /> : <ChevronDown className="size-4" />}
|
|
</Button>
|
|
)}
|
|
<Button size="icon-sm" variant="ghost" aria-label="Close model activity" onClick={onClose}>
|
|
<X className="size-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 overflow-y-auto px-4 py-4">
|
|
{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>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|