@task-mcp/cli 1.0.9 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@task-mcp/cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Zero-dependency CLI for task-mcp with Bun native visualization",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Dashboard command - displays project overview with multiple widgets
2
+ * Dashboard command - displays project overview with unified widgets
3
+ * Inspired by TaskMaster CLI design
3
4
  */
4
5
 
5
6
  import { VERSION } from "../index.js";
@@ -10,11 +11,11 @@ import {
10
11
  table,
11
12
  icons,
12
13
  banner,
13
- hline,
14
14
  pad,
15
15
  truncate,
16
16
  stripAnsi,
17
17
  sideBySide,
18
+ displayWidth,
18
19
  type TableColumn,
19
20
  } from "../ansi.js";
20
21
  import {
@@ -25,28 +26,16 @@ import {
25
26
  calculateDependencyMetrics,
26
27
  suggestNextTask,
27
28
  getTodayTasks,
28
- getThisWeekTasks,
29
29
  getOverdueTasks,
30
- buildTaskTree,
31
- countTreeNodes,
32
- calculateCriticalPath,
33
- calculateAnalysisStats,
34
30
  listInboxItems,
35
31
  type Task,
36
32
  type Project,
37
- type TaskTreeNode,
38
- type InboxItem,
39
33
  } from "../storage.js";
40
34
 
41
35
  // =============================================================================
42
36
  // Formatters
43
37
  // =============================================================================
44
38
 
45
- function formatStatus(status: Task["status"]): string {
46
- const icon = icons[status] ?? icons.pending;
47
- return `${icon} ${status}`;
48
- }
49
-
50
39
  function formatPriority(priority: Task["priority"]): string {
51
40
  const colors: Record<string, (s: string) => string> = {
52
41
  critical: c.red,
@@ -57,322 +46,170 @@ function formatPriority(priority: Task["priority"]): string {
57
46
  return (colors[priority] ?? c.gray)(priority);
58
47
  }
59
48
 
60
- function formatDependencies(deps: Task["dependencies"]): string {
61
- if (!deps || deps.length === 0) return c.gray("None");
62
- return c.cyan(deps.map(d => d.taskId.slice(0, 4)).join(", "));
63
- }
64
-
65
- function formatDate(dateStr: string): string {
66
- const date = new Date(dateStr);
67
- const now = new Date();
68
- const diffDays = Math.floor((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
69
-
70
- if (diffDays < 0) return c.red(`${Math.abs(diffDays)}d overdue`);
71
- if (diffDays === 0) return c.yellow("Today");
72
- if (diffDays === 1) return c.yellow("Tomorrow");
73
- if (diffDays <= 7) return c.blue(`${diffDays}d`);
74
- return c.gray(date.toLocaleDateString());
75
- }
76
-
77
49
  // =============================================================================
78
- // Widget: Project Overview
50
+ // Widget: Unified Status (combines Overview + Schedule + Dependencies)
79
51
  // =============================================================================
80
52
 
81
- function renderProjectOverview(tasks: Task[], project?: Project): string {
53
+ function renderStatusWidget(tasks: Task[], projects: Project[]): string {
82
54
  const stats = calculateStats(tasks);
83
- const activeTasks = stats.total - stats.cancelled;
84
-
85
- const lines = [
86
- c.bold("Progress"),
87
- progressBar(stats.completed, activeTasks, { width: 28 }),
88
- "",
89
- `${c.green("Done")}: ${stats.completed} ${c.blue("In Progress")}: ${stats.inProgress}`,
90
- `${c.yellow("Pending")}: ${stats.pending} ${c.red("Blocked")}: ${stats.blocked}`,
91
- stats.cancelled > 0 ? c.gray(`Cancelled: ${stats.cancelled}`) : "",
92
- "",
93
- c.bold("Priority"),
94
- `${c.red("Critical")}: ${stats.byPriority.critical} ${c.yellow("High")}: ${stats.byPriority.high}`,
95
- `${c.blue("Medium")}: ${stats.byPriority.medium} ${c.gray("Low")}: ${stats.byPriority.low}`,
96
- ].filter(Boolean);
97
-
98
- return box(lines.join("\n"), { title: "Overview", borderColor: "cyan" });
99
- }
100
-
101
- // =============================================================================
102
- // Widget: Time-based View
103
- // =============================================================================
104
-
105
- function renderTimeWidget(tasks: Task[]): string {
55
+ const depMetrics = calculateDependencyMetrics(tasks);
106
56
  const today = getTodayTasks(tasks);
107
57
  const overdue = getOverdueTasks(tasks);
108
- const thisWeek = getThisWeekTasks(tasks);
109
-
110
- const lines = [
111
- c.bold("Due Dates"),
112
- "",
113
- `${c.red(icons.warning)} ${c.red("Overdue")}: ${overdue.length}`,
114
- `${c.yellow("!")} ${c.yellow("Today")}: ${today.length}`,
115
- `${c.blue(">")} ${c.blue("This Week")}: ${thisWeek.length}`,
116
- ];
117
-
118
- // Show up to 3 urgent tasks
119
- const urgent = [...overdue, ...today.filter(t => !overdue.includes(t))].slice(0, 3);
120
- if (urgent.length > 0) {
121
- lines.push("", c.dim("Urgent:"));
122
- for (const t of urgent) {
123
- const due = t.dueDate ? formatDate(t.dueDate) : "";
124
- lines.push(` ${truncate(t.title, 20)} ${due}`);
125
- }
126
- }
127
-
128
- return box(lines.join("\n"), { title: "Schedule", borderColor: "yellow" });
129
- }
130
-
131
- // =============================================================================
132
- // Widget: Dependencies & Next Task
133
- // =============================================================================
58
+ const activeTasks = stats.total - stats.cancelled;
59
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
134
60
 
135
- function renderDependencyWidget(tasks: Task[]): string {
136
- const depMetrics = calculateDependencyMetrics(tasks);
137
- const nextTask = suggestNextTask(tasks);
61
+ const lines: string[] = [];
138
62
 
139
- const lines = [
140
- c.bold("Dependencies"),
141
- "",
142
- `${c.green("Ready")}: ${depMetrics.readyToWork} ${c.red("Blocked")}: ${depMetrics.blockedByDependencies}`,
143
- `${c.gray("No deps")}: ${depMetrics.noDependencies}`,
144
- ];
63
+ // Progress bar with fraction
64
+ const bar = progressBar(stats.completed, activeTasks, { width: 24 });
65
+ lines.push(`${bar} ${c.bold(`${percent}%`)} ${stats.completed}/${activeTasks} tasks`);
66
+ lines.push("");
145
67
 
146
- if (depMetrics.mostDependedOn) {
147
- lines.push(
148
- "",
149
- c.dim("Bottleneck:"),
150
- ` ${truncate(depMetrics.mostDependedOn.title, 18)} (${depMetrics.mostDependedOn.count})`
151
- );
68
+ // Status counts (2 rows)
69
+ lines.push(
70
+ `${c.green("Done")}: ${stats.completed} ` +
71
+ `${c.blue("Progress")}: ${stats.inProgress} ` +
72
+ `${c.yellow("Pending")}: ${stats.pending} ` +
73
+ `${c.red("Blocked")}: ${stats.blocked}`
74
+ );
75
+
76
+ // Schedule info
77
+ const scheduleInfo = [];
78
+ if (overdue.length > 0) scheduleInfo.push(c.red(`Overdue: ${overdue.length}`));
79
+ if (today.length > 0) scheduleInfo.push(c.yellow(`Today: ${today.length}`));
80
+ if (scheduleInfo.length > 0) {
81
+ lines.push(scheduleInfo.join(" "));
152
82
  }
153
83
 
154
- lines.push("", c.bold("Next Task"));
155
- if (nextTask) {
156
- lines.push(
157
- ` ${truncate(nextTask.title, 22)}`,
158
- ` ${formatPriority(nextTask.priority)}`
159
- );
160
- } else {
161
- lines.push(c.gray(" No tasks available"));
162
- }
84
+ lines.push("");
163
85
 
164
- return box(lines.join("\n"), { title: "Work Queue", borderColor: "green" });
86
+ // Priority breakdown
87
+ lines.push(
88
+ `${c.red("Critical")}: ${stats.byPriority.critical} ` +
89
+ `${c.yellow("High")}: ${stats.byPriority.high} ` +
90
+ `${c.blue("Medium")}: ${stats.byPriority.medium} ` +
91
+ `${c.gray("Low")}: ${stats.byPriority.low}`
92
+ );
93
+
94
+ // Dependencies summary
95
+ lines.push(
96
+ `${c.green("Ready")}: ${depMetrics.readyToWork} ` +
97
+ `${c.red("Blocked")}: ${depMetrics.blockedByDependencies}` +
98
+ (depMetrics.mostDependedOn
99
+ ? ` ${c.dim("Bottleneck:")} ${truncate(depMetrics.mostDependedOn.title, 15)}`
100
+ : "")
101
+ );
102
+
103
+ return box(lines.join("\n"), {
104
+ title: "Status",
105
+ borderColor: "cyan",
106
+ padding: 1,
107
+ });
165
108
  }
166
109
 
167
110
  // =============================================================================
168
- // Widget: Inbox Summary
111
+ // Widget: Next Actions (combines Next Task + Inbox)
169
112
  // =============================================================================
170
113
 
171
- async function renderInboxWidget(): Promise<string> {
114
+ async function renderActionsWidget(tasks: Task[]): Promise<string> {
115
+ const nextTask = suggestNextTask(tasks);
172
116
  const pendingItems = await listInboxItems("pending");
173
117
 
174
- const lines = [
175
- c.bold("Quick Capture Inbox"),
176
- "",
177
- ];
178
-
179
- if (pendingItems.length === 0) {
180
- lines.push(c.dim("No pending items"));
181
- lines.push(c.dim("Use 'task inbox add' to capture"));
182
- } else {
183
- lines.push(`${c.yellow("Pending")}: ${pendingItems.length} items`);
184
- lines.push("");
185
-
186
- // Show recent items
187
- const recent = pendingItems.slice(0, 4);
188
- for (const item of recent) {
189
- const date = new Date(item.capturedAt);
190
- const ago = getTimeAgo(date);
191
- const tags = item.tags?.length ? c.dim(` #${item.tags[0]}`) : "";
192
- lines.push(`${c.yellow("○")} ${truncate(item.content, 22)}${tags}`);
193
- lines.push(` ${c.dim(ago)}`);
194
- }
195
-
196
- if (pendingItems.length > 4) {
197
- lines.push(c.gray(`+${pendingItems.length - 4} more`));
118
+ const lines: string[] = [];
119
+
120
+ // Next tasks (top 2 suggestions)
121
+ const readyTasks = tasks
122
+ .filter(t => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
123
+ .sort((a, b) => {
124
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
125
+ return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
126
+ })
127
+ .slice(0, 3);
128
+
129
+ if (readyTasks.length > 0) {
130
+ for (const task of readyTasks) {
131
+ const deps = task.dependencies?.length ?? 0;
132
+ const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
133
+ lines.push(`${c.cyan("→")} ${truncate(task.title, 22)}`);
134
+ lines.push(` ${formatPriority(task.priority)}, ${depsInfo}`);
198
135
  }
199
- }
200
-
201
- return box(lines.join("\n"), { title: "Inbox", borderColor: "yellow" });
202
- }
203
-
204
- function getTimeAgo(date: Date): string {
205
- const now = new Date();
206
- const diffMs = now.getTime() - date.getTime();
207
- const diffMins = Math.floor(diffMs / 60000);
208
- const diffHours = Math.floor(diffMins / 60);
209
- const diffDays = Math.floor(diffHours / 24);
210
-
211
- if (diffMins < 60) return `${diffMins}m ago`;
212
- if (diffHours < 24) return `${diffHours}h ago`;
213
- if (diffDays < 7) return `${diffDays}d ago`;
214
- return date.toLocaleDateString();
215
- }
216
-
217
- // =============================================================================
218
- // Widget: Critical Path
219
- // =============================================================================
220
-
221
- function renderCriticalPathWidget(tasks: Task[]): string {
222
- const cpm = calculateCriticalPath(tasks);
223
-
224
- const lines = [
225
- c.bold("Critical Path Analysis"),
226
- "",
227
- ];
228
-
229
- if (cpm.criticalPath.length === 0) {
230
- lines.push(c.gray("No critical path detected."));
231
- lines.push(c.dim("(Tasks have no dependencies)"));
136
+ } else if (nextTask) {
137
+ const deps = nextTask.dependencies?.length ?? 0;
138
+ const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
139
+ lines.push(`${c.cyan("→")} ${truncate(nextTask.title, 22)}`);
140
+ lines.push(` ${formatPriority(nextTask.priority)}, ${depsInfo}`);
232
141
  } else {
233
- lines.push(`${c.red("Path length")}: ${cpm.criticalPath.length} tasks`);
234
- lines.push(`${c.yellow("Est. duration")}: ~${cpm.totalDuration}m`);
235
- lines.push("");
236
-
237
- // Show critical tasks
238
- lines.push(c.dim("Critical tasks:"));
239
- for (const task of cpm.criticalPath.slice(0, 3)) {
240
- const status = icons[task.status] ?? icons.pending;
241
- lines.push(` ${status} ${truncate(task.title, 20)}`);
242
- }
243
- if (cpm.criticalPath.length > 3) {
244
- lines.push(c.gray(` +${cpm.criticalPath.length - 3} more`));
245
- }
142
+ lines.push(c.dim("No tasks ready"));
246
143
  }
247
144
 
248
- // Bottlenecks
249
- if (cpm.bottlenecks.length > 0) {
250
- lines.push("", c.dim("Bottlenecks:"));
251
- for (const b of cpm.bottlenecks.slice(0, 2)) {
252
- lines.push(` ${truncate(b.task.title, 16)} ${c.red(`(${b.blocksCount})`)}`);
145
+ // Inbox summary
146
+ lines.push("");
147
+ if (pendingItems.length > 0) {
148
+ lines.push(`${c.yellow("Inbox")}: ${pendingItems.length} items`);
149
+ // Show first item preview
150
+ const first = pendingItems[0];
151
+ if (first) {
152
+ lines.push(` ${c.dim(truncate(first.content, 20))}`);
253
153
  }
154
+ } else {
155
+ lines.push(c.dim("Inbox empty"));
254
156
  }
255
157
 
256
- return box(lines.join("\n"), { title: "Critical Path", borderColor: "red" });
158
+ return box(lines.join("\n"), {
159
+ title: "Next Actions",
160
+ borderColor: "green",
161
+ padding: 1,
162
+ });
257
163
  }
258
164
 
259
165
  // =============================================================================
260
- // Widget: Hierarchy Tree
166
+ // Projects Table
261
167
  // =============================================================================
262
168
 
263
- function renderHierarchyWidget(tasks: Task[]): string {
264
- const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
265
- const tree = buildTaskTree(activeTasks);
266
- const totalNodes = countTreeNodes(tree);
267
- const rootCount = tree.length;
268
- const subtaskCount = totalNodes - rootCount;
269
-
270
- const lines = [
271
- c.bold("Task Hierarchy"),
272
- "",
273
- `${c.cyan("Root tasks")}: ${rootCount}`,
274
- `${c.magenta("Subtasks")}: ${subtaskCount}`,
275
- "",
276
- ];
277
-
278
- // Render first few root tasks with their immediate children
279
- const maxRoots = 4;
280
- for (let i = 0; i < Math.min(tree.length, maxRoots); i++) {
281
- const node = tree[i]!;
282
- const status = icons[node.task.status] ?? icons.pending;
283
- lines.push(`${status} ${truncate(node.task.title, 22)}`);
284
-
285
- // Show up to 2 children
286
- for (let j = 0; j < Math.min(node.children.length, 2); j++) {
287
- const child = node.children[j]!;
288
- const childStatus = icons[child.task.status] ?? icons.pending;
289
- lines.push(` ${c.gray("└")} ${childStatus} ${truncate(child.task.title, 18)}`);
290
- }
291
- if (node.children.length > 2) {
292
- lines.push(` ${c.gray(` +${node.children.length - 2} more`)}`);
293
- }
169
+ async function renderProjectsTable(projects: Project[]): Promise<string> {
170
+ if (projects.length === 0) {
171
+ return c.gray("No projects found.");
294
172
  }
295
173
 
296
- if (tree.length > maxRoots) {
297
- lines.push(c.gray(`+${tree.length - maxRoots} more root tasks`));
174
+ const rows: {
175
+ name: string;
176
+ progress: string;
177
+ ready: number;
178
+ blocked: number;
179
+ total: number;
180
+ }[] = [];
181
+
182
+ for (const project of projects.slice(0, 10)) {
183
+ const tasks = await listTasks(project.id);
184
+ const stats = calculateStats(tasks);
185
+ const depMetrics = calculateDependencyMetrics(tasks);
186
+ const activeTasks = stats.total - stats.cancelled;
187
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
188
+
189
+ // Create mini progress bar
190
+ const barWidth = 8;
191
+ const filled = Math.round((percent / 100) * barWidth);
192
+ const empty = barWidth - filled;
193
+ const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
194
+
195
+ rows.push({
196
+ name: truncate(project.name, 20),
197
+ progress: `${miniBar} ${pad(String(percent) + "%", 4, "right")}`,
198
+ ready: depMetrics.readyToWork,
199
+ blocked: depMetrics.blockedByDependencies,
200
+ total: activeTasks,
201
+ });
298
202
  }
299
203
 
300
- return box(lines.join("\n"), { title: "Hierarchy", borderColor: "magenta" });
301
- }
302
-
303
- // =============================================================================
304
- // Widget: Complexity & Tech Stack Analysis
305
- // =============================================================================
306
-
307
- function renderAnalysisWidget(tasks: Task[]): string {
308
- const stats = calculateAnalysisStats(tasks);
309
-
310
- const lines = [
311
- c.bold("Analysis Overview"),
312
- "",
204
+ const columns: TableColumn[] = [
205
+ { header: "Project", key: "name", width: 22 },
206
+ { header: "Progress", key: "progress", width: 16 },
207
+ { header: "Tasks", key: "total", width: 6, align: "right" },
208
+ { header: "Ready", key: "ready", width: 6, align: "right", format: (v) => c.green(String(v)) },
209
+ { header: "Blocked", key: "blocked", width: 8, align: "right", format: (v) => Number(v) > 0 ? c.red(String(v)) : c.gray(String(v)) },
313
210
  ];
314
211
 
315
- // Complexity section
316
- if (stats.complexity.analyzed > 0) {
317
- lines.push(c.cyan("Complexity"));
318
- lines.push(` Analyzed: ${stats.complexity.analyzed} tasks`);
319
- lines.push(` Avg Score: ${c.yellow(String(stats.complexity.avgScore))}/10`);
320
-
321
- const dist = stats.complexity.distribution;
322
- const distBar = [
323
- c.green(`■`.repeat(Math.min(dist.low, 5))),
324
- c.yellow(`■`.repeat(Math.min(dist.medium, 5))),
325
- c.red(`■`.repeat(Math.min(dist.high, 5))),
326
- ].join("");
327
- lines.push(` ${distBar} L:${dist.low} M:${dist.medium} H:${dist.high}`);
328
-
329
- if (stats.complexity.topFactors.length > 0) {
330
- lines.push("");
331
- lines.push(c.dim("Top factors:"));
332
- for (const f of stats.complexity.topFactors.slice(0, 3)) {
333
- lines.push(` ${c.gray("•")} ${truncate(f.factor, 16)} (${f.count})`);
334
- }
335
- }
336
- } else {
337
- lines.push(c.gray("No complexity data"));
338
- lines.push(c.dim("Use save_complexity_analysis"));
339
- }
340
-
341
- lines.push("");
342
-
343
- // Tech Stack section
344
- if (stats.techStack.analyzed > 0) {
345
- lines.push(c.magenta("Tech Stack"));
346
- lines.push(` Analyzed: ${stats.techStack.analyzed} tasks`);
347
-
348
- // Risk distribution
349
- const risk = stats.techStack.byRisk;
350
- const riskItems = [];
351
- if (risk.critical > 0) riskItems.push(c.red(`Crit:${risk.critical}`));
352
- if (risk.high > 0) riskItems.push(c.yellow(`High:${risk.high}`));
353
- if (risk.medium > 0) riskItems.push(c.blue(`Med:${risk.medium}`));
354
- if (risk.low > 0) riskItems.push(c.green(`Low:${risk.low}`));
355
- lines.push(` Risk: ${riskItems.join(" ")}`);
356
-
357
- if (stats.techStack.breakingChanges > 0) {
358
- lines.push(` ${c.red(icons.warning)} ${stats.techStack.breakingChanges} breaking change(s)`);
359
- }
360
-
361
- // Top areas
362
- const areas = Object.entries(stats.techStack.byArea)
363
- .sort((a, b) => b[1] - a[1])
364
- .slice(0, 4);
365
- if (areas.length > 0) {
366
- lines.push("");
367
- lines.push(c.dim("Areas:"));
368
- lines.push(` ${areas.map(([a, n]) => `${a}(${n})`).join(" ")}`);
369
- }
370
- } else {
371
- lines.push(c.gray("No tech stack data"));
372
- lines.push(c.dim("Use save_tech_stack_analysis"));
373
- }
374
-
375
- return box(lines.join("\n"), { title: "Analysis", borderColor: "blue" });
212
+ return table(rows as unknown as Record<string, unknown>[], columns);
376
213
  }
377
214
 
378
215
  // =============================================================================
@@ -417,54 +254,49 @@ export async function dashboard(projectId?: string): Promise<void> {
417
254
  ? `${c.bold("Project:")} ${project.name}`
418
255
  : `${c.bold("All Projects")} (${projects.length} projects)`;
419
256
 
420
- console.log(c.dim(`Version: ${VERSION} ${projectInfo}`));
257
+ console.log(c.dim(`v${VERSION} ${projectInfo}`));
421
258
  console.log();
422
259
 
423
- // Render widgets in grid layout
424
- const overview = renderProjectOverview(tasks, project);
425
- const timeWidget = renderTimeWidget(tasks);
426
- const depWidget = renderDependencyWidget(tasks);
427
- const criticalPathWidget = renderCriticalPathWidget(tasks);
428
- const hierarchyWidget = renderHierarchyWidget(tasks);
429
- const analysisWidget = renderAnalysisWidget(tasks);
430
- const inboxWidget = await renderInboxWidget();
431
-
432
- // Print widgets in 3-column grid layout
433
- // Row 1: Overview | Schedule | Inbox
434
- console.log(sideBySide([overview, timeWidget, inboxWidget]));
435
- console.log();
260
+ // Render widgets
261
+ const statusWidget = renderStatusWidget(tasks, projects);
262
+ const actionsWidget = await renderActionsWidget(tasks);
436
263
 
437
- // Row 2: Work Queue | Critical Path | Analysis
438
- console.log(sideBySide([depWidget, criticalPathWidget, analysisWidget]));
264
+ // Print widgets side by side
265
+ console.log(sideBySide([statusWidget, actionsWidget], 2));
439
266
  console.log();
440
267
 
441
- // Row 3: Hierarchy (full width or single)
442
- console.log(hierarchyWidget);
443
- console.log();
268
+ // Projects table (only if showing all projects)
269
+ if (!project && projects.length > 1) {
270
+ console.log(c.bold("Projects"));
271
+ console.log();
272
+ console.log(await renderProjectsTable(projects));
273
+ console.log();
274
+ }
444
275
 
445
- // Task table
446
- if (tasks.length > 0) {
276
+ // Task list for single project view
277
+ if (project || projects.length === 1) {
447
278
  const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
448
- const displayTasks = activeTasks.slice(0, 15); // Limit to 15 rows
279
+ if (activeTasks.length > 0) {
280
+ const displayTasks = activeTasks.slice(0, 10);
449
281
 
450
- console.log(c.bold(`Active Tasks (${activeTasks.length})`));
451
- console.log();
282
+ console.log(c.bold(`Tasks (${activeTasks.length})`));
283
+ console.log();
452
284
 
453
- const columns: TableColumn[] = [
454
- { header: "ID", key: "id", width: 6, format: (v) => c.cyan(String(v).slice(0, 4)) },
455
- { header: "Title", key: "title", width: 45 },
456
- { header: "Status", key: "status", width: 14, format: (v) => formatStatus(v as Task["status"]) },
457
- { header: "Priority", key: "priority", width: 10, format: (v) => formatPriority(v as Task["priority"]) },
458
- { header: "Due", key: "dueDate", width: 12, format: (v) => v ? formatDate(String(v)) : c.gray("-") },
459
- ];
285
+ const columns: TableColumn[] = [
286
+ { header: "Title", key: "title", width: 40 },
287
+ { header: "Status", key: "status", width: 12, format: (v) => {
288
+ const icon = icons[v as Task["status"]] ?? icons.pending;
289
+ return `${icon} ${v}`;
290
+ }},
291
+ { header: "Priority", key: "priority", width: 10, format: (v) => formatPriority(v as Task["priority"]) },
292
+ ];
460
293
 
461
- console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
294
+ console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
462
295
 
463
- if (activeTasks.length > 15) {
464
- console.log(c.gray(`\n(Showing 15 of ${activeTasks.length} active tasks)`));
296
+ if (activeTasks.length > 10) {
297
+ console.log(c.gray(`\n(+${activeTasks.length - 10} more tasks)`));
298
+ }
465
299
  }
466
- } else {
467
- console.log(c.gray("No tasks found."));
468
300
  }
469
301
 
470
302
  console.log();