While the model works (active session, no pending widget), CentralStatus shows a Claude-Code-style live status: a pulsing dot, elapsed seconds (ticking), and a short tail of the model's current output — so the centre no longer looks stuck during the long F1 loop. Verbose stream stays in the left panel. No tokens yet (would need the SessionBridge to forward Pi usage). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
75 lines
2.8 KiB
TypeScript
75 lines
2.8 KiB
TypeScript
import { useEffect, useReducer, useState } from "react";
|
|
import { useSessionStore } from "../store/sessionStore";
|
|
|
|
/** Last few words of a string — a compact "what the model is doing now" tail. */
|
|
function lastWords(s: string, n = 8): string {
|
|
const w = s.trim().split(/\s+/).filter(Boolean);
|
|
return w.slice(-n).join(" ");
|
|
}
|
|
|
|
/** The minimal central view: the user's last input/choice, a live "working" indicator
|
|
* (elapsed time + a short tail of the model stream) while the model is busy, and the
|
|
* gate's curated messages for the current step. The full verbose stream lives in the
|
|
* left Model-activity panel. */
|
|
export function CentralStatus({ working }: { working: boolean }) {
|
|
const lastUserEntry = useSessionStore((s) => s.lastUserEntry);
|
|
const stepMessages = useSessionStore((s) => s.stepMessages);
|
|
const transcript = useSessionStore((s) => s.transcript);
|
|
|
|
const [startedAt, setStartedAt] = useState<number | null>(null);
|
|
const [, tick] = useReducer((x: number) => x + 1, 0);
|
|
|
|
// Start the per-step timer when work begins; tick once a second so the elapsed
|
|
// display updates; clear it when work stops.
|
|
useEffect(() => {
|
|
if (!working) {
|
|
setStartedAt(null);
|
|
return;
|
|
}
|
|
setStartedAt((prev) => prev ?? Date.now());
|
|
const id = setInterval(tick, 1000);
|
|
return () => clearInterval(id);
|
|
}, [working]);
|
|
|
|
if (!working && !lastUserEntry && stepMessages.length === 0) return null;
|
|
|
|
const elapsed = startedAt ? Math.max(0, Math.floor((Date.now() - startedAt) / 1000)) : 0;
|
|
const tail = lastWords(transcript.at(-1)?.text ?? "");
|
|
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
{lastUserEntry && (
|
|
<div className="rounded-lg border border-border/70 bg-card px-3 py-2">
|
|
<span className="text-[0.65rem] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
{lastUserEntry.kind === "input" ? "You asked" : "You chose"}
|
|
</span>
|
|
<p className="mt-0.5 text-sm text-foreground">{lastUserEntry.text}</p>
|
|
</div>
|
|
)}
|
|
|
|
{working && (
|
|
<div className="flex min-w-0 items-center gap-2 text-sm text-muted-foreground">
|
|
<span className="size-1.5 shrink-0 animate-pulse rounded-full bg-primary" aria-hidden />
|
|
<span className="shrink-0 tabular-nums">{elapsed}s</span>
|
|
{tail && <span className="min-w-0 truncate text-muted-foreground/70">· {tail}</span>}
|
|
<span className="sr-only">ThothII is working</span>
|
|
</div>
|
|
)}
|
|
|
|
{stepMessages.map((m, i) => (
|
|
<p
|
|
key={i}
|
|
className={[
|
|
"text-sm",
|
|
m.level === "error" ? "text-destructive"
|
|
: m.level === "warning" ? "text-amber-600"
|
|
: "text-foreground/90",
|
|
].join(" ")}
|
|
>
|
|
{m.text}
|
|
</p>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|