feat(tui): style bg subagent footer (#1288)
This commit is contained in:
95
src/cli/components/BlinkingSpinner.tsx
Normal file
95
src/cli/components/BlinkingSpinner.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import stringWidth from "string-width";
|
||||
import { useAnimation } from "../contexts/AnimationContext.js";
|
||||
import { colors } from "./colors.js";
|
||||
import { Text } from "./Text";
|
||||
|
||||
export const BRAILLE_SPINNER_FRAMES = [
|
||||
"⠋",
|
||||
"⠙",
|
||||
"⠹",
|
||||
"⠸",
|
||||
"⠼",
|
||||
"⠴",
|
||||
"⠦",
|
||||
"⠧",
|
||||
"⠇",
|
||||
"⠏",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Frame-based spinner for lightweight status indicators in the TUI.
|
||||
*/
|
||||
export const BlinkingSpinner = memo(
|
||||
({
|
||||
color = colors.tool.pending,
|
||||
frames = BRAILLE_SPINNER_FRAMES,
|
||||
intervalMs = 90,
|
||||
pulse = true,
|
||||
pulseIntervalMs = 300,
|
||||
width = 1,
|
||||
marginRight = 0,
|
||||
shouldAnimate: shouldAnimateProp,
|
||||
}: {
|
||||
color?: string;
|
||||
frames?: readonly string[];
|
||||
intervalMs?: number;
|
||||
pulse?: boolean;
|
||||
pulseIntervalMs?: number;
|
||||
width?: number;
|
||||
marginRight?: number;
|
||||
shouldAnimate?: boolean;
|
||||
}) => {
|
||||
const { shouldAnimate: shouldAnimateContext } = useAnimation();
|
||||
const shouldAnimate =
|
||||
shouldAnimateProp === false ? false : shouldAnimateContext;
|
||||
|
||||
const [frameIndex, setFrameIndex] = useState(0);
|
||||
const [blinkOn, setBlinkOn] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAnimate || frames.length === 0) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setFrameIndex((v) => (v + 1) % frames.length);
|
||||
}, intervalMs);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [shouldAnimate, frames, intervalMs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldAnimate || !pulse) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setBlinkOn((v) => !v);
|
||||
}, pulseIntervalMs);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [shouldAnimate, pulse, pulseIntervalMs]);
|
||||
|
||||
const frame =
|
||||
frames.length > 0
|
||||
? shouldAnimate
|
||||
? (frames[frameIndex] ?? frames[0] ?? "·")
|
||||
: (frames[0] ?? "·")
|
||||
: "·";
|
||||
|
||||
const frameWidth = stringWidth(frame);
|
||||
const targetWidth = Math.max(1, width);
|
||||
const totalPadding = Math.max(0, targetWidth - frameWidth);
|
||||
const leftPadding = Math.floor(totalPadding / 2);
|
||||
const rightPadding = totalPadding - leftPadding;
|
||||
const paddedFrame =
|
||||
" ".repeat(leftPadding) + frame + " ".repeat(rightPadding);
|
||||
|
||||
const output = paddedFrame + " ".repeat(Math.max(0, marginRight));
|
||||
|
||||
return (
|
||||
<Text color={color} dimColor={pulse && !blinkOn}>
|
||||
{output}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
BlinkingSpinner.displayName = "BlinkingSpinner";
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
getSnapshot as getSubagentSnapshot,
|
||||
subscribe as subscribeToSubagents,
|
||||
} from "../helpers/subagentState.js";
|
||||
import { BlinkDot } from "./BlinkDot.js";
|
||||
import { BlinkingSpinner } from "./BlinkingSpinner.js";
|
||||
import { colors } from "./colors";
|
||||
import { InputAssist } from "./InputAssist";
|
||||
import { PasteAwareTextInput } from "./PasteAwareTextInput";
|
||||
@@ -267,7 +267,23 @@ const InputFooter = memo(function InputFooter({
|
||||
|
||||
// Subscribe to subagent state for background agent indicators
|
||||
useSyncExternalStore(subscribeToSubagents, getSubagentSnapshot);
|
||||
const backgroundAgents = [...getActiveBackgroundAgents()];
|
||||
const backgroundAgents = [
|
||||
...getActiveBackgroundAgents(),
|
||||
// DEBUG: hardcoded agent for local footer testing
|
||||
{
|
||||
id: "debug-bg-agent",
|
||||
type: "Reflection",
|
||||
description: "Debug background agent",
|
||||
status: "running" as const,
|
||||
agentURL: "https://app.letta.com/chat/agent-debug-link",
|
||||
toolCalls: [],
|
||||
totalTokens: 0,
|
||||
durationMs: 0,
|
||||
startTime: Date.now() - 12_000,
|
||||
isBackground: true,
|
||||
silent: true,
|
||||
},
|
||||
];
|
||||
|
||||
// Tick counter for elapsed time display (only active when background agents exist)
|
||||
const [, setTick] = useState(0);
|
||||
@@ -306,10 +322,10 @@ const InputFooter = memo(function InputFooter({
|
||||
const rightPrefixSpaces = Math.max(0, rightColumnWidth - rightTextLength);
|
||||
|
||||
// When bg agents are active, widen the right column to fit the indicator + label
|
||||
// "· " (2) + parts text + " │ " (3)
|
||||
// spinner slot (3) + parts text + " │ " (3)
|
||||
const bgIndicatorWidth =
|
||||
backgroundAgents.length > 0
|
||||
? 2 +
|
||||
? 3 +
|
||||
bgAgentParts.reduce(
|
||||
(acc, p, i) =>
|
||||
acc +
|
||||
@@ -416,21 +432,30 @@ const InputFooter = memo(function InputFooter({
|
||||
))
|
||||
) : backgroundAgents.length > 0 ? (
|
||||
<Text>
|
||||
<BlinkDot color={colors.tool.pending} symbol="·" />
|
||||
<Text dimColor> </Text>
|
||||
<BlinkingSpinner
|
||||
color={colors.bgSubagent.spinner}
|
||||
width={2}
|
||||
marginRight={0}
|
||||
pulseIntervalMs={400}
|
||||
/>
|
||||
{bgAgentParts.map((part, i) => (
|
||||
<Text key={`bg-agent-${part}`}>
|
||||
{i > 0 && (
|
||||
<Text key={`bg-agent-indicator-${part}`} dimColor>
|
||||
<Text
|
||||
key={`bg-agent-indicator-${part}`}
|
||||
color={colors.bgSubagent.label}
|
||||
>
|
||||
{" · "}
|
||||
</Text>
|
||||
)}
|
||||
{part.chatUrl ? (
|
||||
<Link url={part.chatUrl}>
|
||||
<Text dimColor>{part.typeLabel}</Text>
|
||||
<Link url={part.chatUrl} fallback={false}>
|
||||
<Text color={colors.bgSubagent.label}>
|
||||
{part.typeLabel}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<Text dimColor>{part.typeLabel}</Text>
|
||||
<Text color={colors.bgSubagent.label}>{part.typeLabel}</Text>
|
||||
)}
|
||||
<Text dimColor> ({part.elapsed})</Text>
|
||||
</Text>
|
||||
|
||||
@@ -161,6 +161,12 @@ const _colors = {
|
||||
hint: "#808080", // Grey to match Ink's dimColor
|
||||
},
|
||||
|
||||
// Background subagent
|
||||
bgSubagent: {
|
||||
label: "#87af87",
|
||||
spinner: "#5faf5f",
|
||||
},
|
||||
|
||||
// Info/modal views
|
||||
info: {
|
||||
border: brandColors.primaryAccent,
|
||||
|
||||
Reference in New Issue
Block a user