@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allpepper/task-orchestrator-tui",
3
- "version": "1.2.0",
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.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
@@ -111,6 +111,7 @@ export function App() {
111
111
  { key: 'f', label: 'Project Info' },
112
112
  { key: 'e', label: 'Edit Project' },
113
113
  { key: 'd', label: 'Delete Project' },
114
+ { key: 'r', label: 'Refresh' },
114
115
  { key: 'h', label: 'Back' },
115
116
  ]
116
117
  : []),
@@ -34,20 +34,27 @@ export function Dashboard({ selectedIndex, onSelectedIndexChange, onSelectProjec
34
34
  {
35
35
  key: 'name',
36
36
  label: 'Name',
37
- width: 50,
37
+ width: 45,
38
38
  },
39
39
  {
40
40
  key: 'status',
41
41
  label: 'Status',
42
- width: 20,
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: 15,
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 ?? '',
@@ -405,7 +405,6 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
405
405
  ? currentRow.task.featureId
406
406
  : undefined;
407
407
  adapter.createTask({
408
- projectId,
409
408
  featureId,
410
409
  title: values.title ?? '',
411
410
  summary: values.summary ?? '',
@@ -216,7 +216,6 @@ export class DirectAdapter implements DataAdapter {
216
216
  }
217
217
 
218
218
  async createTask(params: {
219
- projectId?: string;
220
219
  featureId?: string;
221
220
  title: string;
222
221
  summary: string;
@@ -164,7 +164,6 @@ export interface DataAdapter {
164
164
  getTask(id: string): Promise<Result<Task>>;
165
165
 
166
166
  createTask(params: {
167
- projectId?: string;
168
167
  featureId?: string;
169
168
  title: string;
170
169
  summary: string;
@@ -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
- * Project with task count information for dashboard display
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 all tasks in parallel
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
- // Merge task counts into projects
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);
@@ -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
  }