feat(cli): add network phase arrows to streaming status (#765)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user