feat: support /compress and /summary commands for non-interactive & ACP

integration
This commit is contained in:
mingholy.lmh 2025-12-23 17:08:15 +08:00
parent cebe0448d0
commit 8aceddffa2
12 changed files with 720 additions and 147 deletions

View file

@ -26,6 +26,8 @@ export const summaryCommand: SlashCommand = {
action: async (context): Promise<SlashCommandActionReturn> => {
const { config } = context.services;
const { ui } = context;
const executionMode = context.executionMode ?? 'interactive';
if (!config) {
return {
type: 'message',
@ -43,8 +45,8 @@ export const summaryCommand: SlashCommand = {
};
}
// Check if already generating summary
if (ui.pendingItem) {
// Check if already generating summary (interactive UI only)
if (executionMode === 'interactive' && ui.pendingItem) {
ui.addItem(
{
type: 'error' as const,
@ -63,29 +65,15 @@ export const summaryCommand: SlashCommand = {
};
}
try {
const generateSummaryMarkdown = async (): Promise<string> => {
// Get the current chat history
const chat = geminiClient.getChat();
const history = chat.getHistory();
if (history.length <= 2) {
return {
type: 'message',
messageType: 'info',
content: t('No conversation found to summarize.'),
};
throw new Error(t('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,
@ -121,19 +109,21 @@ export const summaryCommand: SlashCommand = {
if (!markdownSummary) {
throw new Error(
'Failed to generate summary - no text content received from LLM response',
t(
'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',
},
});
return markdownSummary;
};
const saveSummaryToDisk = async (
markdownSummary: string,
): Promise<{
filePathForDisplay: string;
fullPath: string;
}> => {
// Ensure .qwen directory exists
const projectRoot = config.getProjectRoot();
const qwenDir = path.join(projectRoot, '.qwen');
@ -155,25 +145,46 @@ export const summaryCommand: SlashCommand = {
await fsPromises.writeFile(summaryPath, summaryContent, 'utf8');
// Clear pending item and show success message
return {
filePathForDisplay: '.qwen/PROJECT_SUMMARY.md',
fullPath: summaryPath,
};
};
const emitInteractivePending = (stage: 'generating' | 'saving') => {
if (executionMode !== 'interactive') {
return;
}
const pendingMessage: HistoryItemSummary = {
type: 'summary',
summary: {
isPending: true,
stage,
},
};
ui.setPendingItem(pendingMessage);
};
const completeInteractive = (filePathForDisplay: string) => {
if (executionMode !== 'interactive') {
return;
}
ui.setPendingItem(null);
const completedSummaryItem: HistoryItemSummary = {
type: 'summary',
summary: {
isPending: false,
stage: 'completed',
filePath: '.qwen/PROJECT_SUMMARY.md',
filePath: filePathForDisplay,
},
};
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
const failInteractive = (error: unknown) => {
if (executionMode !== 'interactive') {
return;
}
ui.setPendingItem(null);
ui.addItem(
{
@ -187,6 +198,96 @@ export const summaryCommand: SlashCommand = {
},
Date.now(),
);
};
if (executionMode === 'acp') {
const messages = async function* () {
try {
emitInteractivePending('generating');
yield {
messageType: 'info' as const,
content: t('Generating project summary...'),
};
const markdownSummary = await generateSummaryMarkdown();
yield {
messageType: 'info' as const,
content: t('Saving project summary...'),
};
const { filePathForDisplay } =
await saveSummaryToDisk(markdownSummary);
completeInteractive(filePathForDisplay);
yield {
messageType: 'info' as const,
content: t('Saved project summary to {{filePathForDisplay}}.', {
filePathForDisplay,
}),
};
} catch (error) {
failInteractive(error);
yield {
messageType: 'error' as const,
content: t(
'Failed to generate project context summary: {{error}}',
{
error: error instanceof Error ? error.message : String(error),
},
),
};
}
};
return {
type: 'stream_messages',
messages: messages(),
};
}
try {
emitInteractivePending('generating');
const markdownSummary = await generateSummaryMarkdown();
emitInteractivePending('saving');
const { filePathForDisplay } = await saveSummaryToDisk(markdownSummary);
completeInteractive(filePathForDisplay);
if (executionMode === 'non_interactive') {
return {
type: 'message',
messageType: 'info',
content: `Saved project summary to ${filePathForDisplay}.`,
};
}
// Interactive mode: UI components already display progress and completion.
return {
type: 'message',
messageType: 'info',
content: '',
};
} catch (error) {
// Convert "no conversation" into a clean info message for non-interactive / interactive modes.
const msg =
error instanceof Error ? error.message : t('Unknown error occurred.');
if (msg === t('No conversation found to summarize.')) {
if (executionMode === 'interactive') {
// Keep interactive behavior: show as a normal message.
return {
type: 'message',
messageType: 'info',
content: msg,
};
}
return {
type: 'message',
messageType: 'info',
content: msg,
};
}
failInteractive(error);
return {
type: 'message',