feat: add loading for summary

This commit is contained in:
pomelo-nwu
2025-09-08 23:09:00 +08:00
parent 28896b4bf8
commit ac2aeaea45
5 changed files with 103 additions and 13 deletions

View File

@@ -20,7 +20,11 @@ import {
getProjectSummaryPrompt, getProjectSummaryPrompt,
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import path from 'path'; import path from 'path';
import { HistoryItemWithoutId, MessageType } from '../types.js'; import {
HistoryItemWithoutId,
HistoryItemSummary,
MessageType,
} from '../types.js';
interface ChatDetail { interface ChatDetail {
name: string; name: string;
@@ -331,9 +335,12 @@ const summaryCommand: SlashCommand = {
} }
// Show loading state // Show loading state
const pendingMessage = { const pendingMessage: HistoryItemSummary = {
type: 'info' as const, type: 'summary',
text: '🔄 Generating project summary...', summary: {
isPending: true,
stage: 'generating',
},
}; };
ui.setPendingItem(pendingMessage); ui.setPendingItem(pendingMessage);
@@ -377,8 +384,11 @@ const summaryCommand: SlashCommand = {
// Update loading message to show saving progress // Update loading message to show saving progress
ui.setPendingItem({ ui.setPendingItem({
type: 'info' as const, type: 'summary',
text: '💾 Saving project summary...', summary: {
isPending: true,
stage: 'saving',
},
}); });
// Ensure .qwen directory exists // Ensure .qwen directory exists
@@ -404,13 +414,14 @@ const summaryCommand: SlashCommand = {
// Clear pending item and show success message // Clear pending item and show success message
ui.setPendingItem(null); ui.setPendingItem(null);
ui.addItem( const completedSummaryItem: HistoryItemSummary = {
{ type: 'summary',
type: 'info' as const, summary: {
text: '✅ Project summary generated and saved to .qwen/PROJECT_SUMMARY.md', isPending: false,
stage: 'completed',
}, },
Date.now(), };
); ui.addItem(completedSummaryItem, Date.now());
return { return {
type: 'message', type: 'message',

View File

@@ -14,6 +14,7 @@ import { ErrorMessage } from './messages/ErrorMessage.js';
import { ToolGroupMessage } from './messages/ToolGroupMessage.js'; import { ToolGroupMessage } from './messages/ToolGroupMessage.js';
import { GeminiMessageContent } from './messages/GeminiMessageContent.js'; import { GeminiMessageContent } from './messages/GeminiMessageContent.js';
import { CompressionMessage } from './messages/CompressionMessage.js'; import { CompressionMessage } from './messages/CompressionMessage.js';
import { SummaryMessage } from './messages/SummaryMessage.js';
import { Box } from 'ink'; import { Box } from 'ink';
import { AboutBox } from './AboutBox.js'; import { AboutBox } from './AboutBox.js';
import { StatsDisplay } from './StatsDisplay.js'; import { StatsDisplay } from './StatsDisplay.js';
@@ -97,5 +98,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
{item.type === 'compression' && ( {item.type === 'compression' && (
<CompressionMessage compression={item.compression} /> <CompressionMessage compression={item.compression} />
)} )}
{item.type === 'summary' && <SummaryMessage summary={item.summary} />}
</Box> </Box>
); );

View File

@@ -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<SummaryDisplayProps> = ({ 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 <Spinner type="dots" />;
}
return <Text color={Colors.AccentGreen}></Text>;
};
return (
<Box flexDirection="row">
<Box marginRight={1}>{getIcon()}</Box>
<Box>
<Text
color={summary.isPending ? Colors.AccentPurple : Colors.AccentGreen}
>
{getText()}
</Text>
</Box>
</Box>
);
};

View File

@@ -157,6 +157,11 @@ export const useSlashCommandProcessor = (
type: 'compression', type: 'compression',
compression: message.compression, compression: message.compression,
}; };
} else if (message.type === MessageType.SUMMARY) {
historyItemContent = {
type: 'summary',
summary: message.summary,
};
} else { } else {
historyItemContent = { historyItemContent = {
type: message.type, type: message.type,

View File

@@ -58,6 +58,11 @@ export interface CompressionProps {
newTokenCount: number | null; newTokenCount: number | null;
} }
export interface SummaryProps {
isPending: boolean;
stage: 'generating' | 'saving' | 'completed';
}
export interface HistoryItemBase { export interface HistoryItemBase {
text?: string; // Text content for user/gemini/info/error messages text?: string; // Text content for user/gemini/info/error messages
} }
@@ -141,6 +146,11 @@ export type HistoryItemCompression = HistoryItemBase & {
compression: CompressionProps; compression: CompressionProps;
}; };
export type HistoryItemSummary = HistoryItemBase & {
type: 'summary';
summary: SummaryProps;
};
// Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's // Using Omit<HistoryItem, 'id'> seems to have some issues with typescript's
// type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that // type inference e.g. historyItem.type === 'tool_group' isn't auto-inferring that
// 'tools' in historyItem. // 'tools' in historyItem.
@@ -160,7 +170,8 @@ export type HistoryItemWithoutId =
| HistoryItemToolStats | HistoryItemToolStats
| HistoryItemQuit | HistoryItemQuit
| HistoryItemQuitConfirmation | HistoryItemQuitConfirmation
| HistoryItemCompression; | HistoryItemCompression
| HistoryItemSummary;
export type HistoryItem = HistoryItemWithoutId & { id: number }; export type HistoryItem = HistoryItemWithoutId & { id: number };
@@ -178,6 +189,7 @@ export enum MessageType {
QUIT_CONFIRMATION = 'quit_confirmation', QUIT_CONFIRMATION = 'quit_confirmation',
GEMINI = 'gemini', GEMINI = 'gemini',
COMPRESSION = 'compression', COMPRESSION = 'compression',
SUMMARY = 'summary',
} }
// Simplified message structure for internal feedback // Simplified message structure for internal feedback
@@ -236,6 +248,11 @@ export type Message =
type: MessageType.COMPRESSION; type: MessageType.COMPRESSION;
compression: CompressionProps; compression: CompressionProps;
timestamp: Date; timestamp: Date;
}
| {
type: MessageType.SUMMARY;
summary: SummaryProps;
timestamp: Date;
}; };
export interface ConsoleMessageItem { export interface ConsoleMessageItem {