@task-mcp/shared 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/dist/utils/dashboard-renderer.d.ts +83 -0
- package/dist/utils/dashboard-renderer.d.ts.map +1 -0
- package/dist/utils/dashboard-renderer.js +416 -0
- package/dist/utils/dashboard-renderer.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +20 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/terminal-ui.d.ts +203 -0
- package/dist/utils/terminal-ui.d.ts.map +1 -0
- package/dist/utils/terminal-ui.js +534 -0
- package/dist/utils/terminal-ui.js.map +1 -0
- package/package.json +1 -1
- package/src/utils/dashboard-renderer.ts +606 -0
- package/src/utils/index.ts +62 -0
- package/src/utils/terminal-ui.ts +706 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared dashboard renderer for CLI and MCP server
|
|
3
|
+
* Provides consistent dashboard layout across all interfaces
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Task, Project, InboxItem } from "../schemas/index.js";
|
|
7
|
+
import {
|
|
8
|
+
c,
|
|
9
|
+
box,
|
|
10
|
+
progressBar,
|
|
11
|
+
table,
|
|
12
|
+
icons,
|
|
13
|
+
sideBySide,
|
|
14
|
+
truncateStr,
|
|
15
|
+
pad,
|
|
16
|
+
banner,
|
|
17
|
+
stripAnsi,
|
|
18
|
+
type TableColumn,
|
|
19
|
+
} from "./terminal-ui.js";
|
|
20
|
+
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// =============================================================================
|
|
24
|
+
|
|
25
|
+
export interface DashboardStats {
|
|
26
|
+
total: number;
|
|
27
|
+
completed: number;
|
|
28
|
+
inProgress: number;
|
|
29
|
+
pending: number;
|
|
30
|
+
blocked: number;
|
|
31
|
+
cancelled: number;
|
|
32
|
+
byPriority: {
|
|
33
|
+
critical: number;
|
|
34
|
+
high: number;
|
|
35
|
+
medium: number;
|
|
36
|
+
low: number;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface DependencyMetrics {
|
|
41
|
+
readyToWork: number;
|
|
42
|
+
blockedByDependencies: number;
|
|
43
|
+
mostDependedOn?: {
|
|
44
|
+
id: string;
|
|
45
|
+
title: string;
|
|
46
|
+
dependentCount: number;
|
|
47
|
+
} | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DashboardData {
|
|
51
|
+
tasks: Task[];
|
|
52
|
+
projects: Project[];
|
|
53
|
+
inboxItems?: InboxItem[] | undefined;
|
|
54
|
+
currentProject?: Project | undefined;
|
|
55
|
+
version?: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Statistics Calculators
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
export function calculateStats(tasks: Task[]): DashboardStats {
|
|
63
|
+
const stats: DashboardStats = {
|
|
64
|
+
total: tasks.length,
|
|
65
|
+
completed: 0,
|
|
66
|
+
inProgress: 0,
|
|
67
|
+
pending: 0,
|
|
68
|
+
blocked: 0,
|
|
69
|
+
cancelled: 0,
|
|
70
|
+
byPriority: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (const task of tasks) {
|
|
74
|
+
switch (task.status) {
|
|
75
|
+
case "completed":
|
|
76
|
+
stats.completed++;
|
|
77
|
+
break;
|
|
78
|
+
case "in_progress":
|
|
79
|
+
stats.inProgress++;
|
|
80
|
+
break;
|
|
81
|
+
case "pending":
|
|
82
|
+
stats.pending++;
|
|
83
|
+
break;
|
|
84
|
+
case "blocked":
|
|
85
|
+
stats.blocked++;
|
|
86
|
+
break;
|
|
87
|
+
case "cancelled":
|
|
88
|
+
stats.cancelled++;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const priority = task.priority ?? "medium";
|
|
93
|
+
if (priority in stats.byPriority) {
|
|
94
|
+
stats.byPriority[priority as keyof typeof stats.byPriority]++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return stats;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function calculateDependencyMetrics(tasks: Task[]): DependencyMetrics {
|
|
102
|
+
const completedIds = new Set(
|
|
103
|
+
tasks.filter((t) => t.status === "completed").map((t) => t.id)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
let readyToWork = 0;
|
|
107
|
+
let blockedByDependencies = 0;
|
|
108
|
+
|
|
109
|
+
// Count dependents for each task
|
|
110
|
+
const dependentCounts = new Map<string, number>();
|
|
111
|
+
|
|
112
|
+
for (const task of tasks) {
|
|
113
|
+
if (task.status === "completed" || task.status === "cancelled") continue;
|
|
114
|
+
|
|
115
|
+
const deps = task.dependencies ?? [];
|
|
116
|
+
if (deps.length === 0) {
|
|
117
|
+
readyToWork++;
|
|
118
|
+
} else {
|
|
119
|
+
const allSatisfied = deps.every((d) => completedIds.has(d.taskId));
|
|
120
|
+
if (allSatisfied) {
|
|
121
|
+
readyToWork++;
|
|
122
|
+
} else {
|
|
123
|
+
blockedByDependencies++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Track dependent counts
|
|
128
|
+
for (const dep of deps) {
|
|
129
|
+
dependentCounts.set(
|
|
130
|
+
dep.taskId,
|
|
131
|
+
(dependentCounts.get(dep.taskId) ?? 0) + 1
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Find most depended on task
|
|
137
|
+
let mostDependedOn: DependencyMetrics["mostDependedOn"];
|
|
138
|
+
let maxCount = 0;
|
|
139
|
+
|
|
140
|
+
for (const [taskId, count] of dependentCounts) {
|
|
141
|
+
if (count > maxCount) {
|
|
142
|
+
maxCount = count;
|
|
143
|
+
const task = tasks.find((t) => t.id === taskId);
|
|
144
|
+
if (task) {
|
|
145
|
+
mostDependedOn = {
|
|
146
|
+
id: taskId,
|
|
147
|
+
title: task.title,
|
|
148
|
+
dependentCount: count,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { readyToWork, blockedByDependencies, mostDependedOn };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// =============================================================================
|
|
158
|
+
// Formatters
|
|
159
|
+
// =============================================================================
|
|
160
|
+
|
|
161
|
+
function formatPriority(priority: Task["priority"]): string {
|
|
162
|
+
const colors: Record<string, (s: string) => string> = {
|
|
163
|
+
critical: c.red,
|
|
164
|
+
high: c.yellow,
|
|
165
|
+
medium: c.blue,
|
|
166
|
+
low: c.gray,
|
|
167
|
+
};
|
|
168
|
+
return (colors[priority] ?? c.gray)(priority);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function getTimeAgo(date: Date): string {
|
|
172
|
+
const now = new Date();
|
|
173
|
+
const diffMs = now.getTime() - date.getTime();
|
|
174
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
175
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
176
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
177
|
+
|
|
178
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
179
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
180
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
181
|
+
return date.toLocaleDateString();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// =============================================================================
|
|
185
|
+
// Widget Renderers
|
|
186
|
+
// =============================================================================
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Render Status widget (progress, counts, priorities, dependencies)
|
|
190
|
+
*/
|
|
191
|
+
export function renderStatusWidget(
|
|
192
|
+
tasks: Task[],
|
|
193
|
+
_projects: Project[]
|
|
194
|
+
): string {
|
|
195
|
+
const stats = calculateStats(tasks);
|
|
196
|
+
const depMetrics = calculateDependencyMetrics(tasks);
|
|
197
|
+
const today = getTodayTasks(tasks);
|
|
198
|
+
const overdue = getOverdueTasks(tasks);
|
|
199
|
+
const activeTasks = stats.total - stats.cancelled;
|
|
200
|
+
const percent =
|
|
201
|
+
activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
202
|
+
|
|
203
|
+
const lines: string[] = [];
|
|
204
|
+
|
|
205
|
+
// Progress bar with fraction
|
|
206
|
+
const bar = progressBar(stats.completed, activeTasks, { width: 24 });
|
|
207
|
+
lines.push(`${bar} ${c.bold(`${percent}%`)} ${stats.completed}/${activeTasks} tasks`);
|
|
208
|
+
lines.push("");
|
|
209
|
+
|
|
210
|
+
// Status counts
|
|
211
|
+
lines.push(
|
|
212
|
+
`${c.green("Done")}: ${stats.completed} ` +
|
|
213
|
+
`${c.blue("Progress")}: ${stats.inProgress} ` +
|
|
214
|
+
`${c.yellow("Pending")}: ${stats.pending} ` +
|
|
215
|
+
`${c.red("Blocked")}: ${stats.blocked}`
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Schedule info
|
|
219
|
+
const scheduleInfo = [];
|
|
220
|
+
if (overdue.length > 0) scheduleInfo.push(c.red(`Overdue: ${overdue.length}`));
|
|
221
|
+
if (today.length > 0) scheduleInfo.push(c.yellow(`Today: ${today.length}`));
|
|
222
|
+
if (scheduleInfo.length > 0) {
|
|
223
|
+
lines.push(scheduleInfo.join(" "));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
lines.push("");
|
|
227
|
+
|
|
228
|
+
// Priority breakdown
|
|
229
|
+
lines.push(
|
|
230
|
+
`${c.red("Critical")}: ${stats.byPriority.critical} ` +
|
|
231
|
+
`${c.yellow("High")}: ${stats.byPriority.high} ` +
|
|
232
|
+
`${c.blue("Medium")}: ${stats.byPriority.medium} ` +
|
|
233
|
+
`${c.gray("Low")}: ${stats.byPriority.low}`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Dependencies summary
|
|
237
|
+
lines.push(
|
|
238
|
+
`${c.green("Ready")}: ${depMetrics.readyToWork} ` +
|
|
239
|
+
`${c.red("Blocked")}: ${depMetrics.blockedByDependencies}` +
|
|
240
|
+
(depMetrics.mostDependedOn
|
|
241
|
+
? ` ${c.dim("Bottleneck:")} ${truncateStr(depMetrics.mostDependedOn.title, 15)}`
|
|
242
|
+
: "")
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
return box(lines.join("\n"), {
|
|
246
|
+
title: "Status",
|
|
247
|
+
borderColor: "cyan",
|
|
248
|
+
padding: 1,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Render Next Actions widget (top ready tasks)
|
|
254
|
+
*/
|
|
255
|
+
export function renderActionsWidget(tasks: Task[]): string {
|
|
256
|
+
const lines: string[] = [];
|
|
257
|
+
|
|
258
|
+
// Get top 4 ready tasks sorted by priority
|
|
259
|
+
const readyTasks = tasks
|
|
260
|
+
.filter(
|
|
261
|
+
(t) =>
|
|
262
|
+
t.status === "pending" &&
|
|
263
|
+
(!t.dependencies || t.dependencies.length === 0)
|
|
264
|
+
)
|
|
265
|
+
.sort((a, b) => {
|
|
266
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
267
|
+
return (
|
|
268
|
+
(priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2)
|
|
269
|
+
);
|
|
270
|
+
})
|
|
271
|
+
.slice(0, 4);
|
|
272
|
+
|
|
273
|
+
if (readyTasks.length > 0) {
|
|
274
|
+
for (const task of readyTasks) {
|
|
275
|
+
const deps = task.dependencies?.length ?? 0;
|
|
276
|
+
const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
|
|
277
|
+
lines.push(`${c.cyan("→")} ${truncateStr(task.title, 24)}`);
|
|
278
|
+
lines.push(` ${formatPriority(task.priority)}, ${depsInfo}`);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
lines.push(c.dim("No tasks ready"));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return box(lines.join("\n"), {
|
|
285
|
+
title: "Next Actions",
|
|
286
|
+
borderColor: "green",
|
|
287
|
+
padding: 1,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Render Inbox widget (pending items)
|
|
293
|
+
*/
|
|
294
|
+
export function renderInboxWidget(inboxItems: InboxItem[]): string | null {
|
|
295
|
+
const pendingItems = inboxItems.filter((item) => item.status === "pending");
|
|
296
|
+
|
|
297
|
+
if (pendingItems.length === 0) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const lines: string[] = [];
|
|
302
|
+
lines.push(`${c.yellow("Pending")}: ${pendingItems.length} items`);
|
|
303
|
+
lines.push("");
|
|
304
|
+
|
|
305
|
+
// Show up to 3 items
|
|
306
|
+
for (const item of pendingItems.slice(0, 3)) {
|
|
307
|
+
const date = new Date(item.capturedAt);
|
|
308
|
+
const ago = getTimeAgo(date);
|
|
309
|
+
const tags = item.tags?.length ? c.dim(` #${item.tags[0]}`) : "";
|
|
310
|
+
lines.push(`${c.yellow("○")} ${truncateStr(item.content, 40)}${tags}`);
|
|
311
|
+
lines.push(` ${c.dim(ago)}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (pendingItems.length > 3) {
|
|
315
|
+
lines.push(c.gray(`+${pendingItems.length - 3} more`));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return box(lines.join("\n"), {
|
|
319
|
+
title: "Inbox",
|
|
320
|
+
borderColor: "yellow",
|
|
321
|
+
padding: 1,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Render Projects table
|
|
327
|
+
*/
|
|
328
|
+
export function renderProjectsTable(
|
|
329
|
+
projects: Project[],
|
|
330
|
+
getProjectTasks: (projectId: string) => Task[]
|
|
331
|
+
): string {
|
|
332
|
+
if (projects.length === 0) {
|
|
333
|
+
return c.gray("No projects found.");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const rows: {
|
|
337
|
+
name: string;
|
|
338
|
+
progress: string;
|
|
339
|
+
ready: number;
|
|
340
|
+
blocked: number;
|
|
341
|
+
total: number;
|
|
342
|
+
}[] = [];
|
|
343
|
+
|
|
344
|
+
for (const project of projects.slice(0, 10)) {
|
|
345
|
+
const tasks = getProjectTasks(project.id);
|
|
346
|
+
const stats = calculateStats(tasks);
|
|
347
|
+
const depMetrics = calculateDependencyMetrics(tasks);
|
|
348
|
+
const activeTasks = stats.total - stats.cancelled;
|
|
349
|
+
const percent =
|
|
350
|
+
activeTasks > 0
|
|
351
|
+
? Math.round((stats.completed / activeTasks) * 100)
|
|
352
|
+
: 0;
|
|
353
|
+
|
|
354
|
+
// Create mini progress bar
|
|
355
|
+
const barWidth = 8;
|
|
356
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
357
|
+
const empty = barWidth - filled;
|
|
358
|
+
const miniBar =
|
|
359
|
+
c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
|
|
360
|
+
|
|
361
|
+
rows.push({
|
|
362
|
+
name: truncateStr(project.name, 20),
|
|
363
|
+
progress: `${miniBar} ${pad(String(percent) + "%", 4, "right")}`,
|
|
364
|
+
ready: depMetrics.readyToWork,
|
|
365
|
+
blocked: depMetrics.blockedByDependencies,
|
|
366
|
+
total: activeTasks,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const columns: TableColumn[] = [
|
|
371
|
+
{ key: "name", header: "Project", width: 22 },
|
|
372
|
+
{ key: "progress", header: "Progress", width: 16 },
|
|
373
|
+
{ key: "total", header: "Tasks", width: 6, align: "right" },
|
|
374
|
+
{
|
|
375
|
+
key: "ready",
|
|
376
|
+
header: "Ready",
|
|
377
|
+
width: 6,
|
|
378
|
+
align: "right",
|
|
379
|
+
format: (v) => c.green(String(v)),
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
key: "blocked",
|
|
383
|
+
header: "Blocked",
|
|
384
|
+
width: 8,
|
|
385
|
+
align: "right",
|
|
386
|
+
format: (v) => (Number(v) > 0 ? c.red(String(v)) : c.gray(String(v))),
|
|
387
|
+
},
|
|
388
|
+
];
|
|
389
|
+
|
|
390
|
+
return table(rows as unknown as Record<string, unknown>[], columns);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Render Tasks table for single project view
|
|
395
|
+
*/
|
|
396
|
+
export function renderTasksTable(tasks: Task[], limit: number = 10): string {
|
|
397
|
+
const activeTasks = tasks.filter(
|
|
398
|
+
(t) => t.status !== "completed" && t.status !== "cancelled"
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
if (activeTasks.length === 0) {
|
|
402
|
+
return c.gray("No active tasks.");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const displayTasks = activeTasks.slice(0, limit);
|
|
406
|
+
|
|
407
|
+
const columns: TableColumn[] = [
|
|
408
|
+
{ key: "title", header: "Title", width: 40 },
|
|
409
|
+
{
|
|
410
|
+
key: "status",
|
|
411
|
+
header: "Status",
|
|
412
|
+
width: 12,
|
|
413
|
+
format: (v) => {
|
|
414
|
+
const icon = icons[v as Task["status"]] ?? icons.pending;
|
|
415
|
+
return `${icon} ${v}`;
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
key: "priority",
|
|
420
|
+
header: "Priority",
|
|
421
|
+
width: 10,
|
|
422
|
+
format: (v) => formatPriority(v as Task["priority"]),
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
let result = table(
|
|
427
|
+
displayTasks as unknown as Record<string, unknown>[],
|
|
428
|
+
columns
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
if (activeTasks.length > limit) {
|
|
432
|
+
result += `\n${c.gray(`(+${activeTasks.length - limit} more tasks)`)}`;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return result;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// =============================================================================
|
|
439
|
+
// Date Helpers
|
|
440
|
+
// =============================================================================
|
|
441
|
+
|
|
442
|
+
function getTodayTasks(tasks: Task[]): Task[] {
|
|
443
|
+
const today = new Date().toISOString().split("T")[0];
|
|
444
|
+
return tasks.filter(
|
|
445
|
+
(t) =>
|
|
446
|
+
t.dueDate === today &&
|
|
447
|
+
t.status !== "completed" &&
|
|
448
|
+
t.status !== "cancelled"
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getOverdueTasks(tasks: Task[]): Task[] {
|
|
453
|
+
const today = new Date().toISOString().split("T")[0] ?? "";
|
|
454
|
+
return tasks.filter(
|
|
455
|
+
(t) =>
|
|
456
|
+
t.dueDate &&
|
|
457
|
+
t.dueDate < today &&
|
|
458
|
+
t.status !== "completed" &&
|
|
459
|
+
t.status !== "cancelled"
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// =============================================================================
|
|
464
|
+
// Full Dashboard Renderer
|
|
465
|
+
// =============================================================================
|
|
466
|
+
|
|
467
|
+
export interface RenderDashboardOptions {
|
|
468
|
+
showBanner?: boolean | undefined;
|
|
469
|
+
showInbox?: boolean | undefined;
|
|
470
|
+
showProjects?: boolean | undefined;
|
|
471
|
+
showTasks?: boolean | undefined;
|
|
472
|
+
stripAnsiCodes?: boolean | undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Render full dashboard (CLI and MCP compatible)
|
|
477
|
+
*/
|
|
478
|
+
export function renderDashboard(
|
|
479
|
+
data: DashboardData,
|
|
480
|
+
getProjectTasks: (projectId: string) => Task[],
|
|
481
|
+
options: RenderDashboardOptions = {}
|
|
482
|
+
): string {
|
|
483
|
+
const {
|
|
484
|
+
showBanner = true,
|
|
485
|
+
showInbox = true,
|
|
486
|
+
showProjects = true,
|
|
487
|
+
showTasks = true,
|
|
488
|
+
stripAnsiCodes = false,
|
|
489
|
+
} = options;
|
|
490
|
+
|
|
491
|
+
const { tasks, projects, inboxItems = [], currentProject, version } = data;
|
|
492
|
+
const lines: string[] = [];
|
|
493
|
+
|
|
494
|
+
// Banner
|
|
495
|
+
if (showBanner) {
|
|
496
|
+
lines.push("");
|
|
497
|
+
lines.push(banner("TASK MCP"));
|
|
498
|
+
lines.push("");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Project info header
|
|
502
|
+
const projectInfo = currentProject
|
|
503
|
+
? `${c.bold("Project:")} ${currentProject.name}`
|
|
504
|
+
: `${c.bold("All Projects")} (${projects.length} projects)`;
|
|
505
|
+
|
|
506
|
+
if (version) {
|
|
507
|
+
lines.push(c.dim(`v${version} ${projectInfo}`));
|
|
508
|
+
} else {
|
|
509
|
+
lines.push(projectInfo);
|
|
510
|
+
}
|
|
511
|
+
lines.push("");
|
|
512
|
+
|
|
513
|
+
// Status + Actions widgets side by side
|
|
514
|
+
const statusWidget = renderStatusWidget(tasks, projects);
|
|
515
|
+
const actionsWidget = renderActionsWidget(tasks);
|
|
516
|
+
lines.push(sideBySide([statusWidget, actionsWidget], 2));
|
|
517
|
+
lines.push("");
|
|
518
|
+
|
|
519
|
+
// Inbox widget
|
|
520
|
+
if (showInbox && inboxItems.length > 0) {
|
|
521
|
+
const inboxWidget = renderInboxWidget(inboxItems);
|
|
522
|
+
if (inboxWidget) {
|
|
523
|
+
lines.push(inboxWidget);
|
|
524
|
+
lines.push("");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Projects table (only for all-projects view)
|
|
529
|
+
if (showProjects && !currentProject && projects.length > 1) {
|
|
530
|
+
lines.push(c.bold("Projects"));
|
|
531
|
+
lines.push("");
|
|
532
|
+
lines.push(renderProjectsTable(projects, getProjectTasks));
|
|
533
|
+
lines.push("");
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Tasks table (for single project view)
|
|
537
|
+
if (showTasks && (currentProject || projects.length === 1)) {
|
|
538
|
+
const activeTasks = tasks.filter(
|
|
539
|
+
(t) => t.status !== "completed" && t.status !== "cancelled"
|
|
540
|
+
);
|
|
541
|
+
if (activeTasks.length > 0) {
|
|
542
|
+
lines.push(c.bold(`Tasks (${activeTasks.length})`));
|
|
543
|
+
lines.push("");
|
|
544
|
+
lines.push(renderTasksTable(tasks));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
lines.push("");
|
|
549
|
+
|
|
550
|
+
const output = lines.join("\n");
|
|
551
|
+
return stripAnsiCodes ? stripAnsi(output) : output;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Render single project dashboard
|
|
556
|
+
*/
|
|
557
|
+
export function renderProjectDashboard(
|
|
558
|
+
project: Project,
|
|
559
|
+
tasks: Task[],
|
|
560
|
+
options: { stripAnsiCodes?: boolean; version?: string } = {}
|
|
561
|
+
): string {
|
|
562
|
+
const data: DashboardData = {
|
|
563
|
+
tasks,
|
|
564
|
+
projects: [project],
|
|
565
|
+
currentProject: project,
|
|
566
|
+
version: options.version,
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
return renderDashboard(
|
|
570
|
+
data,
|
|
571
|
+
() => tasks,
|
|
572
|
+
{
|
|
573
|
+
showBanner: true,
|
|
574
|
+
showInbox: false,
|
|
575
|
+
showProjects: false,
|
|
576
|
+
showTasks: true,
|
|
577
|
+
stripAnsiCodes: options.stripAnsiCodes,
|
|
578
|
+
}
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Render global dashboard (all projects)
|
|
584
|
+
*/
|
|
585
|
+
export function renderGlobalDashboard(
|
|
586
|
+
projects: Project[],
|
|
587
|
+
allTasks: Task[],
|
|
588
|
+
inboxItems: InboxItem[],
|
|
589
|
+
getProjectTasks: (projectId: string) => Task[],
|
|
590
|
+
options: { stripAnsiCodes?: boolean; version?: string } = {}
|
|
591
|
+
): string {
|
|
592
|
+
const data: DashboardData = {
|
|
593
|
+
tasks: allTasks,
|
|
594
|
+
projects,
|
|
595
|
+
inboxItems,
|
|
596
|
+
version: options.version,
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
return renderDashboard(data, getProjectTasks, {
|
|
600
|
+
showBanner: true,
|
|
601
|
+
showInbox: true,
|
|
602
|
+
showProjects: true,
|
|
603
|
+
showTasks: projects.length === 1,
|
|
604
|
+
stripAnsiCodes: options.stripAnsiCodes,
|
|
605
|
+
});
|
|
606
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -45,3 +45,65 @@ export {
|
|
|
45
45
|
truncate,
|
|
46
46
|
summarizeList,
|
|
47
47
|
} from "./projection.js";
|
|
48
|
+
|
|
49
|
+
// Dashboard renderer
|
|
50
|
+
export {
|
|
51
|
+
calculateStats,
|
|
52
|
+
calculateDependencyMetrics,
|
|
53
|
+
renderStatusWidget,
|
|
54
|
+
renderActionsWidget,
|
|
55
|
+
renderInboxWidget,
|
|
56
|
+
renderProjectsTable,
|
|
57
|
+
renderTasksTable,
|
|
58
|
+
renderDashboard,
|
|
59
|
+
renderProjectDashboard,
|
|
60
|
+
renderGlobalDashboard,
|
|
61
|
+
type DashboardStats,
|
|
62
|
+
type DependencyMetrics,
|
|
63
|
+
type DashboardData,
|
|
64
|
+
type RenderDashboardOptions,
|
|
65
|
+
} from "./dashboard-renderer.js";
|
|
66
|
+
|
|
67
|
+
// Terminal UI utilities
|
|
68
|
+
export {
|
|
69
|
+
// Colors and styles
|
|
70
|
+
color,
|
|
71
|
+
style,
|
|
72
|
+
styled,
|
|
73
|
+
c,
|
|
74
|
+
// Box drawing
|
|
75
|
+
BOX,
|
|
76
|
+
box,
|
|
77
|
+
drawBox,
|
|
78
|
+
hline,
|
|
79
|
+
// String utilities
|
|
80
|
+
stripAnsi,
|
|
81
|
+
displayWidth,
|
|
82
|
+
visibleLength,
|
|
83
|
+
pad,
|
|
84
|
+
padEnd,
|
|
85
|
+
padStart,
|
|
86
|
+
center,
|
|
87
|
+
truncateStr,
|
|
88
|
+
// Progress bar
|
|
89
|
+
progressBar,
|
|
90
|
+
type ProgressBarOptions,
|
|
91
|
+
// Layout
|
|
92
|
+
sideBySide,
|
|
93
|
+
sideBySideArrays,
|
|
94
|
+
type BoxOptions,
|
|
95
|
+
// Tables
|
|
96
|
+
table,
|
|
97
|
+
renderTable,
|
|
98
|
+
type TableColumn,
|
|
99
|
+
// Formatters
|
|
100
|
+
statusColors,
|
|
101
|
+
statusIcons,
|
|
102
|
+
icons,
|
|
103
|
+
formatStatus,
|
|
104
|
+
priorityColors,
|
|
105
|
+
formatPriority,
|
|
106
|
+
formatDependencies,
|
|
107
|
+
// Banner
|
|
108
|
+
banner,
|
|
109
|
+
} from "./terminal-ui.js";
|