fix(statusline): re-arm polling when config changes (#1416)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
jnjpng
2026-03-16 17:40:01 -07:00
committed by GitHub
parent ad7177b305
commit f8849c4536
2 changed files with 101 additions and 11 deletions

View File

@@ -56,6 +56,31 @@ export interface StatusLineInputs {
/** ASCII Record Separator used to split left/right column output. */ /** ASCII Record Separator used to split left/right column output. */
const RS = "\x1e"; const RS = "\x1e";
export interface RefreshIntervalPlan {
shouldClearExistingInterval: boolean;
shouldArmInterval: boolean;
nextRefreshIntervalMs: number | null;
}
export function buildRefreshIntervalPlan(
armedRefreshIntervalMs: number | null,
desiredRefreshIntervalMs: number | null,
): RefreshIntervalPlan {
if (armedRefreshIntervalMs === desiredRefreshIntervalMs) {
return {
shouldClearExistingInterval: false,
shouldArmInterval: false,
nextRefreshIntervalMs: armedRefreshIntervalMs,
};
}
return {
shouldClearExistingInterval: armedRefreshIntervalMs !== null,
shouldArmInterval: desiredRefreshIntervalMs !== null,
nextRefreshIntervalMs: desiredRefreshIntervalMs,
};
}
export interface StatusLineState { export interface StatusLineState {
text: string; text: string;
rightText: string; rightText: string;
@@ -116,6 +141,8 @@ export function useConfigurableStatusLine(
const refreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>( const refreshIntervalRef = useRef<ReturnType<typeof setInterval> | null>(
null, null,
); );
const armedRefreshIntervalMsRef = useRef<number | null>(null);
const scheduleDebouncedRunRef = useRef<() => void>(() => {});
useEffect(() => { useEffect(() => {
inputsRef.current = inputs; inputsRef.current = inputs;
@@ -133,8 +160,31 @@ export function useConfigurableStatusLine(
clearInterval(refreshIntervalRef.current); clearInterval(refreshIntervalRef.current);
refreshIntervalRef.current = null; refreshIntervalRef.current = null;
} }
armedRefreshIntervalMsRef.current = null;
}, []); }, []);
const reconcileRefreshInterval = useCallback(
(config: NormalizedStatusLineConfig | null) => {
const desiredRefreshIntervalMs = config?.refreshIntervalMs ?? null;
const plan = buildRefreshIntervalPlan(
armedRefreshIntervalMsRef.current,
desiredRefreshIntervalMs,
);
if (plan.shouldClearExistingInterval) {
clearRefreshInterval();
}
if (plan.shouldArmInterval && plan.nextRefreshIntervalMs !== null) {
refreshIntervalRef.current = setInterval(() => {
scheduleDebouncedRunRef.current();
}, plan.nextRefreshIntervalMs);
armedRefreshIntervalMsRef.current = plan.nextRefreshIntervalMs;
}
},
[clearRefreshInterval],
);
const resolveActiveConfig = useCallback(() => { const resolveActiveConfig = useCallback(() => {
const workingDirectory = inputsRef.current.currentDirectory; const workingDirectory = inputsRef.current.currentDirectory;
const config = resolveStatusLineConfig(workingDirectory); const config = resolveStatusLineConfig(workingDirectory);
@@ -151,14 +201,16 @@ export function useConfigurableStatusLine(
setText(""); setText("");
setRightText(""); setRightText("");
setPadding(0); setPadding(0);
reconcileRefreshInterval(null);
return null; return null;
} }
configRef.current = config; configRef.current = config;
setActive(true); setActive(true);
setPadding(config.padding); setPadding(config.padding);
reconcileRefreshInterval(config);
return config; return config;
}, []); }, [reconcileRefreshInterval]);
const executeNow = useCallback(async () => { const executeNow = useCallback(async () => {
const config = configRef.current ?? resolveActiveConfig(); const config = configRef.current ?? resolveActiveConfig();
@@ -223,6 +275,11 @@ export function useConfigurableStatusLine(
const triggerVersion = inputs.triggerVersion; const triggerVersion = inputs.triggerVersion;
// Keep polling callbacks pointed at the latest debounced scheduler.
useEffect(() => {
scheduleDebouncedRunRef.current = scheduleDebouncedRun;
}, [scheduleDebouncedRun]);
// Event-driven trigger updates. // Event-driven trigger updates.
useEffect(() => { useEffect(() => {
// tie this effect explicitly to triggerVersion for lint + semantics // tie this effect explicitly to triggerVersion for lint + semantics
@@ -232,18 +289,11 @@ export function useConfigurableStatusLine(
const currentDirectory = inputs.currentDirectory; const currentDirectory = inputs.currentDirectory;
// Re-resolve config and optional polling whenever working directory changes. // Re-resolve config whenever working directory changes.
useEffect(() => { useEffect(() => {
// tie this effect explicitly to currentDirectory for lint + semantics // tie this effect explicitly to currentDirectory for lint + semantics
void currentDirectory; void currentDirectory;
const config = resolveActiveConfig(); resolveActiveConfig();
clearRefreshInterval();
if (config?.refreshIntervalMs) {
refreshIntervalRef.current = setInterval(() => {
scheduleDebouncedRun();
}, config.refreshIntervalMs);
}
return () => { return () => {
clearRefreshInterval(); clearRefreshInterval();
@@ -255,7 +305,6 @@ export function useConfigurableStatusLine(
clearDebounceTimer, clearDebounceTimer,
clearRefreshInterval, clearRefreshInterval,
resolveActiveConfig, resolveActiveConfig,
scheduleDebouncedRun,
currentDirectory, currentDirectory,
]); ]);

View File

@@ -3,6 +3,7 @@ import {
DEFAULT_STATUS_LINE_DEBOUNCE_MS, DEFAULT_STATUS_LINE_DEBOUNCE_MS,
normalizeStatusLineConfig, normalizeStatusLineConfig,
} from "../../cli/helpers/statusLineConfig"; } from "../../cli/helpers/statusLineConfig";
import { buildRefreshIntervalPlan } from "../../cli/hooks/useConfigurableStatusLine";
describe("statusline controller-related config", () => { describe("statusline controller-related config", () => {
test("normalizes debounce and refresh interval defaults", () => { test("normalizes debounce and refresh interval defaults", () => {
@@ -29,3 +30,43 @@ describe("statusline controller-related config", () => {
expect(normalized.debounceMs).toBe(50); expect(normalized.debounceMs).toBe(50);
}); });
}); });
describe("buildRefreshIntervalPlan", () => {
test("returns no-op when interval is unchanged", () => {
expect(buildRefreshIntervalPlan(null, null)).toEqual({
shouldClearExistingInterval: false,
shouldArmInterval: false,
nextRefreshIntervalMs: null,
});
expect(buildRefreshIntervalPlan(5000, 5000)).toEqual({
shouldClearExistingInterval: false,
shouldArmInterval: false,
nextRefreshIntervalMs: 5000,
});
});
test("arms interval when moving from off to polling", () => {
expect(buildRefreshIntervalPlan(null, 5000)).toEqual({
shouldClearExistingInterval: false,
shouldArmInterval: true,
nextRefreshIntervalMs: 5000,
});
});
test("re-arms interval when polling cadence changes", () => {
expect(buildRefreshIntervalPlan(5000, 1000)).toEqual({
shouldClearExistingInterval: true,
shouldArmInterval: true,
nextRefreshIntervalMs: 1000,
});
});
test("clears interval when polling is disabled", () => {
expect(buildRefreshIntervalPlan(5000, null)).toEqual({
shouldClearExistingInterval: true,
shouldArmInterval: false,
nextRefreshIntervalMs: null,
});
});
});