feat(frontend): rotating activity icon + 5-line expandable activity panel
- 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>
This commit is contained in:
@@ -1,20 +1,57 @@
|
||||
import { X } from "lucide-react";
|
||||
import { Transcript } from "./Transcript";
|
||||
import { useState } from "react";
|
||||
import { ChevronDown, ChevronUp, X } from "lucide-react";
|
||||
import { useSessionStore } from "../store/sessionStore";
|
||||
import { Button } from "../components/ui/button";
|
||||
|
||||
/** Left drawer showing the active session's streamed model text ("model activity"),
|
||||
* opened on demand from the work-in-progress icon. */
|
||||
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>
|
||||
<Button size="icon-sm" variant="ghost" aria-label="Close model activity" onClick={onClose}>
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
<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">
|
||||
<Transcript />
|
||||
{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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user