@allpepper/task-orchestrator-tui 1.2.1 → 2.0.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/components/status-actions.tsx +59 -21
- package/src/tui/screens/dashboard.tsx +9 -69
- package/src/tui/screens/feature-detail.tsx +77 -49
- package/src/tui/screens/project-detail.tsx +8 -65
- package/src/tui/screens/project-view.tsx +76 -80
- package/src/tui/screens/task-detail.tsx +46 -19
- package/src/ui/adapters/direct.ts +323 -94
- package/src/ui/adapters/types.ts +75 -72
- package/src/ui/hooks/use-data.ts +106 -193
- package/src/ui/hooks/use-feature-kanban.ts +45 -30
- package/src/ui/hooks/use-kanban.ts +35 -27
- package/src/ui/index.ts +1 -0
- package/src/ui/lib/colors.ts +27 -24
- package/src/ui/lib/markdown.tsx +5 -5
- package/src/ui/lib/types.ts +0 -1
- package/src/ui/themes/dark.ts +10 -28
- package/src/ui/themes/light.ts +16 -34
- package/src/ui/themes/types.ts +14 -26
|
@@ -4,13 +4,14 @@ import { useProjectTree } from '../../ui/hooks/use-data';
|
|
|
4
4
|
import { useAdapter } from '../../ui/context/adapter-context';
|
|
5
5
|
import { useTheme } from '../../ui/context/theme-context';
|
|
6
6
|
import { TreeView, type TreeRow } from '../components/tree-view';
|
|
7
|
-
import { StatusBadge } from '../components/status-badge';
|
|
8
7
|
import { ViewModeChips } from '../components/view-mode-chips';
|
|
9
8
|
import { ConfirmDialog } from '../components/confirm-dialog';
|
|
10
9
|
import { FormDialog } from '../components/form-dialog';
|
|
11
10
|
import { ErrorMessage } from '../components/error-message';
|
|
12
11
|
import { EmptyState } from '../components/empty-state';
|
|
13
|
-
import
|
|
12
|
+
import { StatusActions } from '../components/status-actions';
|
|
13
|
+
import type { Priority } from '@allpepper/task-orchestrator';
|
|
14
|
+
import type { WorkflowState } from '../../ui/adapters/types';
|
|
14
15
|
|
|
15
16
|
interface ProjectViewProps {
|
|
16
17
|
projectId: string;
|
|
@@ -34,8 +35,8 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
34
35
|
const { project, features, unassignedTasks, taskCounts, statusGroupedRows, featureStatusGroupedRows, loading, error, refresh } = useProjectTree(projectId, expandedGroups);
|
|
35
36
|
const [mode, setMode] = useState<'idle' | 'create-feature' | 'edit-feature' | 'delete-feature' | 'create-task' | 'edit-task' | 'delete-task' | 'feature-status'>('idle');
|
|
36
37
|
const [localError, setLocalError] = useState<string | null>(null);
|
|
37
|
-
const [
|
|
38
|
-
const [
|
|
38
|
+
const [featureWorkflowState, setFeatureWorkflowState] = useState<WorkflowState | null>(null);
|
|
39
|
+
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
|
|
39
40
|
|
|
40
41
|
// Build flat list of rows - switch based on view mode
|
|
41
42
|
const rows = useMemo(() => {
|
|
@@ -83,6 +84,20 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
83
84
|
return result;
|
|
84
85
|
}, [viewMode, statusGroupedRows, featureStatusGroupedRows, features, unassignedTasks, expandedFeatures]);
|
|
85
86
|
|
|
87
|
+
// Helper: get feature from current row
|
|
88
|
+
const getFeatureFromRow = (row: TreeRow | undefined) => {
|
|
89
|
+
if (!row) return undefined;
|
|
90
|
+
const featureId =
|
|
91
|
+
row.type === 'feature'
|
|
92
|
+
? row.feature.id
|
|
93
|
+
: row.type === 'group'
|
|
94
|
+
? row.featureId
|
|
95
|
+
: row.type === 'task'
|
|
96
|
+
? row.task.featureId
|
|
97
|
+
: undefined;
|
|
98
|
+
return featureId ? features.find((f) => f.id === featureId) : undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
86
101
|
// Handle keyboard
|
|
87
102
|
useInput((input, key) => {
|
|
88
103
|
if (mode !== 'idle') return;
|
|
@@ -92,7 +107,6 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
92
107
|
if (input === 'r') {
|
|
93
108
|
refresh();
|
|
94
109
|
}
|
|
95
|
-
// Cycle view mode with 'v': features → status → feature-status → features
|
|
96
110
|
if (input === 'v') {
|
|
97
111
|
const next = viewMode === 'features' ? 'status' : viewMode === 'status' ? 'feature-status' : 'features';
|
|
98
112
|
onViewModeChange(next);
|
|
@@ -106,7 +120,6 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
106
120
|
if (input === 't') {
|
|
107
121
|
setMode('create-task');
|
|
108
122
|
}
|
|
109
|
-
// Navigate to feature detail screen
|
|
110
123
|
if (input === 'f') {
|
|
111
124
|
const currentRow = rows[selectedIndex];
|
|
112
125
|
const featureId =
|
|
@@ -137,21 +150,11 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
if (input === 's') {
|
|
140
|
-
const
|
|
141
|
-
const featureId =
|
|
142
|
-
currentRow?.type === 'feature'
|
|
143
|
-
? currentRow.feature.id
|
|
144
|
-
: currentRow?.type === 'group'
|
|
145
|
-
? currentRow.featureId
|
|
146
|
-
: currentRow?.type === 'task'
|
|
147
|
-
? currentRow.task.featureId
|
|
148
|
-
: undefined;
|
|
149
|
-
const feature = featureId ? features.find((f) => f.id === featureId) : undefined;
|
|
153
|
+
const feature = getFeatureFromRow(rows[selectedIndex]);
|
|
150
154
|
if (feature) {
|
|
151
|
-
adapter.
|
|
155
|
+
adapter.getWorkflowState('feature', feature.id).then((result) => {
|
|
152
156
|
if (result.success) {
|
|
153
|
-
|
|
154
|
-
setFeatureTransitionIndex(0);
|
|
157
|
+
setFeatureWorkflowState(result.data);
|
|
155
158
|
setMode('feature-status');
|
|
156
159
|
}
|
|
157
160
|
});
|
|
@@ -159,48 +162,55 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
159
162
|
}
|
|
160
163
|
});
|
|
161
164
|
|
|
162
|
-
useInput((
|
|
165
|
+
useInput((_input, key) => {
|
|
163
166
|
if (mode !== 'feature-status') return;
|
|
164
|
-
if (input === 'j' || key.downArrow) {
|
|
165
|
-
setFeatureTransitionIndex((prev) => (prev + 1) % Math.max(1, featureTransitions.length));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
if (input === 'k' || key.upArrow) {
|
|
169
|
-
setFeatureTransitionIndex((prev) => (prev - 1 + Math.max(1, featureTransitions.length)) % Math.max(1, featureTransitions.length));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
167
|
if (key.escape) {
|
|
173
168
|
setMode('idle');
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (key.return) {
|
|
177
|
-
const currentRow = rows[selectedIndex];
|
|
178
|
-
const featureId =
|
|
179
|
-
currentRow?.type === 'feature'
|
|
180
|
-
? currentRow.feature.id
|
|
181
|
-
: currentRow?.type === 'group'
|
|
182
|
-
? currentRow.featureId
|
|
183
|
-
: currentRow?.type === 'task'
|
|
184
|
-
? currentRow.task.featureId
|
|
185
|
-
: undefined;
|
|
186
|
-
const feature = featureId ? features.find((f) => f.id === featureId) : undefined;
|
|
187
|
-
const nextStatus = featureTransitions[featureTransitionIndex] as FeatureStatus | undefined;
|
|
188
|
-
if (feature && nextStatus) {
|
|
189
|
-
adapter.setFeatureStatus(feature.id, nextStatus, feature.version).then((result) => {
|
|
190
|
-
if (!result.success) setLocalError(result.error);
|
|
191
|
-
refresh();
|
|
192
|
-
setMode('idle');
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
169
|
}
|
|
196
170
|
}, { isActive: mode === 'feature-status' });
|
|
197
171
|
|
|
172
|
+
// Pipeline operations for feature status
|
|
173
|
+
const handleFeatureAdvance = async () => {
|
|
174
|
+
const feature = getFeatureFromRow(rows[selectedIndex]);
|
|
175
|
+
if (!feature) return;
|
|
176
|
+
setIsUpdatingStatus(true);
|
|
177
|
+
setLocalError(null);
|
|
178
|
+
const result = await adapter.advance('feature', feature.id, feature.version);
|
|
179
|
+
if (!result.success) setLocalError(result.error);
|
|
180
|
+
await refresh();
|
|
181
|
+
setIsUpdatingStatus(false);
|
|
182
|
+
setMode('idle');
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleFeatureRevert = async () => {
|
|
186
|
+
const feature = getFeatureFromRow(rows[selectedIndex]);
|
|
187
|
+
if (!feature) return;
|
|
188
|
+
setIsUpdatingStatus(true);
|
|
189
|
+
setLocalError(null);
|
|
190
|
+
const result = await adapter.revert('feature', feature.id, feature.version);
|
|
191
|
+
if (!result.success) setLocalError(result.error);
|
|
192
|
+
await refresh();
|
|
193
|
+
setIsUpdatingStatus(false);
|
|
194
|
+
setMode('idle');
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleFeatureTerminate = async () => {
|
|
198
|
+
const feature = getFeatureFromRow(rows[selectedIndex]);
|
|
199
|
+
if (!feature) return;
|
|
200
|
+
setIsUpdatingStatus(true);
|
|
201
|
+
setLocalError(null);
|
|
202
|
+
const result = await adapter.terminate('feature', feature.id, feature.version);
|
|
203
|
+
if (!result.success) setLocalError(result.error);
|
|
204
|
+
await refresh();
|
|
205
|
+
setIsUpdatingStatus(false);
|
|
206
|
+
setMode('idle');
|
|
207
|
+
};
|
|
208
|
+
|
|
198
209
|
// Toggle feature expansion
|
|
199
210
|
const handleToggleFeature = (featureId: string) => {
|
|
200
211
|
const next = new Set(expandedFeatures);
|
|
201
212
|
if (next.has(featureId)) {
|
|
202
213
|
next.delete(featureId);
|
|
203
|
-
// Clamp selectedIndex if it was on a child task
|
|
204
214
|
const featureIndex = rows.findIndex(r => r.type === 'feature' && r.feature.id === featureId);
|
|
205
215
|
if (featureIndex >= 0 && selectedIndex > featureIndex) {
|
|
206
216
|
const nextFeatureIndex = rows.findIndex((r, i) => i > featureIndex && r.type === 'feature');
|
|
@@ -252,11 +262,9 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
252
262
|
|
|
253
263
|
return (
|
|
254
264
|
<Box flexDirection="column" padding={1}>
|
|
255
|
-
{/* Project Header */}
|
|
265
|
+
{/* Project Header (no status - projects are stateless in v2) */}
|
|
256
266
|
<Box marginBottom={1}>
|
|
257
267
|
<Text bold>{project.name}</Text>
|
|
258
|
-
<Text> </Text>
|
|
259
|
-
<StatusBadge status={project.status} />
|
|
260
268
|
<Text dimColor> — {taskCounts.completed}/{taskCounts.total} tasks completed</Text>
|
|
261
269
|
</Box>
|
|
262
270
|
|
|
@@ -318,14 +326,7 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
318
326
|
|
|
319
327
|
{mode === 'edit-feature' ? (
|
|
320
328
|
(() => {
|
|
321
|
-
const
|
|
322
|
-
const featureId =
|
|
323
|
-
currentRow?.type === 'feature'
|
|
324
|
-
? currentRow.feature.id
|
|
325
|
-
: currentRow?.type === 'group'
|
|
326
|
-
? currentRow.featureId
|
|
327
|
-
: undefined;
|
|
328
|
-
const feature = featureId ? features.find((f) => f.id === featureId) : undefined;
|
|
329
|
+
const feature = getFeatureFromRow(rows[selectedIndex]);
|
|
329
330
|
if (!feature) return null;
|
|
330
331
|
return (
|
|
331
332
|
<FormDialog
|
|
@@ -357,14 +358,7 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
357
358
|
|
|
358
359
|
{mode === 'delete-feature' ? (
|
|
359
360
|
(() => {
|
|
360
|
-
const
|
|
361
|
-
const featureId =
|
|
362
|
-
currentRow?.type === 'feature'
|
|
363
|
-
? currentRow.feature.id
|
|
364
|
-
: currentRow?.type === 'group'
|
|
365
|
-
? currentRow.featureId
|
|
366
|
-
: undefined;
|
|
367
|
-
const feature = featureId ? features.find((f) => f.id === featureId) : undefined;
|
|
361
|
+
const feature = getFeatureFromRow(rows[selectedIndex]);
|
|
368
362
|
if (!feature) return null;
|
|
369
363
|
return (
|
|
370
364
|
<ConfirmDialog
|
|
@@ -479,17 +473,19 @@ export function ProjectView({ projectId, expandedFeatures, onExpandedFeaturesCha
|
|
|
479
473
|
|
|
480
474
|
{mode === 'feature-status' ? (
|
|
481
475
|
<Box flexDirection="column" borderStyle="round" borderColor={theme.colors.highlight} paddingX={1} marginTop={1}>
|
|
482
|
-
<
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
476
|
+
<StatusActions
|
|
477
|
+
currentStatus={featureWorkflowState?.currentStatus ?? ''}
|
|
478
|
+
nextStatus={featureWorkflowState?.nextStatus ?? null}
|
|
479
|
+
prevStatus={featureWorkflowState?.prevStatus ?? null}
|
|
480
|
+
isBlocked={featureWorkflowState?.isBlocked ?? false}
|
|
481
|
+
isTerminal={featureWorkflowState?.isTerminal ?? false}
|
|
482
|
+
isActive={true}
|
|
483
|
+
loading={isUpdatingStatus}
|
|
484
|
+
onAdvance={handleFeatureAdvance}
|
|
485
|
+
onRevert={handleFeatureRevert}
|
|
486
|
+
onTerminate={handleFeatureTerminate}
|
|
487
|
+
/>
|
|
488
|
+
<Text dimColor>Esc: cancel</Text>
|
|
493
489
|
</Box>
|
|
494
490
|
) : null}
|
|
495
491
|
</Box>
|
|
@@ -9,7 +9,8 @@ import { SectionList } from '../components/section-list';
|
|
|
9
9
|
import { DependencyList } from '../components/dependency-list';
|
|
10
10
|
import { StatusActions } from '../components/status-actions';
|
|
11
11
|
import { timeAgo } from '../../ui/lib/format';
|
|
12
|
-
import type {
|
|
12
|
+
import type { Priority } from '@allpepper/task-orchestrator';
|
|
13
|
+
import type { WorkflowState } from '../../ui/adapters/types';
|
|
13
14
|
import { FormDialog } from '../components/form-dialog';
|
|
14
15
|
import { ConfirmDialog } from '../components/confirm-dialog';
|
|
15
16
|
import { ErrorMessage } from '../components/error-message';
|
|
@@ -27,18 +28,18 @@ export function TaskDetail({ taskId, onSelectTask, onBack }: TaskDetailProps) {
|
|
|
27
28
|
const { adapter } = useAdapter();
|
|
28
29
|
const { theme } = useTheme();
|
|
29
30
|
const [activePanel, setActivePanel] = useState<ActivePanel>('sections');
|
|
30
|
-
const [
|
|
31
|
+
const [workflowState, setWorkflowState] = useState<WorkflowState | null>(null);
|
|
31
32
|
const [statusError, setStatusError] = useState<string | null>(null);
|
|
32
33
|
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
|
|
33
34
|
const [selectedSectionIndex, setSelectedSectionIndex] = useState(0);
|
|
34
35
|
const [mode, setMode] = useState<'idle' | 'edit' | 'delete'>('idle');
|
|
35
36
|
|
|
36
|
-
// Fetch
|
|
37
|
+
// Fetch workflow state when task loads
|
|
37
38
|
useEffect(() => {
|
|
38
39
|
if (task) {
|
|
39
|
-
adapter.
|
|
40
|
+
adapter.getWorkflowState('task', task.id).then(result => {
|
|
40
41
|
if (result.success) {
|
|
41
|
-
|
|
42
|
+
setWorkflowState(result.data);
|
|
42
43
|
}
|
|
43
44
|
});
|
|
44
45
|
}
|
|
@@ -61,11 +62,9 @@ export function TaskDetail({ taskId, onSelectTask, onBack }: TaskDetailProps) {
|
|
|
61
62
|
refresh();
|
|
62
63
|
}
|
|
63
64
|
if (key.tab) {
|
|
64
|
-
// Cycle through panels (skip sections if none exist)
|
|
65
65
|
setActivePanel(current => {
|
|
66
66
|
if (current === 'sections') return 'dependencies';
|
|
67
67
|
if (current === 'dependencies') return 'status';
|
|
68
|
-
// From status, go to sections only if they exist
|
|
69
68
|
return sections.length > 0 ? 'sections' : 'dependencies';
|
|
70
69
|
});
|
|
71
70
|
}
|
|
@@ -77,22 +76,40 @@ export function TaskDetail({ taskId, onSelectTask, onBack }: TaskDetailProps) {
|
|
|
77
76
|
}
|
|
78
77
|
});
|
|
79
78
|
|
|
80
|
-
//
|
|
81
|
-
const
|
|
79
|
+
// Pipeline operations
|
|
80
|
+
const handleAdvance = async () => {
|
|
82
81
|
if (!task) return;
|
|
83
|
-
|
|
84
82
|
setIsUpdatingStatus(true);
|
|
85
83
|
setStatusError(null);
|
|
84
|
+
const result = await adapter.advance('task', taskId, task.version);
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
setStatusError(result.error);
|
|
87
|
+
}
|
|
88
|
+
await refresh();
|
|
89
|
+
setIsUpdatingStatus(false);
|
|
90
|
+
};
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const handleRevert = async () => {
|
|
93
|
+
if (!task) return;
|
|
94
|
+
setIsUpdatingStatus(true);
|
|
95
|
+
setStatusError(null);
|
|
96
|
+
const result = await adapter.revert('task', taskId, task.version);
|
|
97
|
+
if (!result.success) {
|
|
93
98
|
setStatusError(result.error);
|
|
94
99
|
}
|
|
100
|
+
await refresh();
|
|
101
|
+
setIsUpdatingStatus(false);
|
|
102
|
+
};
|
|
95
103
|
|
|
104
|
+
const handleTerminate = async () => {
|
|
105
|
+
if (!task) return;
|
|
106
|
+
setIsUpdatingStatus(true);
|
|
107
|
+
setStatusError(null);
|
|
108
|
+
const result = await adapter.terminate('task', taskId, task.version);
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
setStatusError(result.error);
|
|
111
|
+
}
|
|
112
|
+
await refresh();
|
|
96
113
|
setIsUpdatingStatus(false);
|
|
97
114
|
};
|
|
98
115
|
|
|
@@ -127,6 +144,9 @@ export function TaskDetail({ taskId, onSelectTask, onBack }: TaskDetailProps) {
|
|
|
127
144
|
<Text bold>{task.title}</Text>
|
|
128
145
|
<Text> </Text>
|
|
129
146
|
<StatusBadge status={task.status} />
|
|
147
|
+
{task.blockedBy.length > 0 && (
|
|
148
|
+
<Text color={theme.colors.blocked}> [BLOCKED]</Text>
|
|
149
|
+
)}
|
|
130
150
|
</Box>
|
|
131
151
|
|
|
132
152
|
{/* Divider */}
|
|
@@ -140,6 +160,8 @@ export function TaskDetail({ taskId, onSelectTask, onBack }: TaskDetailProps) {
|
|
|
140
160
|
<PriorityBadge priority={task.priority} />
|
|
141
161
|
<Text> Modified: </Text>
|
|
142
162
|
<Text dimColor>{timeAgo(new Date(task.modifiedAt))}</Text>
|
|
163
|
+
<Text> ID: </Text>
|
|
164
|
+
<Text dimColor>{task.id}</Text>
|
|
143
165
|
</Box>
|
|
144
166
|
|
|
145
167
|
{/* Divider */}
|
|
@@ -181,7 +203,7 @@ export function TaskDetail({ taskId, onSelectTask, onBack }: TaskDetailProps) {
|
|
|
181
203
|
<Text dimColor>{'─'.repeat(40)}</Text>
|
|
182
204
|
</Box>
|
|
183
205
|
|
|
184
|
-
{/* Sections Panel
|
|
206
|
+
{/* Sections Panel */}
|
|
185
207
|
{sections.length > 0 && (
|
|
186
208
|
<Box flexDirection="column" marginBottom={1}>
|
|
187
209
|
<Box marginBottom={0}>
|
|
@@ -221,10 +243,15 @@ export function TaskDetail({ taskId, onSelectTask, onBack }: TaskDetailProps) {
|
|
|
221
243
|
</Box>
|
|
222
244
|
<StatusActions
|
|
223
245
|
currentStatus={task.status}
|
|
224
|
-
|
|
246
|
+
nextStatus={workflowState?.nextStatus ?? null}
|
|
247
|
+
prevStatus={workflowState?.prevStatus ?? null}
|
|
248
|
+
isBlocked={task.blockedBy.length > 0}
|
|
249
|
+
isTerminal={workflowState?.isTerminal ?? false}
|
|
225
250
|
isActive={activePanel === 'status'}
|
|
226
251
|
loading={isUpdatingStatus}
|
|
227
|
-
|
|
252
|
+
onAdvance={handleAdvance}
|
|
253
|
+
onRevert={handleRevert}
|
|
254
|
+
onTerminate={handleTerminate}
|
|
228
255
|
/>
|
|
229
256
|
{statusError && (
|
|
230
257
|
<Box marginTop={1}>
|