From e54216fa69275ff4831f2dcacb5059d02551fbb5 Mon Sep 17 00:00:00 2001 From: Beau Raines Date: Sun, 22 Mar 2026 21:00:16 -0700 Subject: [PATCH 1/2] feat: add addsubtask command for creating subtasks New command: addsubtask [subtaskName...] Alias: as Creates a subtask under an existing task using the parent task's display index. Supports Smart Add syntax for the subtask name. Handles RTM-specific errors gracefully (Pro-only, nesting limit, repeating task conflicts). Usage: rtm addsubtask 5 Buy bananas rtm as 5 Closes: #151 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cmd/addsubtask.js | 116 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 src/cmd/addsubtask.js diff --git a/src/cmd/addsubtask.js b/src/cmd/addsubtask.js new file mode 100644 index 0000000..c7b4e58 --- /dev/null +++ b/src/cmd/addsubtask.js @@ -0,0 +1,116 @@ +'use strict'; + +const log = require('../utils/log.js'); +const config = require('../utils/config.js'); +const { prompt } = require('../utils/prompt.js'); + +const finish = require('../utils/finish.js'); + + +/** + * This command adds a subtask to an existing task (Pro accounts only) + * @param args [parentTaskIndex, subtaskName...] + * @param env + */ +function action(args, env) { + + // Need at least a parent task index + if ( args.length === 0 || args[0].length === 0 ) { + prompt('Parent Task Index:', 'Subtask Name:', _promptFinished); + } + else if ( args[0].length === 1 ) { + // Have parent index but no subtask name + let parentIndex = parseInt(args[0][0]); + if ( isNaN(parentIndex) ) { + log.spinner.error("Parent task index must be a number"); + return finish(); + } + prompt('Subtask Name:', function(answers) { + for ( let i = 0; i < answers.length; i++ ) { + _process(parentIndex, answers[i][0], i+1, answers.length); + } + }); + } + else { + // Have both parent index and subtask name + let parentIndex = parseInt(args[0][0]); + if ( isNaN(parentIndex) ) { + log.spinner.error("Parent task index must be a number"); + return finish(); + } + let subtaskName = args[0].slice(1).join(' '); + _process(parentIndex, subtaskName); + } + +} + + +/** + * Process the returned prompt answers + * @private + */ +function _promptFinished(answers) { + for ( let i = 0; i < answers.length; i++ ) { + let parentIndex = parseInt(answers[i][0]); + let subtaskName = answers[i][1]; + if ( isNaN(parentIndex) ) { + log.spinner.error("Parent task index must be a number"); + continue; + } + _process(parentIndex, subtaskName, i+1, answers.length); + } +} + + +/** + * Process the request to add a subtask + * @private + */ +function _process(parentIndex, subtaskName, count=1, max=1) { + log.spinner.start("Adding Subtask..."); + config.user(function(user) { + + // Add Subtask + user.tasks.addSubtask(parentIndex, subtaskName, function(err) { + if ( err ) { + if ( err.code === 4040 ) { + log.spinner.error("Subtasks require a Pro account"); + } + else if ( err.code === 4050 ) { + log.spinner.error("Invalid parent task (index: " + parentIndex + ")"); + } + else if ( err.code === 4060 ) { + log.spinner.error("Subtask nested too deep (max 3 levels)"); + } + else if ( err.code === 4070 ) { + log.spinner.error("Cannot add subtask: repeating task in hierarchy"); + } + else { + log.spinner.error("Could not add subtask (" + err.msg + ")"); + } + } + _processFinished(count, max); + }); + }); +} + +/** + * Request Callback + * @private + */ +function _processFinished(count, max) { + log.spinner.start("Adding Subtask [" + count + "/" + max + "]..."); + if ( count === max ) { + log.spinner.success("Subtask(s) Added"); + return finish(); + } +} + + + +module.exports = { + command: 'addsubtask [args...]', + alias: 'as', + description: 'Add a subtask to an existing task (Pro accounts only)', + action: action +}; From 95599bb169bb4dc77e880bc701f2cbca9a9152e5 Mon Sep 17 00:00:00 2001 From: Beau Raines Date: Sun, 22 Mar 2026 21:01:09 -0700 Subject: [PATCH 2/2] feat: display hasSubtasks indicator in task listings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a parentTask indicator ((p) in text mode, ๐Ÿ“‹ in emoji mode) to printIndicator.js and displays it in ls, lsd, lsp, and task detail views when a task has subtasks. Also shows Has Subtasks field in the task detail view. Closes: #124 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cmd/ls.js | 5 +++++ src/cmd/lsd.js | 6 ++++++ src/cmd/lsp.js | 6 ++++++ src/cmd/task.js | 4 +++- src/utils/printIndicator.js | 7 +++++-- 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/cmd/ls.js b/src/cmd/ls.js index 636d6f6..47fec13 100644 --- a/src/cmd/ls.js +++ b/src/cmd/ls.js @@ -97,6 +97,11 @@ function action(args, env) { printIndicator('subtask',task); } + // Display parent task indicator + if (task.hasSubtasks) { + printIndicator('parentTask',task); + } + // Add the Task Name log.style(task.name+' ', namestyle); diff --git a/src/cmd/lsd.js b/src/cmd/lsd.js index ab6ed51..d0ff01d 100644 --- a/src/cmd/lsd.js +++ b/src/cmd/lsd.js @@ -113,6 +113,12 @@ function action(args, env) { printIndicator('subtask',task); } + // Display parent task indicator + if (task.hasSubtasks) { + log.style(' '); + printIndicator('parentTask',task); + } + // Print the Task Name log.style(' '); log.style(task.name + ' ', priStyle); diff --git a/src/cmd/lsp.js b/src/cmd/lsp.js index 07d891c..3d15b86 100644 --- a/src/cmd/lsp.js +++ b/src/cmd/lsp.js @@ -86,6 +86,12 @@ function action(args, env) { printIndicator('subtask',task); } + // Display parent task indicator + if (task.hasSubtasks) { + log.style(' '); + printIndicator('parentTask',task); + } + // Print the Task Name log.style(' '); log.style(task.name + ' ', priStyle); diff --git a/src/cmd/task.js b/src/cmd/task.js index ea1c213..98d8c76 100644 --- a/src/cmd/task.js +++ b/src/cmd/task.js @@ -106,7 +106,7 @@ function displayTask(taskDetails) { debug(taskDetails) let index = taskDetails.index; // eslint-disable-next-line no-unused-vars - const { _list, list_id, location_id, taskseries_id, task_id, _index, name, priority, start, due, completed, isRecurring, recurrenceRuleRaw, isSubtask, estimate, url, tags, notes, ...otherAttributes } = taskDetails.task; + const { _list, list_id, location_id, taskseries_id, task_id, _index, name, priority, start, due, completed, isRecurring, recurrenceRuleRaw, isSubtask, hasSubtasks, estimate, url, tags, notes, ...otherAttributes } = taskDetails.task; const listName = LIST_MAP.get(list_id) || "Not found"; const locationName = LOCATION_MAP.get(location_id) || "Not found"; @@ -133,6 +133,8 @@ function displayTask(taskDetails) { log.style(`Is Subtask: `,styles.index) log(`${isSubtask}`) + log.style(`Has Subtasks: `,styles.index) + log(`${hasSubtasks}`) log.style(`Estimate: `,styles.index) log(humanizeDuration(estimate)) log.style(`Location: `,styles.index) diff --git a/src/utils/printIndicator.js b/src/utils/printIndicator.js index b6ef4ff..82f1657 100644 --- a/src/utils/printIndicator.js +++ b/src/utils/printIndicator.js @@ -14,7 +14,7 @@ function printIndicator(type,task) { let iconType = config.get().iconType; let indicatorStyle = task.isCompleted ? styles.completed : styles[type]; - let notesIndicator,urlIndicator,recurringIndicator,subTaskIndicator; + let notesIndicator,urlIndicator,recurringIndicator,subTaskIndicator,parentTaskIndicator; iconType = iconType || 'text'; // defaults to text if nothing included switch (iconType) { case 'emoji': @@ -22,6 +22,7 @@ function printIndicator(type,task) { urlIndicator = '๐Ÿ”—'; recurringIndicator = '๐Ÿ”'; subTaskIndicator = 'โคด๏ธ ' + parentTaskIndicator = '๐Ÿ“‹ ' break; case 'text': default: @@ -29,13 +30,15 @@ function printIndicator(type,task) { urlIndicator = '+'; recurringIndicator = 'r'; subTaskIndicator = '(s) ' + parentTaskIndicator = '(p) ' break; } let indicators = { notes: notesIndicator, url: urlIndicator, recurring: recurringIndicator, - subtask: subTaskIndicator + subtask: subTaskIndicator, + parentTask: parentTaskIndicator } log.style(indicators[type], indicatorStyle); }