feat(research): Enhance research command with follow-up menu, save functionality, and fix ContextGatherer token counting
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
* @param {boolean} [options.includeProjectTree] - Include project file tree
|
||||
* @param {string} [options.detailLevel] - Detail level: 'low', 'medium', 'high'
|
||||
* @param {string} [options.projectRoot] - Project root directory
|
||||
* @param {boolean} [options.saveToFile] - Whether to save results to file (MCP mode)
|
||||
* @param {Object} [context] - Execution context
|
||||
* @param {Object} [context.session] - MCP session object
|
||||
* @param {Object} [context.mcpLog] - MCP logger object
|
||||
@@ -56,7 +57,8 @@ async function performResearch(
|
||||
customContext = '',
|
||||
includeProjectTree = false,
|
||||
detailLevel = 'medium',
|
||||
projectRoot: providedProjectRoot
|
||||
projectRoot: providedProjectRoot,
|
||||
saveToFile = false
|
||||
} = options;
|
||||
|
||||
const {
|
||||
@@ -275,6 +277,41 @@ async function performResearch(
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MCP save-to-file request
|
||||
if (saveToFile && isMCP) {
|
||||
const conversationHistory = [
|
||||
{
|
||||
question: query,
|
||||
answer: researchResult,
|
||||
type: 'initial',
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
|
||||
const savedFilePath = await handleSaveToFile(
|
||||
conversationHistory,
|
||||
projectRoot,
|
||||
context,
|
||||
logFn
|
||||
);
|
||||
|
||||
// Add saved file path to return data
|
||||
return {
|
||||
query,
|
||||
result: researchResult,
|
||||
contextSize: gatheredContext.length,
|
||||
contextTokens: tokenBreakdown.total,
|
||||
tokenBreakdown,
|
||||
systemPromptTokens,
|
||||
userPromptTokens,
|
||||
totalInputTokens,
|
||||
detailLevel,
|
||||
telemetryData,
|
||||
tagInfo,
|
||||
savedFilePath
|
||||
};
|
||||
}
|
||||
|
||||
logFn.success('Research query completed successfully');
|
||||
|
||||
return {
|
||||
@@ -631,10 +668,11 @@ async function handleFollowUpQuestions(
|
||||
message: 'What would you like to do next?',
|
||||
choices: [
|
||||
{ name: 'Ask a follow-up question', value: 'followup' },
|
||||
{ name: 'Save to file', value: 'savefile' },
|
||||
{ name: 'Save to task/subtask', value: 'save' },
|
||||
{ name: 'Quit', value: 'quit' }
|
||||
],
|
||||
pageSize: 3
|
||||
pageSize: 4
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -642,6 +680,17 @@ async function handleFollowUpQuestions(
|
||||
break;
|
||||
}
|
||||
|
||||
if (action === 'savefile') {
|
||||
// Handle save to file functionality
|
||||
await handleSaveToFile(
|
||||
conversationHistory,
|
||||
projectRoot,
|
||||
context,
|
||||
logFn
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (action === 'save') {
|
||||
// Handle save functionality
|
||||
await handleSaveToTask(
|
||||
@@ -856,6 +905,122 @@ async function handleSaveToTask(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle saving conversation to a file in .taskmaster/docs/research/
|
||||
* @param {Array} conversationHistory - Array of conversation exchanges
|
||||
* @param {string} projectRoot - Project root directory
|
||||
* @param {Object} context - Execution context
|
||||
* @param {Object} logFn - Logger function
|
||||
* @returns {Promise<string>} Path to saved file
|
||||
*/
|
||||
async function handleSaveToFile(
|
||||
conversationHistory,
|
||||
projectRoot,
|
||||
context,
|
||||
logFn
|
||||
) {
|
||||
try {
|
||||
// Create research directory if it doesn't exist
|
||||
const researchDir = path.join(
|
||||
projectRoot,
|
||||
'.taskmaster',
|
||||
'docs',
|
||||
'research'
|
||||
);
|
||||
if (!fs.existsSync(researchDir)) {
|
||||
fs.mkdirSync(researchDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate filename from first query and timestamp
|
||||
const firstQuery = conversationHistory[0]?.question || 'research-query';
|
||||
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||
|
||||
// Create a slug from the query (remove special chars, limit length)
|
||||
const querySlug = firstQuery
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.substring(0, 50) // Limit length
|
||||
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
||||
|
||||
const filename = `${timestamp}_${querySlug}.md`;
|
||||
const filePath = path.join(researchDir, filename);
|
||||
|
||||
// Format conversation for file
|
||||
const fileContent = formatConversationForFile(
|
||||
conversationHistory,
|
||||
firstQuery
|
||||
);
|
||||
|
||||
// Write file
|
||||
fs.writeFileSync(filePath, fileContent, 'utf8');
|
||||
|
||||
const relativePath = path.relative(projectRoot, filePath);
|
||||
console.log(
|
||||
chalk.green(`✅ Research saved to: ${chalk.cyan(relativePath)}`)
|
||||
);
|
||||
|
||||
logFn.success(`Research conversation saved to ${relativePath}`);
|
||||
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`❌ Error saving research file: ${error.message}`));
|
||||
logFn.error(`Error saving research file: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format conversation history for saving to a file
|
||||
* @param {Array} conversationHistory - Array of conversation exchanges
|
||||
* @param {string} initialQuery - The initial query for metadata
|
||||
* @returns {string} Formatted file content
|
||||
*/
|
||||
function formatConversationForFile(conversationHistory, initialQuery) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const date = new Date().toLocaleDateString();
|
||||
const time = new Date().toLocaleTimeString();
|
||||
|
||||
// Create metadata header
|
||||
let content = `---
|
||||
title: Research Session
|
||||
query: "${initialQuery}"
|
||||
date: ${date}
|
||||
time: ${time}
|
||||
timestamp: ${timestamp}
|
||||
exchanges: ${conversationHistory.length}
|
||||
---
|
||||
|
||||
# Research Session
|
||||
|
||||
**Query:** ${initialQuery}
|
||||
**Date:** ${date} ${time}
|
||||
**Exchanges:** ${conversationHistory.length}
|
||||
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
// Add each conversation exchange
|
||||
conversationHistory.forEach((exchange, index) => {
|
||||
if (exchange.type === 'initial') {
|
||||
content += `## Initial Query\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
|
||||
} else {
|
||||
content += `## Follow-up ${index}\n\n**Question:** ${exchange.question}\n\n**Response:**\n\n${exchange.answer}\n\n`;
|
||||
}
|
||||
|
||||
if (index < conversationHistory.length - 1) {
|
||||
content += '---\n\n';
|
||||
}
|
||||
});
|
||||
|
||||
// Add footer
|
||||
content += `\n---\n\n*Generated by Task Master Research Command* \n*Timestamp: ${timestamp}*\n`;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format conversation history for saving to a task/subtask
|
||||
* @param {Array} conversationHistory - Array of conversation exchanges
|
||||
|
||||
Reference in New Issue
Block a user