diff --git a/docs/cli/commands.md b/docs/cli/commands.md index b466251f..62edb45f 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -30,21 +30,22 @@ Slash commands provide meta-level control over the CLI itself. - **`delete`** - **Description:** Deletes a saved conversation checkpoint. - **Usage:** `/chat delete ` - - **`summary`** - - **Description:** Generate a comprehensive project summary from the current conversation history and save it to `.qwen/PROJECT_SUMMARY.md`. This summary includes the overall goal, key knowledge, recent actions, and current plan, making it perfect for resuming work in future sessions. - - **Usage:** `/chat summary` - - **Features:** - - Analyzes the entire conversation history to extract important context - - Creates a structured markdown summary with sections for goals, knowledge, actions, and plans - - Automatically saves to `.qwen/PROJECT_SUMMARY.md` in your project root - - Shows progress indicators during generation and saving - - Integrates with the Welcome Back feature for seamless session resumption - - **Note:** This command requires an active conversation with at least 2 messages to generate a meaningful summary. - **`/clear`** - **Description:** Clear the terminal screen, including the visible session history and scrollback within the CLI. The underlying session data (for history recall) might be preserved depending on the exact implementation, but the visual display is cleared. - **Keyboard shortcut:** Press **Ctrl+L** at any time to perform a clear action. +- **`/summary`** + - **Description:** Generate a comprehensive project summary from the current conversation history and save it to `.qwen/PROJECT_SUMMARY.md`. This summary includes the overall goal, key knowledge, recent actions, and current plan, making it perfect for resuming work in future sessions. + - **Usage:** `/summary` + - **Features:** + - Analyzes the entire conversation history to extract important context + - Creates a structured markdown summary with sections for goals, knowledge, actions, and plans + - Automatically saves to `.qwen/PROJECT_SUMMARY.md` in your project root + - Shows progress indicators during generation and saving + - Integrates with the Welcome Back feature for seamless session resumption + - **Note:** This command requires an active conversation with at least 2 messages to generate a meaningful summary. + - **`/compress`** - **Description:** Replace the entire chat context with a summary. This saves on tokens used for future tasks while retaining a high level summary of what has happened. @@ -142,7 +143,7 @@ Slash commands provide meta-level control over the CLI itself. - **Usage:** `/quit-confirm` - **Features:** - **Quit immediately:** Exit without saving anything (equivalent to `/quit`) - - **Generate summary and quit:** Create a project summary using `/chat summary` before exiting + - **Generate summary and quit:** Create a project summary using `/summary` before exiting - **Save conversation and quit:** Save the current conversation with an auto-generated tag before exiting - **Keyboard shortcut:** Press **Ctrl+C** twice to trigger the quit confirmation dialog - **Note:** This command is automatically triggered when you press Ctrl+C once, providing a safety mechanism to prevent accidental exits. diff --git a/packages/cli/src/services/BuiltinCommandLoader.ts b/packages/cli/src/services/BuiltinCommandLoader.ts index 7b955cdf..2c0759c1 100644 --- a/packages/cli/src/services/BuiltinCommandLoader.ts +++ b/packages/cli/src/services/BuiltinCommandLoader.ts @@ -28,6 +28,7 @@ import { privacyCommand } from '../ui/commands/privacyCommand.js'; import { quitCommand, quitConfirmCommand } from '../ui/commands/quitCommand.js'; import { restoreCommand } from '../ui/commands/restoreCommand.js'; import { statsCommand } from '../ui/commands/statsCommand.js'; +import { summaryCommand } from '../ui/commands/summaryCommand.js'; import { themeCommand } from '../ui/commands/themeCommand.js'; import { toolsCommand } from '../ui/commands/toolsCommand.js'; import { settingsCommand } from '../ui/commands/settingsCommand.js'; @@ -73,6 +74,7 @@ export class BuiltinCommandLoader implements ICommandLoader { quitConfirmCommand, restoreCommand(this.config), statsCommand, + summaryCommand, themeCommand, toolsCommand, settingsCommand, diff --git a/packages/cli/src/ui/commands/chatCommand.ts b/packages/cli/src/ui/commands/chatCommand.ts index 9d91892b..71b9cf78 100644 --- a/packages/cli/src/ui/commands/chatCommand.ts +++ b/packages/cli/src/ui/commands/chatCommand.ts @@ -15,16 +15,9 @@ import { CommandKind, SlashCommandActionReturn, } from './types.js'; -import { - decodeTagName, - getProjectSummaryPrompt, -} from '@qwen-code/qwen-code-core'; +import { decodeTagName } from '@qwen-code/qwen-code-core'; import path from 'path'; -import { - HistoryItemWithoutId, - HistoryItemSummary, - MessageType, -} from '../types.js'; +import { HistoryItemWithoutId, MessageType } from '../types.js'; interface ChatDetail { name: string; @@ -279,189 +272,9 @@ 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: HistoryItemSummary = { - type: 'summary', - summary: { - isPending: true, - stage: 'generating', - }, - }; - 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: 'summary', - summary: { - isPending: true, - stage: 'saving', - }, - }); - - // 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 -**Update time**: ${new Date().toISOString()} -`; - - await fsPromises.writeFile(summaryPath, summaryContent, 'utf8'); - - // Clear pending item and show success message - ui.setPendingItem(null); - const completedSummaryItem: HistoryItemSummary = { - type: 'summary', - summary: { - isPending: false, - stage: 'completed', - filePath: '.qwen/PROJECT_SUMMARY.md', - }, - }; - ui.addItem(completedSummaryItem, Date.now()); - - return { - type: 'message', - messageType: 'info', - content: '', // Empty content since we show the message in UI component - }; - } 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, - summaryCommand, - ], + subCommands: [listCommand, saveCommand, resumeCommand, deleteCommand], }; diff --git a/packages/cli/src/ui/commands/summaryCommand.ts b/packages/cli/src/ui/commands/summaryCommand.ts new file mode 100644 index 00000000..bf7375ec --- /dev/null +++ b/packages/cli/src/ui/commands/summaryCommand.ts @@ -0,0 +1,189 @@ +/** + * @license + * Copyright 2025 Qwen + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as fsPromises from 'fs/promises'; +import path from 'path'; +import { + SlashCommand, + CommandKind, + SlashCommandActionReturn, +} from './types.js'; +import { getProjectSummaryPrompt } from '@qwen-code/qwen-code-core'; +import { HistoryItemSummary } from '../types.js'; + +export 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: HistoryItemSummary = { + type: 'summary', + summary: { + isPending: true, + stage: 'generating', + }, + }; + 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: 'summary', + summary: { + isPending: true, + stage: 'saving', + }, + }); + + // 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 +**Update time**: ${new Date().toISOString()} +`; + + await fsPromises.writeFile(summaryPath, summaryContent, 'utf8'); + + // Clear pending item and show success message + ui.setPendingItem(null); + const completedSummaryItem: HistoryItemSummary = { + type: 'summary', + summary: { + isPending: false, + stage: 'completed', + filePath: '.qwen/PROJECT_SUMMARY.md', + }, + }; + ui.addItem(completedSummaryItem, Date.now()); + + return { + type: 'message', + messageType: 'info', + content: '', // Empty content since we show the message in UI component + }; + } 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) + }`, + }; + } + }, +}; diff --git a/packages/cli/src/ui/components/QuitConfirmationDialog.tsx b/packages/cli/src/ui/components/QuitConfirmationDialog.tsx index 90f29f3d..72389401 100644 --- a/packages/cli/src/ui/components/QuitConfirmationDialog.tsx +++ b/packages/cli/src/ui/components/QuitConfirmationDialog.tsx @@ -1,6 +1,6 @@ /** * @license - * Copyright 2025 Google LLC + * Copyright 2025 Qwen * SPDX-License-Identifier: Apache-2.0 */ @@ -42,7 +42,7 @@ export const QuitConfirmationDialog: React.FC = ({ value: QuitChoice.QUIT, }, { - label: 'Generate summary and quit (/chat summary)', + label: 'Generate summary and quit (/summary)', value: QuitChoice.SUMMARY_AND_QUIT, }, { diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 1ccd9513..f8b38e15 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -434,7 +434,7 @@ export const useSlashCommandProcessor = ( setTimeout(() => handleSlashCommand('/quit'), 100); } else if (action === 'summary_and_quit') { // Generate summary and then quit - handleSlashCommand('/chat summary') + handleSlashCommand('/summary') .then(() => { // Wait for user to see the summary result setTimeout(() => {