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 54005d5486
commit af652978a0
9 changed files with 3047 additions and 3095 deletions

View File

@@ -1,8 +1,8 @@
{
"models": {
"main": {
"provider": "openrouter",
"modelId": "qwen/qwen3-235b-a22b:free",
"provider": "anthropic",
"modelId": "claude-sonnet-4-20250514",
"maxTokens": 50000,
"temperature": 0.2
},

View File

@@ -1,35 +0,0 @@
# Task ID: 97
# Title: Create Taskmaster Jingle Implementation
# Status: pending
# Dependencies: 95, 57, 3, 2
# Priority: medium
# Description: Develop a musical jingle system for Taskmaster that plays sound effects during key CLI interactions to enhance user experience.
# Details:
This task involves implementing a sound system that plays audio cues during Taskmaster CLI operations. Key implementation steps include:
1. Audio System Integration:
- Research and select appropriate audio library compatible with Node.js CLI applications
- Implement cross-platform audio playback (Windows, macOS, Linux)
- Create sound configuration options in .taskmasterconfig
2. Jingle Design:
- Define sound triggers for key events (task creation, completion, errors, etc.)
- Create or source appropriate sound files (WAV/MP3 format)
- Implement volume control and mute option in settings
3. CLI Integration:
- Add sound playback to core CLI commands (init, create, update, delete)
- Implement optional sound effects toggle via command line flags
- Ensure audio playback doesn't interfere with CLI performance
4. Documentation:
- Update user guide with sound configuration instructions
- Add troubleshooting section for audio playback issues
# Test Strategy:
1. Verify audio plays correctly during each supported CLI operation
2. Test sound configuration options across different platforms
3. Confirm volume control and mute functionality works as expected
4. Validate that audio playback doesn't affect CLI performance
5. Test edge cases (no audio hardware, invalid sound files, etc.)
6. Ensure sound effects can be disabled via configuration and CLI flags

View File

@@ -5871,22 +5871,6 @@
"parentTaskId": 96
}
]
},
{
"id": 97,
"title": "Create Taskmaster Jingle Implementation",
"description": "Develop a musical jingle system for Taskmaster that plays sound effects during key CLI interactions to enhance user experience.",
"details": "This task involves implementing a sound system that plays audio cues during Taskmaster CLI operations. Key implementation steps include:\n\n1. Audio System Integration:\n - Research and select appropriate audio library compatible with Node.js CLI applications\n - Implement cross-platform audio playback (Windows, macOS, Linux)\n - Create sound configuration options in .taskmasterconfig\n\n2. Jingle Design:\n - Define sound triggers for key events (task creation, completion, errors, etc.)\n - Create or source appropriate sound files (WAV/MP3 format)\n - Implement volume control and mute option in settings\n\n3. CLI Integration:\n - Add sound playback to core CLI commands (init, create, update, delete)\n - Implement optional sound effects toggle via command line flags\n - Ensure audio playback doesn't interfere with CLI performance\n\n4. Documentation:\n - Update user guide with sound configuration instructions\n - Add troubleshooting section for audio playback issues",
"testStrategy": "1. Verify audio plays correctly during each supported CLI operation\n2. Test sound configuration options across different platforms\n3. Confirm volume control and mute functionality works as expected\n4. Validate that audio playback doesn't affect CLI performance\n5. Test edge cases (no audio hardware, invalid sound files, etc.)\n6. Ensure sound effects can be disabled via configuration and CLI flags",
"status": "pending",
"dependencies": [
95,
57,
3,
2
],
"priority": "medium",
"subtasks": []
}
]
}

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
@@ -772,8 +832,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) {
@@ -1044,8 +1102,6 @@ async function displayTaskById(
complexityReportPath = null,
statusFilter = null
) {
displayBanner();
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
@@ -1500,8 +1556,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(
@@ -2472,5 +2526,8 @@ export {
displayModelConfiguration,
displayAvailableModels,
displayAiUsageSummary,
displayMultipleTasksSummary
succeedLoadingIndicator,
failLoadingIndicator,
warnLoadingIndicator,
infoLoadingIndicator
};