mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-12-07 20:42:42 +00:00
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:
parent
87d97bba00
commit
ca7b0457f1
5
.changeset/ninety-wombats-pull.md
Normal file
5
.changeset/ninety-wombats-pull.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'task-master-ai': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add `--status` flag to `show` command to filter displayed subtasks.
|
||||||
@ -17,12 +17,13 @@ import {
|
|||||||
* @param {Object} args - Command arguments
|
* @param {Object} args - Command arguments
|
||||||
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
|
* @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.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
|
* @param {Object} log - Logger object
|
||||||
* @returns {Promise<Object>} - Task details result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
|
* @returns {Promise<Object>} - Task details result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
|
||||||
*/
|
*/
|
||||||
export async function showTaskDirect(args, log) {
|
export async function showTaskDirect(args, log) {
|
||||||
// Destructure expected args
|
// Destructure expected args
|
||||||
const { tasksJsonPath, id } = args;
|
const { tasksJsonPath, id, status } = args;
|
||||||
|
|
||||||
if (!tasksJsonPath) {
|
if (!tasksJsonPath) {
|
||||||
log.error('showTaskDirect called without 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
|
// Generate cache key using the provided task path, ID, and status filter
|
||||||
const cacheKey = `showTask:${tasksJsonPath}:${taskId}`;
|
const cacheKey = `showTask:${tasksJsonPath}:${taskId}:${status || 'all'}`;
|
||||||
|
|
||||||
// Define the action function to be executed on cache miss
|
// Define the action function to be executed on cache miss
|
||||||
const coreShowTaskAction = async () => {
|
const coreShowTaskAction = async () => {
|
||||||
@ -60,7 +61,7 @@ export async function showTaskDirect(args, log) {
|
|||||||
enableSilentMode();
|
enableSilentMode();
|
||||||
|
|
||||||
log.info(
|
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
|
// Read tasks data using the provided path
|
||||||
@ -76,8 +77,12 @@ export async function showTaskDirect(args, log) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the specific task
|
// Find the specific task, passing the status filter
|
||||||
const task = findTaskById(data.tasks, taskId);
|
const { task, originalSubtaskCount } = findTaskById(
|
||||||
|
data.tasks,
|
||||||
|
taskId,
|
||||||
|
status
|
||||||
|
);
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
disableSilentMode(); // Disable before returning
|
disableSilentMode(); // Disable before returning
|
||||||
@ -85,7 +90,7 @@ export async function showTaskDirect(args, log) {
|
|||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: 'TASK_NOT_FOUND',
|
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
|
// Restore normal logging
|
||||||
disableSilentMode();
|
disableSilentMode();
|
||||||
|
|
||||||
// Return the task data with the full tasks array for reference
|
// Return the task data, the original subtask count (if applicable),
|
||||||
// (needed for formatDependenciesWithStatus function in UI)
|
// and the full tasks array for reference (needed for formatDependenciesWithStatus function in UI)
|
||||||
log.info(`Successfully found task ${taskId}`);
|
log.info(
|
||||||
|
`Successfully found task ${taskId}${status ? ` (with status filter: ${status})` : ''}`
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
task,
|
task,
|
||||||
|
originalSubtaskCount,
|
||||||
allTasks: data.tasks
|
allTasks: data.tasks
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -40,6 +40,10 @@ export function registerShowTaskTool(server) {
|
|||||||
description: 'Get detailed information about a specific task',
|
description: 'Get detailed information about a specific task',
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
id: z.string().describe('Task ID to get'),
|
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'),
|
file: z.string().optional().describe('Absolute path to the tasks file'),
|
||||||
projectRoot: z
|
projectRoot: z
|
||||||
.string()
|
.string()
|
||||||
@ -52,11 +56,9 @@ export function registerShowTaskTool(server) {
|
|||||||
); // Use JSON.stringify for better visibility
|
); // Use JSON.stringify for better visibility
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log.info(`Getting task details for ID: ${args.id}`);
|
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
`Session object received in execute: ${JSON.stringify(session)}`
|
`Getting task details for ID: ${args.id}${args.status ? ` (filtering subtasks by status: ${args.status})` : ''}`
|
||||||
); // Use JSON.stringify for better visibility
|
);
|
||||||
|
|
||||||
// Get project root from args or session
|
// Get project root from args or session
|
||||||
const rootFolder =
|
const rootFolder =
|
||||||
@ -91,10 +93,9 @@ export function registerShowTaskTool(server) {
|
|||||||
|
|
||||||
const result = await showTaskDirect(
|
const result = await showTaskDirect(
|
||||||
{
|
{
|
||||||
// Pass the explicitly resolved path
|
|
||||||
tasksJsonPath: tasksJsonPath,
|
tasksJsonPath: tasksJsonPath,
|
||||||
// Pass other relevant args
|
id: args.id,
|
||||||
id: args.id
|
status: args.status
|
||||||
},
|
},
|
||||||
log
|
log
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1330,9 +1330,11 @@ function registerCommands(programInstance) {
|
|||||||
)
|
)
|
||||||
.argument('[id]', 'Task ID to show')
|
.argument('[id]', 'Task ID to show')
|
||||||
.option('-i, --id <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')
|
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||||
.action(async (taskId, options) => {
|
.action(async (taskId, options) => {
|
||||||
const idArg = taskId || options.id;
|
const idArg = taskId || options.id;
|
||||||
|
const statusFilter = options.status; // ADDED: Capture status filter
|
||||||
|
|
||||||
if (!idArg) {
|
if (!idArg) {
|
||||||
console.error(chalk.red('Error: Please provide a task ID'));
|
console.error(chalk.red('Error: Please provide a task ID'));
|
||||||
@ -1340,7 +1342,8 @@ function registerCommands(programInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tasksPath = options.file;
|
const tasksPath = options.file;
|
||||||
await displayTaskById(tasksPath, idArg);
|
// PASS statusFilter to the display function
|
||||||
|
await displayTaskById(tasksPath, idArg, statusFilter);
|
||||||
});
|
});
|
||||||
|
|
||||||
// add-dependency command
|
// add-dependency command
|
||||||
|
|||||||
@ -1000,8 +1000,9 @@ async function displayNextTask(tasksPath) {
|
|||||||
* Display a specific task by ID
|
* Display a specific task by ID
|
||||||
* @param {string} tasksPath - Path to the tasks.json file
|
* @param {string} tasksPath - Path to the tasks.json file
|
||||||
* @param {string|number} taskId - The ID of the task to display
|
* @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();
|
displayBanner();
|
||||||
|
|
||||||
// Read the tasks file
|
// Read the tasks file
|
||||||
@ -1011,8 +1012,13 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the task by ID
|
// Find the task by ID, applying the status filter if provided
|
||||||
const task = findTaskById(data.tasks, taskId);
|
// Returns { task, originalSubtaskCount, originalSubtasks }
|
||||||
|
const { task, originalSubtaskCount, originalSubtasks } = findTaskById(
|
||||||
|
data.tasks,
|
||||||
|
taskId,
|
||||||
|
statusFilter
|
||||||
|
);
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
console.log(
|
console.log(
|
||||||
@ -1026,7 +1032,7 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle subtask display specially
|
// Handle subtask display specially (This logic remains the same)
|
||||||
if (task.isSubtask || task.parentTask) {
|
if (task.isSubtask || task.parentTask) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
@ -1042,8 +1048,7 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create a table with subtask details
|
const subtaskTable = new Table({
|
||||||
const taskTable = new Table({
|
|
||||||
style: {
|
style: {
|
||||||
head: [],
|
head: [],
|
||||||
border: [],
|
border: [],
|
||||||
@ -1051,18 +1056,11 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
'padding-bottom': 0,
|
'padding-bottom': 0,
|
||||||
compact: true
|
compact: true
|
||||||
},
|
},
|
||||||
chars: {
|
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
|
||||||
mid: '',
|
|
||||||
'left-mid': '',
|
|
||||||
'mid-mid': '',
|
|
||||||
'right-mid': ''
|
|
||||||
},
|
|
||||||
colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
|
colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
|
||||||
wordWrap: true
|
wordWrap: true
|
||||||
});
|
});
|
||||||
|
subtaskTable.push(
|
||||||
// Add subtask details to table
|
|
||||||
taskTable.push(
|
|
||||||
[chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`],
|
[chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`],
|
||||||
[
|
[
|
||||||
chalk.cyan.bold('Parent Task:'),
|
chalk.cyan.bold('Parent Task:'),
|
||||||
@ -1078,10 +1076,8 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
task.description || 'No description provided.'
|
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) {
|
if (task.details && task.details.trim().length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
@ -1096,7 +1092,6 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show action suggestions for subtask
|
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold('Suggested Actions:') +
|
chalk.white.bold('Suggested Actions:') +
|
||||||
@ -1112,85 +1107,10 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
return; // Exit after displaying subtask details
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display a regular task
|
// --- Display Regular Task Details ---
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.white.bold(`Task: #${task.id} - ${task.title}`), {
|
boxen(chalk.white.bold(`Task: #${task.id} - ${task.title}`), {
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
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({
|
const taskTable = new Table({
|
||||||
style: {
|
style: {
|
||||||
head: [],
|
head: [],
|
||||||
@ -1209,17 +1128,10 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
'padding-bottom': 0,
|
'padding-bottom': 0,
|
||||||
compact: true
|
compact: true
|
||||||
},
|
},
|
||||||
chars: {
|
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
|
||||||
mid: '',
|
|
||||||
'left-mid': '',
|
|
||||||
'mid-mid': '',
|
|
||||||
'right-mid': ''
|
|
||||||
},
|
|
||||||
colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
|
colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)],
|
||||||
wordWrap: true
|
wordWrap: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Priority with color
|
|
||||||
const priorityColors = {
|
const priorityColors = {
|
||||||
high: chalk.red.bold,
|
high: chalk.red.bold,
|
||||||
medium: chalk.yellow,
|
medium: chalk.yellow,
|
||||||
@ -1227,8 +1139,6 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
};
|
};
|
||||||
const priorityColor =
|
const priorityColor =
|
||||||
priorityColors[task.priority || 'medium'] || chalk.white;
|
priorityColors[task.priority || 'medium'] || chalk.white;
|
||||||
|
|
||||||
// Add task details to table
|
|
||||||
taskTable.push(
|
taskTable.push(
|
||||||
[chalk.cyan.bold('ID:'), task.id.toString()],
|
[chalk.cyan.bold('ID:'), task.id.toString()],
|
||||||
[chalk.cyan.bold('Title:'), task.title],
|
[chalk.cyan.bold('Title:'), task.title],
|
||||||
@ -1243,10 +1153,8 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
],
|
],
|
||||||
[chalk.cyan.bold('Description:'), task.description]
|
[chalk.cyan.bold('Description:'), task.description]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(taskTable.toString());
|
console.log(taskTable.toString());
|
||||||
|
|
||||||
// If task has details, show them in a separate box
|
|
||||||
if (task.details && task.details.trim().length > 0) {
|
if (task.details && task.details.trim().length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
@ -1260,8 +1168,6 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show test strategy if available
|
|
||||||
if (task.testStrategy && task.testStrategy.trim().length > 0) {
|
if (task.testStrategy && task.testStrategy.trim().length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.white.bold('Test Strategy:') + '\n\n' + task.testStrategy, {
|
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) {
|
if (task.subtasks && task.subtasks.length > 0) {
|
||||||
console.log(
|
console.log(
|
||||||
boxen(chalk.white.bold('Subtasks'), {
|
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;
|
||||||
const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect
|
|
||||||
|
|
||||||
// Define percentage-based column widths
|
|
||||||
const idWidthPct = 10;
|
const idWidthPct = 10;
|
||||||
const statusWidthPct = 15;
|
const statusWidthPct = 15;
|
||||||
const depsWidthPct = 25;
|
const depsWidthPct = 25;
|
||||||
const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct;
|
const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct;
|
||||||
|
|
||||||
// Calculate actual column widths
|
|
||||||
const idWidth = Math.floor(availableWidth * (idWidthPct / 100));
|
const idWidth = Math.floor(availableWidth * (idWidthPct / 100));
|
||||||
const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100));
|
const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100));
|
||||||
const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100));
|
const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100));
|
||||||
const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100));
|
const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100));
|
||||||
|
|
||||||
// Create a table for subtasks with improved handling
|
|
||||||
const subtaskTable = new Table({
|
const subtaskTable = new Table({
|
||||||
head: [
|
head: [
|
||||||
chalk.magenta.bold('ID'),
|
chalk.magenta.bold('ID'),
|
||||||
@ -1315,59 +1215,50 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
'padding-bottom': 0,
|
'padding-bottom': 0,
|
||||||
compact: true
|
compact: true
|
||||||
},
|
},
|
||||||
chars: {
|
chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' },
|
||||||
mid: '',
|
|
||||||
'left-mid': '',
|
|
||||||
'mid-mid': '',
|
|
||||||
'right-mid': ''
|
|
||||||
},
|
|
||||||
wordWrap: true
|
wordWrap: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add subtasks to table
|
// Populate table with the potentially filtered subtasks
|
||||||
task.subtasks.forEach((st) => {
|
task.subtasks.forEach((st) => {
|
||||||
const statusColor =
|
const statusColorMap = {
|
||||||
{
|
done: chalk.green,
|
||||||
done: chalk.green,
|
completed: chalk.green,
|
||||||
completed: chalk.green,
|
pending: chalk.yellow,
|
||||||
pending: chalk.yellow,
|
'in-progress': chalk.blue
|
||||||
'in-progress': chalk.blue
|
};
|
||||||
}[st.status || 'pending'] || chalk.white;
|
const statusColor = statusColorMap[st.status || 'pending'] || chalk.white;
|
||||||
|
|
||||||
// Format subtask dependencies
|
|
||||||
let subtaskDeps = 'None';
|
let subtaskDeps = 'None';
|
||||||
if (st.dependencies && st.dependencies.length > 0) {
|
if (st.dependencies && st.dependencies.length > 0) {
|
||||||
// Format dependencies with correct notation
|
|
||||||
const formattedDeps = st.dependencies.map((depId) => {
|
const formattedDeps = st.dependencies.map((depId) => {
|
||||||
if (typeof depId === 'number' && depId < 100) {
|
// Use the original, unfiltered list for dependency status lookup
|
||||||
const foundSubtask = task.subtasks.find((st) => st.id === depId);
|
const sourceListForDeps = originalSubtasks || task.subtasks;
|
||||||
if (foundSubtask) {
|
const foundDepSubtask =
|
||||||
const isDone =
|
typeof depId === 'number' && depId < 100
|
||||||
foundSubtask.status === 'done' ||
|
? sourceListForDeps.find((sub) => sub.id === depId)
|
||||||
foundSubtask.status === 'completed';
|
: null;
|
||||||
const isInProgress = foundSubtask.status === 'in-progress';
|
|
||||||
|
|
||||||
// Use consistent color formatting instead of emojis
|
if (foundDepSubtask) {
|
||||||
if (isDone) {
|
const isDone =
|
||||||
return chalk.green.bold(`${task.id}.${depId}`);
|
foundDepSubtask.status === 'done' ||
|
||||||
} else if (isInProgress) {
|
foundDepSubtask.status === 'completed';
|
||||||
return chalk.hex('#FFA500').bold(`${task.id}.${depId}`);
|
const isInProgress = foundDepSubtask.status === 'in-progress';
|
||||||
} else {
|
const color = isDone
|
||||||
return chalk.red.bold(`${task.id}.${depId}`);
|
? 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 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 =
|
subtaskDeps =
|
||||||
formattedDeps.length === 1
|
formattedDeps.length === 1
|
||||||
? formattedDeps[0]
|
? formattedDeps[0]
|
||||||
: formattedDeps.join(chalk.white(', '));
|
: formattedDeps.join(chalk.white(', '));
|
||||||
}
|
}
|
||||||
|
|
||||||
subtaskTable.push([
|
subtaskTable.push([
|
||||||
`${task.id}.${st.id}`,
|
`${task.id}.${st.id}`,
|
||||||
statusColor(st.status || 'pending'),
|
statusColor(st.status || 'pending'),
|
||||||
@ -1375,110 +1266,162 @@ async function displayTaskById(tasksPath, taskId) {
|
|||||||
subtaskDeps
|
subtaskDeps
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(subtaskTable.toString());
|
console.log(subtaskTable.toString());
|
||||||
|
|
||||||
// Calculate and display subtask completion progress
|
// Display filter summary line *immediately after the table* if a filter was applied
|
||||||
if (task.subtasks && task.subtasks.length > 0) {
|
if (statusFilter && originalSubtaskCount !== null) {
|
||||||
const totalSubtasks = task.subtasks.length;
|
console.log(
|
||||||
const completedSubtasks = task.subtasks.filter(
|
chalk.cyan(
|
||||||
(st) => st.status === 'done' || st.status === 'completed'
|
` Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.`
|
||||||
).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
|
);
|
||||||
|
// Add a newline for spacing before the progress bar if the filter line was shown
|
||||||
// Status counts for display
|
console.log();
|
||||||
const statusCounts =
|
}
|
||||||
`${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` +
|
// --- Conditional Messages for No Subtasks Shown ---
|
||||||
`${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`;
|
} 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(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold('Subtask Progress:') +
|
chalk.yellow('No subtasks found. Consider breaking down this task:') +
|
||||||
'\n\n' +
|
'\n' +
|
||||||
`${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` +
|
chalk.white(
|
||||||
`${statusCounts}\n` +
|
`Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}`
|
||||||
`${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`,
|
),
|
||||||
{
|
{
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||||
borderColor: 'blue',
|
borderColor: 'yellow',
|
||||||
borderStyle: 'round',
|
borderStyle: 'round',
|
||||||
margin: { top: 1, bottom: 0 },
|
margin: { top: 1, bottom: 0 }
|
||||||
width: Math.min(availableWidth - 10, 100), // Add width constraint to limit the box width
|
|
||||||
textAlignment: 'left'
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} 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(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.yellow('No subtasks found. Consider breaking down this task:') +
|
chalk.white.bold('Subtask Progress:') +
|
||||||
'\n' +
|
'\n\n' +
|
||||||
chalk.white(
|
`${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` +
|
||||||
`Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}`
|
`${statusCounts}\n` +
|
||||||
),
|
`${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`,
|
||||||
{
|
{
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||||
borderColor: 'yellow',
|
borderColor: 'blue',
|
||||||
borderStyle: 'round',
|
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(
|
console.log(
|
||||||
boxen(
|
boxen(
|
||||||
chalk.white.bold('Suggested Actions:') +
|
chalk.white.bold('Suggested Actions:') +
|
||||||
'\n' +
|
'\n' +
|
||||||
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\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` +
|
`${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
|
// Determine action 3 based on whether subtasks *exist* (use the source list for progress)
|
||||||
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}`
|
(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}`)}`),
|
: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`),
|
||||||
{
|
{
|
||||||
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
||||||
|
|||||||
@ -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 {Array} tasks - The tasks array
|
||||||
* @param {string|number} taskId - The task ID to find
|
* @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)) {
|
if (!taskId || !tasks || !Array.isArray(tasks)) {
|
||||||
return null;
|
return { task: null, originalSubtaskCount: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's a subtask ID (e.g., "1.2")
|
// Check if it's a subtask ID (e.g., "1.2")
|
||||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||||
|
// If looking for a subtask, statusFilter doesn't apply directly here.
|
||||||
const [parentId, subtaskId] = taskId
|
const [parentId, subtaskId] = taskId
|
||||||
.split('.')
|
.split('.')
|
||||||
.map((id) => parseInt(id, 10));
|
.map((id) => parseInt(id, 10));
|
||||||
const parentTask = tasks.find((t) => t.id === parentId);
|
const parentTask = tasks.find((t) => t.id === parentId);
|
||||||
|
|
||||||
if (!parentTask || !parentTask.subtasks) {
|
if (!parentTask || !parentTask.subtasks) {
|
||||||
return null;
|
return { task: null, originalSubtaskCount: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
|
const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
|
||||||
@ -322,11 +324,35 @@ function findTaskById(tasks, taskId) {
|
|||||||
subtask.isSubtask = true;
|
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);
|
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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Task ID: 54
|
# Task ID: 54
|
||||||
# Title: Add Research Flag to Add-Task Command
|
# Title: Add Research Flag to Add-Task Command
|
||||||
# Status: pending
|
# Status: done
|
||||||
# Dependencies: None
|
# Dependencies: None
|
||||||
# Priority: medium
|
# 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.
|
# 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.
|
||||||
|
|||||||
@ -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.
|
|
||||||
@ -2852,7 +2852,7 @@
|
|||||||
"id": 54,
|
"id": 54,
|
||||||
"title": "Add Research Flag to Add-Task Command",
|
"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.",
|
"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": [],
|
"dependencies": [],
|
||||||
"priority": "medium",
|
"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.",
|
"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": [],
|
"dependencies": [],
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"subtasks": []
|
"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": []
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user