@allpepper/task-orchestrator-tui 1.2.0 → 1.3.0
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 +2 -2
- package/src/tui/app.tsx +1 -0
- package/src/tui/screens/dashboard.tsx +10 -3
- package/src/tui/screens/feature-detail.tsx +0 -1
- package/src/tui/screens/project-view.tsx +0 -1
- package/src/ui/adapters/direct.ts +0 -1
- package/src/ui/adapters/types.ts +0 -1
- package/src/ui/hooks/use-data.ts +46 -5
- package/src/ui/lib/markdown.tsx +5 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@allpepper/task-orchestrator-tui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Terminal UI for task orchestration - Kanban boards, tree views, and task management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"prepublishOnly": "bun test"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@allpepper/task-orchestrator": "^1.1.
|
|
48
|
+
"@allpepper/task-orchestrator": "^1.1.2",
|
|
49
49
|
"@inkjs/ui": "^2.0.0",
|
|
50
50
|
"ink": "^6.6.0",
|
|
51
51
|
"react": "^19.2.4"
|
package/src/tui/app.tsx
CHANGED
|
@@ -34,20 +34,27 @@ export function Dashboard({ selectedIndex, onSelectedIndexChange, onSelectProjec
|
|
|
34
34
|
{
|
|
35
35
|
key: 'name',
|
|
36
36
|
label: 'Name',
|
|
37
|
-
width:
|
|
37
|
+
width: 45,
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
key: 'status',
|
|
41
41
|
label: 'Status',
|
|
42
|
-
width:
|
|
42
|
+
width: 18,
|
|
43
43
|
render: (_value: unknown, row: ProjectWithCounts, context?: { isSelected: boolean }) => (
|
|
44
44
|
<StatusBadge status={row.status} isSelected={context?.isSelected} />
|
|
45
45
|
),
|
|
46
46
|
},
|
|
47
|
+
{
|
|
48
|
+
key: 'features',
|
|
49
|
+
label: 'Features',
|
|
50
|
+
width: 10,
|
|
51
|
+
render: (_value: unknown, row: ProjectWithCounts) =>
|
|
52
|
+
`${row.featureCounts.completed}/${row.featureCounts.total}`,
|
|
53
|
+
},
|
|
47
54
|
{
|
|
48
55
|
key: 'tasks',
|
|
49
56
|
label: 'Tasks',
|
|
50
|
-
width:
|
|
57
|
+
width: 10,
|
|
51
58
|
render: (_value: unknown, row: ProjectWithCounts) =>
|
|
52
59
|
`${row.taskCounts.completed}/${row.taskCounts.total}`,
|
|
53
60
|
},
|
|
@@ -270,7 +270,6 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
|
|
|
270
270
|
onCancel={() => setMode('idle')}
|
|
271
271
|
onSubmit={(values) => {
|
|
272
272
|
adapter.createTask({
|
|
273
|
-
projectId: feature.projectId,
|
|
274
273
|
featureId: feature.id,
|
|
275
274
|
title: values.title ?? '',
|
|
276
275
|
summary: values.summary ?? '',
|
package/src/ui/adapters/types.ts
CHANGED
package/src/ui/hooks/use-data.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
2
|
import { useAdapter } from '../context/adapter-context';
|
|
3
|
-
import type { Project, Task, Section, EntityType, TaskStatus, Priority, FeatureStatus } from '@allpepper/task-orchestrator';
|
|
3
|
+
import type { Project, Task, Feature, Section, EntityType, TaskStatus, Priority, FeatureStatus } from '@allpepper/task-orchestrator';
|
|
4
4
|
import type { FeatureWithTasks, ProjectOverview, SearchResults, DependencyInfo, BoardCard, BoardTask } from '../lib/types';
|
|
5
5
|
import type { TreeRow } from '../../tui/components/tree-view';
|
|
6
6
|
|
|
@@ -43,10 +43,44 @@ export function calculateTaskCountsByProject(tasks: Task[]): Map<string, TaskCou
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Feature counts structure
|
|
47
|
+
*/
|
|
48
|
+
export interface FeatureCounts {
|
|
49
|
+
total: number;
|
|
50
|
+
completed: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Terminal feature statuses considered "completed"
|
|
55
|
+
*/
|
|
56
|
+
const COMPLETED_FEATURE_STATUSES = ['COMPLETED', 'DEPLOYED'];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Group features by project ID and calculate counts for each
|
|
60
|
+
*/
|
|
61
|
+
export function calculateFeatureCountsByProject(featureList: Feature[]): Map<string, FeatureCounts> {
|
|
62
|
+
const countsByProject = new Map<string, FeatureCounts>();
|
|
63
|
+
|
|
64
|
+
for (const feature of featureList) {
|
|
65
|
+
if (feature.projectId) {
|
|
66
|
+
const counts = countsByProject.get(feature.projectId) || { total: 0, completed: 0 };
|
|
67
|
+
counts.total++;
|
|
68
|
+
if (COMPLETED_FEATURE_STATUSES.includes(feature.status)) {
|
|
69
|
+
counts.completed++;
|
|
70
|
+
}
|
|
71
|
+
countsByProject.set(feature.projectId, counts);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return countsByProject;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Project with task and feature count information for dashboard display
|
|
47
80
|
*/
|
|
48
81
|
export interface ProjectWithCounts extends Project {
|
|
49
82
|
taskCounts: TaskCounts;
|
|
83
|
+
featureCounts: FeatureCounts;
|
|
50
84
|
}
|
|
51
85
|
|
|
52
86
|
/**
|
|
@@ -70,10 +104,11 @@ export function useProjects() {
|
|
|
70
104
|
setLoading(true);
|
|
71
105
|
setError(null);
|
|
72
106
|
|
|
73
|
-
// Fetch projects and
|
|
74
|
-
const [projectsResult, tasksResult] = await Promise.all([
|
|
107
|
+
// Fetch projects, tasks, and features in parallel
|
|
108
|
+
const [projectsResult, tasksResult, featuresResult] = await Promise.all([
|
|
75
109
|
adapter.getProjects(),
|
|
76
110
|
adapter.getTasks({ limit: 1000 }), // Get all tasks to count by project
|
|
111
|
+
adapter.getFeatures({ limit: 1000 }), // Get all features to count by project
|
|
77
112
|
]);
|
|
78
113
|
|
|
79
114
|
if (!projectsResult.success) {
|
|
@@ -87,10 +122,16 @@ export function useProjects() {
|
|
|
87
122
|
? calculateTaskCountsByProject(tasksResult.data)
|
|
88
123
|
: new Map<string, TaskCounts>();
|
|
89
124
|
|
|
90
|
-
//
|
|
125
|
+
// Build feature counts by project using shared utility
|
|
126
|
+
const featureCountsByProject = featuresResult.success
|
|
127
|
+
? calculateFeatureCountsByProject(featuresResult.data)
|
|
128
|
+
: new Map<string, FeatureCounts>();
|
|
129
|
+
|
|
130
|
+
// Merge task and feature counts into projects
|
|
91
131
|
const projectsWithCounts: ProjectWithCounts[] = projectsResult.data.map(project => ({
|
|
92
132
|
...project,
|
|
93
133
|
taskCounts: taskCountsByProject.get(project.id) || { total: 0, completed: 0 },
|
|
134
|
+
featureCounts: featureCountsByProject.get(project.id) || { total: 0, completed: 0 },
|
|
94
135
|
}));
|
|
95
136
|
|
|
96
137
|
setProjects(projectsWithCounts);
|
package/src/ui/lib/markdown.tsx
CHANGED
|
@@ -64,7 +64,7 @@ function MarkdownLine({ line }: MarkdownLineProps) {
|
|
|
64
64
|
|
|
65
65
|
// Header (## Header)
|
|
66
66
|
const headerMatch = line.match(/^(#{1,6})\s+(.*)$/);
|
|
67
|
-
if (headerMatch) {
|
|
67
|
+
if (headerMatch && headerMatch[1] && headerMatch[2] !== undefined) {
|
|
68
68
|
const level = headerMatch[1].length;
|
|
69
69
|
const text = headerMatch[2];
|
|
70
70
|
return (
|
|
@@ -78,7 +78,7 @@ function MarkdownLine({ line }: MarkdownLineProps) {
|
|
|
78
78
|
|
|
79
79
|
// Bullet point (- item or * item)
|
|
80
80
|
const bulletMatch = line.match(/^(\s*)([-*])\s+(.*)$/);
|
|
81
|
-
if (bulletMatch) {
|
|
81
|
+
if (bulletMatch && bulletMatch[1] !== undefined && bulletMatch[3] !== undefined) {
|
|
82
82
|
const indent = bulletMatch[1].length;
|
|
83
83
|
const content = bulletMatch[3];
|
|
84
84
|
return (
|
|
@@ -114,7 +114,7 @@ function InlineMarkdown({ text }: InlineMarkdownProps) {
|
|
|
114
114
|
while (remaining.length > 0) {
|
|
115
115
|
// Check for inline code `code`
|
|
116
116
|
const codeMatch = remaining.match(/^(.*?)`([^`]+)`(.*)$/);
|
|
117
|
-
if (codeMatch) {
|
|
117
|
+
if (codeMatch && codeMatch[2] !== undefined && codeMatch[3] !== undefined) {
|
|
118
118
|
if (codeMatch[1]) {
|
|
119
119
|
parts.push(<BoldMarkdown key={key++} text={codeMatch[1]} />);
|
|
120
120
|
}
|
|
@@ -129,7 +129,7 @@ function InlineMarkdown({ text }: InlineMarkdownProps) {
|
|
|
129
129
|
|
|
130
130
|
// Check for bold **text**
|
|
131
131
|
const boldMatch = remaining.match(/^(.*?)\*\*([^*]+)\*\*(.*)$/);
|
|
132
|
-
if (boldMatch) {
|
|
132
|
+
if (boldMatch && boldMatch[2] !== undefined && boldMatch[3] !== undefined) {
|
|
133
133
|
if (boldMatch[1]) {
|
|
134
134
|
parts.push(<Text key={key++}>{boldMatch[1]}</Text>);
|
|
135
135
|
}
|
|
@@ -160,7 +160,7 @@ function BoldMarkdown({ text }: { text: string }) {
|
|
|
160
160
|
|
|
161
161
|
while (remaining.length > 0) {
|
|
162
162
|
const boldMatch = remaining.match(/^(.*?)\*\*([^*]+)\*\*(.*)$/);
|
|
163
|
-
if (boldMatch) {
|
|
163
|
+
if (boldMatch && boldMatch[2] !== undefined && boldMatch[3] !== undefined) {
|
|
164
164
|
if (boldMatch[1]) {
|
|
165
165
|
parts.push(<Text key={key++}>{boldMatch[1]}</Text>);
|
|
166
166
|
}
|