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