feat: configurable status lines for CLI footer (#904)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
jnjpng
2026-02-11 17:35:34 -08:00
committed by GitHub
parent 74b369d1ca
commit c3a7f6c646
16 changed files with 1689 additions and 15 deletions

View File

@@ -0,0 +1,226 @@
// React hook that executes a user-defined status-line command.
//
// Behavior:
// - Event-driven refreshes with debounce (default 300ms)
// - Cancel in-flight execution on retrigger (latest data wins)
// - Optional polling when refreshIntervalMs is configured
import { useCallback, useEffect, useRef, useState } from "react";
import {
type NormalizedStatusLineConfig,
resolveStatusLineConfig,
} from "../helpers/statusLineConfig";
import {
buildStatusLinePayload,
type StatusLinePayloadBuildInput,
} from "../helpers/statusLinePayload";
import { executeStatusLineCommand } from "../helpers/statusLineRuntime";
/** Inputs supplied by App.tsx to build the payload and triggers. */
export interface StatusLineInputs {
modelId?: string | null;
modelDisplayName?: string | null;
currentDirectory: string;
projectDirectory: string;
sessionId?: string;
agentName?: string | null;
totalDurationMs?: number;
totalApiDurationMs?: number;
totalInputTokens?: number;
totalOutputTokens?: number;
contextWindowSize?: number;
usedContextTokens?: number;
permissionMode?: string;
networkPhase?: "upload" | "download" | "error" | null;
terminalWidth?: number;
triggerVersion: number;
}
/** ASCII Record Separator used to split left/right column output. */
const RS = "\x1e";
export interface StatusLineState {
text: string;
rightText: string;
active: boolean;
executing: boolean;
lastError: string | null;
padding: number;
}
function toPayloadInput(inputs: StatusLineInputs): StatusLinePayloadBuildInput {
return {
modelId: inputs.modelId,
modelDisplayName: inputs.modelDisplayName,
currentDirectory: inputs.currentDirectory,
projectDirectory: inputs.projectDirectory,
sessionId: inputs.sessionId,
agentName: inputs.agentName,
totalDurationMs: inputs.totalDurationMs,
totalApiDurationMs: inputs.totalApiDurationMs,
totalInputTokens: inputs.totalInputTokens,
totalOutputTokens: inputs.totalOutputTokens,
contextWindowSize: inputs.contextWindowSize,
usedContextTokens: inputs.usedContextTokens,
permissionMode: inputs.permissionMode,
networkPhase: inputs.networkPhase,
terminalWidth: inputs.terminalWidth,
};
}
export function useConfigurableStatusLine(
inputs: StatusLineInputs,
): StatusLineState {
const [text, setText] = useState("");
const [rightText, setRightText] = useState("");
const [active, setActive] = useState(false);
const [executing, setExecuting] = useState(false);
const [lastError, setLastError] = useState<string | null>(null);
const [padding, setPadding] = useState(0);
const inputsRef = useRef(inputs);
const configRef = useRef<NormalizedStatusLineConfig | null>(null);
const abortRef = useRef<AbortController | null>(null);
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const refreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
null,
);
useEffect(() => {
inputsRef.current = inputs;
}, [inputs]);
const clearDebounceTimer = useCallback(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
debounceTimerRef.current = null;
}
}, []);
const clearRefreshInterval = useCallback(() => {
if (refreshIntervalRef.current) {
clearInterval(refreshIntervalRef.current);
refreshIntervalRef.current = null;
}
}, []);
const resolveActiveConfig = useCallback(() => {
const workingDirectory = inputsRef.current.currentDirectory;
const config = resolveStatusLineConfig(workingDirectory);
if (!config) {
configRef.current = null;
setActive(false);
setText("");
setRightText("");
setPadding(0);
return null;
}
configRef.current = config;
setActive(true);
setPadding(config.padding);
return config;
}, []);
const executeNow = useCallback(async () => {
const config = configRef.current ?? resolveActiveConfig();
if (!config) return;
// Cancel in-flight execution so only the latest result is used.
abortRef.current?.abort();
const ac = new AbortController();
abortRef.current = ac;
setExecuting(true);
try {
const currentInputs = inputsRef.current;
const result = await executeStatusLineCommand(
config.command,
buildStatusLinePayload(toPayloadInput(currentInputs)),
{
timeout: config.timeout,
signal: ac.signal,
workingDirectory: currentInputs.currentDirectory,
},
);
if (ac.signal.aborted) return;
if (result.ok) {
const rsIdx = result.text.indexOf(RS);
if (rsIdx >= 0) {
setText(result.text.slice(0, rsIdx));
setRightText(result.text.slice(rsIdx + 1));
} else {
setText(result.text);
setRightText("");
}
setLastError(null);
} else {
setLastError(result.error ?? "Unknown error");
}
} catch {
if (!ac.signal.aborted) {
setLastError("Execution exception");
}
} finally {
if (abortRef.current === ac) {
abortRef.current = null;
}
setExecuting(false);
}
}, [resolveActiveConfig]);
const scheduleDebouncedRun = useCallback(() => {
const config = resolveActiveConfig();
if (!config) return;
clearDebounceTimer();
debounceTimerRef.current = setTimeout(() => {
debounceTimerRef.current = null;
void executeNow();
}, config.debounceMs);
}, [clearDebounceTimer, executeNow, resolveActiveConfig]);
const triggerVersion = inputs.triggerVersion;
// Event-driven trigger updates.
useEffect(() => {
// tie this effect explicitly to triggerVersion for lint + semantics
void triggerVersion;
scheduleDebouncedRun();
}, [scheduleDebouncedRun, triggerVersion]);
const currentDirectory = inputs.currentDirectory;
// Re-resolve config and optional polling whenever working directory changes.
useEffect(() => {
// tie this effect explicitly to currentDirectory for lint + semantics
void currentDirectory;
const config = resolveActiveConfig();
clearRefreshInterval();
if (config?.refreshIntervalMs) {
refreshIntervalRef.current = setInterval(() => {
scheduleDebouncedRun();
}, config.refreshIntervalMs);
}
return () => {
clearRefreshInterval();
clearDebounceTimer();
abortRef.current?.abort();
abortRef.current = null;
};
}, [
clearDebounceTimer,
clearRefreshInterval,
resolveActiveConfig,
scheduleDebouncedRun,
currentDirectory,
]);
return { text, rightText, active, executing, lastError, padding };
}