feat(cli): add network phase arrows to streaming status (#765)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-31 20:24:43 -08:00
committed by GitHub
parent f1408a3ce1
commit 5fb039c807
2 changed files with 40 additions and 5 deletions

View File

@@ -793,6 +793,15 @@ export default function App({
// Whether a stream is in flight (disables input)
// Uses synced state to keep ref in sync for reliable async checks
const [streaming, setStreaming, streamingRef] = useSyncedState(false);
const [networkPhase, setNetworkPhase] = useState<
"upload" | "download" | "error" | null
>(null);
useEffect(() => {
if (!streaming) {
setNetworkPhase(null);
}
}, [streaming]);
// Guard ref for preventing concurrent processConversation calls
// Separate from streaming state which may be set early for UI responsiveness
@@ -2419,6 +2428,7 @@ export default function App({
}
setStreaming(true);
setNetworkPhase("upload");
abortControllerRef.current = new AbortController();
// Recover interrupted message: if cache contains ONLY user messages, prepend them
@@ -2743,6 +2753,11 @@ export default function App({
}
};
const handleFirstMessage = () => {
setNetworkPhase("download");
void syncAgentState();
};
const {
stopReason,
approval,
@@ -2755,7 +2770,7 @@ export default function App({
buffersRef.current,
refreshDerivedThrottled,
signal, // Use captured signal, not ref (which may be nulled by handleInterrupt)
syncAgentState,
handleFirstMessage,
);
// Update currentRunId for error reporting in catch block
@@ -3684,6 +3699,7 @@ export default function App({
// If we have a client-side stream error (e.g., JSON parse error), show it directly
// Fallback error: no run_id available, show whatever error message we have
if (fallbackError) {
setNetworkPhase("error");
const errorMsg = lastRunId
? `Stream error: ${fallbackError}\n(run_id: ${lastRunId})`
: `Stream error: ${fallbackError}`;
@@ -9847,6 +9863,7 @@ Plan file path: ${planFilePath}`;
onPasteError={handlePasteError}
restoredInput={restoredInput}
onRestoredInputConsumed={() => setRestoredInput(null)}
networkPhase={networkPhase}
/>
</Box>

View File

@@ -209,6 +209,7 @@ export function Input({
onPasteError,
restoredInput,
onRestoredInputConsumed,
networkPhase = null,
}: {
visible?: boolean;
streaming: boolean;
@@ -238,6 +239,7 @@ export function Input({
onPasteError?: (message: string) => void;
restoredInput?: string | null;
onRestoredInputConsumed?: () => void;
networkPhase?: "upload" | "download" | "error" | null;
}) {
const [value, setValue] = useState("");
const [escapePressed, setEscapePressed] = useState(false);
@@ -838,16 +840,31 @@ export function Input({
streaming && elapsedMs > ELAPSED_DISPLAY_THRESHOLD_MS;
const elapsedMinutes = Math.floor(elapsedMs / 60000);
const networkArrow = useMemo(() => {
if (!networkPhase) return "";
if (networkPhase === "upload") return "↑";
if (networkPhase === "download") return "↓";
return "↑\u0338";
}, [networkPhase]);
// Build the status hint text (esc to interrupt · 2m · 1.2k ↑)
// Uses chalk.dim to match reasoning text styling
// Memoized to prevent unnecessary re-renders during shimmer updates
const statusHintText = useMemo(() => {
const hintColor = chalk.hex(colors.subagent.hint);
const hintBold = hintColor.bold;
const suffix =
(shouldShowElapsed ? ` · ${elapsedMinutes}m` : "") +
(shouldShowTokenCount ? ` · ${formatCompact(estimatedTokens)}` : "") +
")";
const parts: string[] = [];
if (shouldShowElapsed) {
parts.push(`${elapsedMinutes}m`);
}
if (shouldShowTokenCount) {
parts.push(
`${formatCompact(estimatedTokens)}${networkArrow ? ` ${networkArrow}` : ""}`,
);
} else if (networkArrow) {
parts.push(networkArrow);
}
const suffix = `${parts.length > 0 ? ` · ${parts.join(" · ")}` : ""})`;
if (interruptRequested) {
return hintColor(` (interrupting${suffix}`);
}
@@ -860,6 +877,7 @@ export function Input({
shouldShowTokenCount,
estimatedTokens,
interruptRequested,
networkArrow,
]);
// Create a horizontal line using box-drawing characters