feat(status-ui): add compact session status strip
This commit is contained in:
@@ -4,6 +4,14 @@ import { rpcCommandCatalog, type RpcCommand } from "../commands/rpc";
|
||||
import { CommandPalette } from "./CommandPalette";
|
||||
import { ModelSelector } from "./ModelSelector";
|
||||
|
||||
type SessionStatus = {
|
||||
label: string;
|
||||
detail: string;
|
||||
phase: RuntimeView["progress"]["phase"];
|
||||
bridge: string;
|
||||
extensions: string[];
|
||||
};
|
||||
|
||||
export function messageText(message: RuntimeView["messages"][number]) {
|
||||
if (typeof message.content === "string") return message.content;
|
||||
return (message.content ?? [])
|
||||
@@ -22,6 +30,36 @@ function visibleLines(text: string) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function compactCount(value: number) {
|
||||
const units = [
|
||||
{ threshold: 1_000_000_000, divisor: 1_000_000_000, suffix: "b" },
|
||||
{ threshold: 1_000_000, divisor: 1_000_000, suffix: "m" },
|
||||
{ threshold: 1_000, divisor: 1_000, suffix: "k" },
|
||||
];
|
||||
const unit = units.find((candidate) => Math.abs(value) >= candidate.threshold);
|
||||
if (!unit) return String(value);
|
||||
const scaled = Math.round((value / unit.divisor) * 10) / 10;
|
||||
return `${scaled}${unit.suffix}`;
|
||||
}
|
||||
|
||||
function runtimeStatusLabel(isWorking: boolean, state: string) {
|
||||
if (isWorking) return "Working";
|
||||
return state === "idle" ? "Pi ready" : state;
|
||||
}
|
||||
|
||||
function plainStatusText(value: string) {
|
||||
return value
|
||||
.normalize("NFC")
|
||||
.replace(/\u001B\[[0-?]*[ -/]*[@-~]/gu, "")
|
||||
.replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/gu, "")
|
||||
.replace(
|
||||
/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u206F\uE000-\uF8FF\uFEFF\uFFFD]/gu,
|
||||
"",
|
||||
)
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function TranscriptMessage({
|
||||
message,
|
||||
}: {
|
||||
@@ -107,6 +145,7 @@ export function ConversationWorkspace({
|
||||
isModelUpdating = false,
|
||||
locked = false,
|
||||
lockReason = "renewing",
|
||||
status,
|
||||
onCommand,
|
||||
}: {
|
||||
runtime: RuntimeView;
|
||||
@@ -121,6 +160,7 @@ export function ConversationWorkspace({
|
||||
isModelUpdating?: boolean;
|
||||
locked?: boolean;
|
||||
lockReason?: "renewing" | "restarting";
|
||||
status?: SessionStatus;
|
||||
onCommand: (command: RpcCommand) => void;
|
||||
}) {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
@@ -129,6 +169,43 @@ export function ConversationWorkspace({
|
||||
runtime.summary.state === "streaming" || runtime.progress.phase === "working";
|
||||
const isWorking =
|
||||
isSubmitting || runtimeIsWorking || runtime.progress.phase === "recovering";
|
||||
const presentedStatus = status ?? {
|
||||
label: runtimeStatusLabel(isWorking, runtime.summary.state),
|
||||
detail: isSubmitting ? "Sending prompt to Pi…" : runtime.progress.detail,
|
||||
phase: isWorking ? ("working" as const) : runtime.progress.phase,
|
||||
bridge: "unknown",
|
||||
extensions: Object.values(runtime.extensionStatuses),
|
||||
};
|
||||
const visibleStatus = {
|
||||
label: plainStatusText(presentedStatus.label) || "Pi status",
|
||||
detail: plainStatusText(presentedStatus.detail) || "No status details",
|
||||
bridge: plainStatusText(presentedStatus.bridge) || "unknown",
|
||||
extensions: presentedStatus.extensions.map(plainStatusText).filter(Boolean),
|
||||
};
|
||||
const activeTool = runtime.progress.tool
|
||||
? plainStatusText(runtime.progress.tool)
|
||||
: undefined;
|
||||
const inputTokens = runtime.stats.tokens?.input;
|
||||
const outputTokens = runtime.stats.tokens?.output;
|
||||
const cacheReadTokens = runtime.stats.tokens?.cacheRead;
|
||||
const undividedTokens =
|
||||
inputTokens === undefined && outputTokens === undefined
|
||||
? runtime.stats.tokens?.total
|
||||
: undefined;
|
||||
const context = runtime.stats.contextUsage;
|
||||
const contextPercent =
|
||||
context?.percent ??
|
||||
(context?.tokens != null && context.contextWindow
|
||||
? (context.tokens / context.contextWindow) * 100
|
||||
: undefined);
|
||||
let contextDetail: string | undefined;
|
||||
if (context?.tokens != null) {
|
||||
contextDetail = compactCount(context.tokens);
|
||||
if (context.contextWindow !== undefined)
|
||||
contextDetail += ` / ${compactCount(context.contextWindow)}`;
|
||||
if (contextPercent !== undefined)
|
||||
contextDetail += ` · ${Math.round(contextPercent)}%`;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (runtime.scroll.atBottom && ref.current)
|
||||
ref.current.scrollTop = ref.current.scrollHeight;
|
||||
@@ -269,15 +346,69 @@ export function ConversationWorkspace({
|
||||
</label>
|
||||
</section>
|
||||
<div
|
||||
className={`composer-progress ${isSubmitting ? "working" : runtime.progress.phase}`}
|
||||
className={`session-status-strip ${presentedStatus.phase}`}
|
||||
role="status"
|
||||
aria-label="Session status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span className="work-dot" />
|
||||
<span>
|
||||
{isSubmitting ? "Sending prompt to Pi…" : runtime.progress.detail}
|
||||
<span className="session-status-primary">
|
||||
<span className="work-dot" aria-hidden="true" />
|
||||
<strong>{visibleStatus.label}</strong>
|
||||
<span className="session-status-detail">{visibleStatus.detail}</span>
|
||||
</span>
|
||||
<span className="session-status-meta" aria-label="Status details">
|
||||
{inputTokens !== undefined && (
|
||||
<span className="status-chip" title="Input tokens">
|
||||
<small>In</small>
|
||||
{compactCount(inputTokens)}
|
||||
</span>
|
||||
)}
|
||||
{outputTokens !== undefined && (
|
||||
<span className="status-chip" title="Output tokens">
|
||||
<small>Out</small>
|
||||
{compactCount(outputTokens)}
|
||||
</span>
|
||||
)}
|
||||
{cacheReadTokens !== undefined && (
|
||||
<span className="status-chip" title="Cache-read tokens">
|
||||
<small>Cache read</small>
|
||||
{compactCount(cacheReadTokens)}
|
||||
</span>
|
||||
)}
|
||||
{undividedTokens !== undefined && (
|
||||
<span className="status-chip" title="Total tokens">
|
||||
<small>Tokens</small>
|
||||
{compactCount(undividedTokens)}
|
||||
</span>
|
||||
)}
|
||||
{contextDetail && (
|
||||
<span className="status-chip" title="Current context window usage">
|
||||
<small>Context</small>
|
||||
{contextDetail}
|
||||
</span>
|
||||
)}
|
||||
{runtime.summary.queueCount > 0 && (
|
||||
<span className="status-chip">
|
||||
<small>Queue</small>
|
||||
{runtime.summary.queueCount}
|
||||
</span>
|
||||
)}
|
||||
<span className="status-chip bridge-status">
|
||||
<small>Bridge</small>
|
||||
{visibleStatus.bridge}
|
||||
</span>
|
||||
{activeTool && (
|
||||
<span className="status-chip">
|
||||
<small>Tool</small>
|
||||
{activeTool}
|
||||
</span>
|
||||
)}
|
||||
{visibleStatus.extensions.map((entry, index) => (
|
||||
<span className="status-chip extension-status" key={`${entry}-${index}`}>
|
||||
{entry}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{runtime.progress.tool && <small>{runtime.progress.tool}</small>}
|
||||
</div>
|
||||
<div className="composer">
|
||||
<CommandPalette
|
||||
|
||||
Reference in New Issue
Block a user