@task-mcp/cli 1.0.9 → 1.0.11

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.11",
4
4
  "description": "Zero-dependency CLI for task-mcp with Bun native visualization",
5
5
  "type": "module",
6
6
  "bin": {
package/src/ansi.ts CHANGED
@@ -136,7 +136,7 @@ export function box(content: string, options: {
136
136
  const { padding = 1, borderColor = "cyan", title, rounded = true } = options;
137
137
 
138
138
  const lines = content.split("\n");
139
- const maxLen = Math.max(...lines.map(l => stripAnsi(l).length), title ? title.length + 2 : 0);
139
+ const maxLen = Math.max(...lines.map(l => displayWidth(stripAnsi(l))), title ? title.length + 2 : 0);
140
140
  const innerWidth = maxLen + padding * 2;
141
141
 
142
142
  const tl = rounded ? BOX.rTopLeft : BOX.topLeft;
@@ -166,8 +166,8 @@ export function box(content: string, options: {
166
166
 
167
167
  // Content lines
168
168
  const contentLines = lines.map(line => {
169
- const stripped = stripAnsi(line);
170
- const padRight = innerWidth - stripped.length - padding;
169
+ const lineWidth = displayWidth(stripAnsi(line));
170
+ const padRight = innerWidth - lineWidth - padding;
171
171
  return applyBorder(v) + " ".repeat(padding) + line + " ".repeat(Math.max(0, padRight)) + applyBorder(v);
172
172
  });
173
173
 
@@ -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,148 +46,141 @@ 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);
55
+ const depMetrics = calculateDependencyMetrics(tasks);
56
+ const today = getTodayTasks(tasks);
57
+ const overdue = getOverdueTasks(tasks);
83
58
  const activeTasks = stats.total - stats.cancelled;
59
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
84
60
 
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
- }
61
+ const lines: string[] = [];
100
62
 
101
- // =============================================================================
102
- // Widget: Time-based View
103
- // =============================================================================
104
-
105
- function renderTimeWidget(tasks: Task[]): string {
106
- const today = getTodayTasks(tasks);
107
- 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
- ];
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("");
117
67
 
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
- }
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(" "));
126
82
  }
127
83
 
128
- return box(lines.join("\n"), { title: "Schedule", borderColor: "yellow" });
84
+ lines.push("");
85
+
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
+ });
129
108
  }
130
109
 
131
110
  // =============================================================================
132
- // Widget: Dependencies & Next Task
111
+ // Widget: Next Actions (combines Next Task + Inbox)
133
112
  // =============================================================================
134
113
 
135
- function renderDependencyWidget(tasks: Task[]): string {
136
- const depMetrics = calculateDependencyMetrics(tasks);
114
+ async function renderActionsWidget(tasks: Task[]): Promise<string> {
137
115
  const nextTask = suggestNextTask(tasks);
138
116
 
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
- ];
145
-
146
- if (depMetrics.mostDependedOn) {
147
- lines.push(
148
- "",
149
- c.dim("Bottleneck:"),
150
- ` ${truncate(depMetrics.mostDependedOn.title, 18)} (${depMetrics.mostDependedOn.count})`
151
- );
152
- }
153
-
154
- lines.push("", c.bold("Next Task"));
155
- if (nextTask) {
156
- lines.push(
157
- ` ${truncate(nextTask.title, 22)}`,
158
- ` ${formatPriority(nextTask.priority)}`
159
- );
117
+ const lines: string[] = [];
118
+
119
+ // Next tasks (top 3 suggestions)
120
+ const readyTasks = tasks
121
+ .filter(t => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
122
+ .sort((a, b) => {
123
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
124
+ return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
125
+ })
126
+ .slice(0, 4);
127
+
128
+ if (readyTasks.length > 0) {
129
+ for (const task of readyTasks) {
130
+ const deps = task.dependencies?.length ?? 0;
131
+ const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
132
+ lines.push(`${c.cyan("")} ${truncate(task.title, 24)}`);
133
+ lines.push(` ${formatPriority(task.priority)}, ${depsInfo}`);
134
+ }
135
+ } else if (nextTask) {
136
+ const deps = nextTask.dependencies?.length ?? 0;
137
+ const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
138
+ lines.push(`${c.cyan("→")} ${truncate(nextTask.title, 24)}`);
139
+ lines.push(` ${formatPriority(nextTask.priority)}, ${depsInfo}`);
160
140
  } else {
161
- lines.push(c.gray(" No tasks available"));
141
+ lines.push(c.dim("No tasks ready"));
162
142
  }
163
143
 
164
- return box(lines.join("\n"), { title: "Work Queue", borderColor: "green" });
144
+ return box(lines.join("\n"), {
145
+ title: "Next Actions",
146
+ borderColor: "green",
147
+ padding: 1,
148
+ });
165
149
  }
166
150
 
167
151
  // =============================================================================
168
- // Widget: Inbox Summary
152
+ // Widget: Inbox
169
153
  // =============================================================================
170
154
 
171
- async function renderInboxWidget(): Promise<string> {
155
+ async function renderInboxWidget(): Promise<string | null> {
172
156
  const pendingItems = await listInboxItems("pending");
173
157
 
174
- const lines = [
175
- c.bold("Quick Capture Inbox"),
176
- "",
177
- ];
178
-
179
158
  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
- }
159
+ return null; // Don't show if empty
160
+ }
195
161
 
196
- if (pendingItems.length > 4) {
197
- lines.push(c.gray(`+${pendingItems.length - 4} more`));
198
- }
162
+ const lines: string[] = [];
163
+ lines.push(`${c.yellow("Pending")}: ${pendingItems.length} items`);
164
+ lines.push("");
165
+
166
+ // Show up to 3 items
167
+ for (const item of pendingItems.slice(0, 3)) {
168
+ const date = new Date(item.capturedAt);
169
+ const ago = getTimeAgo(date);
170
+ const tags = item.tags?.length ? c.dim(` #${item.tags[0]}`) : "";
171
+ lines.push(`${c.yellow("○")} ${truncate(item.content, 40)}${tags}`);
172
+ lines.push(` ${c.dim(ago)}`);
173
+ }
174
+
175
+ if (pendingItems.length > 3) {
176
+ lines.push(c.gray(`+${pendingItems.length - 3} more`));
199
177
  }
200
178
 
201
- return box(lines.join("\n"), { title: "Inbox", borderColor: "yellow" });
179
+ return box(lines.join("\n"), {
180
+ title: "Inbox",
181
+ borderColor: "yellow",
182
+ padding: 1,
183
+ });
202
184
  }
203
185
 
204
186
  function getTimeAgo(date: Date): string {
@@ -215,164 +197,53 @@ function getTimeAgo(date: Date): string {
215
197
  }
216
198
 
217
199
  // =============================================================================
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)"));
232
- } 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
- }
246
- }
247
-
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})`)}`);
253
- }
254
- }
255
-
256
- return box(lines.join("\n"), { title: "Critical Path", borderColor: "red" });
257
- }
258
-
259
- // =============================================================================
260
- // Widget: Hierarchy Tree
200
+ // Projects Table
261
201
  // =============================================================================
262
202
 
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
- }
203
+ async function renderProjectsTable(projects: Project[]): Promise<string> {
204
+ if (projects.length === 0) {
205
+ return c.gray("No projects found.");
294
206
  }
295
207
 
296
- if (tree.length > maxRoots) {
297
- lines.push(c.gray(`+${tree.length - maxRoots} more root tasks`));
208
+ const rows: {
209
+ name: string;
210
+ progress: string;
211
+ ready: number;
212
+ blocked: number;
213
+ total: number;
214
+ }[] = [];
215
+
216
+ for (const project of projects.slice(0, 10)) {
217
+ const tasks = await listTasks(project.id);
218
+ const stats = calculateStats(tasks);
219
+ const depMetrics = calculateDependencyMetrics(tasks);
220
+ const activeTasks = stats.total - stats.cancelled;
221
+ const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
222
+
223
+ // Create mini progress bar
224
+ const barWidth = 8;
225
+ const filled = Math.round((percent / 100) * barWidth);
226
+ const empty = barWidth - filled;
227
+ const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
228
+
229
+ rows.push({
230
+ name: truncate(project.name, 20),
231
+ progress: `${miniBar} ${pad(String(percent) + "%", 4, "right")}`,
232
+ ready: depMetrics.readyToWork,
233
+ blocked: depMetrics.blockedByDependencies,
234
+ total: activeTasks,
235
+ });
298
236
  }
299
237
 
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
- "",
238
+ const columns: TableColumn[] = [
239
+ { header: "Project", key: "name", width: 22 },
240
+ { header: "Progress", key: "progress", width: 16 },
241
+ { header: "Tasks", key: "total", width: 6, align: "right" },
242
+ { header: "Ready", key: "ready", width: 6, align: "right", format: (v) => c.green(String(v)) },
243
+ { header: "Blocked", key: "blocked", width: 8, align: "right", format: (v) => Number(v) > 0 ? c.red(String(v)) : c.gray(String(v)) },
313
244
  ];
314
245
 
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" });
246
+ return table(rows as unknown as Record<string, unknown>[], columns);
376
247
  }
377
248
 
378
249
  // =============================================================================
@@ -417,54 +288,56 @@ export async function dashboard(projectId?: string): Promise<void> {
417
288
  ? `${c.bold("Project:")} ${project.name}`
418
289
  : `${c.bold("All Projects")} (${projects.length} projects)`;
419
290
 
420
- console.log(c.dim(`Version: ${VERSION} ${projectInfo}`));
291
+ console.log(c.dim(`v${VERSION} ${projectInfo}`));
421
292
  console.log();
422
293
 
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();
294
+ // Render widgets
295
+ const statusWidget = renderStatusWidget(tasks, projects);
296
+ const actionsWidget = await renderActionsWidget(tasks);
431
297
 
432
- // Print widgets in 3-column grid layout
433
- // Row 1: Overview | Schedule | Inbox
434
- console.log(sideBySide([overview, timeWidget, inboxWidget]));
298
+ // Print widgets side by side
299
+ console.log(sideBySide([statusWidget, actionsWidget], 2));
435
300
  console.log();
436
301
 
437
- // Row 2: Work Queue | Critical Path | Analysis
438
- console.log(sideBySide([depWidget, criticalPathWidget, analysisWidget]));
439
- console.log();
302
+ // Inbox widget (if there are pending items)
303
+ const inboxWidget = await renderInboxWidget();
304
+ if (inboxWidget) {
305
+ console.log(inboxWidget);
306
+ console.log();
307
+ }
440
308
 
441
- // Row 3: Hierarchy (full width or single)
442
- console.log(hierarchyWidget);
443
- console.log();
309
+ // Projects table (only if showing all projects)
310
+ if (!project && projects.length > 1) {
311
+ console.log(c.bold("Projects"));
312
+ console.log();
313
+ console.log(await renderProjectsTable(projects));
314
+ console.log();
315
+ }
444
316
 
445
- // Task table
446
- if (tasks.length > 0) {
317
+ // Task list for single project view
318
+ if (project || projects.length === 1) {
447
319
  const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
448
- const displayTasks = activeTasks.slice(0, 15); // Limit to 15 rows
320
+ if (activeTasks.length > 0) {
321
+ const displayTasks = activeTasks.slice(0, 10);
449
322
 
450
- console.log(c.bold(`Active Tasks (${activeTasks.length})`));
451
- console.log();
323
+ console.log(c.bold(`Tasks (${activeTasks.length})`));
324
+ console.log();
452
325
 
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
- ];
326
+ const columns: TableColumn[] = [
327
+ { header: "Title", key: "title", width: 40 },
328
+ { header: "Status", key: "status", width: 12, format: (v) => {
329
+ const icon = icons[v as Task["status"]] ?? icons.pending;
330
+ return `${icon} ${v}`;
331
+ }},
332
+ { header: "Priority", key: "priority", width: 10, format: (v) => formatPriority(v as Task["priority"]) },
333
+ ];
460
334
 
461
- console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
335
+ console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
462
336
 
463
- if (activeTasks.length > 15) {
464
- console.log(c.gray(`\n(Showing 15 of ${activeTasks.length} active tasks)`));
337
+ if (activeTasks.length > 10) {
338
+ console.log(c.gray(`\n(+${activeTasks.length - 10} more tasks)`));
339
+ }
465
340
  }
466
- } else {
467
- console.log(c.gray("No tasks found."));
468
341
  }
469
342
 
470
343
  console.log();