mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
refactor: move /chat summary to standalone /summary command
- Extract summary logic from chatCommand to new summaryCommand - Update /quit-confirm to use /summary instead of /chat summary - Update documentation and UI labels - Fix copyright headers for new files
This commit is contained in:
@@ -30,21 +30,22 @@ Slash commands provide meta-level control over the CLI itself.
|
|||||||
- **`delete`**
|
- **`delete`**
|
||||||
- **Description:** Deletes a saved conversation checkpoint.
|
- **Description:** Deletes a saved conversation checkpoint.
|
||||||
- **Usage:** `/chat delete <tag>`
|
- **Usage:** `/chat delete <tag>`
|
||||||
- **`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`**
|
- **`/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.
|
- **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.
|
- **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`**
|
- **`/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.
|
- **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`
|
- **Usage:** `/quit-confirm`
|
||||||
- **Features:**
|
- **Features:**
|
||||||
- **Quit immediately:** Exit without saving anything (equivalent to `/quit`)
|
- **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
|
- **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
|
- **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.
|
- **Note:** This command is automatically triggered when you press Ctrl+C once, providing a safety mechanism to prevent accidental exits.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { privacyCommand } from '../ui/commands/privacyCommand.js';
|
|||||||
import { quitCommand, quitConfirmCommand } from '../ui/commands/quitCommand.js';
|
import { quitCommand, quitConfirmCommand } from '../ui/commands/quitCommand.js';
|
||||||
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
import { restoreCommand } from '../ui/commands/restoreCommand.js';
|
||||||
import { statsCommand } from '../ui/commands/statsCommand.js';
|
import { statsCommand } from '../ui/commands/statsCommand.js';
|
||||||
|
import { summaryCommand } from '../ui/commands/summaryCommand.js';
|
||||||
import { themeCommand } from '../ui/commands/themeCommand.js';
|
import { themeCommand } from '../ui/commands/themeCommand.js';
|
||||||
import { toolsCommand } from '../ui/commands/toolsCommand.js';
|
import { toolsCommand } from '../ui/commands/toolsCommand.js';
|
||||||
import { settingsCommand } from '../ui/commands/settingsCommand.js';
|
import { settingsCommand } from '../ui/commands/settingsCommand.js';
|
||||||
@@ -73,6 +74,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
|
|||||||
quitConfirmCommand,
|
quitConfirmCommand,
|
||||||
restoreCommand(this.config),
|
restoreCommand(this.config),
|
||||||
statsCommand,
|
statsCommand,
|
||||||
|
summaryCommand,
|
||||||
themeCommand,
|
themeCommand,
|
||||||
toolsCommand,
|
toolsCommand,
|
||||||
settingsCommand,
|
settingsCommand,
|
||||||
|
|||||||
@@ -15,16 +15,9 @@ import {
|
|||||||
CommandKind,
|
CommandKind,
|
||||||
SlashCommandActionReturn,
|
SlashCommandActionReturn,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import { decodeTagName } from '@qwen-code/qwen-code-core';
|
||||||
decodeTagName,
|
|
||||||
getProjectSummaryPrompt,
|
|
||||||
} from '@qwen-code/qwen-code-core';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {
|
import { HistoryItemWithoutId, MessageType } from '../types.js';
|
||||||
HistoryItemWithoutId,
|
|
||||||
HistoryItemSummary,
|
|
||||||
MessageType,
|
|
||||||
} from '../types.js';
|
|
||||||
|
|
||||||
interface ChatDetail {
|
interface ChatDetail {
|
||||||
name: string;
|
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<SlashCommandActionReturn> => {
|
|
||||||
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 = {
|
export const chatCommand: SlashCommand = {
|
||||||
name: 'chat',
|
name: 'chat',
|
||||||
description: 'Manage conversation history.',
|
description: 'Manage conversation history.',
|
||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
subCommands: [
|
subCommands: [listCommand, saveCommand, resumeCommand, deleteCommand],
|
||||||
listCommand,
|
|
||||||
saveCommand,
|
|
||||||
resumeCommand,
|
|
||||||
deleteCommand,
|
|
||||||
summaryCommand,
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|||||||
189
packages/cli/src/ui/commands/summaryCommand.ts
Normal file
189
packages/cli/src/ui/commands/summaryCommand.ts
Normal file
@@ -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<SlashCommandActionReturn> => {
|
||||||
|
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)
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @license
|
* @license
|
||||||
* Copyright 2025 Google LLC
|
* Copyright 2025 Qwen
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export const QuitConfirmationDialog: React.FC<QuitConfirmationDialogProps> = ({
|
|||||||
value: QuitChoice.QUIT,
|
value: QuitChoice.QUIT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Generate summary and quit (/chat summary)',
|
label: 'Generate summary and quit (/summary)',
|
||||||
value: QuitChoice.SUMMARY_AND_QUIT,
|
value: QuitChoice.SUMMARY_AND_QUIT,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ export const useSlashCommandProcessor = (
|
|||||||
setTimeout(() => handleSlashCommand('/quit'), 100);
|
setTimeout(() => handleSlashCommand('/quit'), 100);
|
||||||
} else if (action === 'summary_and_quit') {
|
} else if (action === 'summary_and_quit') {
|
||||||
// Generate summary and then quit
|
// Generate summary and then quit
|
||||||
handleSlashCommand('/chat summary')
|
handleSlashCommand('/summary')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Wait for user to see the summary result
|
// Wait for user to see the summary result
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user