mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-29 22:44:45 -07:00
complete
This commit is contained in:
@@ -35,6 +35,7 @@ import { mcpCommand } from '../ui/commands/mcpCommand.js';
|
||||
import { memoryCommand } from '../ui/commands/memoryCommand.js';
|
||||
import { modelCommand } from '../ui/commands/modelCommand.js';
|
||||
import { permissionsCommand } from '../ui/commands/permissionsCommand.js';
|
||||
import { planCommand } from '../ui/commands/planCommand.js';
|
||||
import { privacyCommand } from '../ui/commands/privacyCommand.js';
|
||||
import { policiesCommand } from '../ui/commands/policiesCommand.js';
|
||||
import { profileCommand } from '../ui/commands/profileCommand.js';
|
||||
@@ -131,6 +132,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
||||
: [mcpCommand]),
|
||||
memoryCommand,
|
||||
modelCommand,
|
||||
...(this.config?.isPlanEnabled() ? [planCommand] : []),
|
||||
...(this.config?.getFolderTrust() ? [permissionsCommand] : []),
|
||||
privacyCommand,
|
||||
policiesCommand,
|
||||
|
||||
@@ -97,6 +97,7 @@ import { useFocus } from './hooks/useFocus.js';
|
||||
import { useKeypress, type Key } from './hooks/useKeypress.js';
|
||||
import { keyMatchers, Command } from './keyMatchers.js';
|
||||
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
|
||||
import { usePlanMonitoring } from './hooks/usePlanMonitoring.js';
|
||||
import { useShellInactivityStatus } from './hooks/useShellInactivityStatus.js';
|
||||
import { useFolderTrust } from './hooks/useFolderTrust.js';
|
||||
import { useIdeTrustListener } from './hooks/useIdeTrustListener.js';
|
||||
@@ -1334,6 +1335,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
retryStatus,
|
||||
});
|
||||
|
||||
const { planTodos, planFileName } = usePlanMonitoring(config);
|
||||
|
||||
const handleGlobalKeypress = useCallback(
|
||||
(key: Key) => {
|
||||
if (copyModeEnabled) {
|
||||
@@ -1688,6 +1691,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
constrainHeight,
|
||||
showErrorDetails,
|
||||
showFullTodos,
|
||||
planTodos,
|
||||
planFileName,
|
||||
filteredConsoleMessages,
|
||||
ideContextState,
|
||||
renderMarkdown,
|
||||
@@ -1786,6 +1791,8 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
constrainHeight,
|
||||
showErrorDetails,
|
||||
showFullTodos,
|
||||
planTodos,
|
||||
planFileName,
|
||||
filteredConsoleMessages,
|
||||
ideContextState,
|
||||
renderMarkdown,
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import type { SlashCommand } from './types.js';
|
||||
import { CommandKind } from './types.js';
|
||||
import { MessageType } from '../types.js';
|
||||
|
||||
export const planCommand: SlashCommand = {
|
||||
name: 'plan',
|
||||
description: 'Manage implementation plans',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
subCommands: [
|
||||
{
|
||||
name: 'discard',
|
||||
description: 'Discard all plan files in the current session',
|
||||
kind: CommandKind.BUILT_IN,
|
||||
autoExecute: true,
|
||||
action: async (context) => {
|
||||
const config = context.services.config;
|
||||
if (!config) return;
|
||||
|
||||
const plansDir = config.storage.getProjectTempPlansDir();
|
||||
try {
|
||||
if (fs.existsSync(plansDir)) {
|
||||
const files = await fs.promises.readdir(plansDir);
|
||||
const mdFiles = files.filter((f) => f.endsWith('.md'));
|
||||
|
||||
for (const file of mdFiles) {
|
||||
await fs.promises.unlink(path.join(plansDir, file));
|
||||
}
|
||||
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Discarded ${mdFiles.length} plan file(s).`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} else {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'No plan files found to discard.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
context.ui.addItem(
|
||||
{
|
||||
type: MessageType.ERROR,
|
||||
text: `Failed to discard plans: ${(error as Error).message}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -16,7 +16,10 @@ import { useUIState } from '../../contexts/UIStateContext.js';
|
||||
import { useMemo } from 'react';
|
||||
import type { HistoryItemToolGroup } from '../../types.js';
|
||||
|
||||
const TodoTitleDisplay: React.FC<{ todos: TodoList }> = ({ todos }) => {
|
||||
const TodoTitleDisplay: React.FC<{
|
||||
todos: TodoList;
|
||||
fileName?: string | null;
|
||||
}> = ({ todos, fileName }) => {
|
||||
const score = useMemo(() => {
|
||||
let total = 0;
|
||||
let completed = 0;
|
||||
@@ -34,7 +37,7 @@ const TodoTitleDisplay: React.FC<{ todos: TodoList }> = ({ todos }) => {
|
||||
return (
|
||||
<Box flexDirection="row" columnGap={2} height={1}>
|
||||
<Text color={theme.text.primary} bold aria-label="Todo list">
|
||||
Todo
|
||||
{fileName ? `Plan: ${fileName}` : 'Todo'}
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>{score} (ctrl+t to toggle)</Text>
|
||||
</Box>
|
||||
@@ -105,6 +108,9 @@ export const TodoTray: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
|
||||
const todos: TodoList | null = useMemo(() => {
|
||||
if (uiState.planTodos && uiState.planTodos.length > 0) {
|
||||
return { todos: uiState.planTodos };
|
||||
}
|
||||
// Find the most recent todo list written by the WriteTodosTool
|
||||
for (let i = uiState.history.length - 1; i >= 0; i--) {
|
||||
const entry = uiState.history[i];
|
||||
@@ -123,7 +129,7 @@ export const TodoTray: React.FC = () => {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [uiState.history]);
|
||||
}, [uiState.history, uiState.planTodos]);
|
||||
|
||||
const inProgress: Todo | null = useMemo(() => {
|
||||
if (todos === null) {
|
||||
@@ -148,6 +154,8 @@ export const TodoTray: React.FC = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPlan = uiState.planTodos && uiState.planTodos.length > 0;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="single"
|
||||
@@ -160,13 +168,19 @@ export const TodoTray: React.FC = () => {
|
||||
>
|
||||
{uiState.showFullTodos ? (
|
||||
<Box flexDirection="column" rowGap={1}>
|
||||
<TodoTitleDisplay todos={todos} />
|
||||
<TodoTitleDisplay
|
||||
todos={todos}
|
||||
fileName={isPlan ? uiState.planFileName : undefined}
|
||||
/>
|
||||
<TodoListDisplay todos={todos} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box flexDirection="row" columnGap={1} height={1}>
|
||||
<Box flexShrink={0} flexGrow={0}>
|
||||
<TodoTitleDisplay todos={todos} />
|
||||
<TodoTitleDisplay
|
||||
todos={todos}
|
||||
fileName={isPlan ? uiState.planFileName : undefined}
|
||||
/>
|
||||
</Box>
|
||||
{inProgress && (
|
||||
<Box flexShrink={1} flexGrow={1}>
|
||||
|
||||
@@ -25,7 +25,7 @@ import type {
|
||||
FallbackIntent,
|
||||
ValidationIntent,
|
||||
AgentDefinition,
|
||||
} from '@google/gemini-cli-core';
|
||||
Todo } from '@google/gemini-cli-core';
|
||||
import type { DOMElement } from 'ink';
|
||||
import type { SessionStatsState } from '../contexts/SessionContext.js';
|
||||
import type { ExtensionUpdateState } from '../state/extensions.js';
|
||||
@@ -144,6 +144,8 @@ export interface UIState {
|
||||
embeddedShellFocused: boolean;
|
||||
showDebugProfiler: boolean;
|
||||
showFullTodos: boolean;
|
||||
planTodos: Todo[] | null;
|
||||
planFileName: string | null;
|
||||
copyModeEnabled: boolean;
|
||||
warningMessage: string | null;
|
||||
bannerData: {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import {
|
||||
parseMarkdownTodos,
|
||||
type Todo,
|
||||
type Config,
|
||||
debugLogger,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
export function usePlanMonitoring(config: Config) {
|
||||
const [planTodos, setPlanTodos] = useState<Todo[] | null>(null);
|
||||
const [planFileName, setPlanFileName] = useState<string | null>(null);
|
||||
const lastModifiedRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const plansDir = config.storage.getProjectTempPlansDir();
|
||||
|
||||
const updatePlan = async () => {
|
||||
try {
|
||||
if (!fs.existsSync(plansDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = await fs.promises.readdir(plansDir);
|
||||
const mdFiles = files.filter((f) => f.endsWith('.md'));
|
||||
|
||||
if (mdFiles.length === 0) {
|
||||
setPlanTodos(null);
|
||||
setPlanFileName(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the most recently modified file
|
||||
let latestFile = '';
|
||||
let latestMtime = 0;
|
||||
|
||||
for (const file of mdFiles) {
|
||||
const filePath = path.join(plansDir, file);
|
||||
const stats = await fs.promises.stat(filePath);
|
||||
if (stats.mtimeMs > latestMtime) {
|
||||
latestMtime = stats.mtimeMs;
|
||||
latestFile = file;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
latestMtime > lastModifiedRef.current ||
|
||||
latestFile !== planFileName
|
||||
) {
|
||||
const content = await fs.promises.readFile(
|
||||
path.join(plansDir, latestFile),
|
||||
'utf8',
|
||||
);
|
||||
const todos = parseMarkdownTodos(content);
|
||||
setPlanTodos(todos);
|
||||
setPlanFileName(latestFile);
|
||||
lastModifiedRef.current = latestMtime;
|
||||
}
|
||||
} catch (error) {
|
||||
debugLogger.error('File operation for updating plan failed', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial check
|
||||
void updatePlan();
|
||||
|
||||
// Poll every 2 seconds for updates to the plan directory
|
||||
const interval = setInterval(() => {
|
||||
void updatePlan();
|
||||
}, 2000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [config, planFileName]);
|
||||
|
||||
return { planTodos, planFileName };
|
||||
}
|
||||
Reference in New Issue
Block a user