@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.
@@ -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 type { FeatureStatus, Priority } from '@allpepper/task-orchestrator';
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 [featureTransitions, setFeatureTransitions] = useState<string[]>([]);
38
- const [featureTransitionIndex, setFeatureTransitionIndex] = useState(0);
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 currentRow = rows[selectedIndex];
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.getAllowedTransitions('FEATURE', feature.status).then((result) => {
155
+ adapter.getWorkflowState('feature', feature.id).then((result) => {
152
156
  if (result.success) {
153
- setFeatureTransitions(result.data);
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((input, key) => {
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 currentRow = rows[selectedIndex];
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 currentRow = rows[selectedIndex];
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
- <Text bold>Set Feature Status</Text>
483
- {featureTransitions.length === 0 ? (
484
- <Text dimColor>No transitions available</Text>
485
- ) : (
486
- featureTransitions.map((status, idx) => (
487
- <Text key={status} inverse={idx === featureTransitionIndex}>
488
- {idx === featureTransitionIndex ? '>' : ' '} {status}
489
- </Text>
490
- ))
491
- )}
492
- <Text dimColor>Enter apply • Esc cancel</Text>
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 { TaskStatus, Priority } from '@allpepper/task-orchestrator';
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 [allowedTransitions, setAllowedTransitions] = useState<string[]>([]);
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 allowed transitions when task loads
37
+ // Fetch workflow state when task loads
37
38
  useEffect(() => {
38
39
  if (task) {
39
- adapter.getAllowedTransitions('TASK', task.status).then(result => {
40
+ adapter.getWorkflowState('task', task.id).then(result => {
40
41
  if (result.success) {
41
- setAllowedTransitions(result.data);
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
- // Handle status change
81
- const handleStatusChange = async (newStatus: string) => {
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
- const result = await adapter.setTaskStatus(taskId, newStatus as TaskStatus, task.version);
88
-
89
- if (result.success) {
90
- // Refresh task data to get updated version
91
- await refresh();
92
- } else {
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 - only show if there are sections */}
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
- allowedTransitions={allowedTransitions}
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
- onTransition={handleStatusChange}
252
+ onAdvance={handleAdvance}
253
+ onRevert={handleRevert}
254
+ onTerminate={handleTerminate}
228
255
  />
229
256
  {statusError && (
230
257
  <Box marginTop={1}>