Conversation
🦋 Changeset detectedLatest commit: b8fa590 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds a new terminal UI package and an Interactive CLI command that launches a keyboard-driven TUI for Task Master; includes components, hooks, stores, utilities, logging, error handling, file-watching, onboarding, and an exported TaskWatcher from core. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as InteractiveCommand
participant UI as launchTerminalUI
participant Detect as detectProjectState
participant Store as useTaskStore / taskStore
participant Watcher as TaskWatcher
participant Render as App/Components
User->>CLI: task-master interactive [--panel] [--project]
CLI->>CLI: validate options & resolve project path
CLI->>UI: launchTerminalUI({ panel, section, projectPath })
UI->>Detect: detectProjectState(projectPath)
alt UNINITIALIZED or NO_TASKS
Detect-->>UI: ProjectStateResult
UI->>User: print onboarding / guidance
UI-->>CLI: exit early
else HAS_TASKS
Detect-->>UI: ProjectStateResult
UI->>Render: render App within ErrorBoundary
Render->>Store: initialize useTaskStore(projectPath)
Store->>Watcher: start TaskWatcher (fs watcher)
Watcher->>Store: emit file-change events
Store->>Render: push updated tasks
Render->>Render: user interactions (tab/arrow/enter/q) update UI state
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 45
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/tm-core/src/storage/file-storage/file-storage.ts (1)
456-465: Guard against undefined parentTask.subtasks in subtask updates (runtime crash).parentTask.subtasks may be undefined; calling findIndex will throw. Add a defensive check.
- // Find the subtask within the parent task - const subtaskIndex = parentTask.subtasks.findIndex( + // Find the subtask within the parent task + if (!Array.isArray(parentTask.subtasks) || parentTask.subtasks.length === 0) { + throw new Error(`Parent task ${parentId} has no subtasks`); + } + const subtaskIndex = parentTask.subtasks.findIndex( (st) => st.id === subtaskNumericId || String(st.id) === subId );
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (33)
apps/cli/src/command-registry.ts(2 hunks)apps/cli/src/commands/interactive.command.ts(1 hunks)apps/cli/src/index.ts(1 hunks)apps/terminal-ui/package.json(1 hunks)apps/terminal-ui/src/components/BottomStatusBar.tsx(1 hunks)apps/terminal-ui/src/components/ErrorBoundary.tsx(1 hunks)apps/terminal-ui/src/components/Onboarding.tsx(1 hunks)apps/terminal-ui/src/components/StatusBar.tsx(1 hunks)apps/terminal-ui/src/components/TopBar.tsx(1 hunks)apps/terminal-ui/src/components/index.ts(1 hunks)apps/terminal-ui/src/hooks/useTaskStore.ts(1 hunks)apps/terminal-ui/src/index.tsx(1 hunks)apps/terminal-ui/src/stores/taskStore.ts(1 hunks)apps/terminal-ui/src/utils/error-handling.ts(1 hunks)apps/terminal-ui/src/utils/logger.ts(1 hunks)apps/terminal-ui/src/utils/project-state.ts(1 hunks)apps/terminal-ui/src/utils/task-helpers.ts(1 hunks)apps/terminal-ui/src/views/layouts/AppLayout.tsx(1 hunks)apps/terminal-ui/src/views/modals/DimmedOverlay.tsx(1 hunks)apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx(1 hunks)apps/terminal-ui/src/views/panels/DashboardPanel.tsx(1 hunks)apps/terminal-ui/src/views/panels/HelpPanel.tsx(1 hunks)apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx(1 hunks)apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx(1 hunks)apps/terminal-ui/src/views/sections/TaskListSection.tsx(1 hunks)apps/terminal-ui/terminal-ui.md(1 hunks)apps/terminal-ui/tsconfig.json(1 hunks)packages/tm-core/package.json(1 hunks)packages/tm-core/src/storage/file-storage/file-storage.ts(4 hunks)packages/tm-core/src/storage/file-storage/index.ts(1 hunks)packages/tm-core/src/storage/file-storage/task-watcher.ts(1 hunks)packages/tm-core/src/storage/index.ts(1 hunks)tsconfig.json(1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{md,mdx}
📄 CodeRabbit inference engine (CLAUDE.md)
Reference documentation at https://docs.task-master.dev rather than local file paths in content
Files:
apps/terminal-ui/terminal-ui.md
🧠 Learnings (5)
📚 Learning: 2025-09-26T19:10:32.906Z
Learnt from: Crunchyman-ralph
PR: eyaltoledano/claude-task-master#1252
File: tsconfig.json:22-28
Timestamp: 2025-09-26T19:10:32.906Z
Learning: In the eyaltoledano/claude-task-master repository, all internal tm/ package path mappings in tsconfig.json consistently point to TypeScript source files (e.g., "./packages/*/src/index.ts") rather than built JavaScript. This is intentional architecture because tsdown bundles internal packages directly from source during build time, eliminating the need for separate compilation of internal packages.
Applied to files:
tsconfig.jsonapps/terminal-ui/package.json
📚 Learning: 2025-09-26T19:05:47.555Z
Learnt from: Crunchyman-ralph
PR: eyaltoledano/claude-task-master#1252
File: packages/ai-sdk-provider-grok-cli/package.json:11-13
Timestamp: 2025-09-26T19:05:47.555Z
Learning: In the eyaltoledano/claude-task-master repository, internal tm/ packages use a specific export pattern where the "exports" field points to TypeScript source files (./src/index.ts) while "main" points to compiled output (./dist/index.js) and "types" points to source files (./src/index.ts). This pattern is used consistently across internal packages like tm/core and tm/ai-sdk-provider-grok-cli because they are consumed directly during build-time bundling with tsdown rather than being published as separate packages.
Applied to files:
tsconfig.jsonapps/terminal-ui/package.json
📚 Learning: 2025-07-18T17:14:29.399Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/tasks.mdc:0-0
Timestamp: 2025-07-18T17:14:29.399Z
Learning: Applies to scripts/modules/task-manager.js : Use consistent formatting for task files, include all task properties in text files, and format dependencies with status indicators.
Applied to files:
apps/terminal-ui/src/utils/task-helpers.ts
📚 Learning: 2025-07-18T17:16:32.622Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/ui.mdc:0-0
Timestamp: 2025-07-18T17:16:32.622Z
Learning: Applies to scripts/modules/ui.js : Use getStatusWithColor for status display, formatDependenciesWithStatus for dependency lists, and truncate for overflowing text
Applied to files:
apps/terminal-ui/src/utils/task-helpers.ts
📚 Learning: 2025-07-31T22:07:49.716Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-31T22:07:49.716Z
Learning: Applies to scripts/modules/commands.js : Export the registerCommands function and keep the CLI setup code clean and maintainable.
Applied to files:
apps/cli/src/index.tsapps/cli/src/command-registry.ts
🧬 Code graph analysis (18)
apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx (3)
scripts/modules/task-manager/list-tasks.js (4)
totalTasks(77-77)reviewSubtasks(113-113)taskStatusBreakdown(305-312)subtaskStatusBreakdown(314-324)apps/cli/src/utils/ui.ts (1)
createProgressBar(156-172)scripts/modules/ui.js (1)
statusBreakdown(1633-1640)
packages/tm-core/src/storage/file-storage/file-storage.ts (2)
packages/tm-core/src/storage/file-storage/task-watcher.ts (2)
TaskWatcher(99-438)TaskWatcherEvent(54-58)packages/tm-core/src/storage/file-storage/path-resolver.ts (1)
PathResolver(10-42)
apps/terminal-ui/src/utils/task-helpers.ts (1)
packages/tm-core/src/types/index.ts (1)
TaskComplexity(38-38)
apps/terminal-ui/src/index.tsx (9)
apps/terminal-ui/src/hooks/useTaskStore.ts (1)
useTaskStore(14-114)apps/terminal-ui/src/components/TopBar.tsx (1)
TopBar(12-38)apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
BottomStatusBar(14-44)apps/terminal-ui/src/utils/logger.ts (2)
error(106-130)logger(323-331)apps/terminal-ui/src/views/panels/DashboardPanel.tsx (1)
DashboardPanel(24-90)apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (1)
TaskDetailsModal(29-187)apps/terminal-ui/src/utils/task-helpers.ts (3)
getStatusColor(10-29)getPriorityColor(34-47)getComplexityDisplay(52-83)apps/terminal-ui/src/components/ErrorBoundary.tsx (2)
ErrorBoundary(38-137)render(77-136)apps/terminal-ui/src/utils/project-state.ts (1)
detectProjectState(40-93)
apps/terminal-ui/src/hooks/useTaskStore.ts (3)
packages/tm-core/src/storage/file-storage/file-storage.ts (1)
loadTasks(103-134)apps/terminal-ui/src/stores/taskStore.ts (1)
taskStore(108-108)packages/tm-core/src/storage/file-storage/task-watcher.ts (1)
TaskWatcher(99-438)
apps/terminal-ui/src/views/panels/DashboardPanel.tsx (3)
apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx (1)
ProjectDashboardSection(13-340)apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (1)
DependencyStatusSection(15-180)apps/terminal-ui/src/views/sections/TaskListSection.tsx (1)
TaskListSection(23-207)
apps/cli/src/commands/interactive.command.ts (1)
apps/terminal-ui/src/index.tsx (1)
launchTerminalUI(329-397)
apps/terminal-ui/src/views/sections/TaskListSection.tsx (1)
apps/terminal-ui/src/utils/task-helpers.ts (4)
getComplexityDisplay(52-83)padText(88-111)getStatusColor(10-29)getPriorityColor(34-47)
apps/terminal-ui/src/components/Onboarding.tsx (2)
apps/terminal-ui/src/components/index.ts (1)
Onboarding(8-8)apps/terminal-ui/src/utils/project-state.ts (2)
ProjectStateResult(26-33)detectProjectState(40-93)
apps/cli/src/command-registry.ts (1)
apps/cli/src/commands/interactive.command.ts (1)
InteractiveCommand(25-132)
apps/terminal-ui/src/components/ErrorBoundary.tsx (2)
apps/terminal-ui/src/utils/logger.ts (2)
error(106-130)logger(323-331)apps/terminal-ui/src/utils/error-handling.ts (1)
classifyError(61-181)
apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
apps/terminal-ui/src/components/index.ts (1)
BottomStatusBar(6-6)
packages/tm-core/src/storage/file-storage/task-watcher.ts (2)
packages/tm-core/src/storage/file-storage/index.ts (7)
TaskFileAddedEvent(16-16)TaskFileChangedEvent(17-17)TaskFileDeletedEvent(18-18)TaskDirectoryChangedEvent(19-19)TaskWatcherEvent(14-14)TaskWatcherOptions(15-15)TaskWatcher(13-13)packages/tm-core/src/storage/index.ts (7)
TaskFileAddedEvent(16-16)TaskFileChangedEvent(17-17)TaskFileDeletedEvent(18-18)TaskDirectoryChangedEvent(19-19)TaskWatcherEvent(14-14)TaskWatcherOptions(15-15)TaskWatcher(13-13)
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (2)
scripts/modules/task-manager/list-tasks.js (1)
tasksWithNoDeps(152-157)apps/terminal-ui/src/utils/task-helpers.ts (1)
getComplexityDisplay(52-83)
apps/terminal-ui/src/components/TopBar.tsx (1)
apps/terminal-ui/src/components/index.ts (1)
TopBar(5-5)
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (2)
apps/terminal-ui/src/utils/task-helpers.ts (3)
getComplexityDisplay(52-83)getPriorityColor(34-47)getStatusColor(10-29)apps/terminal-ui/src/views/modals/DimmedOverlay.tsx (1)
DimmedOverlay(9-22)
apps/terminal-ui/src/utils/logger.ts (1)
apps/terminal-ui/src/utils/error-handling.ts (1)
ClassifiedError(41-56)
apps/terminal-ui/src/utils/error-handling.ts (2)
apps/terminal-ui/src/utils/logger.ts (1)
error(106-130)packages/tm-core/src/storage/file-storage/task-watcher.ts (1)
onError(421-423)
🪛 ast-grep (0.39.6)
packages/tm-core/src/storage/file-storage/task-watcher.ts
[warning] 367-367: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexPattern}$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 LanguageTool
apps/terminal-ui/terminal-ui.md
[style] ~141-~141: Try using a synonym here to elevate your writing.
Context: ...(views/sections/) Sub-components that make up the Dashboard panel: - **ProjectDashboa...
(CONSTITUTE_COMPRISE)
[uncategorized] ~312-~312: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...s - Priority Breakdown: High/Medium/Low priority counts with colored indicators **Data ...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[grammar] ~521-~521: Use a hyphen to join words.
Context: ...errors - Runtime errors Layout: Red bordered box with error message and reco...
(QB_NEW_EN_HYPHEN)
🪛 markdownlint-cli2 (0.18.1)
apps/terminal-ui/terminal-ui.md
40-40: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
45-45: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
50-50: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
58-58: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
65-65: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
70-70: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
89-89: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
128-128: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
135-135: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
140-140: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
146-146: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
151-151: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
159-159: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
163-163: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
167-167: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
213-213: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
219-219: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
225-225: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
230-230: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
234-234: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
243-243: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
270-270: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
339-339: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
381-381: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
745-745: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
768-768: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
779-779: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
786-786: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🔇 Additional comments (20)
packages/tm-core/package.json (1)
34-34: LGTM! Appropriate chokidar version added.Chokidar 4.0.3 is the latest stable release, providing minimal, efficient cross-platform file watching for the TaskWatcher feature. The version choice is current and appropriate for the file-watching capabilities introduced in this PR.
tsconfig.json (1)
28-30: LGTM! Path mappings follow established patterns.The terminal-ui path aliases are consistent with existing internal package mappings and correctly point to TypeScript source files, aligning with the project's tsdown bundling architecture.
Based on learnings
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (1)
29-187: LGTM! Well-structured modal with proper task and subtask handling.The modal correctly:
- Distinguishes between tasks and subtasks with proper ID formatting
- Resolves parent task information when displaying subtasks
- Falls back gracefully between complexity map and task-level complexity
- Provides comprehensive task details with appropriate color coding
Note: This component uses
DimmedOverlaywhich has a performance concern flagged separately.apps/terminal-ui/src/stores/taskStore.ts (1)
48-108: LGTM! Clean data abstraction with appropriate error handling.The task store implementation:
- Properly isolates data loading from UI concerns
- Handles errors gracefully by returning empty/null fallbacks
- Correctly builds the complexity map from the JSON report
- Provides useful task filtering utilities
The approach of silently returning
nullfor missing complexity reports (line 85) is appropriate since the report is optional.apps/terminal-ui/src/utils/task-helpers.ts (1)
10-83: LGTM! Well-designed helper functions with clear logic.The color mapping and display functions provide consistent styling across the terminal UI:
- Status and priority color mappings cover all expected values with sensible defaults
- Complexity conversion correctly handles both string enums and numeric values
- The dot-based complexity indicator (line 74) provides good visual feedback
apps/terminal-ui/src/utils/error-handling.ts (2)
61-181: LGTM! Comprehensive error classification with actionable recovery guidance.The error classification logic:
- Systematically categorizes errors by pattern matching on error messages
- Provides user-friendly messages tailored to each category
- Includes specific recovery actions that guide users toward resolution
- Falls back gracefully to unknown category with generic guidance
The approach of inferring category from error message content is pragmatic given the variety of error sources in the terminal UI.
321-360: LGTM! Solid retry implementation with exponential backoff.The retry logic correctly:
- Implements exponential backoff with configurable limits (line 354)
- Respects
maxDelayto prevent excessive wait times- Provides retry callbacks for observability (lines 349-351)
- Properly propagates the last error after exhausting retries (line 359)
This provides resilient error recovery for transient failures.
apps/cli/src/command-registry.ts (2)
17-17: LGTM!The import follows the established pattern and conventions used by other command imports in this file.
75-81: Well-structured command registration.The new "interactive" command is properly registered with clear metadata and appropriate categorization. The dedicated "UI Commands" section improves code organization.
packages/tm-core/src/storage/index.ts (1)
11-20: LGTM!The TaskWatcher exports follow established patterns and are well-organized. The use of
typekeyword for type-only exports is correct and helps with tree-shaking.apps/cli/src/index.ts (1)
15-15: LGTM!The export is correctly placed with other command exports and follows the established pattern.
apps/terminal-ui/src/components/TopBar.tsx (1)
12-37: Component implementation looks solid.The TopBar component is well-structured with proper TypeScript typing, clear conditional rendering, and appropriate use of Ink components.
packages/tm-core/src/storage/file-storage/index.ts (1)
12-20: LGTM!The TaskWatcher exports are well-organized and follow the established export pattern. The two-level export structure (file-storage/index.ts → storage/index.ts) provides a clean public API.
apps/terminal-ui/src/views/panels/HelpPanel.tsx (1)
12-56: Clean and well-structured component.The HelpPanel is properly implemented with clear organization, appropriate styling, and good use of Ink components.
apps/terminal-ui/src/components/index.ts (1)
1-15: LGTM!The barrel export file is well-organized and follows best practices for component module exports. Type-only exports are correctly marked with the
typekeyword.apps/terminal-ui/tsconfig.json (1)
1-49: Path aliases are already properly configured and inherited.The root
tsconfig.jsoncontains the required@tm/terminal-uipath mappings, and sinceapps/terminal-ui/tsconfig.jsonextends the root config, these aliases are automatically available. No additional configuration is needed in this file.apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
14-29: LGTM — clean, focused status bar.Props and conditional controls look good; Ink usage is idiomatic.
Also applies to: 31-41
apps/terminal-ui/terminal-ui.md (1)
45-49: Verified: dependency versions are correctly documented and match package.json.All versions mentioned (React 18.3.1, Ink 5.1.0, marked, string-width, Vitest, Biome, etc.) are present and match their package.json entries. No version drift detected.
apps/terminal-ui/src/index.tsx (1)
262-271: No changes needed; logger.fatal existsVerification confirms that the
fatalmethod is implemented as a public method inapps/terminal-ui/src/utils/logger.ts(line 135) with the correct signature matching the usage in the ErrorBoundary. The code is correct as-is.apps/terminal-ui/src/utils/project-state.ts (1)
71-79: Counting only master.tasks may mislabel projects with tasks in other tagsIf tasks live under non-master tags, state becomes NO_TASKS. Consider counting across all tags or falling back to sum of all tag arrays.
- // Count tasks in the master tag (or default tag) - const masterTasks = tasksData.master?.tasks || tasksData.tasks || []; - result.taskCount = Array.isArray(masterTasks) ? masterTasks.length : 0; + // Count tasks across known locations (master tag, default, or aggregate) + const masterTasks = tasksData.master?.tasks || tasksData.tasks || []; + const tagTaskCounts = + tasksData.tags && typeof tasksData.tags === 'object' + ? Object.values(tasksData.tags).reduce((acc: number, t: any) => { + const arr = t?.tasks || []; + return acc + (Array.isArray(arr) ? arr.length : 0); + }, 0) + : 0; + const counted = Array.isArray(masterTasks) ? masterTasks.length : 0; + result.taskCount = Math.max(counted, tagTaskCounts);⛔ Skipped due to learnings
Learnt from: CR PR: eyaltoledano/claude-task-master#0 File: .cursor/rules/tasks.mdc:0-0 Timestamp: 2025-07-18T17:14:29.399Z Learning: Applies to scripts/modules/task-manager.js : The default tag 'master' must be used for all existing and new tasks unless otherwise specified.
| .option('-p, --project <path>', 'Project root directory', process.cwd()) | ||
| .action(async (options: InteractiveCommandOptions) => { |
There was a problem hiding this comment.
Fix --project default; don’t treat it as explicitly provided
Providing a default for --project makes options.project always truthy, which:
- Forces explicitProject true, altering onboarding behavior.
- Triggers “path does not exist” check even when user didn’t pass the flag.
- .option('-p, --project <path>', 'Project root directory', process.cwd())
+ .option('-p, --project <path>', 'Project root directory')- const projectPath = options.project
- ? resolve(options.project)
- : process.cwd();
+ const projectPath = options.project ? resolve(options.project) : process.cwd();- explicitProject: !!options.project // Flag to indicate --project was used
+ explicitProject: options.project !== undefined // True only if user passed --projectAlso applies to: 84-89, 57-68
🤖 Prompt for AI Agents
In apps/cli/src/commands/interactive.command.ts around lines 34-35 (and also
apply same change to 57-68 and 84-89), the --project option currently provides
process.cwd() as a default which makes options.project always truthy and
prevents detecting whether the user explicitly passed the flag; remove the
default from the .option(...) declaration so options.project is undefined when
not provided, then update downstream logic to (a) treat explicitProject as
options.project !== undefined (not truthy) and (b) only run the "path exists"
validation when options.project !== undefined, and if you need a runtime project
path use process.cwd() as the fallback where appropriate.
| "main": "./dist/index.js", | ||
| "types": "./src/index.tsx", | ||
| "exports": { | ||
| ".": "./src/index.tsx" | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Package metadata polish: align with monorepo conventions and harden consumer experience.
- exports → TS source is fine in this repo if bundling from source; verify all consumers rely on tsdown/path mapping. Otherwise Node will try to load TSX at runtime.
- Use workspace ranges for internal deps.
- Avoid empty version; some tooling chokes on it.
- Consider moving react to peerDependencies (with matching devDependency) to prevent duplicate Reacts.
Proposed changes:
"main": "./dist/index.js",
- "types": "./src/index.tsx",
+ "types": "./src/index.tsx",
"exports": {
- ".": "./src/index.tsx"
+ ".": "./src/index.tsx"
},
@@
- "dependencies": {
- "@tm/cli": "*",
- "@tm/core": "*",
+ "dependencies": {
+ "@tm/cli": "workspace:*",
+ "@tm/core": "workspace:*",
"ink": "^5.1.0",
@@
- "version": ""
+ "version": "0.0.0"
+,
+ "peerDependencies": {
+ "react": "^18.3.0"
+ },
+ "devDependencies": {
+ "react": "^18.3.1",
+ // ... keep existing dev deps
+ }Based on learnings.
Also applies to: 25-33, 57-63
🤖 Prompt for AI Agents
In apps/terminal-ui/package.json around lines 6-10 (and similarly 25-33, 57-63)
the package metadata is unsafe for consumers: the "exports" currently points to
TSX source (which will break Node runtime), "types" points to source, internal
dependencies lack workspace ranges, version is empty and react is a regular
dependency. Update exports to target the compiled entry (e.g., ./dist/index.js)
and ensure "types" points to generated .d.ts in dist (not ./src/*.tsx); replace
internal dependency versions with workspace: ranges; set a valid non-empty
version string; move react from dependencies to peerDependencies and keep it in
devDependencies for testing/build. Also verify consumers/build tooling rely on
source-mapped TS and, if you must expose source, add a separate "exports"
conditional for "import:./src/index.tsx" only if you guarantee transpilation in
consumers.
| export interface ErrorBoundaryProps { | ||
| /** Child components */ | ||
| children: ReactNode; | ||
| /** Fallback component to render on error */ | ||
| fallback?: ReactNode; | ||
| /** Callback when error occurs */ | ||
| onError?: (error: Error, errorInfo: React.ErrorInfo) => void; | ||
| /** Whether to show technical details */ | ||
| showDetails?: boolean; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Fallback as a render function to receive the error
fallback: ReactNode prevents custom fallbacks from accessing the error. Prefer a render prop or component receiving { error, errorInfo }.
-export interface ErrorBoundaryProps {
+export interface ErrorBoundaryProps {
/** Child components */
children: ReactNode;
- /** Fallback component to render on error */
- fallback?: ReactNode;
+ /** Fallback to render on error */
+ fallback?: ReactNode | ((error: Error, info: React.ErrorInfo | null) => ReactNode);And in render():
- if (this.props.fallback) {
- return this.props.fallback;
- }
+ if (this.props.fallback) {
+ return typeof this.props.fallback === 'function'
+ ? this.props.fallback(this.state.error, this.state.errorInfo)
+ : this.props.fallback;
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/terminal-ui/src/components/ErrorBoundary.tsx around lines 14 to 23, the
fallback prop is typed as ReactNode which prevents custom fallbacks from
receiving the caught error and errorInfo; change the fallback prop to accept
either a ReactNode or a render function/component that receives { error,
errorInfo } (e.g., ReactNode | ((props: { error: Error | null; errorInfo?:
React.ErrorInfo | null }) => ReactNode) or React.ComponentType<{ error: Error |
null; errorInfo?: React.ErrorInfo | null }>), update the prop JSDoc accordingly,
and modify the component render logic to detect if fallback is a function or
component and call it with { error, errorInfo } (fall back to rendering the
ReactNode directly if not a function/component).
| componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { | ||
| // Classify and log the error | ||
| const classified = classifyError(error); | ||
| logger.error('React component error', error, { | ||
| componentStack: errorInfo.componentStack, | ||
| category: classified.category, | ||
| severity: classified.severity | ||
| }); | ||
|
|
||
| // Update state with error info | ||
| this.setState({ errorInfo }); | ||
|
|
||
| // Call onError callback if provided | ||
| if (this.props.onError) { | ||
| this.props.onError(error, errorInfo); | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Avoid duplicate logging when onError is provided
componentDidCatch logs via logger.error and then invokes onError (which in App logs fatal), generating duplicate entries. Gate internal logging when a custom onError is supplied.
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
- // Classify and log the error
- const classified = classifyError(error);
- logger.error('React component error', error, {
- componentStack: errorInfo.componentStack,
- category: classified.category,
- severity: classified.severity
- });
+ // Classify (for context) and optionally log
+ const classified = classifyError(error);
+ if (!this.props.onError) {
+ logger.error('React component error', error, {
+ componentStack: errorInfo.componentStack,
+ category: classified.category,
+ severity: classified.severity
+ });
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { | |
| // Classify and log the error | |
| const classified = classifyError(error); | |
| logger.error('React component error', error, { | |
| componentStack: errorInfo.componentStack, | |
| category: classified.category, | |
| severity: classified.severity | |
| }); | |
| // Update state with error info | |
| this.setState({ errorInfo }); | |
| // Call onError callback if provided | |
| if (this.props.onError) { | |
| this.props.onError(error, errorInfo); | |
| } | |
| } | |
| componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { | |
| // Classify (for context) and optionally log | |
| const classified = classifyError(error); | |
| if (!this.props.onError) { | |
| logger.error('React component error', error, { | |
| componentStack: errorInfo.componentStack, | |
| category: classified.category, | |
| severity: classified.severity | |
| }); | |
| } | |
| // Update state with error info | |
| this.setState({ errorInfo }); | |
| // Call onError callback if provided | |
| if (this.props.onError) { | |
| this.props.onError(error, errorInfo); | |
| } | |
| } |
🤖 Prompt for AI Agents
In apps/terminal-ui/src/components/ErrorBoundary.tsx around lines 59 to 75, the
componentDidCatch implementation always logs errors via logger.error and then
calls this.props.onError (which in App also logs), causing duplicate entries;
update componentDidCatch to only perform internal logger.error when no onError
prop is provided (or provide a boolean flag like suppressInternalLogging when
onError exists), otherwise skip or reduce internal logging and still set state
and invoke the onError callback so the host app can control final
logging/handling.
| **Layout Logic** (`DashboardPanel.tsx:32-62`): | ||
| ```typescript | ||
| if (maximizedSection === 'project') { | ||
| return <ProjectDashboardSection />; // Full height | ||
| } | ||
| if (maximizedSection === 'dependency') { | ||
| return <DependencyStatusSection />; // Full height | ||
| } | ||
| if (maximizedSection === 'tasklist') { | ||
| return <TaskListSection maxHeight={contentHeight} />; // Full height | ||
| } | ||
| // Default: Show all sections in 2-row layout | ||
| ``` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Avoid brittle file path and line-number references; link to canonical docs instead.
Inline paths/line numbers in docs rot quickly. Replace with stable references on the documentation site and high-level pointers (e.g., “see DashboardPanel layout section”). As per coding guidelines, prefer https://docs.task-master.dev over local paths.
As per coding guidelines
Also applies to: 405-425, 739-758
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
270-270: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🤖 Prompt for AI Agents
In apps/terminal-ui/terminal-ui.md around lines 269 to 281 (also apply same
change to ranges 405-425 and 739-758), the doc uses brittle local file paths and
explicit line numbers; replace those inline references with stable links to the
canonical documentation (prefer https://docs.task-master.dev) and a short
high-level pointer such as “see DashboardPanel layout section” or “see Layout >
DashboardPanel” so the guide points to the canonical docs rather than local
paths/line numbers; update the three mentioned ranges accordingly and ensure
wording follows the coding guidelines for documentation links.
| "compilerOptions": { | ||
| "target": "ES2022", | ||
| "module": "NodeNext", | ||
| "lib": ["ES2022"], | ||
| "jsx": "react", | ||
| "jsxFactory": "React.createElement", | ||
| "jsxFragmentFactory": "React.Fragment", | ||
| "declaration": true, | ||
| "declarationMap": true, | ||
| "sourceMap": true, | ||
| "outDir": "./dist", | ||
| "baseUrl": ".", | ||
| "rootDir": "./src", | ||
| "strict": true, | ||
| "noImplicitAny": true, | ||
| "strictNullChecks": true, | ||
| "strictFunctionTypes": true, | ||
| "strictBindCallApply": true, | ||
| "strictPropertyInitialization": true, | ||
| "noImplicitThis": true, | ||
| "alwaysStrict": true, | ||
| "noUnusedLocals": true, | ||
| "noUnusedParameters": true, | ||
| "noImplicitReturns": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| "esModuleInterop": true, | ||
| "skipLibCheck": true, | ||
| "forceConsistentCasingInFileNames": true, | ||
| "moduleResolution": "NodeNext", | ||
| "moduleDetection": "force", | ||
| "types": ["node"], | ||
| "resolveJsonModule": true, | ||
| "isolatedModules": true, | ||
| "allowImportingTsExtensions": false |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider removing redundant compiler option.
Line 36 sets allowImportingTsExtensions: false, which is TypeScript's default. Unless there's a specific reason to explicitly document this setting, consider removing it to reduce configuration noise.
🤖 Prompt for AI Agents
In apps/terminal-ui/tsconfig.json around lines 3 to 36, the compiler option
"allowImportingTsExtensions": false is redundant since false is TypeScript's
default; remove that key-value pair from the "compilerOptions" object to reduce
noise and keep configuration concise, ensuring trailing commas and JSON validity
are preserved after deletion.
| async close(): Promise<void> { | ||
| // Stop watching if active | ||
| if (this.taskWatcher?.getIsWatching()) { | ||
| await this.stopWatching(); | ||
| } | ||
| await this.fileOps.cleanup(); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Watcher lifecycle is sound; consider exposing an unsubscribe helper.
start/stop and close() integration look good. Add a convenience offTaskFileChange(handler) to mirror onTaskFileChange.
onTaskFileChange(handler: (event: TaskWatcherEvent) => void): void {
if (!this.taskWatcher) {
throw new Error('TaskWatcher not initialized');
}
this.taskWatcher.onChange(handler);
}
+
+ /**
+ * Remove a previously registered file change handler
+ */
+ offTaskFileChange(handler: (event: TaskWatcherEvent) => void): void {
+ if (!this.taskWatcher) {
+ throw new Error('TaskWatcher not initialized');
+ }
+ this.taskWatcher.off('change', handler as any);
+ }Also applies to: 674-701
🤖 Prompt for AI Agents
In packages/tm-core/src/storage/file-storage/file-storage.ts around lines 46-51
(and also apply same change around lines 674-701), add a public
offTaskFileChange(handler) method to mirror the existing
onTaskFileChange(handler): implement it to remove/unsubscribe the provided
handler from the watcher (delegate to the taskWatcher unsubscribe/removeListener
API or to the underlying fileOps/event emitter used by onTaskFileChange), ensure
it safely no-ops if no watcher is present, and export/declare the method
alongside the existing onTaskFileChange so callers can unregister handlers
cleanly.
| public error( | ||
| message: string, | ||
| error?: Error | ClassifiedError, | ||
| context?: Record<string, unknown> | ||
| ): void { | ||
| const entry: LogEntry = { | ||
| timestamp: new Date(), | ||
| level: LogLevel.ERROR, | ||
| message, | ||
| context, | ||
| error: error instanceof Error ? error : undefined | ||
| }; | ||
|
|
||
| if (error && 'category' in error) { | ||
| // ClassifiedError | ||
| entry.context = { | ||
| ...entry.context, | ||
| category: error.category, | ||
| severity: error.severity, | ||
| recoveryActions: error.recoveryActions | ||
| }; | ||
| } | ||
|
|
||
| this.write(entry); |
There was a problem hiding this comment.
ClassifiedError loses its underlying Error and useful fields.
You drop the original error and omit message/technicalDetails/timestamp in context.
Apply this to retain full diagnostic info:
@@ public error(
- const entry: LogEntry = {
+ const entry: LogEntry = {
timestamp: new Date(),
level: LogLevel.ERROR,
message,
context,
- error: error instanceof Error ? error : undefined
+ error: error instanceof Error ? error : (error ? (error as ClassifiedError).error : undefined)
};
@@
- if (error && 'category' in error) {
+ if (error && 'category' in (error as any)) {
// ClassifiedError
- entry.context = {
+ const ce = error as ClassifiedError;
+ entry.context = {
...entry.context,
- category: error.category,
- severity: error.severity,
- recoveryActions: error.recoveryActions
+ category: ce.category,
+ severity: ce.severity,
+ recoveryActions: ce.recoveryActions,
+ message: ce.message,
+ technicalDetails: ce.technicalDetails,
+ classifiedAt: ce.timestamp?.toISOString?.() ?? undefined
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public error( | |
| message: string, | |
| error?: Error | ClassifiedError, | |
| context?: Record<string, unknown> | |
| ): void { | |
| const entry: LogEntry = { | |
| timestamp: new Date(), | |
| level: LogLevel.ERROR, | |
| message, | |
| context, | |
| error: error instanceof Error ? error : undefined | |
| }; | |
| if (error && 'category' in error) { | |
| // ClassifiedError | |
| entry.context = { | |
| ...entry.context, | |
| category: error.category, | |
| severity: error.severity, | |
| recoveryActions: error.recoveryActions | |
| }; | |
| } | |
| this.write(entry); | |
| public error( | |
| message: string, | |
| error?: Error | ClassifiedError, | |
| context?: Record<string, unknown> | |
| ): void { | |
| const entry: LogEntry = { | |
| timestamp: new Date(), | |
| level: LogLevel.ERROR, | |
| message, | |
| context, | |
| error: error instanceof Error ? error : (error ? (error as ClassifiedError).error : undefined) | |
| }; | |
| if (error && 'category' in (error as any)) { | |
| // ClassifiedError | |
| const ce = error as ClassifiedError; | |
| entry.context = { | |
| ...entry.context, | |
| category: ce.category, | |
| severity: ce.severity, | |
| recoveryActions: ce.recoveryActions, | |
| message: ce.message, | |
| technicalDetails: ce.technicalDetails, | |
| classifiedAt: ce.timestamp?.toISOString?.() ?? undefined | |
| }; | |
| } | |
| this.write(entry); |
🤖 Prompt for AI Agents
In apps/terminal-ui/src/utils/logger.ts around lines 106 to 129, the current
error() handler discards the underlying Error when a ClassifiedError is provided
and omits valuable fields (message, technicalDetails, timestamp) from context;
update the logic so that if error has classified properties you still set
entry.error to the underlying Error (or keep error if it's an Error), and merge
the ClassifiedError fields into entry.context including message,
technicalDetails, and timestamp (preserving any existing context), and ensure
timestamp is populated (use classified timestamp if present or preserve
entry.timestamp) before calling this.write(entry).
| private formatEntry(entry: LogEntry, includeStack: boolean): string { | ||
| let output = ''; | ||
|
|
||
| // Timestamp | ||
| if (this.config.timestamps) { | ||
| output += `[${entry.timestamp.toISOString()}] `; | ||
| } | ||
|
|
||
| // Level | ||
| output += `[${entry.level.toUpperCase()}] `; | ||
|
|
||
| // Message | ||
| output += entry.message; | ||
|
|
||
| // Context | ||
| if (this.config.includeContext && entry.context) { | ||
| output += ` | Context: ${JSON.stringify(entry.context)}`; | ||
| } | ||
|
|
||
| // Error stack | ||
| if (includeStack && entry.error && entry.error.stack) { | ||
| output += `\nStack: ${entry.error.stack}`; | ||
| } | ||
|
|
||
| return output; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
JSON.stringify(context) can throw on circulars; also noisy.
Guard serialization to avoid crashes and reduce log noise.
Example:
@@ private formatEntry
- if (this.config.includeContext && entry.context) {
- output += ` | Context: ${JSON.stringify(entry.context)}`;
- }
+ if (this.config.includeContext && entry.context) {
+ const safe = safeStringify(entry.context);
+ output += ` | Context: ${safe}`;
+ }
+ }
+
+ // below the class methods
+ function safeStringify(obj: unknown): string {
+ const seen = new WeakSet();
+ try {
+ return JSON.stringify(obj, (_k, v) => {
+ if (typeof v === 'object' && v !== null) {
+ if (seen.has(v as object)) return '[Circular]';
+ seen.add(v as object);
+ }
+ if (v instanceof Error) {
+ return { name: v.name, message: v.message, stack: v.stack };
+ }
+ return v;
+ });
+ } catch {
+ return '[Unserializable Context]';
+ }
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/terminal-ui/src/utils/logger.ts around lines 246 to 271, the current
JSON.stringify(entry.context) can throw on circular references and produces
noisy, unbounded output; replace it with a safe, bounded serializer: wrap
serialization in a try/catch and on success include the serialized context,
otherwise fall back to a concise placeholder (e.g., "[unserializable context]");
additionally limit verbosity by truncating long strings or by using a
replacer/depth-limiting strategy (or a small safeStringify helper that handles
circular refs and max length) so logs never crash the logger and remain compact.
| try { | ||
| this.watcher = watch(this.watchPath, { | ||
| ignored: this.options.ignorePatterns, | ||
| ignoreInitial: this.options.ignoreInitial, | ||
| persistent: this.options.persistent, | ||
| depth: 10, | ||
| awaitWriteFinish: { | ||
| stabilityThreshold: 200, | ||
| pollInterval: 100 | ||
| } | ||
| }); | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
Confirm chokidar v4 glob semantics for 'ignored'.
If v4 removed glob expansion, passing string globs here may not behave as expected. Using the function form (above) is future‑proof.
Based on learnings.
🌐 Web query:
Is glob pattern support removed in chokidar v4 for the 'ignored' option and watcher paths? What is the recommended way to filter files in v4?
💡 Result:
Yes — v4 removed built-in glob support for both watch paths and the ignored option. Use a filter (ignored as a function or RegExp) or expand globs yourself and pass an array of concrete paths. Example approaches shown in the v4 docs:
- Use ignored as a function/RegExp to include only desired files.
- Or run a glob (e.g., node:fspromises glob, tinyglobby, fast-glob) and pass the resulting file list to chokidar.watch (or unwatch those paths later). [1][2]
Sources: chokidar v4 README / upgrading notes and migration examples. [1] https://www.npmjs.com/package/chokidar/v/4.0.0 [2] Migration writeup / examples. https://dev.to/43081j/migrating-from-chokidar-3x-to-4x
I need to verify the actual type and usage of ignorePatterns in the codebase to confirm whether the code needs fixing.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find where ignorePatterns is defined/used in task-watcher context
rg -n "ignorePatterns" packages/tm-core/src/storage/file-storage/ -A2 -B2
Length of output: 1325
🏁 Script executed:
#!/bin/bash
# Search for type definitions of options object
rg -n "interface.*[Oo]ptions|type.*[Oo]ptions" packages/tm-core/src/storage/file-storage/task-watcher.ts -A5 -B1Length of output: 166
🏁 Script executed:
#!/bin/bash
# Find where TaskWatcher is instantiated to see ignorePatterns values
rg -n "new TaskWatcher|TaskWatcher\(" packages/tm-core/src -A3 -B1 | head -50Length of output: 928
🏁 Script executed:
#!/bin/bash
# Check chokidar version in package.json
fd -n "package.json" packages/tm-core -x cat {} \; | grep -A2 -B2 "chokidar"Length of output: 230
Convert string glob patterns to function or RegExp for chokidar v4 compatibility.
Chokidar v4 removes glob support for the ignored option. The code passes ignorePatterns (a string[] of glob patterns like '**/node_modules/**') directly to the ignored option, which will not work in v4. Use a function or RegExp instead. For example:
ignored: (path, stats) => this.options.ignorePatterns.some(pattern =>
// Use minimatch or similar to match glob patterns
)Or pre-expand globs at initialization time and pass concrete file paths.
🤖 Prompt for AI Agents
In packages/tm-core/src/storage/file-storage/task-watcher.ts around lines 139 to
150, the watcher currently passes a string[] of glob patterns to chokidar's
ignored option which is incompatible with chokidar v4; replace this by
converting the patterns into a matcher function or RegExp before calling watch
(e.g. import and use minimatch or precompile minimatch.makeRe for each glob) and
set ignored to a function that returns true when any compiled pattern matches
the incoming path (ensure dot files, negation, and options used match previous
behavior); add the necessary minimatch import and initialization so the watch
call uses the function/RegExp-compatible matcher instead of raw glob strings.
| await new Promise<void>((resolve, reject) => { | ||
| if (!this.watcher) { | ||
| reject(new Error('Watcher failed to initialize')); | ||
| return; | ||
| } | ||
|
|
||
| this.watcher.once('ready', () => { | ||
| this.isWatching = true; | ||
| this.emit('ready'); | ||
| resolve(); | ||
| }); | ||
|
|
||
| const timeout = setTimeout(() => { | ||
| reject(new Error('Watcher initialization timeout')); | ||
| }, 10000); | ||
|
|
||
| this.watcher.once('ready', () => clearTimeout(timeout)); | ||
| }); |
There was a problem hiding this comment.
start() never rejects on watcher 'error' before 'ready'.
If chokidar emits error early, you wait until timeout. Hook error to reject and clear the timeout.
Apply:
@@
- this.watcher.once('ready', () => {
+ const onReady = () => {
this.isWatching = true;
this.emit('ready');
- resolve();
- });
+ clearTimeout(timeout);
+ this.watcher?.off('error', onError);
+ resolve();
+ };
+ const onError = (err: Error) => {
+ clearTimeout(timeout);
+ this.watcher?.off('ready', onReady);
+ reject(err);
+ };
+ this.watcher.once('ready', onReady);
+ this.watcher.once('error', onError);
@@
- this.watcher.once('ready', () => clearTimeout(timeout));
+ // timeout cleared in onReady/onError📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await new Promise<void>((resolve, reject) => { | |
| if (!this.watcher) { | |
| reject(new Error('Watcher failed to initialize')); | |
| return; | |
| } | |
| this.watcher.once('ready', () => { | |
| this.isWatching = true; | |
| this.emit('ready'); | |
| resolve(); | |
| }); | |
| const timeout = setTimeout(() => { | |
| reject(new Error('Watcher initialization timeout')); | |
| }, 10000); | |
| this.watcher.once('ready', () => clearTimeout(timeout)); | |
| }); | |
| await new Promise<void>((resolve, reject) => { | |
| if (!this.watcher) { | |
| reject(new Error('Watcher failed to initialize')); | |
| return; | |
| } | |
| const onReady = () => { | |
| this.isWatching = true; | |
| this.emit('ready'); | |
| clearTimeout(timeout); | |
| this.watcher?.off('error', onError); | |
| resolve(); | |
| }; | |
| const onError = (err: Error) => { | |
| clearTimeout(timeout); | |
| this.watcher?.off('ready', onReady); | |
| reject(err); | |
| }; | |
| this.watcher.once('ready', onReady); | |
| this.watcher.once('error', onError); | |
| const timeout = setTimeout(() => { | |
| reject(new Error('Watcher initialization timeout')); | |
| }, 10000); | |
| // timeout cleared in onReady/onError | |
| }); |
🤖 Prompt for AI Agents
In packages/tm-core/src/storage/file-storage/task-watcher.ts around lines 159 to
176, the Promise waits for 'ready' or timeout but ignores early 'error' events
from chokidar; wire the watcher's 'error' event to immediately reject the
Promise (and clear the timeout) and remove any listeners to avoid duplicate
resolves/rejects or leaks. Specifically: add a one-time 'error' listener that
clears the timeout, rejects with the error, and removes the 'ready' listener;
ensure the existing 'ready' handler clears the timeout and removes the 'error'
listener when resolving; and keep the timeout reject as fallback.
| private matchesFilePatterns(filePath: string): boolean { | ||
| const relativePath = relative(this.watchPath, filePath); | ||
|
|
||
| for (const pattern of this.options.filePatterns) { | ||
| // Use placeholder to prevent ** from being affected by * replacement | ||
| const regexPattern = pattern | ||
| .replace(/\./g, '\\.') | ||
| .replace(/\*\*/g, '___DBLSTAR___') | ||
| .replace(/\*/g, '[^/]*') | ||
| .replace(/___DBLSTAR___/g, '.*'); | ||
|
|
||
| const regex = new RegExp(`^${regexPattern}$`); | ||
| if (regex.test(relativePath)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
Glob matching is OS‑fragile and potentially vulnerable; switch to picomatch with POSIX normalization.
- relative() yields backslashes on Windows, so patterns like **/*.json won’t match.
- Hand‑rolled regex with .* risks ReDoS and subtle mismatches.
Replace custom regex with picomatch and normalize to POSIX:
@@
-import { normalize, relative } from 'node:path';
+import { normalize, relative, sep } from 'node:path';
+import picomatch from 'picomatch';
@@ export class TaskWatcher extends EventEmitter {
- private options: Required<TaskWatcherOptions>;
+ private options: Required<TaskWatcherOptions>;
+ private fileMatchers: Array<(p: string) => boolean> = [];
+ private ignoreMatchers: Array<(p: string) => boolean> = [];
@@ constructor
this.options = {
@@
};
+ const toPosix = (p: string) => p.replace(/\\/g, '/');
+ this.fileMatchers = this.options.filePatterns.map((p) => picomatch(p, { posix: true }));
+ this.ignoreMatchers = this.options.ignorePatterns.map((p) => picomatch(p, { posix: true }));
@@ private matchesFilePatterns(filePath: string): boolean {
- const relativePath = relative(this.watchPath, filePath);
-
- for (const pattern of this.options.filePatterns) {
- // Use placeholder to prevent ** from being affected by * replacement
- const regexPattern = pattern
- .replace(/\./g, '\\.')
- .replace(/\*\*/g, '___DBLSTAR___')
- .replace(/\*/g, '[^/]*')
- .replace(/___DBLSTAR___/g, '.*');
-
- const regex = new RegExp(`^${regexPattern}$`);
- if (regex.test(relativePath)) {
- return true;
- }
- }
- return false;
+ const rel = relative(this.watchPath, filePath).replace(/\\/g, '/');
+ return this.fileMatchers.some((m) => m(rel));
}Also pass ignored as a function to avoid glob dependency:
@@ public async start(): Promise<void> {
- this.watcher = watch(this.watchPath, {
- ignored: this.options.ignorePatterns,
+ this.watcher = watch(this.watchPath, {
+ ignored: (p: string) => this.ignoreMatchers.some((m) => m(relative(this.watchPath, p).replace(/\\/g,'/'))),Based on learnings.
Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 ast-grep (0.39.6)
[warning] 367-367: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexPattern}$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
|
@claude review this PR please! |
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
@mwguerra did a very quick pass through, its looking good, give it a quick rebase so I can review again |
Crunchyman-ralph
left a comment
There was a problem hiding this comment.
have to go through it, but here's a first brush
| > | ||
| <Text wrap="truncate-end" dimColor> | ||
| {dimensions.width}x{dimensions.height} | {panelText} | ||
| {controls && ` | ${controls}`} |
There was a problem hiding this comment.
| {controls && ` | ${controls}`} | |
| {getControls()} |
1b822d8 to
0be8669
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (37)
apps/terminal-ui/terminal-ui.md (4)
89-124: Add language to project tree fence and surround with blank lines (MD040/MD031).Use a language hint like text and ensure an empty line before/after the fence.
-``` +```text apps/terminal-ui/ ├── src/ ... -``` +```
126-141: Headings/fence spacing: insert blank lines before/after headings and code fences (MD022/MD031).Apply uniformly across the doc to satisfy markdownlint.
-### Architectural Layers -#### 1. **Entry Point** (`index.tsx`) +### Architectural Layers + +#### 1. **Entry Point** (`index.tsx`)-**Layout Logic** (`DashboardPanel.tsx:32-62`): -```typescript +**Layout Logic**: + +```typescriptAlso applies to: 269-281, 381-395, 745-748
269-281: Avoid brittle file paths/line numbers; link to canonical docs per repo guidelines.Replace local paths like views/…:L32–62 with stable references to the docs site (e.g., “see DashboardPanel layout section”).
As per coding guidelines
-**Layout Logic** (`DashboardPanel.tsx:32-62`): +See “DashboardPanel layout” in the official docs: https://docs.task-master.dev/terminal-ui/dashboard-**File**: `apps/cli/src/commands/interactive.command.ts` +See CLI “Interactive” command docs: https://docs.task-master.dev/cli/interactiveAlso applies to: 739-758
521-522: Hyphenate compound adjective (“Red‑bordered box”).Minor wording polish.
-**Layout**: Red bordered box with error message and recovery actions +**Layout**: Red-bordered box with error message and recovery actionsapps/terminal-ui/package.json (1)
6-10: Align package metadata with repo conventions; harden consumer experience.
- Use workspace:* for internal deps.
- Set a non-empty version (e.g., 0.0.0).
- Move react to peerDependencies (+ keep in devDependencies).
- Consider pointing exports to built files if consumers import at runtime; or explicitly confirm tsdown bundles from source.
- Ensure types point to emitted d.ts or rely solely on typesVersions.
Based on learnings
"main": "./dist/index.js", - "types": "./src/index.tsx", + "types": "./dist/index.d.ts", - "exports": { - ".": "./src/index.tsx" - }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, @@ "dependencies": { - "@tm/cli": "*", - "@tm/core": "*", + "@tm/cli": "workspace:*", + "@tm/core": "workspace:*", "ink": "^5.1.0", @@ - "react": "^18.3.1", + "react": "^18.3.1", @@ - "version": "" + "version": "0.0.0", + "peerDependencies": { + "react": "^18.3.0" + }If you intentionally expose TS source via exports for bundling, keep the current map but add a clear comment and ensure no runtime consumer imports this package unbundled.
#!/bin/bash # 1) Check for runtime imports of @tm/terminal-ui (risk if exports -> src) rg -n --hidden -P "from ['\"]@tm/terminal-ui['\"]" -g '!**/dist/**' # 2) Confirm TypeScript types resolution doesn't rely on @types/marked-terminal v6 for v7 lib jq -r '.devDependencies["@types/marked-terminal"] // "absent"' apps/terminal-ui/package.json echo "Check if marked-terminal v7 bundles its own types:" echo "If not, @types/marked-terminal may mismatch major version; consider removing or updating."Also applies to: 25-33, 57-63
apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx (2)
169-174: Unify colors with shared mapping; use gray for remainder.Align with getStatusColor used elsewhere and avoid yellow remainder conflicting with pending.
- <Text key="in-progress" color="blue"> + <Text key="in-progress" color="yellow"> ... - <Text key="review" color="magenta"> + <Text key="review" color="cyan"> ... - <Text key="pending" color="yellow"> + <Text key="pending" color="blue"> ... - <Text key="blocked" color="red"> + <Text key="blocked" color="magenta"> ... - // Fill any remaining space with yellow empty blocks + // Fill any remaining space with neutral gray empty blocks if (charsUsed < width) { segments.push( - <Text key="remaining" color="yellow"> + <Text key="remaining" color="gray">And in the legends:
- In Progress: <Text color="blue">{inProgressTasks}</Text> Review:{' '} - <Text color="magenta">{reviewTasks}</Text> Pending:{' '} - <Text color="yellow">{pendingTasks}</Text> Blocked:{' '} - <Text color="red">{blockedTasks}</Text> + In Progress: <Text color="yellow">{inProgressTasks}</Text> Review:{' '} + <Text color="cyan">{reviewTasks}</Text> Pending:{' '} + <Text color="blue">{pendingTasks}</Text> Blocked:{' '} + <Text color="magenta">{blockedTasks}</Text>Also applies to: 183-188, 197-202, 211-216, 219-226, 284-287, 313-317
103-114: Handle empty status breakdown; show neutral bar or “No tasks”.Empty objects are truthy, so totals=0 still render segmented bar. Detect empty breakdown and fall back.
- if (!statusBreakdown) { + if (!statusBreakdown || Object.keys(statusBreakdown).length === 0) { const filled = Math.round((completionPercentage / 100) * width); const empty = width - filled; return ( <Text> <Text color="green">{'█'.repeat(filled)}</Text> <Text color="gray">{'░'.repeat(empty)}</Text> </Text> ); } + if (completionPercentage === 0 && Object.keys(statusBreakdown).length === 0) { + return <Text color="gray">No tasks</Text>; + }apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (1)
19-21: Exclude completed tasks from “no dependencies” metric.Align with the reference logic by excluding tasks with status 'done' or 'completed'.
Apply:
- const tasksWithNoDeps = tasks.filter( - (t) => !t.dependencies || t.dependencies.length === 0 - ).length; + const tasksWithNoDeps = tasks.filter( + (t) => + t.status !== 'done' && + t.status !== 'completed' && + (!t.dependencies || t.dependencies.length === 0) + ).length;Based on learnings.
apps/terminal-ui/src/components/StatusBar.tsx (5)
424-435: Match exit hint with TopBar: include “q”.Show both keys to reduce confusion.
Apply:
- <Text dimColor> - Press <Text color="yellow" bold>{exitKeyText}</Text> to exit - </Text> + <Text dimColor> + Press <Text color="yellow" bold>{exitKeyText}</Text> or <Text color="yellow" bold>q</Text> to exit + </Text>
113-165: Break core re-init loop; reuse instance and narrow deps.Remove 'core' from deps, reuse existing state, and only close instances created here.
Apply:
useEffect(() => { let mounted = true; + let createdHere = false; const fetchTaskStats = async () => { @@ - let coreInstance = tmCore; - if (!coreInstance) { + let coreInstance = tmCore ?? core; + if (!coreInstance) { coreInstance = await createTaskMasterCore({ projectPath }); - if (mounted) { - setCore(coreInstance); - } + createdHere = true; + if (mounted) setCore(coreInstance); } @@ return () => { mounted = false; - if (core && !tmCore) { - core.close().catch(() => {}); - } + if (createdHere) { + coreInstance?.close?.().catch(() => {}); + } }; -}, [projectPath, tag, tmCore, core]); +}, [projectPath, tag, tmCore]);
133-141: Fix counts mapping; guard undefined and read blocked from byStatus.Prevents NaN and ensures consistent source across sites.
Apply:
- completed: stats.byStatus.done + (stats.byStatus.completed || 0), + completed: (stats.byStatus.done || 0) + (stats.byStatus.completed || 0), @@ - blocked: stats.blocked || 0 + blocked: stats.byStatus.blocked || 0Optionally extract a helper:
const mapStatsToCounts = (s: any): TaskCounts => ({ total: s.total, inProgress: s.byStatus['in-progress'] || 0, completed: (s.byStatus.done || 0) + (s.byStatus.completed || 0), pending: s.byStatus.pending || 0, review: s.byStatus.review || 0, blocked: s.byStatus.blocked || 0 });Also applies to: 175-182, 221-229
167-191: Start polling only when core is ready; drop non-null assertion.Avoids noisy errors when core is null.
Apply:
- if (!enableLiveUpdates || !core) { - // If live updates disabled, use polling instead - if (!enableLiveUpdates && refreshInterval > 0) { + if (!enableLiveUpdates) { + // Use polling instead (only if core is ready) + if (core && refreshInterval > 0) { const interval = setInterval(async () => { try { - const stats = await core!.getTaskStats(tag); + const stats = await core.getTaskStats(tag); setCounts({ total: stats.total, inProgress: stats.byStatus['in-progress'] || 0, - completed: stats.byStatus.done + (stats.byStatus.completed || 0), + completed: (stats.byStatus.done || 0) + (stats.byStatus.completed || 0), pending: stats.byStatus.pending || 0, review: stats.byStatus.review || 0, - blocked: stats.blocked || 0 + blocked: stats.byStatus.blocked || 0 }); } catch {} }, refreshInterval); return () => clearInterval(interval); } return undefined; }
195-206: Symmetric unsubscribe and stop watcher on cleanup.Prevents leaks and dangling watchers.
Apply:
@@ - // Start watching + // Start watching await storage.startWatching(); @@ - taskWatcher.onChange(handleChange); + taskWatcher.onChange(handleChange); @@ - return () => { - taskWatcher.removeListener('change', handleChange); - }; + return () => { + if (typeof taskWatcher.offChange === 'function') { + taskWatcher.offChange(handleChange); + } else if (typeof taskWatcher.removeListener === 'function') { + taskWatcher.removeListener('change', handleChange); + } + if (typeof storage.stopWatching === 'function') { + storage.stopWatching().catch(() => {}); + } + };Also applies to: 238-244
apps/terminal-ui/src/utils/logger.ts (5)
236-239: Avoid sync file I/O; switch to async writes or a small queue.appendFileSync can jank the TUI under bursty logs.
Consider fs/promises.appendFile with a bounded in-memory queue flushed on a timer (e.g., every 250ms) to provide backpressure and non-blocking writes.
260-263: Safe context serialization to avoid crashes on circulars.JSON.stringify can throw; add a safeStringify helper.
Example:
function safeStringify(obj: unknown): string { const seen = new WeakSet<object>(); try { return JSON.stringify(obj, (k, v) => { if (v instanceof Error) return { name: v.name, message: v.message, stack: v.stack }; if (typeof v === 'object' && v) { if (seen.has(v as object)) return '[Circular]'; seen.add(v as object); } return v; }); } catch { return '[Unserializable Context]'; } }Then:
- output += ` | Context: ${JSON.stringify(entry.context)}`; + output += ` | Context: ${safeStringify(entry.context)}`;
68-79: Harden file logging: prevent path escape and ensure dir exists at write time.Use resolve/basename and mkdirSync on demand; fixes runtime configure() too.
Apply:
- import { appendFileSync, existsSync, mkdirSync } from 'node:fs'; - import { join } from 'node:path'; + import { appendFileSync, existsSync, mkdirSync } from 'node:fs'; + import { join, resolve, dirname, basename } from 'node:path'; @@ - // Create log directory if file logging is enabled + // Best-effort base dir creation; actual write re-validates path if (this.config.file && this.config.filePath) { - const logDir = join(process.cwd(), '.taskmaster', 'logs'); + const logDir = join(process.cwd(), '.taskmaster', 'logs'); if (!existsSync(logDir)) { try { mkdirSync(logDir, { recursive: true }); } catch (error) { console.error('Failed to create log directory:', error); this.config.file = false; } } } @@ - const formatted = this.formatEntry(entry, true); - const logPath = join( - process.cwd(), - '.taskmaster', - 'logs', - this.config.filePath - ); + const formatted = this.formatEntry(entry, true); + const baseDir = join(process.cwd(), '.taskmaster', 'logs'); + const safeFile = basename(this.config.filePath); + const logPath = resolve(baseDir, safeFile); + if (!existsSync(dirname(logPath))) { + try { mkdirSync(dirname(logPath), { recursive: true }); } catch {} + }Also applies to: 223-241
106-129: Preserve underlying Error and enrich context for ClassifiedError.Avoid losing the original error and useful fields.
Apply:
- const entry: LogEntry = { + const entry: LogEntry = { timestamp: new Date(), level: LogLevel.ERROR, message, context, - error: error instanceof Error ? error : undefined + error: error instanceof Error ? error : (error ? (error as ClassifiedError).error : undefined) }; -if (error && 'category' in error) { +if (error && 'category' in (error as any)) { // ClassifiedError - entry.context = { + const ce = error as ClassifiedError; + entry.context = { ...entry.context, - category: error.category, - severity: error.severity, - recoveryActions: error.recoveryActions + category: ce.category, + severity: ce.severity, + recoveryActions: ce.recoveryActions, + message: ce.message, + technicalDetails: ce.technicalDetails, + classifiedAt: ce.timestamp?.toISOString?.() }; }
336-363: createLogger should return a full Logger without unsafe casts.Use a Proxy to inject context while preserving API.
Apply:
-export function createLogger( - name: string, - config?: Partial<LoggerConfig> -): Logger { - const contextLogger = new Logger(config); - // Wrap methods... - const wrappedLogger = { ... }; - return wrappedLogger as unknown as Logger; -} +export function createLogger(name: string, config?: Partial<LoggerConfig>): Logger { + const base = new Logger(config); + const augment = (ctx?: Record<string, unknown>) => ({ ...ctx, logger: name }); + return new Proxy(base, { + get(target, prop, receiver) { + if (prop === 'debug') return (m: string, c?: Record<string, unknown>) => target.debug(m, augment(c)); + if (prop === 'info') return (m: string, c?: Record<string, unknown>) => target.info(m, augment(c)); + if (prop === 'warn') return (m: string, c?: Record<string, unknown>) => target.warn(m, augment(c)); + if (prop === 'error') return (m: string, e?: Error | ClassifiedError, c?: Record<string, unknown>) => target.error(m, e, augment(c)); + if (prop === 'fatal') return (m: string, e?: Error, c?: Record<string, unknown>) => target.fatal(m, e, augment(c)); + return Reflect.get(target, prop, receiver); + } + }); +}packages/tm-core/src/storage/file-storage/task-watcher.ts (3)
139-149: Chokidar v4: convert ignored globs to a function; string arrays won’t work.Pass a matcher function (precompiled with picomatch) instead of raw glob strings.
Apply (with constructor changes shown in another comment):
- this.watcher = watch(this.watchPath, { - ignored: this.options.ignorePatterns, + this.watcher = watch(this.watchPath, { + ignored: (p: string) => + this.ignoreMatchers.some((m) => + m(relative(this.watchPath, p).replace(/\\/g, '/')) + ), ignoreInitial: this.options.ignoreInitial, persistent: this.options.persistent,Based on learnings.
159-176: Reject start() on early watcher errors; clear timeout.Avoid hanging until timeout when chokidar emits 'error' before 'ready'.
Apply:
- this.watcher.once('ready', () => { - this.isWatching = true; - this.emit('ready'); - resolve(); - }); - const timeout = setTimeout(() => { reject(new Error('Watcher initialization timeout')); }, 10000); - this.watcher.once('ready', () => clearTimeout(timeout)); + const onReady = () => { + this.isWatching = true; + this.emit('ready'); + clearTimeout(timeout); + this.watcher?.off('error', onError); + resolve(); + }; + const onError = (err: Error) => { + clearTimeout(timeout); + this.watcher?.off('ready', onReady); + reject(err); + }; + this.watcher.once('ready', onReady); + this.watcher.once('error', onError); + const timeout = setTimeout(() => { reject(new Error('Watcher initialization timeout')); }, 10000);
357-375: Replace ad‑hoc glob regex with picomatch; normalize to POSIX.Improves correctness across OSes and avoids ReDoS.
Apply (plus constructor/import tweaks below):
- private matchesFilePatterns(filePath: string): boolean { - const relativePath = relative(this.watchPath, filePath); - for (const pattern of this.options.filePatterns) { - const regexPattern = pattern - .replace(/\./g, '\\.') - .replace(/\*\*/g, '___DBLSTAR___') - .replace(/\*/g, '[^/]*') - .replace(/___DBLSTAR___/g, '.*'); - const regex = new RegExp(`^${regexPattern}$`); - if (regex.test(relativePath)) { - return true; - } - } - return false; - } + private matchesFilePatterns(filePath: string): boolean { + const rel = relative(this.watchPath, filePath).replace(/\\/g, '/'); + return this.fileMatchers.some((m) => m(rel)); + }And elsewhere in this file:
- import { normalize, relative } from 'node:path'; + import { normalize, relative } from 'node:path'; + import picomatch from 'picomatch'; @@ - private options: Required<TaskWatcherOptions>; + private options: Required<TaskWatcherOptions>; + private fileMatchers: Array<(p: string) => boolean> = []; + private ignoreMatchers: Array<(p: string) => boolean> = []; @@ constructor this.options = { ... }; + this.fileMatchers = this.options.filePatterns.map((p) => picomatch(p, { posix: true })); + this.ignoreMatchers = this.options.ignorePatterns.map((p) => picomatch(p, { posix: true }));Also consider emitting relativePath with POSIX separators for consistency:
- relativePath: relative(this.watchPath, normalizedPath), + relativePath: relative(this.watchPath, normalizedPath).replace(/\\/g, '/'),Based on learnings.
packages/tm-core/src/storage/file-storage/file-storage.ts (1)
725-734: Add offTaskFileChange(handler) to mirror onTaskFileChange.Symmetric unsubscribe prevents leaks and eases lifecycle handling. This was requested previously.
onTaskFileChange(handler: (event: TaskWatcherEvent) => void): void { if (!this.taskWatcher) { throw new Error('TaskWatcher not initialized'); } this.taskWatcher.onChange(handler); } + + /** + * Remove a previously registered file change handler + */ + offTaskFileChange(handler: (event: TaskWatcherEvent) => void): void { + if (!this.taskWatcher) { + throw new Error('TaskWatcher not initialized'); + } + // EventEmitter typing isn’t per-event; cast for compatibility + this.taskWatcher.off('change', handler as any); + }apps/terminal-ui/src/views/panels/HelpPanel.tsx (1)
27-43: Document the remaining shortcuts (PgUp/PgDn, 1/2/3, 0, Esc).Add a brief line so Help matches behavior.
<Text> <Text bold>Enter</Text> - View task details </Text> <Text> <Text bold>q / Ctrl+C</Text> - Exit </Text> + <Text> + Other: <Text bold>PgUp/PgDn</Text> scroll, <Text bold>1/2/3</Text> maximize section, <Text bold>0</Text> restore, <Text bold>Esc/Enter/q</Text> close modal + </Text>apps/terminal-ui/src/views/modals/DimmedOverlay.tsx (1)
15-20: Reduce overlay node count to O(1) to avoid sluggish renders.Rendering one row component per terminal line is costly; use a single Text with newline-joined spaces.
export const DimmedOverlay: React.FC<DimmedOverlayProps> = ({ width, height }) => { - return ( - <Box position="absolute" width="100%" height="100%" flexDirection="column"> - {Array.from({ length: height }).map((_, i) => ( - <Box key={i} width="100%"> - <Text dimColor>{' '.repeat(width)}</Text> - </Box> - ))} - </Box> - ); + const overlay = Array.from({ length: height }, () => ' '.repeat(width)).join('\n'); + return ( + <Box position="absolute" width="100%" height="100%" flexDirection="column"> + <Text dimColor>{overlay}</Text> + </Box> + ); };apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
22-44: Consider inlining the getControls call.The
controlsvariable is only used once. You can callgetControls()directly in the JSX template for a more compact implementation.Apply this diff if desired:
- const controls = getControls(); - return ( <Box borderStyle="single" paddingX={1} justifyContent="center" flexShrink={0} > <Text wrap="truncate-end" dimColor> {dimensions.width}x{dimensions.height} | {panelText} - {controls && ` | ${controls}`} + {getControls() && ` | ${getControls()}`} </Text> </Box> );apps/terminal-ui/src/views/sections/TaskListSection.tsx (4)
191-200: Use the icon from getComplexityDisplay.You compute
complexityDisplay.iconbut render a hardcoded●. Use the helper's icon for consistency.Apply this diff:
<Box width={complexityWidth} flexShrink={0}> <Text color={isSelected ? 'white' : complexityDisplay.color} wrap="truncate-end" > - ● {complexityDisplay.text} + {complexityDisplay.icon} {complexityDisplay.text} </Text> </Box>
126-129: Use stable keys for list items.Using array indices as keys can cause rendering issues when the list order changes. Generate a stable key per row.
Apply this diff:
+ const rowKey = task.isSubtask + ? `sub-${task.parentId}-${task.id}` + : String(task.id); + return ( <Box - key={actualIndex} + key={rowKey}
30-44: ReplaceArray<any>with a typed union.Using
Array<any>breaks type safety. Define a union type for the flattened task rows.Apply this diff:
+type FlattenedTask = Task | (Task['subtasks'][number] & { + isSubtask: true; + isLastSubtask: boolean; + parentId: Task['id'] +}); - const flattenedTasks: Array<any> = []; + const flattenedTasks: FlattenedTask[] = [];
126-135: Apply backgroundColor to Text cells, not Box.In Ink,
backgroundColoronBoxis ignored. Apply it to eachTextelement within the row for selection highlighting to work.Apply this diff:
+ const cellBg = isSelected ? 'blue' : undefined; + return ( <Box key={actualIndex} flexDirection="row" flexShrink={0} columnGap={1} - {...({ - backgroundColor: isSelected ? 'blue' : 'transparent' - } as any)} >Then add
backgroundColor={cellBg}to eachTextcomponent in the row (ID, Title, Status, Priority, Dependencies, Complexity).apps/terminal-ui/src/components/Onboarding.tsx (1)
14-29: Remove unused onSkip prop.The
onSkipprop is declared but never invoked. Either implement a "Skip" affordance or remove the prop to eliminate dead code.Apply this diff:
export interface OnboardingProps { projectPath?: string; onComplete?: () => void; - onSkip?: () => void; } export const Onboarding: React.FC<OnboardingProps> = ({ projectPath = process.cwd(), - onComplete, - onSkip: _onSkip + onComplete }) => {Also applies to: 35-39
apps/terminal-ui/src/index.tsx (4)
1-1: Remove shebang from library entry.This file is imported by the CLI, not executed directly. The shebang breaks bundlers and ESM consumers. Place it only in the CLI bin file.
Apply this diff:
-#!/usr/bin/env node import React, { useState, useEffect } from 'react';
38-40: Validate initialPanel before casting to PanelType.Casting
initialPanel as PanelTypewithout validation can admit invalid strings iflaunchTerminalUIis called programmatically.Apply this diff:
- const [currentPanel, setCurrentPanel] = useState<PanelType>( - (initialPanel as PanelType) || 'dashboard' - ); + const validPanel: PanelType = + initialPanel === 'help' ? 'help' : 'dashboard'; + const [currentPanel, setCurrentPanel] = useState<PanelType>(validPanel);
21-26: Remove unused section prop.The
sectionprop is accepted inAppPropsbut never destructured or used byAppContent. There's no CLI flag for--sectioneither.Apply this diff:
interface AppProps { panel?: string; - section?: string; projectPath?: string; explicitProject?: boolean; }
59-71: Replace Array with a typed union.Using
Array<any>breaks type safety. Define a union for Task and subtask shapes.Apply this diff:
+ type FlattenedTask = Task | (Task['subtasks'][number] & { + isSubtask: true; + parentId: Task['id'] + }); - const flattenedTasks: Array<any> = []; + const flattenedTasks: FlattenedTask[] = [];apps/terminal-ui/src/components/ErrorBoundary.tsx (3)
59-75: Avoid duplicate logging when onError is provided.
componentDidCatchlogs vialogger.error, thenonErrorin App logs vialogger.fatal. This creates duplicate log entries.Apply this diff:
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { - // Classify and log the error const classified = classifyError(error); - logger.error('React component error', error, { - componentStack: errorInfo.componentStack, - category: classified.category, - severity: classified.severity - }); + + // Only log internally if no custom handler + if (!this.props.onError) { + logger.error('React component error', error, { + componentStack: errorInfo.componentStack, + category: classified.category, + severity: classified.severity + }); + } this.setState({ errorInfo }); if (this.props.onError) { this.props.onError(error, errorInfo); } }
14-23: Allow fallback to access error details.Typing
fallbackasReactNodeprevents custom fallbacks from accessing the caught error. Consider accepting a render function.Apply this diff:
export interface ErrorBoundaryProps { children: ReactNode; - fallback?: ReactNode; + fallback?: ReactNode | ((props: { error: Error | null; errorInfo: React.ErrorInfo | null }) => ReactNode); onError?: (error: Error, errorInfo: React.ErrorInfo) => void; showDetails?: boolean; }Then in
render():if (this.state.hasError && this.state.error) { if (this.props.fallback) { - return this.props.fallback; + return typeof this.props.fallback === 'function' + ? this.props.fallback({ error: this.state.error, errorInfo: this.state.errorInfo }) + : this.props.fallback; }
84-93: Map border color to error severity.The fallback always uses a red border. Derive the color from
classified.severityfor clearer UX.Apply this diff:
const classified = classifyError(this.state.error); + + const severityColors: Record<string, string> = { + CRITICAL: 'red', + ERROR: 'red', + WARNING: 'yellow', + INFO: 'blue' + }; + const borderColor = severityColors[classified.severity] || 'red'; return ( <Box flexDirection="column" padding={1} borderStyle="round" - borderColor="red" + borderColor={borderColor} >
🧹 Nitpick comments (10)
apps/terminal-ui/src/stores/taskStore.ts (2)
48-61: Use shared logger instead of console for consistency.Route errors through the module logger to keep formatting and log levels consistent across the UI.
-import { createTaskMasterCore } from '@tm/core'; +import { createTaskMasterCore } from '@tm/core'; +import { logger } from '../utils/logger.js'; @@ - } catch (error: any) { - console.error('Failed to load tasks:', error.message); + } catch (error: any) { + logger.error({ msg: 'Failed to load tasks', error }); return []; }
48-56: Allow injecting a shared core instance to avoid re-creating it per load.Accept an optional tmCore in factory options; reuse it when present to reduce overhead.
-export const createTaskMasterStore = (): TaskStore => { +export const createTaskMasterStore = (opts?: { tmCore?: any }): TaskStore => { return { loadTasks: async (projectPath: string): Promise<Task[]> => { try { - const tmCore = await createTaskMasterCore({ projectPath }); + const tmCore = opts?.tmCore ?? (await createTaskMasterCore({ projectPath }));Also applies to: 97-104
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (1)
24-31: Treat 'completed' as satisfied dependency and avoid O(n²) lookups.
- Consider dependencies satisfied when depTask.status is 'done' or 'completed'.
- Pre-index tasks by id to avoid repeated linear searches.
Apply:
+ const byId = React.useMemo(() => { + const m = new Map<string | number, Task>(); + for (const t of tasks) m.set(t.id as any, t); + return m; + }, [tasks]); @@ - return t.dependencies.every((depId) => { - const depTask = tasks.find((dt) => dt.id === depId); - return depTask?.status === 'done'; - }); + return t.dependencies.every((depId) => { + const s = byId.get(depId)?.status; + return s === 'done' || s === 'completed'; + });Also applies to: 63-70
apps/terminal-ui/src/utils/error-handling.ts (1)
61-83: Prefer errno codes over substring matching when available.More robust and locale‑safe classification for file system errors.
Example:
const err = error instanceof Error ? error : new Error(String(error)); const code = (err as NodeJS.ErrnoException).code; if (code === 'ENOENT') { /* not found */ } else if (code === 'EACCES' || code === 'EPERM') { /* permission */ } else if (code === 'EISDIR') { /* is dir */ } else if (code === 'ENOTDIR') { /* not dir */ }Fallback to string checks only when code is absent.
Also applies to: 186-231
packages/tm-core/src/storage/file-storage/file-storage.ts (3)
46-51: Don’t block cleanup if stopWatching throws.Wrap stopWatching() so fileOps.cleanup() always runs.
- // Stop watching if active - if (this.taskWatcher?.getIsWatching()) { - await this.stopWatching(); - } + // Stop watching if active (best-effort) + if (this.taskWatcher?.getIsWatching()) { + try { + await this.stopWatching(); + } catch { + /* noop: proceed with cleanup */ + } + }
681-692: Ensure tasks dir exists before starting watcher.startWatching can be called before initialize(); guarantee the watch path exists.
async startWatching(): Promise<void> { if (!this.taskWatcher) { throw new Error('TaskWatcher not initialized'); } if (this.taskWatcher.getIsWatching()) { // Already watching, no-op return; } - await this.taskWatcher.start(); + await this.fileOps.ensureDir(this.pathResolver.getTasksDir()); + await this.taskWatcher.start(); }
33-34: Pass explicitdebounceDelay: 500to TaskWatcher in FileStorage to align with PR specification.Verification confirms TaskWatcher defaults to 100ms debounce. The file-storage.ts instantiation at line 33 does not pass options and uses this default, while the terminal-ui useTaskStore.ts explicitly passes
{debounceDelay: 500}. For consistency with the PR's specified 500ms debounce behavior, the FileStorage instantiation should also pass this value.Apply the suggested fix:
- this.taskWatcher = new TaskWatcher(this.pathResolver.getTasksDir()); + this.taskWatcher = new TaskWatcher(this.pathResolver.getTasksDir(), { + debounceDelay: 500 + });.changeset/add-terminal-ui.md (2)
1-19: Condense to a user-facing one‑liner per Changeset guidelines.Current entry reads like a mini‑README. Prefer a concise summary.
--- "@tm/cli": minor "task-master-ai": minor --- -Add interactive Terminal UI for real-time task monitoring and navigation - -Introduces a new `task-master interactive` command (alias: `tui`) that launches a full-featured, keyboard-driven terminal dashboard with: - -- Real-time task monitoring with automatic updates when project files change (~500ms) -- Dashboard with three sections: Project overview, Dependency status, and Task list -- Keyboard navigation: arrow keys, PgUp/PgDn, Enter for task details -- Section maximize/restore with keys 0-3 -- Task details modal showing parent task, dependencies, complexity, and test strategy -- Help panel with keyboard shortcuts (Tab to toggle) -- Onboarding guidance for uninitialized projects - -Usage: `task-master interactive` or `task-master tui` -Options: `--panel=dashboard|help` and `--project=/path/to/project` +Add interactive Terminal UI (`task-master interactive` / `tui`) with live updates, dashboard/help panels, keyboard navigation, and task details modal.
- If packages like tm-core added public APIs (watcher methods), include them in this changeset (e.g., "@tm/core": minor).
18-19: Include all documented CLI options.Options mention omits --section; align wording if that line is retained elsewhere.
-Options: `--panel=dashboard|help` and `--project=/path/to/project` +Options: `--panel=dashboard|help`, `--section=project|dependencies|tasks`, `--project=/path/to/project`apps/terminal-ui/src/components/Onboarding.tsx (1)
45-55: Stabilize onComplete to avoid re-detection.Including
onCompletein the dependency array causes the effect to re-run whenever the callback reference changes, re-detecting project state unnecessarily.Wrap the
onComplete?.()call in a separate effect or use a ref to store the latest callback:+ const onCompleteRef = useRef(onComplete); + useEffect(() => { + onCompleteRef.current = onComplete; + }, [onComplete]); + useEffect(() => { const state = detectProjectState(projectPath); setProjectState(state); setIsLoading(false); if (state.state === ProjectState.HAS_TASKS) { - onComplete?.(); + onCompleteRef.current?.(); } - }, [projectPath, onComplete]); + }, [projectPath]);
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (34)
.changeset/add-terminal-ui.md(1 hunks)apps/cli/src/command-registry.ts(2 hunks)apps/cli/src/commands/interactive.command.ts(1 hunks)apps/cli/src/index.ts(1 hunks)apps/terminal-ui/package.json(1 hunks)apps/terminal-ui/src/components/BottomStatusBar.tsx(1 hunks)apps/terminal-ui/src/components/ErrorBoundary.tsx(1 hunks)apps/terminal-ui/src/components/Onboarding.tsx(1 hunks)apps/terminal-ui/src/components/StatusBar.tsx(1 hunks)apps/terminal-ui/src/components/TopBar.tsx(1 hunks)apps/terminal-ui/src/components/index.ts(1 hunks)apps/terminal-ui/src/hooks/useTaskStore.ts(1 hunks)apps/terminal-ui/src/index.tsx(1 hunks)apps/terminal-ui/src/stores/taskStore.ts(1 hunks)apps/terminal-ui/src/utils/error-handling.ts(1 hunks)apps/terminal-ui/src/utils/logger.ts(1 hunks)apps/terminal-ui/src/utils/project-state.ts(1 hunks)apps/terminal-ui/src/utils/task-helpers.ts(1 hunks)apps/terminal-ui/src/views/layouts/AppLayout.tsx(1 hunks)apps/terminal-ui/src/views/modals/DimmedOverlay.tsx(1 hunks)apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx(1 hunks)apps/terminal-ui/src/views/panels/DashboardPanel.tsx(1 hunks)apps/terminal-ui/src/views/panels/HelpPanel.tsx(1 hunks)apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx(1 hunks)apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx(1 hunks)apps/terminal-ui/src/views/sections/TaskListSection.tsx(1 hunks)apps/terminal-ui/terminal-ui.md(1 hunks)apps/terminal-ui/tsconfig.json(1 hunks)packages/tm-core/package.json(1 hunks)packages/tm-core/src/storage/file-storage/file-storage.ts(4 hunks)packages/tm-core/src/storage/file-storage/index.ts(1 hunks)packages/tm-core/src/storage/file-storage/task-watcher.ts(1 hunks)packages/tm-core/src/storage/index.ts(1 hunks)tsconfig.json(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (11)
- apps/cli/src/command-registry.ts
- apps/terminal-ui/src/components/TopBar.tsx
- packages/tm-core/package.json
- apps/terminal-ui/src/utils/task-helpers.ts
- apps/terminal-ui/tsconfig.json
- tsconfig.json
- apps/terminal-ui/src/hooks/useTaskStore.ts
- apps/cli/src/commands/interactive.command.ts
- apps/terminal-ui/src/views/panels/DashboardPanel.tsx
- apps/terminal-ui/src/views/layouts/AppLayout.tsx
- packages/tm-core/src/storage/file-storage/index.ts
🧰 Additional context used
📓 Path-based instructions (3)
.changeset/*.md
📄 CodeRabbit inference engine (.cursor/rules/changeset.mdc)
.changeset/*.md: When runningnpm run changesetornpx changeset add, provide a concise summary of the changes for theCHANGELOG.mdin imperative mood, typically a single line, and not a detailed Git commit message.
The changeset summary should be user-facing, describing what changed in the released version that is relevant to users or consumers of the package.
Do not use your detailed Git commit message body as the changeset summary.Write changeset entries as user-facing summaries of what users get or what was fixed, not low-level implementation details
Files:
.changeset/add-terminal-ui.md
.changeset/*
📄 CodeRabbit inference engine (.cursor/rules/new_features.mdc)
Create appropriate changesets for new features, use semantic versioning, include tagged system information in release notes, and document breaking changes if any.
Files:
.changeset/add-terminal-ui.md
**/*.{md,mdx}
📄 CodeRabbit inference engine (CLAUDE.md)
Reference documentation at https://docs.task-master.dev rather than local file paths in content
Files:
apps/terminal-ui/terminal-ui.md
🧠 Learnings (7)
📚 Learning: 2025-07-31T22:07:49.716Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-31T22:07:49.716Z
Learning: Applies to scripts/modules/commands.js : Export the registerCommands function and keep the CLI setup code clean and maintainable.
Applied to files:
apps/cli/src/index.ts
📚 Learning: 2025-10-08T19:57:00.982Z
Learnt from: Crunchyman-ralph
PR: eyaltoledano/claude-task-master#1282
File: packages/tm-core/src/utils/index.ts:16-34
Timestamp: 2025-10-08T19:57:00.982Z
Learning: For the tm-core package in the eyaltoledano/claude-task-master repository, the team prefers a minimal, need-based export strategy in index files rather than exposing all internal utilities. Exports should only be added when functions are actually consumed by other packages in the monorepo.
Applied to files:
packages/tm-core/src/storage/index.tsapps/terminal-ui/package.json
📚 Learning: 2025-07-18T17:14:29.399Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: .cursor/rules/tasks.mdc:0-0
Timestamp: 2025-07-18T17:14:29.399Z
Learning: Applies to scripts/modules/task-manager.js : Use consistent formatting for task files, include all task properties in text files, and format dependencies with status indicators.
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-08-06T21:13:05.240Z
Learnt from: Crunchyman-ralph
PR: eyaltoledano/claude-task-master#1091
File: assets/claude/agents/task-orchestrator.md:69-78
Timestamp: 2025-08-06T21:13:05.240Z
Learning: For Claude agent instruction files in .claude/agents/ and assets/claude/agents/ directories, ignore markdownlint issues (like MD031 blanks-around-fences, MD040 fenced-code-language, MD041 missing top-level heading) as these files follow Claude's specific agent format requirements rather than standard markdown documentation conventions.
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-10-20T16:44:58.151Z
Learnt from: CR
PR: eyaltoledano/claude-task-master#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-20T16:44:58.151Z
Learning: Applies to **/*.{md,mdx} : Reference documentation at https://docs.task-master.dev rather than local file paths in content
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-09-26T19:05:47.555Z
Learnt from: Crunchyman-ralph
PR: eyaltoledano/claude-task-master#1252
File: packages/ai-sdk-provider-grok-cli/package.json:11-13
Timestamp: 2025-09-26T19:05:47.555Z
Learning: In the eyaltoledano/claude-task-master repository, internal tm/ packages use a specific export pattern where the "exports" field points to TypeScript source files (./src/index.ts) while "main" points to compiled output (./dist/index.js) and "types" points to source files (./src/index.ts). This pattern is used consistently across internal packages like tm/core and tm/ai-sdk-provider-grok-cli because they are consumed directly during build-time bundling with tsdown rather than being published as separate packages.
Applied to files:
apps/terminal-ui/package.json
📚 Learning: 2025-09-26T19:10:32.906Z
Learnt from: Crunchyman-ralph
PR: eyaltoledano/claude-task-master#1252
File: tsconfig.json:22-28
Timestamp: 2025-09-26T19:10:32.906Z
Learning: In the eyaltoledano/claude-task-master repository, all internal tm/ package path mappings in tsconfig.json consistently point to TypeScript source files (e.g., "./packages/*/src/index.ts") rather than built JavaScript. This is intentional architecture because tsdown bundles internal packages directly from source during build time, eliminating the need for separate compilation of internal packages.
Applied to files:
apps/terminal-ui/package.json
🧬 Code graph analysis (12)
apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx (3)
scripts/modules/task-manager/list-tasks.js (4)
totalTasks(77-77)reviewSubtasks(113-113)taskStatusBreakdown(305-312)subtaskStatusBreakdown(314-324)apps/cli/src/utils/ui.ts (1)
createProgressBar(156-172)scripts/modules/ui.js (2)
statusBreakdown(1633-1640)actualChars(304-304)
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (2)
apps/terminal-ui/src/utils/task-helpers.ts (3)
getComplexityDisplay(52-83)getPriorityColor(34-47)getStatusColor(10-29)apps/terminal-ui/src/views/modals/DimmedOverlay.tsx (1)
DimmedOverlay(9-22)
packages/tm-core/src/storage/file-storage/file-storage.ts (2)
packages/tm-core/src/storage/file-storage/task-watcher.ts (2)
TaskWatcher(99-438)TaskWatcherEvent(54-58)packages/tm-core/src/storage/file-storage/path-resolver.ts (1)
PathResolver(10-42)
apps/terminal-ui/src/utils/error-handling.ts (1)
apps/terminal-ui/src/utils/logger.ts (1)
error(106-130)
apps/terminal-ui/src/components/ErrorBoundary.tsx (2)
apps/terminal-ui/src/utils/logger.ts (2)
error(106-130)logger(323-331)apps/terminal-ui/src/utils/error-handling.ts (1)
classifyError(61-181)
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (2)
scripts/modules/task-manager/list-tasks.js (1)
tasksWithNoDeps(152-157)apps/terminal-ui/src/utils/task-helpers.ts (1)
getComplexityDisplay(52-83)
apps/terminal-ui/src/index.tsx (10)
apps/terminal-ui/src/hooks/useTaskStore.ts (1)
useTaskStore(14-114)apps/terminal-ui/src/components/TopBar.tsx (1)
TopBar(12-38)apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
BottomStatusBar(14-48)apps/terminal-ui/src/utils/logger.ts (2)
error(106-130)logger(323-331)apps/terminal-ui/src/views/panels/DashboardPanel.tsx (1)
DashboardPanel(24-90)apps/terminal-ui/src/views/panels/HelpPanel.tsx (1)
HelpPanel(12-57)apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (1)
TaskDetailsModal(29-187)apps/terminal-ui/src/utils/task-helpers.ts (3)
getStatusColor(10-29)getPriorityColor(34-47)getComplexityDisplay(52-83)apps/terminal-ui/src/components/ErrorBoundary.tsx (2)
ErrorBoundary(38-137)render(77-136)apps/terminal-ui/src/utils/project-state.ts (1)
detectProjectState(40-93)
apps/terminal-ui/src/utils/logger.ts (2)
packages/tm-core/src/logger/index.ts (1)
LogLevel(6-6)apps/terminal-ui/src/utils/error-handling.ts (1)
ClassifiedError(41-56)
apps/terminal-ui/src/components/Onboarding.tsx (2)
apps/terminal-ui/src/components/index.ts (1)
Onboarding(8-8)apps/terminal-ui/src/utils/project-state.ts (2)
ProjectStateResult(26-33)detectProjectState(40-93)
apps/terminal-ui/src/views/sections/TaskListSection.tsx (1)
apps/terminal-ui/src/utils/task-helpers.ts (4)
getComplexityDisplay(52-83)padText(88-111)getStatusColor(10-29)getPriorityColor(34-47)
apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
apps/terminal-ui/src/components/index.ts (1)
BottomStatusBar(6-6)
packages/tm-core/src/storage/file-storage/task-watcher.ts (2)
packages/tm-core/src/storage/file-storage/index.ts (7)
TaskFileAddedEvent(16-16)TaskFileChangedEvent(17-17)TaskFileDeletedEvent(18-18)TaskDirectoryChangedEvent(19-19)TaskWatcherEvent(14-14)TaskWatcherOptions(15-15)TaskWatcher(13-13)packages/tm-core/src/storage/index.ts (7)
TaskFileAddedEvent(25-25)TaskFileChangedEvent(26-26)TaskFileDeletedEvent(27-27)TaskDirectoryChangedEvent(28-28)TaskWatcherEvent(23-23)TaskWatcherOptions(24-24)TaskWatcher(22-22)
🪛 ast-grep (0.39.6)
packages/tm-core/src/storage/file-storage/task-watcher.ts
[warning] 367-367: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(^${regexPattern}$)
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🪛 LanguageTool
apps/terminal-ui/terminal-ui.md
[style] ~141-~141: Try using a synonym here to elevate your writing.
Context: ...(views/sections/) Sub-components that make up the Dashboard panel: - **ProjectDashboa...
(CONSTITUTE_COMPRISE)
[uncategorized] ~312-~312: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...s - Priority Breakdown: High/Medium/Low priority counts with colored indicators **Data ...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[grammar] ~521-~521: Use a hyphen to join words.
Context: ...errors - Runtime errors Layout: Red bordered box with error message and reco...
(QB_NEW_EN_HYPHEN)
🪛 markdownlint-cli2 (0.18.1)
apps/terminal-ui/terminal-ui.md
89-89: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🔇 Additional comments (5)
apps/cli/src/index.ts (1)
16-16: Export is properly wired and registered.Verification complete:
- Import at apps/cli/src/command-registry.ts:18 ✓
- Registration entry at apps/cli/src/command-registry.ts:87 ✓
InteractiveCommand is included in the CommandMetadata array and will be registered via the existing registration infrastructure.
packages/tm-core/src/storage/index.ts (1)
20-29: TaskWatcher and related types properly exported — verified.All symbols exported in storage/index.ts lines 20-29 exist in file-storage/index.ts and are correctly sourced from task-watcher.ts. Export chain is intact with no missing references.
apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx (1)
90-102: Now I need to check the actual codebase for the task-helpers file and what functions it exports. Let me search directly in the repository:Perfect! Now I understand the context. The codebase uses Ink (React renderer for terminals) which supports color properties on Text components like
color="green". The review comment is suggesting centralizing color management through a helper function.Now I must verify if this helper function actually exists in the codebase. Let me search the repository directly for the task-helpers file and any color-related functions:
Based on the information gathered, I can now provide a comprehensive assessment:
Key findings from verification:
- Ink provides component-based UI building for command-line apps and uses Flexbox layouts, so CSS-like properties are available
- In Ink, text can use color properties including "green", hex colors, and RGB colors
- The script output confirms extensive hardcoded colors across the terminal-UI codebase (blue, green, yellow, red, magenta, gray)
Issue with the review comment:
The review comment suggests importing
getStatusColorfromutils/task-helpers.js, but I cannot verify this function exists in the codebase without direct access to search it. The learnings referencegetStatusWithColor(different name), which suggests either:
- The function name in the review is incorrect
- The function doesn't exist yet and needs to be created
- The path/location is wrong
Critical observation: The code in ProjectDashboardSection.tsx at lines 279-287 and 313-317 shows explicit status-to-color mappings (e.g., done→green, blocked→red, pending→yellow). These ARE candidates for centralization, but only if a verified helper function exists or should be created.
Refactor status and legend colors through a centralized mapping helper.
The hardcoded color values across ProjectDashboardSection.tsx progress bars and status legends (lines 90–102, 279–287, 313–317) should be replaced with a centralized helper to ensure consistency. Verify that
getStatusColor(or the correct function name fromutils/task-helpers) exists and accepts status strings ('done', 'pending', 'blocked', 'in-progress', 'review', 'deferred', 'cancelled'). If the function doesn't exist, create it to map status types to Ink color names.Current pattern:
<Text color="green">{doneTasks}</Text>
Recommended:<Text color={getStatusColor('done')}>{doneTasks}</Text>apps/terminal-ui/src/utils/project-state.ts (1)
60-76: The current implementation is correct; the review comment misunderstands the storage architecture.The codebase uses a single canonical
tasks.jsonfile for storage (not per-task JSON files). FileStorage explicitly states it uses "a single tasks.json file," and all tasks—including different tags—are stored within that one file (e.g.,tasksData.master?.tasks). Thegenerate-taskscommand creates individual files fromtasks.json, not as the primary storage format.The current
detectProjectState()function correctly aligns with this architecture by checking for.taskmaster/tasks/tasks.json. There is no misclassification risk for projects using per-file JSON storage because the codebase does not support that as a primary storage format. The proposed refactor adds unnecessary async complexity without addressing any real problem.Likely an incorrect or invalid review comment.
apps/terminal-ui/src/components/index.ts (1)
1-15: LGTM – clear, minimal barrel exports.Exports align with component boundaries and use
typefor TS-friendly type-only exports.
| getComplexityDisplay | ||
| }) => { | ||
| // Calculate proper task ID | ||
| const taskId = task.isSubtask ? `${task.parentId}.${task.id}` : task.id; |
There was a problem hiding this comment.
Guard against missing parentId when computing taskId.
If task.isSubtask is true but task.parentId is undefined, this will produce "undefined.{id}" as the displayed taskId.
Apply this diff to add a fallback:
- const taskId = task.isSubtask ? `${task.parentId}.${task.id}` : task.id;
+ const taskId = task.isSubtask && task.parentId ? `${task.parentId}.${task.id}` : task.id;🤖 Prompt for AI Agents
In apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx around line 39,
computing taskId using task.isSubtask currently concatenates task.parentId
directly which can produce "undefined.{id}" when parentId is missing; update the
logic to guard against a missing parentId by using a safe fallback (for example
'unknown' or an empty string) when task.isSubtask is true but task.parentId is
undefined, and then build taskId from that safe parent value so the displayed id
never contains "undefined.".
| Priority: {nextTask.priority || <Text color="gray">N/A</Text>}{' '} | ||
| Dependencies:{' '} | ||
| {nextTask.dependencies && nextTask.dependencies.length > 0 ? ( | ||
| <Text color="cyan">{nextTask.dependencies.join(', ')}</Text> | ||
| ) : ( | ||
| <Text color="gray">None</Text> | ||
| )} |
There was a problem hiding this comment.
Use nullish coalescing; avoid treating 0 as “N/A”.
'||' will hide valid 0 values and mixes ReactNode types oddly.
Apply:
- const nextTaskComplexity = nextTask
- ? complexityMap?.get(String(nextTask.id)) || nextTask.complexity
- : undefined;
+ const nextTaskComplexity = nextTask
+ ? (complexityMap?.get(String(nextTask.id)) ?? nextTask.complexity)
+ : undefined;
@@
- Priority: {nextTask.priority || <Text color="gray">N/A</Text>}{' '}
+ Priority: {nextTask.priority ?? <Text color="gray">N/A</Text>}{' '}Also applies to: 73-75
🤖 Prompt for AI Agents
In apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx around lines
147-153 (and similarly lines 73-75), the code uses the logical OR operator (||)
to fall back to "N/A", which treats valid values like 0 as missing and mixes
ReactNode types; replace the || fallback with the nullish coalescing operator
(??) so only null/undefined trigger the fallback, and ensure the rendered types
remain consistent (wrap the fallback in the same Text component or render the
numeric value directly as a ReactNode) so priority 0 displays correctly.
| - **Quick Controls**: "Tab to switch | q to quit" hint in dimmed text | ||
|
|
There was a problem hiding this comment.
Quick controls text: mention Ctrl+C for consistency with Keyboard Shortcuts.
The doc elsewhere says q/Ctrl+C quit; include both here.
-- **Quick Controls**: "Tab to switch | q to quit" hint in dimmed text
+- **Quick Controls**: "Tab to switch | q or Ctrl+C to quit" hint in dimmed text🤖 Prompt for AI Agents
In apps/terminal-ui/terminal-ui.md around lines 471 to 472, the "Quick Controls"
hint currently only says 'Tab to switch | q to quit'; update the line to include
Ctrl+C so it reads something like 'Tab to switch | q/Ctrl+C to quit' for
consistency with the Keyboard Shortcuts section, ensuring formatting
(bold/dimmed) is preserved.
|
Way to go @mwguerra! I think this is a good foundation - will give it some testing and possibly touch some stuff up with @Crunchyman-ralph |
|
This is cool, cheers @mwguerra! |
|
@mwguerra after the next release or so, which is mostly concerned with taskmaster <> hamster stuff, i'm gonan start a push for 1.0 which includes TUI/REPL and i'll look at this to see if i can use it as the foundation since there are some deliberate design choices i want to make that may already have been made if you don't mind bringing this up to speed with that way I can pick it up from here |
- Export TaskWatcher and related types from @tm/core public API - Configure terminal-ui to build TypeScript output (noEmit: false) - Update terminal-ui package.json exports to point to dist/ - Fix import paths to use @tm/core instead of @tm/core/storage - Update terminal-ui to use new TmCore API (createTmCore, TmCore) - Add chokidar dependency for TaskWatcher file watching
0be8669 to
98c6320
Compare
There was a problem hiding this comment.
This PR is being reviewed by Cursor Bugbot
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
| } | ||
| return undefined; | ||
| } | ||
|
|
There was a problem hiding this comment.
Bug: StatusBar polling uses null core without guard
The polling fallback code in the second useEffect hook attempts to call core!.tasks.list() when enableLiveUpdates is false and refreshInterval > 0. However, core can be null at this point. The outer condition if (!enableLiveUpdates || !core) permits entry when either condition is true, and the inner condition only checks !enableLiveUpdates. When core is null but enableLiveUpdates is false, the polling code executes and crashes trying to access methods on null. The non-null assertion operator provides no runtime protection.
Done. If I can help in anyway else just let me know. |
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (24)
apps/terminal-ui/src/views/panels/HelpPanel.tsx (1)
27-43: Expand HelpPanel shortcuts to cover the full keyboard surface.The Help panel currently only lists Tab, ↑↓, Enter, and q/Ctrl+C, but the UI also supports:
PageUp/PageDownfor larger jumps in the task list0/1/2/3for section restore/maximizeEsc(plus Enter/q) to close the Task Details modalConsider adding lines for these (or a concise “Other: PageUp/PageDown – scroll, 1/2/3 – maximize, 0 – restore, Esc – close modals” line) so the Help view matches the documented behavior and terminal-ui.md.
apps/terminal-ui/terminal-ui.md (3)
514-522: Minor wording: hyphenate “Red-bordered box”.For the ErrorBoundary layout description:
**Layout**: Red bordered box with error message and recovery actionsuse the hyphenated compound adjective instead:
**Layout**: Red-bordered box with error message and recovery actionsPurely stylistic, but it reads more naturally and matches common style guides.
85-124: Fix remaining markdownlint issues and avoid brittle file/line references.There are a couple of lingering doc hygiene points:
Fenced code block language (MD040)
The project tree block still uses a bare fence:apps/terminal-ui/
...Add a language token, e.g.:
```text apps/terminal-ui/ ...Blank lines around fences (MD031)
Some code blocks (e.g., the “Layout Logic” snippet, the watcher config, and CLI registration) are immediately adjacent to preceding text. Add a blank line before each opening ``` so they’re surrounded by empty lines.Brittle file path + line number references
Phrases likeDashboardPanel.tsx:32-62andDependencyStatusSection.tsx:63-70will rot quickly. Prefer high-level references (“see DashboardPanel layout”) or, once available, links to canonical docs instead of in-repo paths + line numbers. As per coding guidelinesAlso applies to: 269-281, 401-425, 738-758
467-472: Align “Quick Controls” text with the actual quit shortcuts.Here the docs say:
Quick Controls: "Tab to switch | q to quit"
Elsewhere (and in the UI) you support both
qandCtrl+Cto exit.Recommend updating this line to include both, e.g.:
-**Quick Controls**: "Tab to switch | q to quit" hint in dimmed text +**Quick Controls**: "Tab to switch | q or Ctrl+C to quit" hint in dimmed textapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (2)
19-21: Align “tasks with no dependencies” metric with CLI behavior (exclude completed tasks).Right now completed tasks are counted in
tasksWithNoDeps. The CLIlist-tasksimplementation only counts actionable tasks (notdone/completed), so this metric will feel inflated compared to the rest of the tool. Based on learnings.Consider:
- const tasksWithNoDeps = tasks.filter( - (t) => !t.dependencies || t.dependencies.length === 0 - ).length; + const tasksWithNoDeps = tasks.filter( + (t) => + t.status !== 'done' && + t.status !== 'completed' && + (!t.dependencies || t.dependencies.length === 0) + ).length;
73-75: Use nullish coalescing for complexity/priority; don’t treat0as “missing”.Using
||will treat valid0values as falsy and trigger the fallback, which is different from “not set”. Past comments already called this out here. Based on learnings.You can keep behavior but tighten semantics:
- const nextTaskComplexity = nextTask - ? complexityMap?.get(String(nextTask.id)) || nextTask.complexity - : undefined; + const nextTaskComplexity = nextTask + ? (complexityMap?.get(String(nextTask.id)) ?? nextTask.complexity) + : undefined; @@ - Priority: {nextTask.priority || <Text color="gray">N/A</Text>}{' '} + Priority: {nextTask.priority ?? <Text color="gray">N/A</Text>}{' '}Also applies to: 147-153
apps/terminal-ui/src/components/Onboarding.tsx (1)
14-29: Remove or implementonSkip; it’s currently dead API.
onSkipis exposed onOnboardingPropsbut never used (it’s renamed to_onSkipand ignored). This creates an illusion of a feature that doesn’t exist and complicates the public surface. Based on learnings.If you don’t plan to add a Skip affordance right now, it’s cleaner to drop the prop:
export interface OnboardingProps { @@ - /** - * Callback to skip onboarding - */ - onSkip?: () => void; } @@ export const Onboarding: React.FC<OnboardingProps> = ({ projectPath = process.cwd(), - onComplete, - onSkip: _onSkip + onComplete }) => {You can reintroduce
onSkiplater when there’s a concrete interaction that uses it.Also applies to: 35-39
apps/terminal-ui/src/views/sections/TaskListSection.tsx (3)
30-44: TypeflattenedTasksprecisely and use stable row keys.
Array<any>pluskey={actualIndex}loses type-safety and can cause subtle UI bugs on reordering. Past review already suggested a typed union and stable keys.One possible refactor:
-import React from 'react'; +import React from 'react'; @@ -export const TaskListSection: React.FC<TaskListSectionProps> = ({ +type FlatTask = + | (Task & { + isSubtask?: false; + isLastSubtask?: false; + parentId?: undefined; + }) + | (NonNullable<Task['subtasks']>[number] & { + isSubtask: true; + isLastSubtask: boolean; + parentId: Task['id']; + }); + +export const TaskListSection: React.FC<TaskListSectionProps> = ({ @@ - const flattenedTasks: Array<any> = []; + const flattenedTasks: FlatTask[] = []; @@ - const actualIndex = scrollOffset + index; - const isSelected = selectedIndex === actualIndex; + const actualIndex = scrollOffset + index; + const isSelected = selectedIndex === actualIndex; + const rowKey = task.isSubtask + ? `sub-${task.parentId}-${task.id}` + : String(task.id); @@ - return ( - <Box - key={actualIndex} + return ( + <Box + key={rowKey}This keeps runtime behavior while giving TS more structure and a stable key per logical row.
Also applies to: 98-106, 126-129
126-135: Apply row highlight viaTextbackgroundColor, notBox.Ink doesn’t render
backgroundColoronBoxthe way it does onText, so the current spread/cast is likely a no-op at runtime. Past review already called this out.A minimal change is to compute a
cellBgand apply it on eachText:- return ( - <Box - key={actualIndex} - flexDirection="row" - flexShrink={0} - columnGap={1} - {...({ - backgroundColor: isSelected ? 'blue' : 'transparent' - } as any)} - > + const cellBg = isSelected ? 'blue' : undefined; + + return ( + <Box + key={actualIndex} + flexDirection="row" + flexShrink={0} + columnGap={1} + > @@ - <Text + <Text + backgroundColor={cellBg} @@ - <Text wrap="truncate-end"> - <Text color={isSelected ? 'white' : 'gray'}> + <Text wrap="truncate-end"> + <Text backgroundColor={cellBg} color={isSelected ? 'white' : 'gray'}> @@ - <Text color={isSelected ? 'white' : undefined}> + <Text backgroundColor={cellBg} color={isSelected ? 'white' : undefined}> @@ - <Text + <Text + backgroundColor={cellBg} @@ - <Text + <Text + backgroundColor={cellBg} @@ - <Text + <Text + backgroundColor={cellBg} @@ - <Text + <Text + backgroundColor={cellBg}(Apply
backgroundColor={cellBg}to all cellTextelements.) This makes selection visually obvious.Also applies to: 137-190
191-200: LeveragegetComplexityDisplay.iconfor richer complexity UI.You already call
getComplexityDisplay, which returns anicon, but render a hardcoded●. Prior review suggested using the helper’s icon.- <Text - color={isSelected ? 'white' : complexityDisplay.color} - wrap="truncate-end" - > - ● {complexityDisplay.text} - </Text> + <Text + color={isSelected ? 'white' : complexityDisplay.color} + wrap="truncate-end" + > + {complexityDisplay.icon || '●'} {complexityDisplay.text} + </Text>This keeps backward compatibility while reflecting the full display data.
apps/terminal-ui/src/index.tsx (3)
1-1: Remove shebang from this module; keep it in the actual CLI bin.This file is the library entry that gets imported by the CLI. Keeping
#!/usr/bin/env nodehere can confuse bundlers/ESM consumers. Prior review already noted this.-#!/usr/bin/env nodeMove the shebang to the dedicated CLI bin file instead.
21-26: TightenAppPropsand clamppanelto known values.
panelis typed asstringand then cast toPanelTypevia(initialPanel as PanelType), which allows invalid strings at runtime;sectionis accepted but unused anywhere. Earlier review already highlighted both.A small cleanup:
-type PanelType = 'dashboard' | 'help'; - -interface AppProps { - panel?: string; - section?: string; +type PanelType = 'dashboard' | 'help'; + +interface AppProps { + panel?: PanelType; projectPath?: string; explicitProject?: boolean; // Flag to indicate --project was explicitly provided } @@ - const [currentPanel, setCurrentPanel] = useState<PanelType>( - (initialPanel as PanelType) || 'dashboard' - ); + const initial: PanelType = initialPanel === 'help' ? 'help' : 'dashboard'; + const [currentPanel, setCurrentPanel] = useState<PanelType>(initial);If you don’t plan to support
sectionin this version, also drop it from any CLI docs/flags.Also applies to: 28-40
57-71: GiveflattenedTasksa concrete type instead ofArray<any>.Using
Array<any>loses most of the benefits of TypeScript. Past feedback suggested a simple derived type here.For example:
- const { tasks, complexityMap, loading, error } = useTaskStore(projectPath); - - const flattenedTasks: Array<any> = []; + const { tasks, complexityMap, loading, error } = useTaskStore(projectPath); + + type FlatTask = Task & { isSubtask?: boolean; parentId?: Task['id'] }; + const flattenedTasks: FlatTask[] = [];The rest of the function can stay as-is (
flattenedTasks.push(task)/ push subtasks withisSubtask/parentId), and you keep better type checking around the flattened shape.apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (1)
39-45: GuardtaskIdagainst missingparentIdfor subtasks.If
task.isSubtaskis true buttask.parentIdis undefined, this will render"undefined.{id}"as the ID, which is confusing. Past review already noted this.You can guard and fall back to the plain id:
- const taskId = task.isSubtask ? `${task.parentId}.${task.id}` : task.id; + const taskId = + task.isSubtask && task.parentId != null + ? `${task.parentId}.${task.id}` + : task.id;apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx (2)
68-88: Zero-total projects should avoid segmented bar and show a neutral stateWhen
stats.total === 0,calculateStatusBreakdownreturns{}, which is truthy, socreateProgressBarstill takes the segmented path. With zero tasks, this yields only the “remaining” segment instead of a simpler neutral bar or “No tasks” message, and it diverges from the CLI behavior.Consider treating an empty breakdown as “no breakdown”:
- Detect empty objects and fall back to the simple green/gray bar.
- Optionally render a short placeholder when both
completionPercentage === 0and breakdown is empty (e.g., “No tasks”).For example:
- const calculateStatusBreakdown = (stats: { + const calculateStatusBreakdown = (stats: { @@ - if (stats.total === 0) return {}; + if (stats.total === 0) return {}; @@ - if (!statusBreakdown) { + if ( + !statusBreakdown || + Object.keys(statusBreakdown).length === 0 + ) { const filled = Math.round((completionPercentage / 100) * width); const empty = width - filled; return ( <Text> <Text color="green">{'█'.repeat(filled)}</Text> <Text color="gray">{'░'.repeat(empty)}</Text> </Text> ); } + + if ( + statusBreakdown && + Object.keys(statusBreakdown).length === 0 && + completionPercentage === 0 + ) { + return <Text color="gray">No tasks</Text>; + }Also applies to: 90-113
161-217: Align status colors and remainder segment with shared mappingThe progress bar segments and legends use a different color mapping than the shared status color helper (e.g., in‑progress blue vs expected yellow, pending yellow vs expected blue, review magenta vs expected cyan, blocked red vs expected magenta). The remainder segment also uses yellow, which conflicts with pending.
To keep the TUI consistent with the rest of Task Master:
- Use the canonical mapping:
- in-progress → yellow
- pending → blue
- review → cyan
- blocked → magenta
- cancelled/deferred → gray (current usage is fine)
- Use neutral gray for the “remaining” segment.
Example changes:
- <Text key="in-progress" color="blue"> + <Text key="in-progress" color="yellow"> @@ - <Text key="review" color="magenta"> + <Text key="review" color="cyan"> @@ - <Text key="pending" color="yellow"> + <Text key="pending" color="blue"> @@ - <Text key="blocked" color="red"> + <Text key="blocked" color="magenta"> @@ - // Fill any remaining space with yellow empty blocks + // Fill any remaining space with neutral gray empty blocks @@ - segments.push( - <Text key="remaining" color="yellow"> + segments.push( + <Text key="remaining" color="gray">And update legends accordingly:
- In Progress: <Text color="blue">{inProgressTasks}</Text> Review:{' '} - <Text color="magenta">{reviewTasks}</Text> Pending:{' '} - <Text color="yellow">{pendingTasks}</Text> Blocked:{' '} - <Text color="red">{blockedTasks}</Text> + In Progress: <Text color="yellow">{inProgressTasks}</Text> Review:{' '} + <Text color="cyan">{reviewTasks}</Text> Pending:{' '} + <Text color="blue">{pendingTasks}</Text> Blocked:{' '} + <Text color="magenta">{blockedTasks}</Text>Apply the same mapping to the Subtasks legend.
Also applies to: 219-226, 279-287, 313-317
apps/terminal-ui/src/utils/logger.ts (4)
58-80: Harden file logging path handling and support runtime enablementCurrent file logging behavior has a couple of issues:
filePathis joined directly, so values like'../../etc/passwd'can escape.taskmaster/logs.- The logs directory is only created in the constructor when
fileis initially true; enabling file logging later viaconfigure()won’t ensure the directory exists or thatfilePathis valid.Consider:
- Constraining writes to a fixed base dir using
basename+resolve.- Ensuring the target directory exists at write time (and when file logging is enabled) with
mkdirSync(..., { recursive: true }).- Disabling
fileif path validation or dir creation fails.Example sketch:
-import { appendFileSync, existsSync, mkdirSync } from 'node:fs'; -import { join } from 'node:path'; +import { appendFileSync, existsSync, mkdirSync } from 'node:fs'; +import { join, resolve, dirname, basename } from 'node:path'; @@ constructor - // Create log directory if file logging is enabled - if (this.config.file && this.config.filePath) { - const logDir = join(process.cwd(), '.taskmaster', 'logs'); - if (!existsSync(logDir)) { - try { - mkdirSync(logDir, { recursive: true }); - } catch (error) { - console.error('Failed to create log directory:', error); - this.config.file = false; - } - } - } + // Best-effort base dir creation; detailed checks happen at write time + try { + const baseDir = join(process.cwd(), '.taskmaster', 'logs'); + if (!existsSync(baseDir)) mkdirSync(baseDir, { recursive: true }); + } catch { + // ignore; file logging will fail gracefully on write + } @@ private writeToFile(entry: LogEntry) - const formatted = this.formatEntry(entry, true); - const logPath = join( - process.cwd(), - '.taskmaster', - 'logs', - this.config.filePath - ); + const formatted = this.formatEntry(entry, true); + const baseDir = join(process.cwd(), '.taskmaster', 'logs'); + const safeFile = basename(this.config.filePath); + const logPath = resolve(baseDir, safeFile); + if (!logPath.startsWith(baseDir)) { + // Prevent path escape + return; + } + if (!existsSync(dirname(logPath))) { + mkdirSync(dirname(logPath), { recursive: true }); + }And in
configure, whenfileflips from false to true andfilePathis set, run the same validation/dir-creation logic.Also applies to: 223-241, 308-310
106-129: Preserve ClassifiedError’s underlying Error and rich fields in logsThe
error()method currently:
- Sets
entry.erroronly whenerror instanceof Error, soClassifiedError.erroris dropped.- Only adds
category,severity, andrecoveryActionstocontext, omittingmessage,technicalDetails, andtimestamp.To keep diagnostics intact, map
ClassifiedErrorfully:public error( @@ - const entry: LogEntry = { + const entry: LogEntry = { timestamp: new Date(), level: LogLevel.ERROR, message, context, - error: error instanceof Error ? error : undefined + error: + error instanceof Error + ? error + : error + ? (error as ClassifiedError).error + : undefined }; @@ - if (error && 'category' in error) { - // ClassifiedError - entry.context = { - ...entry.context, - category: error.category, - severity: error.severity, - recoveryActions: error.recoveryActions - }; - } + if (error && 'category' in (error as any)) { + const ce = error as ClassifiedError; + entry.context = { + ...entry.context, + category: ce.category, + severity: ce.severity, + recoveryActions: ce.recoveryActions, + message: ce.message, + technicalDetails: ce.technicalDetails, + classifiedAt: ce.timestamp?.toISOString?.() + }; + }Confirm that `ClassifiedError` in apps/terminal-ui/src/utils/error-handling.ts exposes `error`, `category`, `severity`, `message`, `technicalDetails`, `recoveryActions`, and `timestamp` as used above.
246-271: Guard JSON serialization of context to avoid crashes and noisy logs
JSON.stringify(entry.context)can throw on circular structures or large, complex objects and can produce very noisy output.Use a safe serializer with circular protection and a fallback:
- // Context - if (this.config.includeContext && entry.context) { - output += ` | Context: ${JSON.stringify(entry.context)}`; - } + // Context + if (this.config.includeContext && entry.context) { + const safe = safeStringify(entry.context); + output += ` | Context: ${safe}`; + } @@ - return output; + return output; } + +function safeStringify(obj: unknown): string { + const seen = new WeakSet<object>(); + try { + const json = JSON.stringify(obj, (_k, v) => { + if (typeof v === 'object' && v !== null) { + if (seen.has(v as object)) return '[Circular]'; + seen.add(v as object); + } + if (v instanceof Error) { + return { name: v.name, message: v.message, stack: v.stack }; + } + return v; + }); + return json.length > 2000 ? json.slice(0, 2000) + '…[truncated]' : json; + } catch { + return '[Unserializable Context]'; + } +}
323-331: createLogger returns a partial logger object; prefer a Proxy wrapper
createLoggercurrently builds a plain object with only logging methods and casts it toLogger. This loses methods likegetRecentLogs,clearBuffer,configure, etc., and can cause runtime errors if callers treat the result as a fullLogger.A Proxy keeps the full API while injecting context:
-export function createLogger( - name: string, - config?: Partial<LoggerConfig> -): Logger { - const contextLogger = new Logger(config); - - // Wrap methods to include name in context - const wrappedLogger = { - debug: (message: string, context?: Record<string, unknown>) => - contextLogger.debug(message, { ...context, logger: name }), - info: (message: string, context?: Record<string, unknown>) => - contextLogger.info(message, { ...context, logger: name }), - warn: (message: string, context?: Record<string, unknown>) => - contextLogger.warn(message, { ...context, logger: name }), - error: ( - message: string, - error?: Error | ClassifiedError, - context?: Record<string, unknown> - ) => contextLogger.error(message, error, { ...context, logger: name }), - fatal: ( - message: string, - error?: Error, - context?: Record<string, unknown> - ) => contextLogger.fatal(message, error, { ...context, logger: name }) - }; - - return wrappedLogger as unknown as Logger; -} +export function createLogger( + name: string, + config?: Partial<LoggerConfig> +): Logger { + const base = new Logger(config); + const augment = (ctx?: Record<string, unknown>) => ({ ...ctx, logger: name }); + + return new Proxy(base, { + get(target, prop, receiver) { + if (prop === 'debug') { + return (m: string, c?: Record<string, unknown>) => + target.debug(m, augment(c)); + } + if (prop === 'info') { + return (m: string, c?: Record<string, unknown>) => + target.info(m, augment(c)); + } + if (prop === 'warn') { + return (m: string, c?: Record<string, unknown>) => + target.warn(m, augment(c)); + } + if (prop === 'error') { + return ( + m: string, + e?: Error | ClassifiedError, + c?: Record<string, unknown> + ) => target.error(m, e, augment(c)); + } + if (prop === 'fatal') { + return (m: string, e?: Error, c?: Record<string, unknown>) => + target.fatal(m, e, augment(c)); + } + return Reflect.get(target, prop, receiver); + } + }); +}Confirm that using ES `Proxy` in the target Node/runtime environment is acceptable (supported Node version and no constraints against Proxy).Also applies to: 336-363
apps/terminal-ui/src/components/StatusBar.tsx (4)
113-167: Fix core initialization loop and effect dependenciesThe first
useEffectcan repeatedly create new cores:
coreis in the dependency array.- When
tmCoreis undefined, you create a new core and callsetCore(coreInstance), which updatescoreand retriggers the effect.- On re-run,
tmCoreis still undefined, so a new core is created again.This can thrash core instances.
Suggested fix:
- Reuse the existing
corestate when initializing.- Track whether the instance was created here (if you want cleanup).
- Remove
corefrom the dependency list to avoid re-init loops.Example:
- useEffect(() => { + useEffect(() => { let mounted = true; const fetchTaskStats = async () => { @@ - // Initialize TmCore if not provided - let coreInstance = tmCore; - if (!coreInstance) { - coreInstance = await createTmCore({ projectPath }); - if (mounted) { - setCore(coreInstance); - } - } + // Initialize TmCore if not provided; reuse existing core state + let coreInstance = tmCore ?? core; + if (!coreInstance) { + coreInstance = await createTmCore({ projectPath }); + if (mounted) setCore(coreInstance); + } @@ - // Cleanup - return () => { - mounted = false; - // TmCore doesn't require explicit cleanup - }; - }, [projectPath, tag, tmCore, core]); + return () => { + mounted = false; + }; + }, [projectPath, tag, tmCore]);If you need explicit shutdown, track created instances via a ref and close only those.
169-201: Only start polling once core is ready when live updates are disabledIn the live-updates effect, the polling branch runs when
!enableLiveUpdates && refreshInterval > 0, but the surrounding condition isif (!enableLiveUpdates || !core), and the interval callback usescore!. IfenableLiveUpdatesis false andcoreis still null, polling can start and attempt to use a null core.Restructure to gate polling solely on
!enableLiveUpdatesand ensurecoreexists:- useEffect(() => { - if (!enableLiveUpdates || !core) { - // If live updates disabled, use polling instead - if (!enableLiveUpdates && refreshInterval > 0) { + useEffect(() => { + if (!enableLiveUpdates) { + // Use polling instead (only if core is ready) + if (core && refreshInterval > 0) { const interval = setInterval(async () => { try { - const result = await core!.tasks.list({ tag }); + const result = await core.tasks.list({ tag }); @@ }, refreshInterval); return () => clearInterval(interval); } return undefined; } @@ - }, [core, enableLiveUpdates, tag, refreshInterval]); + }, [core, enableLiveUpdates, tag, refreshInterval]);Also applies to: 184-192
203-276: Ensure watcher cleanup also stops underlying storage watchingThe watcher setup subscribes to
taskWatcher.onChange(handleChange)and starts storage watching, but cleanup only removes the listener:return () => { taskWatcher.removeListener('change', handleChange); };This leaves the underlying file watcher running after unmount, leaking resources.
Extend cleanup to stop watching when a watcher was started:
- if (taskWatcher) { + if (taskWatcher) { @@ - taskWatcher.onChange(handleChange); - - // Return cleanup function - return () => { - taskWatcher.removeListener('change', handleChange); - }; + taskWatcher.onChange(handleChange); + + // Return cleanup function + return () => { + taskWatcher.removeListener('change', handleChange); + if (typeof storage.stopWatching === 'function') { + storage.stopWatching().catch(() => { + // Ignore cleanup errors + }); + } + }; }This matches the storage API expectations and prevents lingering watchers across component lifetimes.
Also applies to: 221-262
442-453: Align exit instructions with actual keybindings (“q” vs Ctrl+C)Top-level UI also supports
qto quit, but the StatusBar only mentions{exitKeyText}(Ctrl+C by default). This can confuse users.Render both keys in the hint:
- {showExitInstructions && ( - <Box> - <Text dimColor> - Press{' '} - <Text color="yellow" bold> - {exitKeyText} - </Text>{' '} - to exit - </Text> - </Box> - )} + {showExitInstructions && ( + <Box> + <Text dimColor> + Press{' '} + <Text color="yellow" bold">{exitKeyText}</Text> or{' '} + <Text color="yellow" bold>q</Text> to exit + </Text> + </Box> + )}
🧹 Nitpick comments (6)
apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
10-18: Keep BottomStatusBar docstring aligned with what it actually renders.The comment mentions “last update time”, but the component only shows dimensions, panel name, and controls—there’s no timestamp prop or rendering.
Either:
- drop “last update time” from the comment, or
- add a
lastUpdatedprop and render it in the bar.Also, you could simplify the prop typing to something like:
maximizedSection?: 'project' | 'dependency' | 'tasklist' | null;to avoid the optional+
nullmix.apps/terminal-ui/src/stores/taskStore.ts (2)
13-31: Reuse theComplexityReporttype from@tm/coreinstead of redefining it here.Since
ComplexityReportis already exported from@tm/core, duplicating the full interface in this file risks silent drift if the core type evolves.Prefer importing the type and dropping the local interface:
-import type { Task } from '@tm/core'; +import type { Task, ComplexityReport } from '@tm/core'; - -export interface ComplexityReport { - // ... -}This keeps the store aligned with the canonical schema and avoids double maintenance. Based on learnings
48-61: Consider distinguishing “load failed” from “no data” and using the shared logger.Right now:
loadTaskslogs toconsole.errorand returns[]on any failure.loadComplexityReportswallows all errors and returnsnull.This makes it impossible for
useTaskStore/UI to tell “no tasks yet” from “couldn’t talk to @tm/core / couldn’t read the report”, which can complicate troubleshooting and dedicated error states.Two potential refinements:
- Return a richer result, e.g.
{ tasks, error }or{ complexityMap, error }, or throw and let the caller capture error into state.- Use your
loggerutility instead ofconsole.errorfor consistency with the rest of the app.Not strictly required for correctness, but it would improve observability and UX of failure modes.
Also applies to: 83-86
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (1)
24-31: Avoid repeatedtasks.findcalls; pre-index tasks by id.
tasksReadyToWorkOn,tasksBlockedByDeps, andnextTaskall calltasks.findinside dependency loops, leading to O(n²) scans on larger task sets and duplicated logic.A small refactor to build a
taskByIdmap once keeps this linear and centralizes the lookup:const taskById = new Map<string, Task>(); tasks.forEach((t) => taskById.set(String(t.id), t)); const areDepsDone = (depIds: Array<string | number> | undefined) => depIds?.every((depId) => taskById.get(String(depId))?.status === 'done') ?? true; const hasBlockingDeps = (depIds: Array<string | number> | undefined) => depIds?.some((depId) => taskById.get(String(depId))?.status !== 'done') ?? false;Then reuse
areDepsDone/hasBlockingDepsin the three filters. This keeps behavior identical but improves clarity and performance.Also applies to: 34-41, 63-70
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (1)
48-55: Use??and explicit null checks for complexity; avoid truthiness gates.
taskComplexity/parentComplexityuse||and latertaskComplexity && getComplexityDisplay, which can treat legitimate0values as “no complexity” and skip rendering.To make this more robust and consistent with
getComplexityDisplay(which already handlesundefined/null), consider:- const taskComplexity = task.isSubtask - ? undefined - : complexityMap?.get(String(task.id)) || task.complexity; + const taskComplexity = task.isSubtask + ? undefined + : complexityMap?.get(String(task.id)) ?? task.complexity; @@ - const parentComplexity = parentTask - ? complexityMap?.get(String(parentTask.id)) || parentTask.complexity - : undefined; + const parentComplexity = parentTask + ? (complexityMap?.get(String(parentTask.id)) ?? parentTask.complexity) + : undefined; @@ - const complexityDisplay = - taskComplexity && getComplexityDisplay - ? getComplexityDisplay(taskComplexity) - : null; + const complexityDisplay = + taskComplexity !== undefined && + taskComplexity !== null && + getComplexityDisplay + ? getComplexityDisplay(taskComplexity) + : null; @@ - const parentComplexityDisplay = - parentComplexity && getComplexityDisplay - ? getComplexityDisplay(parentComplexity) - : null; + const parentComplexityDisplay = + parentComplexity !== undefined && + parentComplexity !== null && + getComplexityDisplay + ? getComplexityDisplay(parentComplexity) + : null;This keeps behavior the same for typical values but avoids subtle bugs if
0(or other falsy values) ever appear.Also applies to: 57-65
apps/terminal-ui/src/views/sections/TaskListSection.tsx (1)
121-125: Use??for complexity fallback instead of||.Same pattern as elsewhere:
||will treat0as “no value” and fall back totask.complexity, which is different from “map has an explicit 0”. Safer to use nullish coalescing:- const complexity = task.isSubtask - ? undefined - : complexityMap?.get(String(task.id)) || task.complexity; + const complexity = task.isSubtask + ? undefined + : complexityMap?.get(String(task.id)) ?? task.complexity;
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
.changeset/add-terminal-ui.md(1 hunks)apps/cli/src/command-registry.ts(2 hunks)apps/cli/src/commands/interactive.command.ts(1 hunks)apps/cli/src/index.ts(1 hunks)apps/terminal-ui/package.json(1 hunks)apps/terminal-ui/src/components/BottomStatusBar.tsx(1 hunks)apps/terminal-ui/src/components/ErrorBoundary.tsx(1 hunks)apps/terminal-ui/src/components/Onboarding.tsx(1 hunks)apps/terminal-ui/src/components/StatusBar.tsx(1 hunks)apps/terminal-ui/src/components/TopBar.tsx(1 hunks)apps/terminal-ui/src/components/index.ts(1 hunks)apps/terminal-ui/src/hooks/useTaskStore.ts(1 hunks)apps/terminal-ui/src/index.tsx(1 hunks)apps/terminal-ui/src/stores/taskStore.ts(1 hunks)apps/terminal-ui/src/utils/error-handling.ts(1 hunks)apps/terminal-ui/src/utils/logger.ts(1 hunks)apps/terminal-ui/src/utils/project-state.ts(1 hunks)apps/terminal-ui/src/utils/task-helpers.ts(1 hunks)apps/terminal-ui/src/views/layouts/AppLayout.tsx(1 hunks)apps/terminal-ui/src/views/modals/DimmedOverlay.tsx(1 hunks)apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx(1 hunks)apps/terminal-ui/src/views/panels/DashboardPanel.tsx(1 hunks)apps/terminal-ui/src/views/panels/HelpPanel.tsx(1 hunks)apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx(1 hunks)apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx(1 hunks)apps/terminal-ui/src/views/sections/TaskListSection.tsx(1 hunks)apps/terminal-ui/terminal-ui.md(1 hunks)apps/terminal-ui/tsconfig.json(1 hunks)packages/tm-core/src/index.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (15)
- apps/cli/src/command-registry.ts
- apps/cli/src/index.ts
- apps/terminal-ui/src/views/modals/DimmedOverlay.tsx
- apps/terminal-ui/src/components/index.ts
- apps/cli/src/commands/interactive.command.ts
- apps/terminal-ui/src/utils/task-helpers.ts
- apps/terminal-ui/src/utils/error-handling.ts
- apps/terminal-ui/src/views/layouts/AppLayout.tsx
- apps/terminal-ui/src/components/TopBar.tsx
- apps/terminal-ui/src/views/panels/DashboardPanel.tsx
- apps/terminal-ui/package.json
- apps/terminal-ui/src/utils/project-state.ts
- apps/terminal-ui/src/components/ErrorBoundary.tsx
- apps/terminal-ui/tsconfig.json
- apps/terminal-ui/src/hooks/useTaskStore.ts
🧰 Additional context used
🧠 Learnings (36)
📓 Common learnings
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1252
File: packages/ai-sdk-provider-grok-cli/package.json:11-13
Timestamp: 2025-09-26T19:05:47.555Z
Learning: In the eyaltoledano/claude-task-master repository, internal tm/ packages use a specific export pattern where the "exports" field points to TypeScript source files (./src/index.ts) while "main" points to compiled output (./dist/index.js) and "types" points to source files (./src/index.ts). This pattern is used consistently across internal packages like tm/core and tm/ai-sdk-provider-grok-cli because they are consumed directly during build-time bundling with tsdown rather than being published as separate packages.
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-18T17:08:48.695Z
Learning: Applies to scripts/modules/commands.js : For AI-powered commands that benefit from project context, use the ContextGatherer utility for multi-source context extraction, support task IDs, file paths, custom context, and project tree, implement fuzzy search for automatic task discovery, and display detailed token breakdown for transparency.
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1252
File: tsconfig.json:22-28
Timestamp: 2025-09-26T19:10:32.906Z
Learning: In the eyaltoledano/claude-task-master repository, all internal tm/ package path mappings in tsconfig.json consistently point to TypeScript source files (e.g., "./packages/*/src/index.ts") rather than built JavaScript. This is intentional architecture because tsdown bundles internal packages directly from source during build time, eliminating the need for separate compilation of internal packages.
📚 Learning: 2025-09-26T19:05:47.555Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1252
File: packages/ai-sdk-provider-grok-cli/package.json:11-13
Timestamp: 2025-09-26T19:05:47.555Z
Learning: In the eyaltoledano/claude-task-master repository, internal tm/ packages use a specific export pattern where the "exports" field points to TypeScript source files (./src/index.ts) while "main" points to compiled output (./dist/index.js) and "types" points to source files (./src/index.ts). This pattern is used consistently across internal packages like tm/core and tm/ai-sdk-provider-grok-cli because they are consumed directly during build-time bundling with tsdown rather than being published as separate packages.
Applied to files:
packages/tm-core/src/index.tsapps/terminal-ui/src/stores/taskStore.tsapps/terminal-ui/src/views/sections/TaskListSection.tsx
📚 Learning: 2025-10-08T19:57:00.982Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1282
File: packages/tm-core/src/utils/index.ts:16-34
Timestamp: 2025-10-08T19:57:00.982Z
Learning: For the tm-core package in the eyaltoledano/claude-task-master repository, the team prefers a minimal, need-based export strategy in index files rather than exposing all internal utilities. Exports should only be added when functions are actually consumed by other packages in the monorepo.
Applied to files:
packages/tm-core/src/index.tsapps/terminal-ui/src/stores/taskStore.ts
📚 Learning: 2025-09-26T19:10:32.906Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1252
File: tsconfig.json:22-28
Timestamp: 2025-09-26T19:10:32.906Z
Learning: In the eyaltoledano/claude-task-master repository, all internal tm/ package path mappings in tsconfig.json consistently point to TypeScript source files (e.g., "./packages/*/src/index.ts") rather than built JavaScript. This is intentional architecture because tsdown bundles internal packages directly from source during build time, eliminating the need for separate compilation of internal packages.
Applied to files:
packages/tm-core/src/index.tsapps/terminal-ui/src/stores/taskStore.tsapps/terminal-ui/src/views/sections/TaskListSection.tsx
📚 Learning: 2025-09-26T19:03:33.225Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1252
File: package.json:130-132
Timestamp: 2025-09-26T19:03:33.225Z
Learning: In the eyaltoledano/claude-task-master repository, packages are bundled using tsdown during the build process, which means dependencies imported by the source code (including tm internal packages like tm/ai-sdk-provider-grok-cli) are included in the final bundle and don't need to be available as separate runtime dependencies, so they should remain as devDependencies rather than being moved to dependencies.
Applied to files:
packages/tm-core/src/index.tsapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-09-03T12:16:15.866Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1178
File: packages/tm-core/package.json:13-64
Timestamp: 2025-09-03T12:16:15.866Z
Learning: For internal packages in the claude-task-master project, Crunchyman-ralph prefers pointing package.json "types" entries to src .ts files rather than dist .d.ts files for better developer experience (DX), as the packages are not being exported as SDKs.
Applied to files:
packages/tm-core/src/index.ts
📚 Learning: 2025-07-18T17:10:02.683Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dev_workflow.mdc:0-0
Timestamp: 2025-07-18T17:10:02.683Z
Learning: Applies to .taskmaster/config.json : Store Taskmaster configuration settings (AI model selections, parameters, logging level, default subtasks/priority, project name, tag management) in `.taskmaster/config.json` in the project root. Do not configure these via environment variables.
Applied to files:
apps/terminal-ui/src/stores/taskStore.tsapps/terminal-ui/src/utils/logger.ts
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Represent task dependencies as arrays of task IDs
Applied to files:
apps/terminal-ui/src/views/sections/TaskListSection.tsxapps/terminal-ui/src/index.tsxapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-09-02T21:51:27.921Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1178
File: packages/tm-core/src/auth/config.ts:5-7
Timestamp: 2025-09-02T21:51:27.921Z
Learning: The user Crunchyman-ralph prefers not to use node: scheme imports (e.g., 'node:os', 'node:path') for Node.js core modules and considers suggestions to change bare imports to node: scheme as too nitpicky.
Applied to files:
apps/terminal-ui/src/index.tsx
📚 Learning: 2025-07-18T17:08:48.695Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-18T17:08:48.695Z
Learning: Applies to scripts/modules/commands.js : Use path.join() to construct file paths, follow established naming conventions (like task_001.txt), check file existence before deletion, and handle file deletion errors gracefully.
Applied to files:
apps/terminal-ui/src/index.tsxapps/terminal-ui/src/utils/logger.ts
📚 Learning: 2025-07-18T17:08:48.695Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-18T17:08:48.695Z
Learning: Applies to scripts/modules/commands.js : Suggest non-destructive alternatives when appropriate, explain the difference between deletion and status changes, and include examples of alternative commands.
Applied to files:
apps/terminal-ui/src/index.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Allow numeric subtask IDs to reference other subtasks within the same parent
Applied to files:
apps/terminal-ui/src/index.tsxapps/terminal-ui/src/views/modals/TaskDetailsModal.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Support both task and subtask dependencies in cycle detection
Applied to files:
apps/terminal-ui/src/index.tsxapps/terminal-ui/src/views/modals/TaskDetailsModal.tsxapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Do not create circular dependencies between subtasks
Applied to files:
apps/terminal-ui/src/index.tsxapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Remove references to non-existent tasks during validation
Applied to files:
apps/terminal-ui/src/index.tsxapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:10:02.683Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dev_workflow.mdc:0-0
Timestamp: 2025-07-18T17:10:02.683Z
Learning: When breaking down complex tasks in Taskmaster, use the `expand_task` command with appropriate flags (`--num`, `--research`, `--force`, `--prompt`) and review generated subtasks for accuracy.
Applied to files:
apps/terminal-ui/src/index.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Format task and dependency IDs consistently when adding dependencies
Applied to files:
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsxapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Use string IDs with dot notation (e.g., "1.2") for subtask references
Applied to files:
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Use numeric IDs for direct task references
Applied to files:
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsxapps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Use visual indicators to show dependency status (✅/⏱️)
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsxapps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Format dependency lists consistently for visualization
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Track and report changes made during dependency cleanup
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Check for and remove references to non-existent tasks during cleanup
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Validate that referenced tasks exist before adding dependencies
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-07-18T17:09:40.548Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/dependencies.mdc:0-0
Timestamp: 2025-07-18T17:09:40.548Z
Learning: Applies to scripts/modules/dependency-manager.js : Prevent tasks from depending on themselves
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-11-12T18:23:46.054Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1386
File: apps/cli/src/ui/formatters/complexity-formatters.ts:27-37
Timestamp: 2025-11-12T18:23:46.054Z
Learning: In the eyaltoledano/claude-task-master codebase, task complexity scores are always integers, never fractional values (e.g., never 7.8 or 8.8). Using parseInt for complexity parsing is appropriate and correct.
Applied to files:
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx
📚 Learning: 2025-09-03T12:20:36.005Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1178
File: packages/tm-core/src/index.ts:56-57
Timestamp: 2025-09-03T12:20:36.005Z
Learning: The logger functionality in tm-core should only be available through the main package entry point (import { getLogger, createLogger, setGlobalLogger } from 'tm-core'), not as a separate subpath export like other modules such as auth or storage.
Applied to files:
apps/terminal-ui/src/utils/logger.ts
📚 Learning: 2025-07-18T17:08:48.695Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/commands.mdc:0-0
Timestamp: 2025-07-18T17:08:48.695Z
Learning: Applies to scripts/modules/commands.js : Do not construct file paths with string concatenation.
Applied to files:
apps/terminal-ui/src/utils/logger.ts
📚 Learning: 2025-09-03T12:15:03.208Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1178
File: apps/cli/src/commands/auth.command.ts:222-224
Timestamp: 2025-09-03T12:15:03.208Z
Learning: The CLI can be invoked using both "task-master" and "tm" as aliases - both forms are valid and acceptable in help text, documentation, and examples.
Applied to files:
.changeset/add-terminal-ui.md
📚 Learning: 2025-08-02T14:54:52.216Z
Learnt from: eyaltoledano
Repo: eyaltoledano/claude-task-master PR: 1069
File: .changeset/floppy-news-buy.md:7-38
Timestamp: 2025-08-02T14:54:52.216Z
Learning: For major feature additions like new CLI commands, eyaltoledano prefers detailed changesets with comprehensive descriptions, usage examples, and feature explanations rather than minimal single-line summaries.
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-08-06T21:13:05.240Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1091
File: assets/claude/agents/task-orchestrator.md:69-78
Timestamp: 2025-08-06T21:13:05.240Z
Learning: For Claude agent instruction files in .claude/agents/ and assets/claude/agents/ directories, ignore markdownlint issues (like MD031 blanks-around-fences, MD040 fenced-code-language, MD041 missing top-level heading) as these files follow Claude's specific agent format requirements rather than standard markdown documentation conventions.
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-08-06T21:11:37.259Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1091
File: .claude/agents/task-orchestrator.md:0-0
Timestamp: 2025-08-06T21:11:37.259Z
Learning: For files in .claude/agents/ and assets/claude/agents/ directories, ignore markdownlint issues like MD041 (missing top-level heading) as these are Claude Code agent instruction files that follow a specific format with YAML frontmatter and don't need to conform to standard markdown documentation conventions.
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-07-18T17:09:23.831Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/cursor_rules.mdc:0-0
Timestamp: 2025-07-18T17:09:23.831Z
Learning: Applies to .cursor/rules/*.mdc : When including code examples in Cursor rule files, use language-specific code blocks and clearly mark DO and DON'T examples.
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-08-06T21:14:23.071Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1091
File: .changeset/wide-actors-report.md:0-0
Timestamp: 2025-08-06T21:14:23.071Z
Learning: For changeset files (.changeset/*.md), the first line after the frontmatter must be a plain, unstyled summary line that gets integrated directly into the changelog. Do not add markdown headings or styling as this would interfere with the changelog generation process. Ignore markdownlint MD041 rule for these files.
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-07-23T16:03:42.784Z
Learnt from: Crunchyman-ralph
Repo: eyaltoledano/claude-task-master PR: 1035
File: .changeset/quiet-rabbits-bathe.md:5-10
Timestamp: 2025-07-23T16:03:42.784Z
Learning: For changeset files (.changeset/*.md), avoid suggesting punctuation for bullet points as the project intentionally omits punctuation to make the generated changelog feel more natural.
Applied to files:
apps/terminal-ui/terminal-ui.md
📚 Learning: 2025-07-18T17:09:23.831Z
Learnt from: CR
Repo: eyaltoledano/claude-task-master PR: 0
File: .cursor/rules/cursor_rules.mdc:0-0
Timestamp: 2025-07-18T17:09:23.831Z
Learning: Applies to .cursor/rules/*.mdc : Use bullet points for clarity, keep descriptions concise, include both DO and DON'T examples, reference actual code over theoretical examples, and use consistent formatting across rules.
Applied to files:
apps/terminal-ui/terminal-ui.md
🧬 Code graph analysis (10)
apps/terminal-ui/src/stores/taskStore.ts (3)
packages/tm-core/src/index.ts (2)
ComplexityReport(115-115)createTmCore(29-29)apps/cli/src/index.ts (1)
Task(38-38)scripts/modules/task-manager/list-tasks.js (1)
tmCore(48-50)
apps/terminal-ui/src/components/StatusBar.tsx (3)
apps/terminal-ui/src/components/index.ts (4)
MessageType(14-14)SystemMessage(13-13)StatusBarProps(12-12)StatusBar(11-11)packages/tm-core/src/index.ts (2)
TmCore(29-29)createTmCore(29-29)scripts/modules/task-manager/list-tasks.js (1)
tmCore(48-50)
apps/terminal-ui/src/views/sections/TaskListSection.tsx (1)
apps/terminal-ui/src/utils/task-helpers.ts (4)
getComplexityDisplay(52-83)padText(88-111)getStatusColor(10-29)getPriorityColor(34-47)
apps/terminal-ui/src/index.tsx (9)
apps/terminal-ui/src/hooks/useTaskStore.ts (1)
useTaskStore(14-114)apps/terminal-ui/src/components/TopBar.tsx (1)
TopBar(12-38)apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
BottomStatusBar(14-48)apps/terminal-ui/src/utils/logger.ts (2)
error(106-130)logger(323-331)apps/terminal-ui/src/views/panels/DashboardPanel.tsx (1)
DashboardPanel(24-90)apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (1)
TaskDetailsModal(29-187)apps/terminal-ui/src/utils/task-helpers.ts (3)
getStatusColor(10-29)getPriorityColor(34-47)getComplexityDisplay(52-83)apps/terminal-ui/src/components/ErrorBoundary.tsx (2)
ErrorBoundary(38-137)render(77-136)apps/terminal-ui/src/utils/project-state.ts (1)
detectProjectState(40-93)
apps/terminal-ui/src/views/modals/TaskDetailsModal.tsx (2)
apps/terminal-ui/src/utils/task-helpers.ts (3)
getComplexityDisplay(52-83)getPriorityColor(34-47)getStatusColor(10-29)apps/terminal-ui/src/views/modals/DimmedOverlay.tsx (1)
DimmedOverlay(9-22)
apps/terminal-ui/src/views/sections/DependencyStatusSection.tsx (2)
scripts/modules/task-manager/list-tasks.js (1)
tasksWithNoDeps(175-180)apps/terminal-ui/src/utils/task-helpers.ts (1)
getComplexityDisplay(52-83)
apps/terminal-ui/src/components/BottomStatusBar.tsx (1)
apps/terminal-ui/src/components/index.ts (1)
BottomStatusBar(6-6)
apps/terminal-ui/src/utils/logger.ts (1)
apps/terminal-ui/src/utils/error-handling.ts (1)
ClassifiedError(41-56)
apps/terminal-ui/src/views/sections/ProjectDashboardSection.tsx (2)
scripts/modules/task-manager/list-tasks.js (4)
totalTasks(100-100)reviewSubtasks(136-136)taskStatusBreakdown(328-335)subtaskStatusBreakdown(337-347)scripts/modules/ui.js (2)
statusBreakdown(1633-1640)actualChars(304-304)
apps/terminal-ui/src/components/Onboarding.tsx (2)
apps/terminal-ui/src/components/index.ts (1)
Onboarding(8-8)apps/terminal-ui/src/utils/project-state.ts (2)
ProjectStateResult(26-33)detectProjectState(40-93)
🪛 LanguageTool
apps/terminal-ui/terminal-ui.md
[style] ~141-~141: Try using a synonym here to elevate your writing.
Context: ...(views/sections/) Sub-components that make up the Dashboard panel: - **ProjectDashboa...
(CONSTITUTE_COMPRISE)
[uncategorized] ~312-~312: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ...s - Priority Breakdown: High/Medium/Low priority counts with colored indicators **Data ...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
[grammar] ~521-~521: Use a hyphen to join words.
Context: ...errors - Runtime errors Layout: Red bordered box with error message and reco...
(QB_NEW_EN_HYPHEN)
🪛 markdownlint-cli2 (0.18.1)
apps/terminal-ui/terminal-ui.md
89-89: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Cursor Bugbot
🔇 Additional comments (1)
packages/tm-core/src/index.ts (1)
147-156: Exports verified—LGTM!All seven exports (TaskWatcher class and six related types) are confirmed to exist in the storage module and are correctly re-exported through the module hierarchy. The code follows the established Advanced API pattern and aligns with the need-based export strategy for the Terminal UI feature.
| Add interactive Terminal UI for real-time task monitoring and navigation | ||
|
|
||
| Introduces a new `task-master interactive` command (alias: `tui`) that launches a full-featured, keyboard-driven terminal dashboard with: | ||
|
|
||
| - Real-time task monitoring with automatic updates when project files change (~500ms) | ||
| - Dashboard with three sections: Project overview, Dependency status, and Task list | ||
| - Keyboard navigation: arrow keys, PgUp/PgDn, Enter for task details | ||
| - Section maximize/restore with keys 0-3 | ||
| - Task details modal showing parent task, dependencies, complexity, and test strategy | ||
| - Help panel with keyboard shortcuts (Tab to toggle) | ||
| - Onboarding guidance for uninitialized projects | ||
|
|
||
| Usage: `task-master interactive` or `task-master tui` | ||
| Options: `--panel=dashboard|help` and `--project=/path/to/project` |
There was a problem hiding this comment.
Ensure CLI options in the changeset match the interactive command’s API.
The Options: line only mentions --panel and --project, but the PR (and docs) also support --section. That will make the generated changelog under‑document the CLI surface.
Consider updating the tail of the changeset like:
-Usage: `task-master interactive` or `task-master tui`
-Options: `--panel=dashboard|help` and `--project=/path/to/project`
+Usage: `task-master interactive` or `task-master tui`
+Options: `--panel=dashboard|help`, `--section=project|dependency|tasklist`, and `--project=/path/to/project`Optionally, you could also mention the tm alias here (e.g., tm interactive) since the CLI supports both entrypoints. Based on learnings
🤖 Prompt for AI Agents
.changeset/add-terminal-ui.md around lines 6 to 19: the changeset's "Options:"
list omits the supported --section flag (and doesn't mention the alternative CLI
alias), so update the tail to list --panel=dashboard|help,
--project=/path/to/project, and --section=<panel|dashboard|help|…> (use the
exact option name and allowed values used by the CLI), and optionally add a
brief note that the command is also available via the tm alias (e.g., "Usage:
task-master interactive | task-master tui (alias: tm)"), ensuring wording
matches the command's actual API and aliases.
| useInput((input, key) => { | ||
| if (showModal) { | ||
| if (key.escape || key.return || input === 'q') { | ||
| setShowModal(false); | ||
| setModalTask(null); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| if ((key.ctrl && input === 'c') || input === 'q') { | ||
| exit(); | ||
| } | ||
|
|
There was a problem hiding this comment.
🧩 Analysis chain
Let Ctrl+C always exit, even when the task details modal is open.
When showModal is true, the first if (showModal) branch returns early, so Ctrl+C does nothing; only Esc/Enter/q close the modal. This diverges from the documented “q or Ctrl+C quits” behavior and may surprise users.
You can handle Ctrl+C before the modal branch:
- useInput((input, key) => {
- if (showModal) {
+ useInput((input, key) => {
+ // Ctrl+C should always exit
+ if (key.ctrl && input === 'c') {
+ exit();
+ return;
+ }
+
+ if (showModal) {
if (key.escape || key.return || input === 'q') {
setShowModal(false);
setModalTask(null);
}
return;
}
- if ((key.ctrl && input === 'c') || input === 'q') {
- exit();
- }
+ if (input === 'q') {
+ exit();
+ }This keeps q as “close modal” while modal is open, and ensures Ctrl+C exits from anywhere.
I'll verify this review comment by examining the actual code and understanding the control flow.
🏁 Script executed:
#!/bin/bash
# Find and examine the target file
fd -t f "index.tsx" | grep -E "terminal-ui.*src" | head -20Length of output: 30
🏁 Script executed:
#!/bin/bash
# Check if file exists and view content around the target lines
if [ -f "apps/terminal-ui/src/index.tsx" ]; then
echo "=== File exists, showing lines 100-130 ==="
sed -n '100,130p' "apps/terminal-ui/src/index.tsx"
else
echo "File not found at expected path"
fd -t f "index.tsx"
fiLength of output: 689
🏁 Script executed:
#!/bin/bash
# Search for exit function definition and useInput hook usage
echo "=== Searching for exit() function ==="
rg -n "export.*exit|const exit|function exit" --type=ts --type=tsx -B2 -A2
echo ""
echo "=== Searching for useInput hook definition ==="
rg -n "useInput" --type=ts --type=tsx -B2 -A2 | head -40Length of output: 141
🏁 Script executed:
#!/bin/bash
# Check the full context of the useInput block to understand control flow
rg -n "useInput\(\(" --type=tsx -A25 "apps/terminal-ui/src/index.tsx"Length of output: 27
🏁 Script executed:
#!/bin/bash
# Verify modal-related state and confirm early return behavior
rg -n "showModal|setShowModal" --type=tsx "apps/terminal-ui/src/index.tsx" -B1 -A1Length of output: 27
🏁 Script executed:
#!/bin/bash
# Check if there are any other input handlers or exit mechanisms
rg -n "key\.ctrl|key\.escape|input === 'q'|exit()" --type=tsx "apps/terminal-ui/src/index.tsx" -B1 -A1Length of output: 27
🏁 Script executed:
#!/bin/bash
# Look for what exit() function is imported from
rg -n "import.*exit|from.*exit" --type=tsx -B1 -A1Length of output: 27
🏁 Script executed:
#!/bin/bash
# Search for useInput from ink or similar library
rg -n "from 'ink'" --type=tsx -B2 -A5Length of output: 27
Let me use a different approach to examine the imports and verify the exit function:
---
🏁 Script executed:
```shell
#!/bin/bash
# View the top of the file to see imports
head -30 "apps/terminal-ui/src/index.tsx"
Length of output: 1106
🏁 Script executed:
#!/bin/bash
# Search for exit import/definition using grep
grep -n "exit\|useInput" "apps/terminal-ui/src/index.tsx" | head -20Length of output: 385
🏁 Script executed:
#!/bin/bash
# View the entire useInput callback to understand full context
sed -n '107,145p' "apps/terminal-ui/src/index.tsx"Length of output: 783
🏁 Script executed:
#!/bin/bash
# Check if exit is defined locally or imported
grep -n "import.*exit\|export.*exit" "apps/terminal-ui/src/index.tsx"Length of output: 0
🏁 Script executed:
#!/bin/bash
# Search for where exit comes from (likely process.exit or ink)
rg "exit" apps/terminal-ui/src/index.tsx -n | head -30Length of output: 281
Let Ctrl+C always exit, even when the task details modal is open.
When showModal is true, the early return at line 114 prevents the Ctrl+C check (line 117) from executing. This means users cannot exit with Ctrl+C while the modal is open—only Esc, Enter, or 'q' will close it. This diverges from the documented "q or Ctrl+C quits" behavior.
Move the Ctrl+C check before the modal branch:
useInput((input, key) => {
+ // Ctrl+C should always exit
+ if (key.ctrl && input === 'c') {
+ exit();
+ return;
+ }
+
if (showModal) {
if (key.escape || key.return || input === 'q') {
setShowModal(false);
setModalTask(null);
}
return;
}
- if ((key.ctrl && input === 'c') || input === 'q') {
- exit();
- }
+ if (input === 'q') {
+ exit();
+ }This ensures Ctrl+C exits from anywhere, while 'q' still closes the modal when open.
🤖 Prompt for AI Agents
In apps/terminal-ui/src/index.tsx around lines 107 to 119, the current early
return inside the showModal branch prevents the Ctrl+C check from running when
the modal is open; move the Ctrl+C check to the top of the useInput handler so
that (key.ctrl && input === 'c') immediately calls exit() regardless of
showModal state, then proceed to the existing modal handling where 'q' (or
escape/return) closes the modal and, when no modal is open, 'q' falls through to
call exit(); ensure the ordering preserves the modal-close behavior for 'q'
while making Ctrl+C always quit.
| if (!enableLiveUpdates && refreshInterval > 0) { | ||
| const interval = setInterval(async () => { | ||
| try { | ||
| const result = await core!.tasks.list({ tag }); |
There was a problem hiding this comment.
Bug: Null reference when polling with uninitialized core
The polling interval setup inside the inner condition if (!enableLiveUpdates && refreshInterval > 0) doesn't check whether core is non-null before accessing core!.tasks.list(). The outer condition !enableLiveUpdates || !core allows entry when core is null (since either condition passes). When enableLiveUpdates is false and core is still null (before asynchronous initialization completes), the interval fires and crashes with a null reference error.
| watcher.stop().catch(() => { | ||
| // Ignore cleanup errors | ||
| }); | ||
| } |
There was a problem hiding this comment.
Bug: File watcher resource leak on early unmount
The cleanup function only calls watcher.stop() when watcherReady is true, but watcherReady is only set in the onReady callback. If the component unmounts before the watcher becomes ready, stop() is never called. Since the chokidar file watcher is created at the start of start() (before the ready event fires), this can leak file handles and system resources. The cleanup should always attempt to call stop() regardless of watcherReady status.
| .alias('tui') | ||
| .option('--panel <panel>', 'Initial panel to display (dashboard, help)') | ||
| .option('--section <section>', 'Initial section to focus') | ||
| .option('-p, --project <path>', 'Project root directory', process.cwd()) |
There was a problem hiding this comment.
Bug: Explicit project flag always true due to default
The --project option has a default value of process.cwd() on line 34, meaning options.project is always truthy. On line 88, explicitProject: !!options.project will therefore always evaluate to true, even when the user didn't explicitly provide the --project flag. This defeats the intended behavior: users running task-master interactive without --project in an uninitialized directory will see error messages instead of the friendlier onboarding messages, because launchTerminalUI thinks the project path was explicitly provided.
What type of PR is this?
Title
Add interactive Terminal UI (Ink/React) for real-time task monitoring and navigation
Description
Introduces a full-featured, keyboard-driven Terminal UI for Task Master, built with React 18 + Ink 5 and integrated into the CLI via the
interactive/tuicommand. It renders a responsive dashboard with sections for Project, Dependencies, and a Task List, plus a Help panel and a Task Details modal. The UI updates automatically on file changes through a debounced watcher and includes onboarding guidance for uninitialized projects.Highlights
TaskWatcher) with 500ms debounce.1,2,3,0).task-master interactive(alias:tui) with options for panel/section and project path; also exposeslaunchTerminalUI()API.Tech/Architecture
useTaskStore,taskStore), and utilities (status colors, formatting, error handling).CLI Usage
Programmatic API
Keyboard shortcuts (core):
Tab→ toggle Dashboard/HelpqorCtrl+C→ quit↑/↓navigate,PgUp/PgDnjump 10,Enterdetails modal,0/1/2/3layout controlEsc/Enter/q→ closePanels & Sections (what you’ll see)
Dashboard Panel
1,2, or3; restore with0.Help Panel: shortcuts & usage tips.
Related Issues
None.
How to Test This
Uninitialized project (no
.taskmaster/):In an empty folder, run
task-master tuiand confirm onboarding steps are shown.No tasks yet (
.taskmasterexists, but no task files):Confirm guidance to add PRD or
add-task.Ready project (has tasks):
UI should load Dashboard with three sections + status bars.
Expected: UI updates within ~500ms after file write; no manual refresh required.
↑/↓,PgUp/PgDnto move; highlight follows selection.Enteron any row → Task Details modal appears with parent, dependencies, complexity dots, and subtasks.1,2,3, then0to verify maximization/restoration logic.Tabto view shortcuts and confirm hints match behavior.Ctrl+C/qto exit; verify clean terminal restore.Dev Notes / Project Structure
Key paths (simplified):
Integration:
InteractiveCommandregistersinteractive/tuiin the CLI with--panel,--section, and--projectoptions.Non-Goals / Deferred
Contributor Checklist
npm testnpm run lint,format,typecheck)Changelog Entry
Add interactive Terminal UI: real-time, debounced file-watched dashboard with Project/Dependencies/Task List sections, Task Details modal, Help panel, onboarding, error boundary, and keyboard-first navigation (
task-master interactive/tui).For Maintainers
feat(tui): …)Troubleshooting (quick)
.taskmaster/exists, valid JSON, terminal ≥ ~40×15.Summary by CodeRabbit
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.
Note
Adds an Ink/React terminal UI with real-time task monitoring and a
task-master interactive(tui) CLI command, backed by a newTaskWatcherin core.apps/terminal-ui):launchTerminalUIwith panels (dashboard,help), task details modal, onboarding messages, and keyboard controls (Tab, arrows, PgUp/PgDn, Enter, 0–3).interactivecommand (alias:tui) with options--panel,--section,--project; registered incommand-registryand exported from@tm/cli.@tm/core):TaskWatcher(chokidar-based) and related types for real-time.taskmasterfile change events.@tm/terminal-uipackage, path aliases, TypeScript configs, and scripts.@tm/cliandtask-master-ai.Written by Cursor Bugbot for commit b8fa590. This will update automatically on new commits. Configure here.