feat: show scheduled loops in /tasks drawer

This commit is contained in:
Samee Zahid
2026-03-30 16:44:38 -07:00
parent 21b2b42e5e
commit 6643f71872
8 changed files with 209 additions and 57 deletions
+16 -2
View File
@@ -83,9 +83,9 @@ import {
logBillingEvent,
ApiKeyUpdatedEvent,
type InjectionSource,
cronSchedulerService,
type ScheduledTask} from '@google/gemini-cli-core';
type ScheduledTask,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
import { useHistory } from './hooks/useHistoryManager.js';
@@ -1200,6 +1200,18 @@ Logging in with Google... Restarting Gemini CLI to continue.
const { isMcpReady } = useMcpStatus(config);
const [scheduledTasks, setScheduledTasks] = useState<ScheduledTask[]>([]);
useEffect(() => {
const updateTasks = () => {
setScheduledTasks(cronSchedulerService.listTasks());
};
updateTasks();
// Poor man's sync: polling for list updates since it's mostly local
const interval = setInterval(updateTasks, 2000);
return () => clearInterval(interval);
}, []);
const {
messageQueue,
addMessage,
@@ -2337,6 +2349,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
terminalBackgroundColor: config.getTerminalBackground(),
settingsNonce,
backgroundTasks,
scheduledTasks,
activeBackgroundTaskPid,
backgroundTaskHeight,
isBackgroundTaskListOpen,
@@ -2464,6 +2477,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
settingsNonce,
backgroundTaskHeight,
isBackgroundTaskListOpen,
scheduledTasks,
activeBackgroundTaskPid,
backgroundTasks,
adminSettingsChanged,
@@ -149,6 +149,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -169,6 +170,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -189,6 +191,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -209,6 +212,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -229,6 +233,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={100}
height={30}
@@ -252,6 +257,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -272,6 +278,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -303,6 +310,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -335,6 +343,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell1.pid}
width={width}
height={24}
@@ -360,6 +369,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={shell2.pid}
width={width}
height={24}
@@ -391,6 +401,7 @@ describe('<BackgroundTaskDisplay />', () => {
<ScrollProvider>
<BackgroundTaskDisplay
shells={mockShells}
scheduledTasks={[]}
activePid={exitedShell.pid}
width={width}
height={24}
@@ -15,6 +15,7 @@ import {
type AnsiOutput,
type AnsiLine,
type AnsiToken,
type ScheduledTask,
} from '@google/gemini-cli-core';
import { cpLen, cpSlice, getCachedStringWidth } from '../utils/textUtils.js';
import { type BackgroundTask } from '../hooks/useExecutionLifecycle.js';
@@ -36,6 +37,7 @@ import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
interface BackgroundTaskDisplayProps {
shells: Map<number, BackgroundTask>;
scheduledTasks: ScheduledTask[];
activePid: number;
width: number;
height: number;
@@ -63,6 +65,7 @@ const formatShellCommandForDisplay = (command: string, maxWidth: number) => {
export const BackgroundTaskDisplay = ({
shells,
scheduledTasks,
activePid,
width,
height,
@@ -335,60 +338,74 @@ export const BackgroundTaskDisplay = ({
</Text>
</Box>
<Box flexGrow={1} width="100%">
<RadioButtonSelect
items={items}
initialIndex={initialIndex >= 0 ? initialIndex : 0}
onSelect={(pid) => {
setActiveBackgroundTaskPid(pid);
setIsBackgroundTaskListOpen(false);
}}
onHighlight={(pid) => setHighlightedPid(pid)}
isFocused={isFocused}
maxItemsToShow={Math.max(
1,
height - TOTAL_OVERHEAD_HEIGHT - PROCESS_LIST_HEADER_HEIGHT,
)}
renderItem={(
item,
{ isSelected: _isSelected, titleColor: _titleColor },
) => {
// Custom render to handle exit code coloring if needed,
// or just use default. The default RadioButtonSelect renderer
// handles standard label.
// But we want to color exit code differently?
// The previous implementation colored exit code green/red.
// Let's reimplement that.
{shells.size > 0 && (
<RadioButtonSelect
items={items}
initialIndex={initialIndex >= 0 ? initialIndex : 0}
onSelect={(pid) => {
setActiveBackgroundTaskPid(pid);
setIsBackgroundTaskListOpen(false);
}}
onHighlight={(pid) => setHighlightedPid(pid)}
isFocused={isFocused}
maxItemsToShow={Math.max(
1,
height -
TOTAL_OVERHEAD_HEIGHT -
PROCESS_LIST_HEADER_HEIGHT -
(scheduledTasks.length > 0 ? scheduledTasks.length + 2 : 0),
)}
renderItem={(
item,
{ isSelected: _isSelected, titleColor: _titleColor },
) => {
const shell = shells.get(item.value);
if (!shell) return <Text>{item.label}</Text>;
// We need access to shell details here.
// We can put shell details in the item or lookup.
// Lookup from shells map.
const shell = shells.get(item.value);
if (!shell) return <Text>{item.label}</Text>;
const truncatedCommand = formatShellCommandForDisplay(
shell.command,
maxCommandLength,
);
const truncatedCommand = formatShellCommandForDisplay(
shell.command,
maxCommandLength,
);
return (
<Text>
{truncatedCommand} (PID: {shell.pid})
{shell.status === 'exited' ? (
<Text
color={
shell.exitCode === 0
? theme.status.success
: theme.status.error
}
>
{' '}
(Exit Code: {shell.exitCode})
</Text>
) : null}
return (
<Text>
{truncatedCommand} (PID: {shell.pid})
{shell.status === 'exited' ? (
<Text
color={
shell.exitCode === 0
? theme.status.success
: theme.status.error
}
>
{' '}
(Exit Code: {shell.exitCode})
</Text>
) : null}
</Text>
);
}}
/>
)}
{scheduledTasks.length > 0 && (
<Box flexDirection="column" marginTop={shells.size > 0 ? 1 : 0}>
<Box
borderStyle="single"
borderBottom={false}
borderLeft={false}
borderRight={false}
paddingTop={1}
marginBottom={1}
>
<Text bold>Scheduled Loops</Text>
</Box>
{scheduledTasks.map((task) => (
<Text key={task.id}>
{` ● [${task.id}] every ${task.intervalMs! / 1000}s: "${task.prompt}"`}
</Text>
);
}}
/>
))}
</Box>
)}
</Box>
</Box>
);
@@ -29,7 +29,7 @@ import type {
AgentDefinition,
FolderDiscoveryResults,
PolicyUpdateConfirmationRequest,
} from '@google/gemini-cli-core';
type ScheduledTask } from '@google/gemini-cli-core';
import { type TransientMessageType } from '../../utils/events.js';
import type { DOMElement } from 'ink';
import type { SessionStatsState } from '../contexts/SessionContext.js';
@@ -216,6 +216,7 @@ export interface UIState {
terminalBackgroundColor: TerminalBackgroundColor;
settingsNonce: number;
backgroundTasks: Map<number, BackgroundTask>;
scheduledTasks: ScheduledTask[];
activeBackgroundTaskPid: number | null;
backgroundTaskHeight: number;
isBackgroundTaskListOpen: boolean;
@@ -40,14 +40,15 @@ export const DefaultAppLayout: React.FC = () => {
<MainContent />
{uiState.isBackgroundTaskVisible &&
uiState.backgroundTasks.size > 0 &&
uiState.activeBackgroundTaskPid &&
(uiState.backgroundTasks.size > 0 ||
uiState.scheduledTasks.length > 0) &&
uiState.backgroundTaskHeight > 0 &&
uiState.streamingState !== StreamingState.WaitingForConfirmation && (
<Box height={uiState.backgroundTaskHeight} flexShrink={0}>
<BackgroundTaskDisplay
shells={uiState.backgroundTasks}
activePid={uiState.activeBackgroundTaskPid}
scheduledTasks={uiState.scheduledTasks}
activePid={uiState.activeBackgroundTaskPid ?? 0}
width={uiState.terminalWidth}
height={uiState.backgroundTaskHeight}
isFocused={