Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions packages/cli/src/__tests__/tui/console/hooks/pollSchedule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
CONSOLE_POLL_INTERVAL_MS,
CONSOLE_POLL_PHASE_MS,
schedulePeriodicRefresh,
} from '../../../../tui/console/hooks/pollSchedule.js';

describe('console poll schedule', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(0);
});

afterEach(() => {
vi.useRealTimers();
});

it('separates recurring refresh phases while preserving each source cadence', async () => {
const calls: Array<{ source: string; time: number }> = [];
const cleanups = Object.entries(CONSOLE_POLL_PHASE_MS).map(([source, phaseMs]) => (
schedulePeriodicRefresh(
() => { calls.push({ source, time: Date.now() }); },
CONSOLE_POLL_INTERVAL_MS,
phaseMs,
)
));

await vi.advanceTimersByTimeAsync(CONSOLE_POLL_INTERVAL_MS * 2);

expect(calls).toEqual([
{ source: 'selectedAgentPreview', time: 750 },
{ source: 'channelStatus', time: 1500 },
{ source: 'configuredChannels', time: 2250 },
{ source: 'agentList', time: 3000 },
{ source: 'selectedAgentPreview', time: 3750 },
{ source: 'channelStatus', time: 4500 },
{ source: 'configuredChannels', time: 5250 },
{ source: 'agentList', time: 6000 },
]);

for (const source of Object.keys(CONSOLE_POLL_PHASE_MS)) {
const times = calls.filter(call => call.source === source).map(call => call.time);
expect(times[1] - times[0]).toBe(CONSOLE_POLL_INTERVAL_MS);
}

cleanups.forEach(cleanup => cleanup());
});

it('cancels both pending phase starts and active intervals', async () => {
const beforeStart = vi.fn();
const stopBeforeStart = schedulePeriodicRefresh(
beforeStart,
CONSOLE_POLL_INTERVAL_MS,
CONSOLE_POLL_PHASE_MS.configuredChannels,
);
stopBeforeStart();

const afterStart = vi.fn();
const stopAfterStart = schedulePeriodicRefresh(
afterStart,
CONSOLE_POLL_INTERVAL_MS,
CONSOLE_POLL_PHASE_MS.selectedAgentPreview,
);
await vi.advanceTimersByTimeAsync(CONSOLE_POLL_PHASE_MS.selectedAgentPreview);
expect(afterStart).toHaveBeenCalledTimes(1);
stopAfterStart();

await vi.advanceTimersByTimeAsync(CONSOLE_POLL_INTERVAL_MS * 2);

expect(beforeStart).not.toHaveBeenCalled();
expect(afterStart).toHaveBeenCalledTimes(1);
});
});
33 changes: 33 additions & 0 deletions packages/cli/src/tui/console/hooks/pollSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const CONSOLE_POLL_INTERVAL_MS = 3000;

export const CONSOLE_POLL_PHASE_MS = {
agentList: 0,
selectedAgentPreview: 750,
channelStatus: 1500,
configuredChannels: 2250,
} as const;

export function schedulePeriodicRefresh(
refresh: () => void,
intervalMs: number,
phaseMs: number,
): () => void {
const normalizedPhaseMs = ((phaseMs % intervalMs) + intervalMs) % intervalMs;
const elapsedInPeriodMs = Date.now() % intervalMs;
const timeUntilPhaseMs = (normalizedPhaseMs - elapsedInPeriodMs + intervalMs) % intervalMs;
const initialDelayMs = timeUntilPhaseMs === 0 ? intervalMs : timeUntilPhaseMs;
let intervalHandle: ReturnType<typeof setInterval> | undefined;
let stopped = false;

const timeoutHandle = setTimeout(() => {
if (stopped) return;
refresh();
intervalHandle = setInterval(refresh, intervalMs);
}, initialDelayMs);

return () => {
stopped = true;
clearTimeout(timeoutHandle);
if (intervalHandle !== undefined) clearInterval(intervalHandle);
};
}
15 changes: 12 additions & 3 deletions packages/cli/src/tui/console/hooks/useAgentConversation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import fs from 'fs';
import { useEffect, useRef, useState } from 'react';
import type { AgentInfo, AgentManager, ConversationMessage } from '@ai-devkit/agent-manager';
import {
CONSOLE_POLL_INTERVAL_MS,
CONSOLE_POLL_PHASE_MS,
schedulePeriodicRefresh,
} from './pollSchedule.js';

export interface ConversationFetchError {
kind: 'no-session-file' | 'no-adapter' | 'parse-error' | 'agent-not-found';
Expand All @@ -14,7 +19,7 @@ export interface UseAgentConversationResult {
isLoading: boolean;
}

export const PREVIEW_POLL_INTERVAL_MS = 3000;
export const PREVIEW_POLL_INTERVAL_MS = CONSOLE_POLL_INTERVAL_MS;
export const PREVIEW_TAIL = 20;
export const SELECTION_DEBOUNCE_MS = 150;

Expand Down Expand Up @@ -177,11 +182,15 @@ export function useAgentConversation({
};
}

const intervalHandle = setInterval(fetchOnce, intervalMs);
const stopPolling = schedulePeriodicRefresh(
fetchOnce,
intervalMs,
CONSOLE_POLL_PHASE_MS.selectedAgentPreview,
);
return () => {
mountedRef.current = false;
clearTimeout(debounceHandle);
clearInterval(intervalHandle);
stopPolling();
};
}, [manager, agent?.name, agent?.type, agent?.sessionFilePath, intervalMs, tail, paused]);

Expand Down
15 changes: 12 additions & 3 deletions packages/cli/src/tui/console/hooks/useAgentList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { AgentInfo, AgentManager } from '@ai-devkit/agent-manager';
import {
CONSOLE_POLL_INTERVAL_MS,
CONSOLE_POLL_PHASE_MS,
schedulePeriodicRefresh,
} from './pollSchedule.js';

export interface UseAgentListResult {
agents: AgentInfo[];
Expand All @@ -11,7 +16,7 @@ export interface UseAgentListResult {

type AgentListState = Omit<UseAgentListResult, 'refresh'>;

export const LIST_POLL_INTERVAL_MS = 3000;
export const LIST_POLL_INTERVAL_MS = CONSOLE_POLL_INTERVAL_MS;

export function agentsEqual(a: AgentInfo[], b: AgentInfo[]): boolean {
if (a.length !== b.length) return false;
Expand Down Expand Up @@ -91,11 +96,15 @@ export function useAgentList(
return () => { mountedRef.current = false; };
}
void refresh();
const handle = setInterval(() => { void refresh(); }, intervalMs);
const stopPolling = schedulePeriodicRefresh(
() => { void refresh(); },
intervalMs,
CONSOLE_POLL_PHASE_MS.agentList,
);

return () => {
mountedRef.current = false;
clearInterval(handle);
stopPolling();
};
}, [intervalMs, paused, refresh]);

Expand Down
30 changes: 25 additions & 5 deletions packages/cli/src/tui/console/hooks/useChannelState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { ConfigStore, type ChannelConfig, type TelegramConfig } from '@ai-devkit/channel-connector';
import { ChannelService, type ChannelBridgeProcess } from '../../../services/channel/channel.service.js';
import type { AgentChannelStatusMap, ConfiguredChannel } from '../types.js';
import {
CONSOLE_POLL_INTERVAL_MS,
CONSOLE_POLL_PHASE_MS,
schedulePeriodicRefresh,
} from './pollSchedule.js';

export interface UseChannelStateResult {
channelStatuses: AgentChannelStatusMap;
Expand Down Expand Up @@ -70,7 +75,7 @@ export function configuredChannelsEqual(a: ConfiguredChannel[], b: ConfiguredCha
export function useChannelState(
channelService?: ChannelService,
configStore?: ConfigStore,
intervalMs = 3000,
intervalMs = CONSOLE_POLL_INTERVAL_MS,
paused = false,
): UseChannelStateResult {
const serviceRef = useRef<ChannelService>(channelService ?? new ChannelService());
Expand All @@ -93,18 +98,33 @@ export function useChannelState(
useEffect(() => {
if (paused) return undefined;

const refreshAll = (): void => {
const refreshChannelStatuses = (): void => {
void refreshChannels().catch(() => {
setChannelStatuses(prev => channelStatusesEqual(prev, {}) ? prev : {});
});
};
const refreshChannelConfig = (): void => {
void refreshConfiguredChannels().catch(() => {
setConfiguredChannels(prev => configuredChannelsEqual(prev, []) ? prev : []);
});
};

refreshAll();
const handle = setInterval(refreshAll, intervalMs);
return () => clearInterval(handle);
refreshChannelStatuses();
refreshChannelConfig();
const stopStatusPolling = schedulePeriodicRefresh(
refreshChannelStatuses,
intervalMs,
CONSOLE_POLL_PHASE_MS.channelStatus,
);
const stopConfigPolling = schedulePeriodicRefresh(
refreshChannelConfig,
intervalMs,
CONSOLE_POLL_PHASE_MS.configuredChannels,
);
return () => {
stopStatusPolling();
stopConfigPolling();
};
}, [intervalMs, paused, refreshChannels, refreshConfiguredChannels]);

return {
Expand Down
Loading