diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 991f2c30..f2f04e8e 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -20,7 +20,11 @@ import { getProjectSummaryPrompt, } from '@qwen-code/qwen-code-core'; import path from 'path'; -import { HistoryItemWithoutId, MessageType } from '../types.js'; +import { + HistoryItemWithoutId, + HistoryItemSummary, + MessageType, +} from '../types.js'; interface ChatDetail { name: string; @@ -331,9 +335,12 @@ const summaryCommand: SlashCommand = { } // Show loading state - const pendingMessage = { - type: 'info' as const, - text: '🔄 Generating project summary...', + const pendingMessage: HistoryItemSummary = { + type: 'summary', + summary: { + isPending: true, + stage: 'generating', + }, }; ui.setPendingItem(pendingMessage); @@ -377,8 +384,11 @@ const summaryCommand: SlashCommand = { // Update loading message to show saving progress ui.setPendingItem({ - type: 'info' as const, - text: '💾 Saving project summary...', + type: 'summary', + summary: { + isPending: true, + stage: 'saving', + }, }); // Ensure .qwen directory exists @@ -404,13 +414,14 @@ const summaryCommand: SlashCommand = { // Clear pending item and show success message ui.setPendingItem(null); - ui.addItem( - { - type: 'info' as const, - text: '✅ Project summary generated and saved to .qwen/PROJECT_SUMMARY.md', + const completedSummaryItem: HistoryItemSummary = { + type: 'summary', + summary: { + isPending: false, + stage: 'completed', }, - Date.now(), - ); + }; + ui.addItem(completedSummaryItem, Date.now()); return { type: 'message', diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index 627a04f7..dc487664 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -14,6 +14,7 @@ import { ErrorMessage } from './messages/ErrorMessage.js'; import { ToolGroupMessage } from './messages/ToolGroupMessage.js'; import { GeminiMessageContent } from './messages/GeminiMessageContent.js'; import { CompressionMessage } from './messages/CompressionMessage.js'; +import { SummaryMessage } from './messages/SummaryMessage.js'; import { Box } from 'ink'; import { AboutBox } from './AboutBox.js'; import { StatsDisplay } from './StatsDisplay.js'; @@ -97,5 +98,6 @@ export const HistoryItemDisplay: React.FC = ({ {item.type === 'compression' && ( )} + {item.type === 'summary' && } ); diff --git a/packages/cli/src/ui/components/messages/SummaryMessage.tsx b/packages/cli/src/ui/components/messages/SummaryMessage.tsx new file mode 100644 index 00000000..f8377bdb --- /dev/null +++ b/packages/cli/src/ui/components/messages/SummaryMessage.tsx @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from 'react'; +import { Box, Text } from 'ink'; +import { SummaryProps } from '../../types.js'; +import Spinner from 'ink-spinner'; +import { Colors } from '../../colors.js'; + +export interface SummaryDisplayProps { + summary: SummaryProps; +} + +/* + * Summary messages appear when the /chat summary command is run, and show a loading spinner + * while summary generation is in progress, followed up by success confirmation. + */ +export const SummaryMessage: React.FC = ({ summary }) => { + const getText = () => { + if (summary.isPending) { + switch (summary.stage) { + case 'generating': + return 'Generating project summary...'; + case 'saving': + return 'Saving project summary...'; + default: + return 'Processing summary...'; + } + } + return 'Project summary generated and saved successfully!'; + }; + + const getIcon = () => { + if (summary.isPending) { + return ; + } + return ✅; + }; + + return ( + + {getIcon()} + + + {getText()} + + + + ); +}; diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 6eb06960..798e8e76 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -157,6 +157,11 @@ export const useSlashCommandProcessor = ( type: 'compression', compression: message.compression, }; + } else if (message.type === MessageType.SUMMARY) { + historyItemContent = { + type: 'summary', + summary: message.summary, + }; } else { historyItemContent = { type: message.type, diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 9e7b603b..795bc7ae 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -58,6 +58,11 @@ export interface CompressionProps { newTokenCount: number | null; } +export interface SummaryProps { + isPending: boolean; + stage: 'generating' | 'saving' | 'completed'; +} + export interface HistoryItemBase { text?: string; // Text content for user/gemini/info/error messages } @@ -141,6 +146,11 @@ export type HistoryItemCompression = HistoryItemBase & { compression: CompressionProps; }; +export type HistoryItemSummary = HistoryItemBase & { + type: 'summary'; + summary: SummaryProps; +}; + // Using Omit seems to have some issues with typescript's // type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that // 'tools' in historyItem. @@ -160,7 +170,8 @@ export type HistoryItemWithoutId = | HistoryItemToolStats | HistoryItemQuit | HistoryItemQuitConfirmation - | HistoryItemCompression; + | HistoryItemCompression + | HistoryItemSummary; export type HistoryItem = HistoryItemWithoutId & { id: number }; @@ -178,6 +189,7 @@ export enum MessageType { QUIT_CONFIRMATION = 'quit_confirmation', GEMINI = 'gemini', COMPRESSION = 'compression', + SUMMARY = 'summary', } // Simplified message structure for internal feedback @@ -236,6 +248,11 @@ export type Message = type: MessageType.COMPRESSION; compression: CompressionProps; timestamp: Date; + } + | { + type: MessageType.SUMMARY; + summary: SummaryProps; + timestamp: Date; }; export interface ConsoleMessageItem {