feat(cli): Add --status/-s filter flag to show command and get-task MCP tool

Implements the ability to filter subtasks displayed by the `task-master show <id>` command using the `--status` (or `-s`) flag. This is also available in the MCP context.

- Modified `commands.js` to add the `--status` option to the `show` command definition.

- Updated `utils.js` (`findTaskById`) to handle the filtering logic and return original subtask counts/arrays when filtering.

- Updated `ui.js` (`displayTaskById`) to use the filtered subtasks for the table, display a summary line when filtering, and use the original subtask list for the progress bar calculation.

- Updated MCP `get_task` tool and `showTaskDirect` function to accept and pass the `status` parameter.

- Added changeset entry.
This commit is contained in:
Eyal Toledano 2025-04-27 18:50:47 -04:00
parent 87d97bba00
commit ca7b0457f1
9 changed files with 245 additions and 306 deletions

View File

@ -0,0 +1,5 @@
---
'task-master-ai': patch
---
Add `--status` flag to `show` command to filter displayed subtasks.

View File

@ -17,12 +17,13 @@ import {
* @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID of the task or subtask to show.
* @param {string} [args.status] - Optional status to filter subtasks by.
* @param {Object} log - Logger object
* @returns {Promise<Object>} - Task details result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
*/
export async function showTaskDirect(args, log) {
// Destructure expected args
const { tasksJsonPath, id } = args;
const { tasksJsonPath, id, status } = args;
if (!tasksJsonPath) {
log.error('showTaskDirect called without tasksJsonPath');
@ -50,8 +51,8 @@ export async function showTaskDirect(args, log) {
};
}
// Generate cache key using the provided task path and ID
const cacheKey = `showTask:${tasksJsonPath}:${taskId}`;
// Generate cache key using the provided task path, ID, and status filter
const cacheKey = `showTask:${tasksJsonPath}:${taskId}:${status || 'all'}`;
// Define the action function to be executed on cache miss
const coreShowTaskAction = async () => {
@ -60,7 +61,7 @@ export async function showTaskDirect(args, log) {
enableSilentMode();
log.info(
`Retrieving task details for ID: ${taskId} from ${tasksJsonPath}`
`Retrieving task details for ID: ${taskId} from ${tasksJsonPath}${status ? ` (filtering by status: ${status})` : ''}`
);
// Read tasks data using the provided path
@ -76,8 +77,12 @@ export async function showTaskDirect(args, log) {
};
}
// Find the specific task
const task = findTaskById(data.tasks, taskId);
// Find the specific task, passing the status filter
const { task, originalSubtaskCount } = findTaskById(
data.tasks,
taskId,
status
);
if (!task) {
disableSilentMode(); // Disable before returning
@ -85,7 +90,7 @@ export async function showTaskDirect(args, log) {
success: false,
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
message: `Task with ID ${taskId} not found${status ? ` or no subtasks match status '${status}'` : ''}`
}
};
}
@ -93,13 +98,16 @@ export async function showTaskDirect(args, log) {
// Restore normal logging
disableSilentMode();
// Return the task data with the full tasks array for reference
// (needed for formatDependenciesWithStatus function in UI)
log.info(`Successfully found task ${taskId}`);
// Return the task data, the original subtask count (if applicable),
// and the full tasks array for reference (needed for formatDependenciesWithStatus function in UI)
log.info(
`Successfully found task ${taskId}${status ? ` (with status filter: ${status})` : ''}`
);
return {
success: true,
data: {
task,
originalSubtaskCount,
allTasks: data.tasks
}
};

View File

@ -40,6 +40,10 @@ export function registerShowTaskTool(server) {
description: 'Get detailed information about a specific task',
parameters: z.object({
id: z.string().describe('Task ID to get'),
status: z
.string()
.optional()
.describe("Filter subtasks by status (e.g., 'pending', 'done')"),
file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z
.string()
@ -52,11 +56,9 @@ export function registerShowTaskTool(server) {
); // Use JSON.stringify for better visibility
try {
log.info(`Getting task details for ID: ${args.id}`);
log.info(
`Session object received in execute: ${JSON.stringify(session)}`
); // Use JSON.stringify for better visibility
`Getting task details for ID: ${args.id}${args.status ? ` (filtering subtasks by status: ${args.status})` : ''}`
);
// Get project root from args or session
const rootFolder =
@ -91,10 +93,9 @@ export function registerShowTaskTool(server) {
const result = await showTaskDirect(
{
// Pass the explicitly resolved path
tasksJsonPath: tasksJsonPath,
// Pass other relevant args
id: args.id
id: args.id,
status: args.status
},
log
);

View File

@ -1330,9 +1330,11 @@ function registerCommands(programInstance) {
)
.argument('[id]', 'Task ID to show')
.option('-i, --id <id>', 'Task ID to show')
.option('-s, --status <status>', 'Filter subtasks by status') // ADDED status option
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.action(async (taskId, options) => {
const idArg = taskId || options.id;
const statusFilter = options.status; // ADDED: Capture status filter
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
@ -1340,7 +1342,8 @@ function registerCommands(programInstance) {
}
const tasksPath = options.file;
await displayTaskById(tasksPath, idArg);
// PASS statusFilter to the display function
await displayTaskById(tasksPath, idArg, statusFilter);
});
// add-dependency command

View File

@ -1000,8 +1000,9 @@ async function displayNextTask(tasksPath) {
* Display a specific task by ID
* @param {string} tasksPath - Path to the tasks.json file
* @param {string|number} taskId - The ID of the task to display
* @param {string} [statusFilter] - Optional status to filter subtasks by
*/
async function displayTaskById(tasksPath, taskId) {
async function displayTaskById(tasksPath, taskId, statusFilter = null) {
displayBanner();
// Read the tasks file
@ -1011,8 +1012,13 @@ async function displayTaskById(tasksPath, taskId) {
process.exit(1);
}
// Find the task by ID
const task = findTaskById(data.tasks, taskId);
// Find the task by ID, applying the status filter if provided
// Returns { task, originalSubtaskCount, originalSubtasks }
const { task, originalSubtaskCount, originalSubtasks } = findTaskById(
data.tasks,
taskId,
statusFilter
);
if (!task) {
console.log(
@ -1026,7 +1032,7 @@ async function displayTaskById(tasksPath, taskId) {
return;
}
// Handle subtask display specially
// Handle subtask display specially (This logic remains the same)
if (task.isSubtask || task.parentTask) {
console.log(
boxen(
@ -1042,8 +1048,7 @@ async function displayTaskById(tasksPath, taskId) {
)
);
// Create a table with subtask details
const taskTable = new Table({
const subtaskTable = new Table({
style: {
head: [],
border: [],
@ -1051,18 +1056,11 @@ async function displayTaskById(tasksPath, taskId) {
'padding-bottom': 0,
compact: true
},
chars: {
mid: '',
'left-mid': '',
'mid-mid': '',
'right-mid': ''
},
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
wordWrap: true
});
// Add subtask details to table
taskTable.push(
subtaskTable.push(
[chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`],
[
chalk.cyan.bold('Parent Task:'),
@ -1078,10 +1076,8 @@ async function displayTaskById(tasksPath, taskId) {
task.description || 'No description provided.'
]
);
console.log(subtaskTable.toString());
console.log(taskTable.toString());
// Show details if they exist for subtasks
if (task.details && task.details.trim().length > 0) {
console.log(
boxen(
@ -1096,7 +1092,6 @@ async function displayTaskById(tasksPath, taskId) {
);
}
// Show action suggestions for subtask
console.log(
boxen(
chalk.white.bold('Suggested Actions:') +
@ -1112,85 +1107,10 @@ async function displayTaskById(tasksPath, taskId) {
}
)
);
// Calculate and display subtask completion progress
if (task.subtasks && task.subtasks.length > 0) {
const totalSubtasks = task.subtasks.length;
const completedSubtasks = task.subtasks.filter(
(st) => st.status === 'done' || st.status === 'completed'
).length;
// Count other statuses for the subtasks
const inProgressSubtasks = task.subtasks.filter(
(st) => st.status === 'in-progress'
).length;
const pendingSubtasks = task.subtasks.filter(
(st) => st.status === 'pending'
).length;
const blockedSubtasks = task.subtasks.filter(
(st) => st.status === 'blocked'
).length;
const deferredSubtasks = task.subtasks.filter(
(st) => st.status === 'deferred'
).length;
const cancelledSubtasks = task.subtasks.filter(
(st) => st.status === 'cancelled'
).length;
// Calculate status breakdown as percentages
const statusBreakdown = {
'in-progress': (inProgressSubtasks / totalSubtasks) * 100,
pending: (pendingSubtasks / totalSubtasks) * 100,
blocked: (blockedSubtasks / totalSubtasks) * 100,
deferred: (deferredSubtasks / totalSubtasks) * 100,
cancelled: (cancelledSubtasks / totalSubtasks) * 100
};
const completionPercentage = (completedSubtasks / totalSubtasks) * 100;
// Calculate appropriate progress bar length based on terminal width
// Subtract padding (2), borders (2), and the percentage text (~5)
const availableWidth = process.stdout.columns || 80; // Default to 80 if can't detect
const boxPadding = 2; // 1 on each side
const boxBorders = 2; // 1 on each side
const percentTextLength = 5; // ~5 chars for " 100%"
// Reduce the length by adjusting the subtraction value from 20 to 35
const progressBarLength = Math.max(
20,
Math.min(
60,
availableWidth - boxPadding - boxBorders - percentTextLength - 35
)
); // Min 20, Max 60
// Status counts for display
const statusCounts =
`${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` +
`${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`;
console.log(
boxen(
chalk.white.bold('Subtask Progress:') +
'\n\n' +
`${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` +
`${statusCounts}\n` +
`${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`,
{
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 0 },
width: Math.min(availableWidth - 10, 100), // Add width constraint to limit the box width
textAlignment: 'left'
}
)
);
}
return;
return; // Exit after displaying subtask details
}
// Display a regular task
// --- Display Regular Task Details ---
console.log(
boxen(chalk.white.bold(`Task: #${task.id} - ${task.title}`), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
@ -1200,7 +1120,6 @@ async function displayTaskById(tasksPath, taskId) {
})
);
// Create a table with task details with improved handling
const taskTable = new Table({
style: {
head: [],
@ -1209,17 +1128,10 @@ async function displayTaskById(tasksPath, taskId) {
'padding-bottom': 0,
compact: true
},
chars: {
mid: '',
'left-mid': '',
'mid-mid': '',
'right-mid': ''
},
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
wordWrap: true
});
// Priority with color
const priorityColors = {
high: chalk.red.bold,
medium: chalk.yellow,
@ -1227,8 +1139,6 @@ async function displayTaskById(tasksPath, taskId) {
};
const priorityColor =
priorityColors[task.priority || 'medium'] || chalk.white;
// Add task details to table
taskTable.push(
[chalk.cyan.bold('ID:'), task.id.toString()],
[chalk.cyan.bold('Title:'), task.title],
@ -1243,10 +1153,8 @@ async function displayTaskById(tasksPath, taskId) {
],
[chalk.cyan.bold('Description:'), task.description]
);
console.log(taskTable.toString());
// If task has details, show them in a separate box
if (task.details && task.details.trim().length > 0) {
console.log(
boxen(
@ -1260,8 +1168,6 @@ async function displayTaskById(tasksPath, taskId) {
)
);
}
// Show test strategy if available
if (task.testStrategy && task.testStrategy.trim().length > 0) {
console.log(
boxen(chalk.white.bold('Test Strategy:') + '\n\n' + task.testStrategy, {
@ -1273,7 +1179,7 @@ async function displayTaskById(tasksPath, taskId) {
);
}
// Show subtasks if they exist
// --- Subtask Table Display (uses filtered list: task.subtasks) ---
if (task.subtasks && task.subtasks.length > 0) {
console.log(
boxen(chalk.white.bold('Subtasks'), {
@ -1284,22 +1190,16 @@ async function displayTaskById(tasksPath, taskId) {
})
);
// Calculate available width for the subtask table
const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect
// Define percentage-based column widths
const availableWidth = process.stdout.columns - 10 || 100;
const idWidthPct = 10;
const statusWidthPct = 15;
const depsWidthPct = 25;
const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct;
// Calculate actual column widths
const idWidth = Math.floor(availableWidth * (idWidthPct / 100));
const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100));
const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100));
const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100));
// Create a table for subtasks with improved handling
const subtaskTable = new Table({
head: [
chalk.magenta.bold('ID'),
@ -1315,59 +1215,50 @@ async function displayTaskById(tasksPath, taskId) {
'padding-bottom': 0,
compact: true
},
chars: {
mid: '',
'left-mid': '',
'mid-mid': '',
'right-mid': ''
},
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
wordWrap: true
});
// Add subtasks to table
// Populate table with the potentially filtered subtasks
task.subtasks.forEach((st) => {
const statusColor =
{
done: chalk.green,
completed: chalk.green,
pending: chalk.yellow,
'in-progress': chalk.blue
}[st.status || 'pending'] || chalk.white;
// Format subtask dependencies
const statusColorMap = {
done: chalk.green,
completed: chalk.green,
pending: chalk.yellow,
'in-progress': chalk.blue
};
const statusColor = statusColorMap[st.status || 'pending'] || chalk.white;
let subtaskDeps = 'None';
if (st.dependencies && st.dependencies.length > 0) {
// Format dependencies with correct notation
const formattedDeps = st.dependencies.map((depId) => {
if (typeof depId === 'number' && depId < 100) {
const foundSubtask = task.subtasks.find((st) => st.id === depId);
if (foundSubtask) {
const isDone =
foundSubtask.status === 'done' ||
foundSubtask.status === 'completed';
const isInProgress = foundSubtask.status === 'in-progress';
// Use the original, unfiltered list for dependency status lookup
const sourceListForDeps = originalSubtasks || task.subtasks;
const foundDepSubtask =
typeof depId === 'number' && depId < 100
? sourceListForDeps.find((sub) => sub.id === depId)
: null;
// Use consistent color formatting instead of emojis
if (isDone) {
return chalk.green.bold(`${task.id}.${depId}`);
} else if (isInProgress) {
return chalk.hex('#FFA500').bold(`${task.id}.${depId}`);
} else {
return chalk.red.bold(`${task.id}.${depId}`);
}
}
if (foundDepSubtask) {
const isDone =
foundDepSubtask.status === 'done' ||
foundDepSubtask.status === 'completed';
const isInProgress = foundDepSubtask.status === 'in-progress';
const color = isDone
? chalk.green.bold
: isInProgress
? chalk.hex('#FFA500').bold
: chalk.red.bold;
return color(`${task.id}.${depId}`);
} else if (typeof depId === 'number' && depId < 100) {
return chalk.red(`${task.id}.${depId} (Not found)`);
}
return depId;
return depId; // Assume it's a top-level task ID if not a number < 100
});
// Join the formatted dependencies directly instead of passing to formatDependenciesWithStatus again
subtaskDeps =
formattedDeps.length === 1
? formattedDeps[0]
: formattedDeps.join(chalk.white(', '));
}
subtaskTable.push([
`${task.id}.${st.id}`,
statusColor(st.status || 'pending'),
@ -1375,110 +1266,162 @@ async function displayTaskById(tasksPath, taskId) {
subtaskDeps
]);
});
console.log(subtaskTable.toString());
// Calculate and display subtask completion progress
if (task.subtasks && task.subtasks.length > 0) {
const totalSubtasks = task.subtasks.length;
const completedSubtasks = task.subtasks.filter(
(st) => st.status === 'done' || st.status === 'completed'
).length;
// Count other statuses for the subtasks
const inProgressSubtasks = task.subtasks.filter(
(st) => st.status === 'in-progress'
).length;
const pendingSubtasks = task.subtasks.filter(
(st) => st.status === 'pending'
).length;
const blockedSubtasks = task.subtasks.filter(
(st) => st.status === 'blocked'
).length;
const deferredSubtasks = task.subtasks.filter(
(st) => st.status === 'deferred'
).length;
const cancelledSubtasks = task.subtasks.filter(
(st) => st.status === 'cancelled'
).length;
// Calculate status breakdown as percentages
const statusBreakdown = {
'in-progress': (inProgressSubtasks / totalSubtasks) * 100,
pending: (pendingSubtasks / totalSubtasks) * 100,
blocked: (blockedSubtasks / totalSubtasks) * 100,
deferred: (deferredSubtasks / totalSubtasks) * 100,
cancelled: (cancelledSubtasks / totalSubtasks) * 100
};
const completionPercentage = (completedSubtasks / totalSubtasks) * 100;
// Calculate appropriate progress bar length based on terminal width
// Subtract padding (2), borders (2), and the percentage text (~5)
const availableWidth = process.stdout.columns || 80; // Default to 80 if can't detect
const boxPadding = 2; // 1 on each side
const boxBorders = 2; // 1 on each side
const percentTextLength = 5; // ~5 chars for " 100%"
// Reduce the length by adjusting the subtraction value from 20 to 35
const progressBarLength = Math.max(
20,
Math.min(
60,
availableWidth - boxPadding - boxBorders - percentTextLength - 35
// Display filter summary line *immediately after the table* if a filter was applied
if (statusFilter && originalSubtaskCount !== null) {
console.log(
chalk.cyan(
` Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.`
)
); // Min 20, Max 60
// Status counts for display
const statusCounts =
`${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` +
`${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`;
);
// Add a newline for spacing before the progress bar if the filter line was shown
console.log();
}
// --- Conditional Messages for No Subtasks Shown ---
} else if (statusFilter && originalSubtaskCount === 0) {
// Case where filter applied, but the parent task had 0 subtasks originally
console.log(
boxen(
chalk.yellow(
`No subtasks found matching status: ${statusFilter} (Task has no subtasks)`
),
{
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 1, bottom: 0 },
borderColor: 'yellow',
borderStyle: 'round'
}
)
);
} else if (
statusFilter &&
originalSubtaskCount > 0 &&
task.subtasks.length === 0
) {
// Case where filter applied, original subtasks existed, but none matched
console.log(
boxen(
chalk.yellow(
`No subtasks found matching status: ${statusFilter} (out of ${originalSubtaskCount} total)`
),
{
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 1, bottom: 0 },
borderColor: 'yellow',
borderStyle: 'round'
}
)
);
} else if (
!statusFilter &&
(!originalSubtasks || originalSubtasks.length === 0)
) {
// Case where NO filter applied AND the task genuinely has no subtasks
// Use the authoritative originalSubtasks if it exists (from filtering), else check task.subtasks
const actualSubtasks = originalSubtasks || task.subtasks;
if (!actualSubtasks || actualSubtasks.length === 0) {
console.log(
boxen(
chalk.white.bold('Subtask Progress:') +
'\n\n' +
`${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` +
`${statusCounts}\n` +
`${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`,
chalk.yellow('No subtasks found. Consider breaking down this task:') +
'\n' +
chalk.white(
`Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}`
),
{
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'blue',
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1, bottom: 0 },
width: Math.min(availableWidth - 10, 100), // Add width constraint to limit the box width
textAlignment: 'left'
margin: { top: 1, bottom: 0 }
}
)
);
}
} else {
// Suggest expanding if no subtasks
}
// --- Subtask Progress Bar Display (uses originalSubtasks or task.subtasks) ---
// Determine the list to use for progress calculation (always the original if available and filtering happened)
const subtasksForProgress = originalSubtasks || task.subtasks; // Use original if filtering occurred, else the potentially empty task.subtasks
// Only show progress if there are actually subtasks
if (subtasksForProgress && subtasksForProgress.length > 0) {
const totalSubtasks = subtasksForProgress.length;
const completedSubtasks = subtasksForProgress.filter(
(st) => st.status === 'done' || st.status === 'completed'
).length;
// Count other statuses from the original/complete list
const inProgressSubtasks = subtasksForProgress.filter(
(st) => st.status === 'in-progress'
).length;
const pendingSubtasks = subtasksForProgress.filter(
(st) => st.status === 'pending'
).length;
const blockedSubtasks = subtasksForProgress.filter(
(st) => st.status === 'blocked'
).length;
const deferredSubtasks = subtasksForProgress.filter(
(st) => st.status === 'deferred'
).length;
const cancelledSubtasks = subtasksForProgress.filter(
(st) => st.status === 'cancelled'
).length;
const statusBreakdown = {
// Calculate breakdown based on the complete list
'in-progress': (inProgressSubtasks / totalSubtasks) * 100,
pending: (pendingSubtasks / totalSubtasks) * 100,
blocked: (blockedSubtasks / totalSubtasks) * 100,
deferred: (deferredSubtasks / totalSubtasks) * 100,
cancelled: (cancelledSubtasks / totalSubtasks) * 100
};
const completionPercentage = (completedSubtasks / totalSubtasks) * 100;
const availableWidth = process.stdout.columns || 80;
const boxPadding = 2;
const boxBorders = 2;
const percentTextLength = 5;
const progressBarLength = Math.max(
20,
Math.min(
60,
availableWidth - boxPadding - boxBorders - percentTextLength - 35
)
);
const statusCounts =
`${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` +
`${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`;
console.log(
boxen(
chalk.yellow('No subtasks found. Consider breaking down this task:') +
'\n' +
chalk.white(
`Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}`
),
chalk.white.bold('Subtask Progress:') +
'\n\n' +
`${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` +
`${statusCounts}\n` +
`${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`,
{
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'yellow',
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 0 }
margin: { top: 1, bottom: 0 },
width: Math.min(availableWidth - 10, 100),
textAlignment: 'left'
}
)
);
}
// Show action suggestions
// --- Suggested Actions ---
console.log(
boxen(
chalk.white.bold('Suggested Actions:') +
'\n' +
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` +
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}\n` +
(task.subtasks && task.subtasks.length > 0
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}`
// Determine action 3 based on whether subtasks *exist* (use the source list for progress)
(subtasksForProgress && subtasksForProgress.length > 0
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` // Example uses .1
: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`),
{
padding: { top: 0, bottom: 0, left: 1, right: 1 },

View File

@ -290,25 +290,27 @@ function formatTaskId(id) {
}
/**
* Finds a task by ID in the tasks array
* Finds a task by ID in the tasks array. Optionally filters subtasks by status.
* @param {Array} tasks - The tasks array
* @param {string|number} taskId - The task ID to find
* @returns {Object|null} The task object or null if not found
* @param {string} [statusFilter] - Optional status to filter subtasks by
* @returns {{task: Object|null, originalSubtaskCount: number|null}} The task object (potentially with filtered subtasks) and the original subtask count if filtered, or nulls if not found.
*/
function findTaskById(tasks, taskId) {
function findTaskById(tasks, taskId, statusFilter = null) {
if (!taskId || !tasks || !Array.isArray(tasks)) {
return null;
return { task: null, originalSubtaskCount: null };
}
// Check if it's a subtask ID (e.g., "1.2")
if (typeof taskId === 'string' && taskId.includes('.')) {
// If looking for a subtask, statusFilter doesn't apply directly here.
const [parentId, subtaskId] = taskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = tasks.find((t) => t.id === parentId);
if (!parentTask || !parentTask.subtasks) {
return null;
return { task: null, originalSubtaskCount: null };
}
const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
@ -322,11 +324,35 @@ function findTaskById(tasks, taskId) {
subtask.isSubtask = true;
}
return subtask || null;
// Return the found subtask (or null) and null for originalSubtaskCount
return { task: subtask || null, originalSubtaskCount: null };
}
// Find the main task
const id = parseInt(taskId, 10);
return tasks.find((t) => t.id === id) || null;
const task = tasks.find((t) => t.id === id) || null;
// If task not found, return nulls
if (!task) {
return { task: null, originalSubtaskCount: null };
}
// If task found and statusFilter provided, filter its subtasks
if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) {
const originalSubtaskCount = task.subtasks.length;
// Clone the task to avoid modifying the original array
const filteredTask = { ...task };
filteredTask.subtasks = task.subtasks.filter(
(subtask) =>
subtask.status &&
subtask.status.toLowerCase() === statusFilter.toLowerCase()
);
// Return the filtered task and the original count
return { task: filteredTask, originalSubtaskCount: originalSubtaskCount };
}
// Return original task and null count if no filter or no subtasks
return { task: task, originalSubtaskCount: null };
}
/**

View File

@ -1,6 +1,6 @@
# Task ID: 54
# Title: Add Research Flag to Add-Task Command
# Status: pending
# Status: done
# Dependencies: None
# Priority: medium
# Description: Enhance the add-task command with a --research flag that allows users to perform quick research on the task topic before finalizing task creation.

View File

@ -1,36 +0,0 @@
# Task ID: 74
# Title: Task 74: Implement Local Kokoro TTS Support
# Status: pending
# Dependencies: None
# Priority: medium
# Description: Integrate Text-to-Speech (TTS) functionality using a locally running Google Cloud Text-to-Speech (Kokoro) instance, enabling the application to synthesize speech from text.
# Details:
Implementation Details:
1. **Kokoro Setup:** Assume the user has a local Kokoro TTS instance running and accessible via a network address (e.g., http://localhost:port).
2. **Configuration:** Introduce new configuration options (e.g., in `.taskmasterconfig`) to enable/disable TTS, specify the provider ('kokoro_local'), and configure the Kokoro endpoint URL (`tts.kokoro.url`). Consider adding options for voice selection and language if the Kokoro API supports them.
3. **API Interaction:** Implement a client module to interact with the local Kokoro TTS API. This module should handle sending text input and receiving audio data (likely in formats like WAV or MP3).
4. **Audio Playback:** Integrate a cross-platform audio playback library (e.g., `playsound`, `simpleaudio`, or platform-specific APIs) to play the synthesized audio received from Kokoro.
5. **Integration Point:** Identify initial areas in the application where TTS will be used (e.g., a command to read out the current task's title and description). Design the integration to be extensible for future use cases.
6. **Error Handling:** Implement robust error handling for scenarios like: Kokoro instance unreachable, API errors during synthesis, invalid configuration, audio playback failures. Provide informative feedback to the user.
7. **Dependencies:** Add any necessary HTTP client or audio playback libraries as project dependencies.
# Test Strategy:
1. **Unit Tests:**
* Mock the Kokoro API client. Verify that the TTS module correctly formats requests based on input text and configuration.
* Test handling of successful API responses (parsing audio data placeholder).
* Test handling of various API error responses (e.g., 404, 500).
* Mock the audio playback library. Verify that the received audio data is passed correctly to the playback function.
* Test configuration loading and validation logic.
2. **Integration Tests:**
* Requires a running local Kokoro TTS instance (or a compatible mock server).
* Send actual text snippets through the TTS module to the local Kokoro instance.
* Verify that valid audio data is received (e.g., check format, non-zero size). Direct audio playback verification might be difficult in automated tests, focus on the data transfer.
* Test the end-to-end flow by triggering TTS from an application command and ensuring no exceptions occur during synthesis and playback initiation.
* Test error handling by attempting synthesis with the Kokoro instance stopped or misconfigured.
3. **Manual Testing:**
* Configure the application to point to a running local Kokoro instance.
* Trigger TTS for various text inputs (short, long, special characters).
* Verify that the audio is played back clearly and accurately reflects the input text.
* Test enabling/disabling TTS via configuration.
* Test behavior when the Kokoro endpoint is incorrect or the server is down.
* Verify performance and responsiveness.

View File

@ -2852,7 +2852,7 @@
"id": 54,
"title": "Add Research Flag to Add-Task Command",
"description": "Enhance the add-task command with a --research flag that allows users to perform quick research on the task topic before finalizing task creation.",
"status": "pending",
"status": "done",
"dependencies": [],
"priority": "medium",
"details": "Modify the existing add-task command to accept a new optional flag '--research'. When this flag is provided, the system should pause the task creation process and invoke the Perplexity research functionality (similar to Task #51) to help users gather information about the task topic before finalizing the task details. The implementation should:\n\n1. Update the command parser to recognize the new --research flag\n2. When the flag is present, extract the task title/description as the research topic\n3. Call the Perplexity research functionality with this topic\n4. Display research results to the user\n5. Allow the user to refine their task based on the research (modify title, description, etc.)\n6. Continue with normal task creation flow after research is complete\n7. Ensure the research results can be optionally attached to the task as reference material\n8. Add appropriate help text explaining this feature in the command help\n\nThe implementation should leverage the existing Perplexity research command from Task #51, ensuring code reuse where possible.",
@ -3920,17 +3920,6 @@
"dependencies": [],
"priority": "medium",
"subtasks": []
},
{
"id": 74,
"title": "Task 74: Implement Local Kokoro TTS Support",
"description": "Integrate Text-to-Speech (TTS) functionality using a locally running Google Cloud Text-to-Speech (Kokoro) instance, enabling the application to synthesize speech from text.",
"details": "Implementation Details:\n1. **Kokoro Setup:** Assume the user has a local Kokoro TTS instance running and accessible via a network address (e.g., http://localhost:port).\n2. **Configuration:** Introduce new configuration options (e.g., in `.taskmasterconfig`) to enable/disable TTS, specify the provider ('kokoro_local'), and configure the Kokoro endpoint URL (`tts.kokoro.url`). Consider adding options for voice selection and language if the Kokoro API supports them.\n3. **API Interaction:** Implement a client module to interact with the local Kokoro TTS API. This module should handle sending text input and receiving audio data (likely in formats like WAV or MP3).\n4. **Audio Playback:** Integrate a cross-platform audio playback library (e.g., `playsound`, `simpleaudio`, or platform-specific APIs) to play the synthesized audio received from Kokoro.\n5. **Integration Point:** Identify initial areas in the application where TTS will be used (e.g., a command to read out the current task's title and description). Design the integration to be extensible for future use cases.\n6. **Error Handling:** Implement robust error handling for scenarios like: Kokoro instance unreachable, API errors during synthesis, invalid configuration, audio playback failures. Provide informative feedback to the user.\n7. **Dependencies:** Add any necessary HTTP client or audio playback libraries as project dependencies.",
"testStrategy": "1. **Unit Tests:** \n * Mock the Kokoro API client. Verify that the TTS module correctly formats requests based on input text and configuration.\n * Test handling of successful API responses (parsing audio data placeholder).\n * Test handling of various API error responses (e.g., 404, 500).\n * Mock the audio playback library. Verify that the received audio data is passed correctly to the playback function.\n * Test configuration loading and validation logic.\n2. **Integration Tests:**\n * Requires a running local Kokoro TTS instance (or a compatible mock server).\n * Send actual text snippets through the TTS module to the local Kokoro instance.\n * Verify that valid audio data is received (e.g., check format, non-zero size). Direct audio playback verification might be difficult in automated tests, focus on the data transfer.\n * Test the end-to-end flow by triggering TTS from an application command and ensuring no exceptions occur during synthesis and playback initiation.\n * Test error handling by attempting synthesis with the Kokoro instance stopped or misconfigured.\n3. **Manual Testing:**\n * Configure the application to point to a running local Kokoro instance.\n * Trigger TTS for various text inputs (short, long, special characters).\n * Verify that the audio is played back clearly and accurately reflects the input text.\n * Test enabling/disabling TTS via configuration.\n * Test behavior when the Kokoro endpoint is incorrect or the server is down.\n * Verify performance and responsiveness.",
"status": "pending",
"dependencies": [],
"priority": "medium",
"subtasks": []
}
]
}