fix(statusline): re-arm polling when config changes (#1416)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user