mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-08 17:16:48 -07:00
feat: show scheduled loops in /tasks drawer
This commit is contained in:
@@ -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={
|
||||
|
||||
Reference in New Issue
Block a user