From a01bfa696d5f131d8eacc92d31a73515e7eafb41 Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:29:56 -0800 Subject: [PATCH] feat(tui): background agent status indicator in footer (#1233) --- src/cli/components/InputRich.tsx | 61 ++++++++++++++++++++++++++------ src/cli/helpers/subagentState.ts | 10 ++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index 16ff968..e4a5ed8 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -15,6 +15,7 @@ import { useMemo, useRef, useState, + useSyncExternalStore, } from "react"; import stringWidth from "string-width"; import type { ModelReasoningEffort } from "../../agent/model"; @@ -30,6 +31,12 @@ import { ralphMode } from "../../ralph/mode"; import { settingsManager } from "../../settings-manager"; import { charsToTokens, formatCompact } from "../helpers/format"; import type { QueuedMessage } from "../helpers/messageQueueBridge"; +import { + getActiveBackgroundAgents, + getSnapshot as getSubagentSnapshot, + subscribe as subscribeToSubagents, +} from "../helpers/subagentState.js"; +import { BlinkDot } from "./BlinkDot.js"; import { colors } from "./colors"; import { InputAssist } from "./InputAssist"; import { PasteAwareTextInput } from "./PasteAwareTextInput"; @@ -256,6 +263,34 @@ const InputFooter = memo(function InputFooter({ footerNotification?: string | null; }) { const hideFooterContent = hideFooter; + + // Subscribe to subagent state for background agent indicators + useSyncExternalStore(subscribeToSubagents, getSubagentSnapshot); + const backgroundAgents = getActiveBackgroundAgents(); + + // Tick counter for elapsed time display (only active when background agents exist) + const [, setTick] = useState(0); + useEffect(() => { + if (backgroundAgents.length === 0) return; + const t = setInterval(() => setTick((v) => v + 1), 1000); + return () => clearInterval(t); + }, [backgroundAgents.length]); + + // Build background agent display text (no useMemo — must recalculate each tick for elapsed time) + const bgAgentText = + backgroundAgents.length === 0 + ? "" + : backgroundAgents + .map((a) => { + const elapsedS = Math.round((Date.now() - a.startTime) / 1000); + return `${a.type.toLowerCase()} (${elapsedS}s)`; + }) + .join(" · "); + + // Width of the background agent indicator: "· " + text + " │ " + const bgIndicatorWidth = + backgroundAgents.length > 0 ? 2 + stringWidth(bgAgentText) + 3 : 0; + const maxAgentChars = Math.max(10, Math.floor(rightColumnWidth * 0.45)); const displayAgentName = truncateEnd(agentName || "Unnamed", maxAgentChars); const reasoningTag = getReasoningEffortTag(currentReasoningEffort); @@ -271,9 +306,10 @@ const InputFooter = memo(function InputFooter({ const rightTextLength = displayAgentName.length + displayModel.length + byokExtraChars + 3; const rightPrefixSpaces = Math.max(0, rightColumnWidth - rightTextLength); - const rightLabel = useMemo(() => { + + // Agent label without leading spaces (used by both default and bg-agent cases) + const rightLabelCore = useMemo(() => { const parts: string[] = []; - parts.push(" ".repeat(rightPrefixSpaces)); parts.push(chalk.hex(colors.footer.agentName)(displayAgentName)); parts.push(chalk.dim(" [")); parts.push(chalk.dim(displayModel)); @@ -284,15 +320,13 @@ const InputFooter = memo(function InputFooter({ ); } parts.push(chalk.dim("]")); - return parts.join(""); - }, [ - rightPrefixSpaces, - displayAgentName, - displayModel, - isByokProvider, - isOpenAICodexProvider, - ]); + }, [displayAgentName, displayModel, isByokProvider, isOpenAICodexProvider]); + + const rightLabel = useMemo( + () => " ".repeat(rightPrefixSpaces) + rightLabelCore, + [rightPrefixSpaces, rightLabelCore], + ); return ( @@ -358,6 +392,13 @@ const InputFooter = memo(function InputFooter({ {parseOsc8Line(line, `r${i}`)} )) + ) : backgroundAgents.length > 0 ? ( + + {" ".repeat(Math.max(0, rightPrefixSpaces - bgIndicatorWidth))} + + {` ${bgAgentText} │ `} + {rightLabelCore} + ) : ( {rightLabel} )} diff --git a/src/cli/helpers/subagentState.ts b/src/cli/helpers/subagentState.ts index 3fc7849..f877726 100644 --- a/src/cli/helpers/subagentState.ts +++ b/src/cli/helpers/subagentState.ts @@ -225,6 +225,16 @@ export function getSubagents(): SubagentState[] { return Array.from(store.agents.values()); } +/** + * Get silent background agents that are still pending or running + */ +export function getActiveBackgroundAgents(): SubagentState[] { + return Array.from(store.agents.values()).filter( + (a) => + a.silent === true && (a.status === "pending" || a.status === "running"), + ); +} + /** * Get subagents grouped by type */