From c33e709802c98966ea3845a90ff17cfb57961a17 Mon Sep 17 00:00:00 2001 From: pomelo-nwu Date: Mon, 8 Sep 2025 20:43:38 +0800 Subject: [PATCH] feat: add /chat summary command --- .../cli/src/ui/commands/chatCommand.test.ts | 2 +- packages/cli/src/ui/commands/chatCommand.ts | 179 +++++++++++++++++- packages/core/src/core/prompts.ts | 30 +++ 3 files changed, 208 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/ui/commands/chatCommand.test.ts b/packages/cli/src/ui/commands/chatCommand.test.ts index 1efb0711..8347061d 100644 --- a/packages/cli/src/ui/commands/chatCommand.test.ts +++ b/packages/cli/src/ui/commands/chatCommand.test.ts @@ -90,7 +90,7 @@ describe('chatCommand', () => { it('should have the correct main command definition', () => { expect(chatCommand.name).toBe('chat'); expect(chatCommand.description).toBe('Manage conversation history.'); - expect(chatCommand.subCommands).toHaveLength(4); + expect(chatCommand.subCommands).toHaveLength(5); }); describe('list subcommand', () => { diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 71b9cf78..6885c030 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -15,7 +15,10 @@ import { CommandKind, SlashCommandActionReturn, } from './types.js'; -import { decodeTagName } from '@qwen-code/qwen-code-core'; +import { + decodeTagName, + getProjectSummaryPrompt, +} from '@qwen-code/qwen-code-core'; import path from 'path'; import { HistoryItemWithoutId, MessageType } from '../types.js'; @@ -272,9 +275,181 @@ const deleteCommand: SlashCommand = { }, }; +const summaryCommand: SlashCommand = { + name: 'summary', + description: + 'Generate a project summary and save it to .qwen/PROJECT_SUMMARY.md', + kind: CommandKind.BUILT_IN, + action: async (context): Promise => { + const { config } = context.services; + const { ui } = context; + if (!config) { + return { + type: 'message', + messageType: 'error', + content: 'Config not loaded.', + }; + } + + const geminiClient = config.getGeminiClient(); + if (!geminiClient) { + return { + type: 'message', + messageType: 'error', + content: 'No chat client available to generate summary.', + }; + } + + // Check if already generating summary + if (ui.pendingItem) { + ui.addItem( + { + type: 'error' as const, + text: 'Already generating summary, wait for previous request to complete', + }, + Date.now(), + ); + return { + type: 'message', + messageType: 'error', + content: + 'Already generating summary, wait for previous request to complete', + }; + } + + try { + // Get the current chat history + const chat = geminiClient.getChat(); + const history = chat.getHistory(); + + if (history.length <= 2) { + return { + type: 'message', + messageType: 'info', + content: 'No conversation found to summarize.', + }; + } + + // Show loading state + const pendingMessage = { + type: 'info' as const, + text: '🔄 Generating project summary...', + }; + ui.setPendingItem(pendingMessage); + + // Build the conversation context for summary generation + const conversationContext = history.map((message) => ({ + role: message.role, + parts: message.parts, + })); + + // Use generateContent with chat history as context + const response = await geminiClient.generateContent( + [ + ...conversationContext, + { + role: 'user', + parts: [ + { + text: '' + getProjectSummaryPrompt(), + }, + ], + }, + ], + {}, + new AbortController().signal, + ); + + // Extract text from response + const parts = response.candidates?.[0]?.content?.parts; + + const markdownSummary = + parts + ?.map((part) => part.text) + .filter((text): text is string => typeof text === 'string') + .join('') || ''; + + if (!markdownSummary) { + throw new Error( + 'Failed to generate summary - no text content received from LLM response', + ); + } + + // Update loading message to show saving progress + ui.setPendingItem({ + type: 'info' as const, + text: '💾 Saving project summary...', + }); + + // Ensure .qwen directory exists + const projectRoot = config.getProjectRoot(); + const qwenDir = path.join(projectRoot, '.qwen'); + try { + await fsPromises.mkdir(qwenDir, { recursive: true }); + } catch (_err) { + // Directory might already exist, ignore error + } + + // Save the summary to PROJECT_SUMMARY.md + const summaryPath = path.join(qwenDir, 'PROJECT_SUMMARY.md'); + const summaryContent = `${markdownSummary} + +--- + +## Summary Metadata +**Generated**: ${new Date().toISOString()} This summary was automatically generated from the conversation history and is designed to provide comprehensive context for future development sessions.* +`; + + await fsPromises.writeFile(summaryPath, summaryContent, 'utf8'); + + // 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', + }, + Date.now(), + ); + + return { + type: 'message', + messageType: 'info', + content: `Project summary generated and saved to .qwen/PROJECT_SUMMARY.md`, + }; + } catch (error) { + // Clear pending item on error + ui.setPendingItem(null); + ui.addItem( + { + type: 'error' as const, + text: `❌ Failed to generate project context summary: ${ + error instanceof Error ? error.message : String(error) + }`, + }, + Date.now(), + ); + + return { + type: 'message', + messageType: 'error', + content: `Failed to generate project context summary: ${ + error instanceof Error ? error.message : String(error) + }`, + }; + } + }, +}; + export const chatCommand: SlashCommand = { name: 'chat', description: 'Manage conversation history.', kind: CommandKind.BUILT_IN, - subCommands: [listCommand, saveCommand, resumeCommand, deleteCommand], + subCommands: [ + listCommand, + saveCommand, + resumeCommand, + deleteCommand, + summaryCommand, + ], }; diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index 9c0e4dab..ca359b9d 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -540,3 +540,33 @@ The structure MUST be as follows: `.trim(); } + +/** + * Provides the system prompt for generating project summaries in markdown format. + * This prompt instructs the model to create a structured markdown summary + * that can be saved to a file for future reference. + */ +export function getProjectSummaryPrompt(): string { + return `Please analyze the conversation history above and generate a comprehensive project summary in markdown format. Focus on extracting the most important context, decisions, and progress that would be valuable for future sessions. Generate the summary directly without using any tools. +You are a specialized context summarizer that creates a comprehensive markdown summary from chat history for future reference. The markdown format is as follows: + +# Project Summary + +## Overall Goal + + +## Key Knowledge + + + +## Recent Actions + + + +## Current Plan + + + + +`.trim(); +}