refactor: enhance add-task fuzzy search and fix duplicate banner display

- **Remove hardcoded category system** in add-task that always matched 'Task management'
- **Eliminate arbitrary limits** in fuzzy search results (5→25 high relevance, 3→10 medium relevance, 8→20 detailed tasks)
- **Improve semantic weighting** in Fuse.js search (details=3, description=2, title=1.5) for better relevance
- **Fix duplicate banner issue** by removing console.clear() and redundant displayBanner() calls from UI functions
- **Enhance context generation** to rely on semantic similarity rather than rigid pattern matching
- **Preserve terminal history** to address GitHub issue #553 about eating terminal lines
- **Remove displayBanner() calls** from: displayHelp, displayNextTask, displayTaskById, displayComplexityReport, set-task-status, clear-subtasks, dependency-manager functions

The add-task system now provides truly relevant task context based on semantic similarity rather than arbitrary categories and limits, while maintaining a cleaner terminal experience.

Changes span: add-task.js, ui.js, set-task-status.js, clear-subtasks.js, list-tasks.js, dependency-manager.js

Closes #553
This commit is contained in:
Eyal Toledano
2025-06-07 20:23:55 -04:00
parent cc04d53720
commit b1390e4ddf
9 changed files with 3047 additions and 3094 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
import { displayBanner } from '../ui.js';
import generateTaskFiles from './generate-task-files.js';
import { log, readJSON, writeJSON, truncate, isSilentMode } from "../utils.js";
import { displayBanner } from "../ui.js";
import generateTaskFiles from "./generate-task-files.js";
/**
* Clear subtasks from specified tasks
@@ -13,140 +13,138 @@ import generateTaskFiles from './generate-task-files.js';
* @param {string} taskIds - Task IDs to clear subtasks from
*/
function clearSubtasks(tasksPath, taskIds) {
displayBanner();
log("info", `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log("error", "No valid tasks found.");
process.exit(1);
}
log('info', `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found.');
process.exit(1);
}
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold("Clearing Subtasks"), {
padding: 1,
borderColor: "blue",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
})
);
}
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold('Clearing Subtasks'), {
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
})
);
}
// Handle multiple task IDs (comma-separated)
const taskIdArray = taskIds.split(",").map((id) => id.trim());
let clearedCount = 0;
// Handle multiple task IDs (comma-separated)
const taskIdArray = taskIds.split(',').map((id) => id.trim());
let clearedCount = 0;
// Create a summary table for the cleared subtasks
const summaryTable = new Table({
head: [
chalk.cyan.bold("Task ID"),
chalk.cyan.bold("Task Title"),
chalk.cyan.bold("Subtasks Cleared"),
],
colWidths: [10, 50, 20],
style: { head: [], border: [] },
});
// Create a summary table for the cleared subtasks
const summaryTable = new Table({
head: [
chalk.cyan.bold('Task ID'),
chalk.cyan.bold('Task Title'),
chalk.cyan.bold('Subtasks Cleared')
],
colWidths: [10, 50, 20],
style: { head: [], border: [] }
});
taskIdArray.forEach((taskId) => {
const id = parseInt(taskId, 10);
if (isNaN(id)) {
log("error", `Invalid task ID: ${taskId}`);
return;
}
taskIdArray.forEach((taskId) => {
const id = parseInt(taskId, 10);
if (isNaN(id)) {
log('error', `Invalid task ID: ${taskId}`);
return;
}
const task = data.tasks.find((t) => t.id === id);
if (!task) {
log("error", `Task ${id} not found`);
return;
}
const task = data.tasks.find((t) => t.id === id);
if (!task) {
log('error', `Task ${id} not found`);
return;
}
if (!task.subtasks || task.subtasks.length === 0) {
log("info", `Task ${id} has no subtasks to clear`);
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.yellow("No subtasks"),
]);
return;
}
if (!task.subtasks || task.subtasks.length === 0) {
log('info', `Task ${id} has no subtasks to clear`);
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.yellow('No subtasks')
]);
return;
}
const subtaskCount = task.subtasks.length;
task.subtasks = [];
clearedCount++;
log("info", `Cleared ${subtaskCount} subtasks from task ${id}`);
const subtaskCount = task.subtasks.length;
task.subtasks = [];
clearedCount++;
log('info', `Cleared ${subtaskCount} subtasks from task ${id}`);
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.green(`${subtaskCount} subtasks cleared`),
]);
});
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.green(`${subtaskCount} subtasks cleared`)
]);
});
if (clearedCount > 0) {
writeJSON(tasksPath, data);
if (clearedCount > 0) {
writeJSON(tasksPath, data);
// Show summary table
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold("Subtask Clearing Summary:"), {
padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 },
borderColor: "blue",
borderStyle: "round",
})
);
console.log(summaryTable.toString());
}
// Show summary table
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold('Subtask Clearing Summary:'), {
padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 },
borderColor: 'blue',
borderStyle: 'round'
})
);
console.log(summaryTable.toString());
}
// Regenerate task files to reflect changes
log("info", "Regenerating task files...");
generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Regenerate task files to reflect changes
log('info', 'Regenerating task files...');
generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Success message
if (!isSilentMode()) {
console.log(
boxen(
chalk.green(
`Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)`
),
{
padding: 1,
borderColor: "green",
borderStyle: "round",
margin: { top: 1 },
}
)
);
// Success message
if (!isSilentMode()) {
console.log(
boxen(
chalk.green(
`Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)`
),
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
// Next steps suggestion
console.log(
boxen(
chalk.white.bold('Next Steps:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`,
{
padding: 1,
borderColor: 'cyan',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
}
} else {
if (!isSilentMode()) {
console.log(
boxen(chalk.yellow('No subtasks were cleared'), {
padding: 1,
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1 }
})
);
}
}
// Next steps suggestion
console.log(
boxen(
chalk.white.bold("Next Steps:") +
"\n\n" +
`${chalk.cyan("1.")} Run ${chalk.yellow("task-master expand --id=<id>")} to generate new subtasks\n` +
`${chalk.cyan("2.")} Run ${chalk.yellow("task-master list --with-subtasks")} to verify changes`,
{
padding: 1,
borderColor: "cyan",
borderStyle: "round",
margin: { top: 1 },
}
)
);
}
} else {
if (!isSilentMode()) {
console.log(
boxen(chalk.yellow("No subtasks were cleared"), {
padding: 1,
borderColor: "yellow",
borderStyle: "round",
margin: { top: 1 },
})
);
}
}
}
export default clearSubtasks;

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import { log, readJSON, writeJSON, findTaskById } from '../utils.js';
import { displayBanner } from '../ui.js';
import { validateTaskDependencies } from '../dependency-manager.js';
import { getDebugFlag } from '../config-manager.js';
import updateSingleTaskStatus from './update-single-task-status.js';
import generateTaskFiles from './generate-task-files.js';
import { log, readJSON, writeJSON, findTaskById } from "../utils.js";
import { displayBanner } from "../ui.js";
import { validateTaskDependencies } from "../dependency-manager.js";
import { getDebugFlag } from "../config-manager.js";
import updateSingleTaskStatus from "./update-single-task-status.js";
import generateTaskFiles from "./generate-task-files.js";
import {
isValidTaskStatus,
TASK_STATUS_OPTIONS
} from '../../../src/constants/task-status.js';
isValidTaskStatus,
TASK_STATUS_OPTIONS,
} from "../../../src/constants/task-status.js";
/**
* Set the status of a task
@@ -22,102 +22,100 @@ import {
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
*/
async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
try {
if (!isValidTaskStatus(newStatus)) {
throw new Error(
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
);
}
// Determine if we're in MCP mode by checking for mcpLog
const isMcpMode = !!options?.mcpLog;
try {
if (!isValidTaskStatus(newStatus)) {
throw new Error(
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}`
);
}
// Determine if we're in MCP mode by checking for mcpLog
const isMcpMode = !!options?.mcpLog;
// Only display UI elements if not in MCP mode
if (!isMcpMode) {
displayBanner();
// Only display UI elements if not in MCP mode
if (!isMcpMode) {
console.log(
boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
padding: 1,
borderColor: "blue",
borderStyle: "round",
})
);
}
console.log(
boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
padding: 1,
borderColor: 'blue',
borderStyle: 'round'
})
);
}
log("info", `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}
log('info', `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}
// Handle multiple task IDs (comma-separated)
const taskIds = taskIdInput.split(",").map((id) => id.trim());
const updatedTasks = [];
// Handle multiple task IDs (comma-separated)
const taskIds = taskIdInput.split(',').map((id) => id.trim());
const updatedTasks = [];
// Update each task
for (const id of taskIds) {
await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode);
updatedTasks.push(id);
}
// Update each task
for (const id of taskIds) {
await updateSingleTaskStatus(tasksPath, id, newStatus, data, !isMcpMode);
updatedTasks.push(id);
}
// Write the updated tasks to the file
writeJSON(tasksPath, data);
// Write the updated tasks to the file
writeJSON(tasksPath, data);
// Validate dependencies after status update
log("info", "Validating dependencies after status update...");
validateTaskDependencies(data.tasks);
// Validate dependencies after status update
log('info', 'Validating dependencies after status update...');
validateTaskDependencies(data.tasks);
// Generate individual task files
log("info", "Regenerating task files...");
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
mcpLog: options.mcpLog,
});
// Generate individual task files
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
mcpLog: options.mcpLog
});
// Display success message - only in CLI mode
if (!isMcpMode) {
for (const id of updatedTasks) {
const task = findTaskById(data.tasks, id);
const taskName = task ? task.title : id;
// Display success message - only in CLI mode
if (!isMcpMode) {
for (const id of updatedTasks) {
const task = findTaskById(data.tasks, id);
const taskName = task ? task.title : id;
console.log(
boxen(
chalk.white.bold(`Successfully updated task ${id} status:`) +
"\n" +
`From: ${chalk.yellow(task ? task.status : "unknown")}\n` +
`To: ${chalk.green(newStatus)}`,
{ padding: 1, borderColor: "green", borderStyle: "round" }
)
);
}
}
console.log(
boxen(
chalk.white.bold(`Successfully updated task ${id} status:`) +
'\n' +
`From: ${chalk.yellow(task ? task.status : 'unknown')}\n` +
`To: ${chalk.green(newStatus)}`,
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
)
);
}
}
// Return success value for programmatic use
return {
success: true,
updatedTasks: updatedTasks.map((id) => ({
id,
status: newStatus,
})),
};
} catch (error) {
log("error", `Error setting task status: ${error.message}`);
// Return success value for programmatic use
return {
success: true,
updatedTasks: updatedTasks.map((id) => ({
id,
status: newStatus
}))
};
} catch (error) {
log('error', `Error setting task status: ${error.message}`);
// Only show error UI in CLI mode
if (!options?.mcpLog) {
console.error(chalk.red(`Error: ${error.message}`));
// Only show error UI in CLI mode
if (!options?.mcpLog) {
console.error(chalk.red(`Error: ${error.message}`));
// Pass session to getDebugFlag
if (getDebugFlag(options?.session)) {
// Use getter
console.error(error);
}
// Pass session to getDebugFlag
if (getDebugFlag(options?.session)) {
// Use getter
console.error(error);
}
process.exit(1);
} else {
// In MCP mode, throw the error for the caller to handle
throw error;
}
}
process.exit(1);
} else {
// In MCP mode, throw the error for the caller to handle
throw error;
}
}
}
export default setTaskStatus;

View File

@@ -40,7 +40,7 @@ const warmGradient = gradient(["#fb8b24", "#e36414", "#9a031e"]);
function displayBanner() {
if (isSilentMode()) return;
console.clear();
// console.clear(); // Removing this to avoid clearing the terminal per command
const bannerText = figlet.textSync("Task Master", {
font: "Standard",
horizontalLayout: "default",
@@ -78,6 +78,8 @@ function displayBanner() {
* @returns {Object} Spinner object
*/
function startLoadingIndicator(message) {
if (isSilentMode()) return null;
const spinner = ora({
text: message,
color: "cyan",
@@ -87,15 +89,75 @@ function startLoadingIndicator(message) {
}
/**
* Stop a loading indicator
* Stop a loading indicator (basic stop, no success/fail indicator)
* @param {Object} spinner - Spinner object to stop
*/
function stopLoadingIndicator(spinner) {
if (spinner && spinner.stop) {
if (spinner && typeof spinner.stop === "function") {
spinner.stop();
}
}
/**
* Complete a loading indicator with success (shows checkmark)
* @param {Object} spinner - Spinner object to complete
* @param {string} message - Optional success message (defaults to current text)
*/
function succeedLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.succeed === "function") {
if (message) {
spinner.succeed(message);
} else {
spinner.succeed();
}
}
}
/**
* Complete a loading indicator with failure (shows X)
* @param {Object} spinner - Spinner object to fail
* @param {string} message - Optional failure message (defaults to current text)
*/
function failLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.fail === "function") {
if (message) {
spinner.fail(message);
} else {
spinner.fail();
}
}
}
/**
* Complete a loading indicator with warning (shows warning symbol)
* @param {Object} spinner - Spinner object to warn
* @param {string} message - Optional warning message (defaults to current text)
*/
function warnLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.warn === "function") {
if (message) {
spinner.warn(message);
} else {
spinner.warn();
}
}
}
/**
* Complete a loading indicator with info (shows info symbol)
* @param {Object} spinner - Spinner object to complete with info
* @param {string} message - Optional info message (defaults to current text)
*/
function infoLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.info === "function") {
if (message) {
spinner.info(message);
} else {
spinner.info();
}
}
}
/**
* Create a colored progress bar
* @param {number} percent - The completion percentage
@@ -232,14 +294,14 @@ function getStatusWithColor(status, forTable = false) {
}
const statusConfig = {
done: { color: chalk.green, icon: "", tableIcon: "✓" },
completed: { color: chalk.green, icon: "", tableIcon: "✓" },
pending: { color: chalk.yellow, icon: "⏱️", tableIcon: "⏱" },
done: { color: chalk.green, icon: "", tableIcon: "✓" },
completed: { color: chalk.green, icon: "", tableIcon: "✓" },
pending: { color: chalk.yellow, icon: "", tableIcon: "⏱" },
"in-progress": { color: chalk.hex("#FFA500"), icon: "🔄", tableIcon: "►" },
deferred: { color: chalk.gray, icon: "⏱️", tableIcon: "⏱" },
blocked: { color: chalk.red, icon: "", tableIcon: "✗" },
review: { color: chalk.magenta, icon: "👀", tableIcon: "👁" },
cancelled: { color: chalk.gray, icon: "❌", tableIcon: "" },
deferred: { color: chalk.gray, icon: "x", tableIcon: "⏱" },
blocked: { color: chalk.red, icon: "!", tableIcon: "✗" },
review: { color: chalk.magenta, icon: "?", tableIcon: "?" },
cancelled: { color: chalk.gray, icon: "❌", tableIcon: "x" },
};
const config = statusConfig[status.toLowerCase()] || {
@@ -383,8 +445,6 @@ function formatDependenciesWithStatus(
* Display a comprehensive help guide
*/
function displayHelp() {
displayBanner();
// Get terminal width - moved to top of function to make it available throughout
const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect
@@ -767,8 +827,6 @@ function truncateString(str, maxLength) {
* @param {string} tasksPath - Path to the tasks.json file
*/
async function displayNextTask(tasksPath, complexityReportPath = null) {
displayBanner();
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
@@ -1039,8 +1097,6 @@ async function displayTaskById(
complexityReportPath = null,
statusFilter = null
) {
displayBanner();
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
@@ -1495,8 +1551,6 @@ async function displayTaskById(
* @param {string} reportPath - Path to the complexity report file
*/
async function displayComplexityReport(reportPath) {
displayBanner();
// Check if the report exists
if (!fs.existsSync(reportPath)) {
console.log(
@@ -2094,4 +2148,8 @@ export {
displayModelConfiguration,
displayAvailableModels,
displayAiUsageSummary,
succeedLoadingIndicator,
failLoadingIndicator,
warnLoadingIndicator,
infoLoadingIndicator,
};