@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allpepper/task-orchestrator-tui",
3
- "version": "1.2.1",
3
+ "version": "2.0.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": "^2.0.0",
49
49
  "@inkjs/ui": "^2.0.0",
50
50
  "ink": "^6.6.0",
51
51
  "react": "^19.2.4"
@@ -3,77 +3,115 @@ import { Box, Text, useInput } from 'ink';
3
3
  import { StatusBadge } from './status-badge';
4
4
  import { useTheme } from '../../ui/context/theme-context';
5
5
 
6
+ /**
7
+ * v2 pipeline actions instead of free-form status picker
8
+ */
9
+ interface PipelineAction {
10
+ id: string;
11
+ label: string;
12
+ description: string;
13
+ }
14
+
6
15
  export interface StatusActionsProps {
7
16
  currentStatus: string;
8
- allowedTransitions: string[];
9
- onTransition: (newStatus: string) => void;
17
+ /** Next status in the pipeline (null if at end) */
18
+ nextStatus: string | null;
19
+ /** Previous status in the pipeline (null if at start) */
20
+ prevStatus: string | null;
21
+ /** Whether the entity is blocked */
22
+ isBlocked?: boolean;
23
+ /** Whether the entity is in a terminal state */
24
+ isTerminal?: boolean;
25
+ onAdvance: () => void;
26
+ onRevert: () => void;
27
+ onTerminate: () => void;
10
28
  isActive?: boolean;
11
29
  loading?: boolean;
12
30
  }
13
31
 
14
32
  export function StatusActions({
15
33
  currentStatus,
16
- allowedTransitions,
17
- onTransition,
34
+ nextStatus,
35
+ prevStatus,
36
+ isBlocked = false,
37
+ isTerminal = false,
38
+ onAdvance,
39
+ onRevert,
40
+ onTerminate,
18
41
  isActive = true,
19
42
  loading = false,
20
43
  }: StatusActionsProps) {
21
44
  const { theme } = useTheme();
45
+
46
+ // Build available actions
47
+ const actions: PipelineAction[] = [];
48
+ if (nextStatus && !isBlocked && !isTerminal) {
49
+ actions.push({ id: 'advance', label: `Advance to ${nextStatus}`, description: 'Move forward in pipeline' });
50
+ }
51
+ if (prevStatus && !isTerminal) {
52
+ actions.push({ id: 'revert', label: `Revert to ${prevStatus}`, description: 'Move backward in pipeline' });
53
+ }
54
+ if (!isTerminal) {
55
+ actions.push({ id: 'terminate', label: 'Will Not Implement', description: 'Close without completing' });
56
+ }
57
+
22
58
  const [selectedIndex, setSelectedIndex] = useState(0);
23
59
 
24
60
  useInput((input, key) => {
25
- if (!isActive || loading || allowedTransitions.length === 0) return;
61
+ if (!isActive || loading || actions.length === 0) return;
26
62
 
27
63
  if (input === 'j' || key.downArrow) {
28
- const nextIndex = (selectedIndex + 1) % allowedTransitions.length;
29
- setSelectedIndex(nextIndex);
64
+ setSelectedIndex((selectedIndex + 1) % actions.length);
30
65
  } else if (input === 'k' || key.upArrow) {
31
- const prevIndex = (selectedIndex - 1 + allowedTransitions.length) % allowedTransitions.length;
32
- setSelectedIndex(prevIndex);
66
+ setSelectedIndex((selectedIndex - 1 + actions.length) % actions.length);
33
67
  } else if (key.return) {
34
- const selectedStatus = allowedTransitions[selectedIndex];
35
- if (selectedStatus !== undefined) {
36
- onTransition(selectedStatus);
68
+ const action = actions[selectedIndex];
69
+ if (action) {
70
+ switch (action.id) {
71
+ case 'advance': onAdvance(); break;
72
+ case 'revert': onRevert(); break;
73
+ case 'terminate': onTerminate(); break;
74
+ }
37
75
  }
38
76
  }
39
77
  });
40
78
 
41
79
  return (
42
80
  <Box flexDirection="column">
43
- {/* Current Status */}
44
81
  <Box marginBottom={1}>
45
82
  <Text>Status: </Text>
46
83
  <StatusBadge status={currentStatus} />
84
+ {isBlocked && (
85
+ <Text color={theme.colors.blocked}> [BLOCKED]</Text>
86
+ )}
47
87
  </Box>
48
88
 
49
- {/* Loading Indicator */}
50
89
  {loading && (
51
90
  <Box>
52
91
  <Text dimColor>Loading...</Text>
53
92
  </Box>
54
93
  )}
55
94
 
56
- {/* Transitions Section */}
57
95
  {!loading && (
58
96
  <>
59
- {allowedTransitions.length === 0 ? (
97
+ {actions.length === 0 ? (
60
98
  <Box>
61
- <Text dimColor>No transitions available</Text>
99
+ <Text dimColor>No actions available (terminal state)</Text>
62
100
  </Box>
63
101
  ) : (
64
102
  <Box flexDirection="column">
65
103
  <Box marginBottom={1}>
66
- <Text>Change to:</Text>
104
+ <Text>Actions:</Text>
67
105
  </Box>
68
- {allowedTransitions.map((status, index) => {
106
+ {actions.map((action, index) => {
69
107
  const isSelected = index === selectedIndex;
70
108
  return (
71
- <Box key={status} marginLeft={2}>
109
+ <Box key={action.id} marginLeft={2}>
72
110
  <Text color={isSelected ? theme.colors.highlight : undefined}>
73
111
  {isSelected ? '▎' : ' '}
74
112
  </Text>
75
113
  <Text bold={isSelected}>
76
- {' '}{status}
114
+ {' '}{action.label}
77
115
  </Text>
78
116
  </Box>
79
117
  );
@@ -1,17 +1,15 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useState } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { useProjects } 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 { EntityTable } from '../components/entity-table';
7
- import { StatusBadge } from '../components/status-badge';
8
7
  import { timeAgo } from '../../ui/lib/format';
9
8
  import type { ProjectWithCounts } from '../../ui/hooks/use-data';
10
9
  import { ConfirmDialog } from '../components/confirm-dialog';
11
10
  import { ErrorMessage } from '../components/error-message';
12
11
  import { EmptyState } from '../components/empty-state';
13
12
  import { FormDialog } from '../components/form-dialog';
14
- import type { ProjectStatus } from '@allpepper/task-orchestrator';
15
13
 
16
14
  interface DashboardProps {
17
15
  selectedIndex: number;
@@ -25,29 +23,26 @@ export function Dashboard({ selectedIndex, onSelectedIndexChange, onSelectProjec
25
23
  const { adapter } = useAdapter();
26
24
  const { theme } = useTheme();
27
25
  const { projects, loading, error, refresh } = useProjects();
28
- const [mode, setMode] = useState<'idle' | 'create' | 'edit' | 'delete' | 'status'>('idle');
26
+ const [mode, setMode] = useState<'idle' | 'create' | 'edit' | 'delete'>('idle');
29
27
  const [localError, setLocalError] = useState<string | null>(null);
30
- const [allowedTransitions, setAllowedTransitions] = useState<string[]>([]);
31
- const [statusIndex, setStatusIndex] = useState(0);
32
28
 
33
29
  const columns = [
34
30
  {
35
31
  key: 'name',
36
32
  label: 'Name',
37
- width: 50,
33
+ width: 45,
38
34
  },
39
35
  {
40
- key: 'status',
41
- label: 'Status',
42
- width: 20,
43
- render: (_value: unknown, row: ProjectWithCounts, context?: { isSelected: boolean }) => (
44
- <StatusBadge status={row.status} isSelected={context?.isSelected} />
45
- ),
36
+ key: 'features',
37
+ label: 'Features',
38
+ width: 10,
39
+ render: (_value: unknown, row: ProjectWithCounts) =>
40
+ `${row.featureCounts.completed}/${row.featureCounts.total}`,
46
41
  },
47
42
  {
48
43
  key: 'tasks',
49
44
  label: 'Tasks',
50
- width: 15,
45
+ width: 10,
51
46
  render: (_value: unknown, row: ProjectWithCounts) =>
52
47
  `${row.taskCounts.completed}/${row.taskCounts.total}`,
53
48
  },
@@ -64,37 +59,7 @@ export function Dashboard({ selectedIndex, onSelectedIndexChange, onSelectProjec
64
59
  const effectiveSelectedIndex = projects.length > 0 ? clampedSelectedIndex : 0;
65
60
  const selectedProject = projects[effectiveSelectedIndex];
66
61
 
67
- const statusTargets = useMemo(() => allowedTransitions as ProjectStatus[], [allowedTransitions]);
68
-
69
62
  useInput((input, key) => {
70
- // Handle status mode
71
- if (mode === 'status') {
72
- if (input === 'j' || key.downArrow) {
73
- setStatusIndex((prev) => (prev + 1) % Math.max(1, statusTargets.length));
74
- return;
75
- }
76
- if (input === 'k' || key.upArrow) {
77
- setStatusIndex((prev) => (prev - 1 + Math.max(1, statusTargets.length)) % Math.max(1, statusTargets.length));
78
- return;
79
- }
80
- if (key.escape) {
81
- setMode('idle');
82
- return;
83
- }
84
- if (key.return && selectedProject && statusTargets[statusIndex]) {
85
- const next = statusTargets[statusIndex];
86
- adapter.setProjectStatus(selectedProject.id, next, selectedProject.version).then((result) => {
87
- if (!result.success) {
88
- setLocalError(result.error);
89
- }
90
- refresh();
91
- setMode('idle');
92
- });
93
- }
94
- return;
95
- }
96
-
97
- // Handle idle mode
98
63
  if (mode === 'idle') {
99
64
  if (input === 'n') {
100
65
  setMode('create');
@@ -112,15 +77,6 @@ export function Dashboard({ selectedIndex, onSelectedIndexChange, onSelectProjec
112
77
  setMode('delete');
113
78
  return;
114
79
  }
115
- if (input === 's' && selectedProject) {
116
- adapter.getAllowedTransitions('PROJECT', selectedProject.status).then((result) => {
117
- if (result.success) {
118
- setAllowedTransitions(result.data);
119
- setMode('status');
120
- }
121
- });
122
- return;
123
- }
124
80
  if (input === 'r') {
125
81
  refresh();
126
82
  }
@@ -227,22 +183,6 @@ export function Dashboard({ selectedIndex, onSelectedIndexChange, onSelectProjec
227
183
  />
228
184
  ) : null}
229
185
 
230
- {mode === 'status' && selectedProject ? (
231
- <Box flexDirection="column" borderStyle="round" borderColor={theme.colors.highlight} paddingX={1} marginTop={1}>
232
- <Text bold>Set Project Status</Text>
233
- {statusTargets.length === 0 ? (
234
- <Text dimColor>No transitions available</Text>
235
- ) : (
236
- statusTargets.map((status, idx) => (
237
- <Text key={status} inverse={idx === statusIndex}>
238
- {idx === statusIndex ? '>' : ' '} {status}
239
- </Text>
240
- ))
241
- )}
242
- <Text dimColor>Enter apply • Esc cancel</Text>
243
- </Box>
244
- ) : null}
245
-
246
186
  </Box>
247
187
  );
248
188
  }
@@ -1,8 +1,9 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { useAdapter } from '../../ui/context/adapter-context';
4
4
  import { useFeature } from '../../ui/hooks/use-data';
5
- import type { FeatureStatus, Priority } from '@allpepper/task-orchestrator';
5
+ import type { Priority } from '@allpepper/task-orchestrator';
6
+ import type { WorkflowState } from '../../ui/adapters/types';
6
7
  import { StatusBadge } from '../components/status-badge';
7
8
  import { PriorityBadge } from '../components/priority-badge';
8
9
  import { SectionList } from '../components/section-list';
@@ -11,6 +12,7 @@ import { FormDialog } from '../components/form-dialog';
11
12
  import { ErrorMessage } from '../components/error-message';
12
13
  import { EmptyState } from '../components/empty-state';
13
14
  import { useTheme } from '../../ui/context/theme-context';
15
+ import { StatusActions } from '../components/status-actions';
14
16
 
15
17
  interface FeatureDetailProps {
16
18
  featureId: string;
@@ -26,8 +28,19 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
26
28
  const [selectedSectionIndex, setSelectedSectionIndex] = useState(0);
27
29
  const [mode, setMode] = useState<'idle' | 'edit-feature' | 'create-task' | 'feature-status'>('idle');
28
30
  const [localError, setLocalError] = useState<string | null>(null);
29
- const [transitions, setTransitions] = useState<string[]>([]);
30
- const [transitionIndex, setTransitionIndex] = useState(0);
31
+ const [workflowState, setWorkflowState] = useState<WorkflowState | null>(null);
32
+ const [isUpdatingStatus, setIsUpdatingStatus] = useState(false);
33
+
34
+ // Fetch workflow state when feature loads
35
+ useEffect(() => {
36
+ if (feature) {
37
+ adapter.getWorkflowState('feature', feature.id).then((result) => {
38
+ if (result.success) {
39
+ setWorkflowState(result.data);
40
+ }
41
+ });
42
+ }
43
+ }, [adapter, feature]);
31
44
 
32
45
  // Handle keyboard navigation
33
46
  useInput((input, key) => {
@@ -45,13 +58,7 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
45
58
  setMode('create-task');
46
59
  }
47
60
  if (input === 's' && feature) {
48
- adapter.getAllowedTransitions('FEATURE', feature.status).then((result) => {
49
- if (result.success) {
50
- setTransitions(result.data);
51
- setTransitionIndex(0);
52
- setMode('feature-status');
53
- }
54
- });
61
+ setMode('feature-status');
55
62
  }
56
63
  if (tasks.length > 0) {
57
64
  if (input === 'j' || key.downArrow) {
@@ -69,31 +76,48 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
69
76
  }
70
77
  });
71
78
 
72
- useInput((input, key) => {
73
- if (mode !== 'feature-status' || !feature) return;
74
- if (input === 'j' || key.downArrow) {
75
- setTransitionIndex((prev) => (prev + 1) % Math.max(1, transitions.length));
76
- return;
77
- }
78
- if (input === 'k' || key.upArrow) {
79
- setTransitionIndex((prev) => (prev - 1 + Math.max(1, transitions.length)) % Math.max(1, transitions.length));
80
- return;
81
- }
79
+ // Handle status panel (when in feature-status mode, Esc goes back to idle)
80
+ useInput((_input, key) => {
81
+ if (mode !== 'feature-status') return;
82
82
  if (key.escape) {
83
83
  setMode('idle');
84
- return;
85
- }
86
- if (key.return) {
87
- const nextStatus = transitions[transitionIndex] as FeatureStatus | undefined;
88
- if (!nextStatus) return;
89
- adapter.setFeatureStatus(feature.id, nextStatus, feature.version).then((result) => {
90
- if (!result.success) setLocalError(result.error);
91
- refresh();
92
- setMode('idle');
93
- });
94
84
  }
95
85
  }, { isActive: mode === 'feature-status' });
96
86
 
87
+ // Pipeline operations
88
+ const handleAdvance = async () => {
89
+ if (!feature) return;
90
+ setIsUpdatingStatus(true);
91
+ setLocalError(null);
92
+ const result = await adapter.advance('feature', feature.id, feature.version);
93
+ if (!result.success) setLocalError(result.error);
94
+ await refresh();
95
+ setIsUpdatingStatus(false);
96
+ setMode('idle');
97
+ };
98
+
99
+ const handleRevert = async () => {
100
+ if (!feature) return;
101
+ setIsUpdatingStatus(true);
102
+ setLocalError(null);
103
+ const result = await adapter.revert('feature', feature.id, feature.version);
104
+ if (!result.success) setLocalError(result.error);
105
+ await refresh();
106
+ setIsUpdatingStatus(false);
107
+ setMode('idle');
108
+ };
109
+
110
+ const handleTerminate = async () => {
111
+ if (!feature) return;
112
+ setIsUpdatingStatus(true);
113
+ setLocalError(null);
114
+ const result = await adapter.terminate('feature', feature.id, feature.version);
115
+ if (!result.success) setLocalError(result.error);
116
+ await refresh();
117
+ setIsUpdatingStatus(false);
118
+ setMode('idle');
119
+ };
120
+
97
121
  if (loading) {
98
122
  return (
99
123
  <Box padding={1}>
@@ -125,6 +149,9 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
125
149
  <Text bold>{feature.name}</Text>
126
150
  <Text> </Text>
127
151
  <StatusBadge status={feature.status} />
152
+ {feature.blockedBy.length > 0 && (
153
+ <Text color={theme.colors.blocked}> [BLOCKED]</Text>
154
+ )}
128
155
  </Box>
129
156
 
130
157
  {/* Divider */}
@@ -138,6 +165,8 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
138
165
  <PriorityBadge priority={feature.priority} />
139
166
  <Text> Modified: </Text>
140
167
  <Text dimColor>{timeAgo(new Date(feature.modifiedAt))}</Text>
168
+ <Text> ID: </Text>
169
+ <Text dimColor>{feature.id}</Text>
141
170
  </Box>
142
171
 
143
172
  {/* Divider */}
@@ -195,6 +224,9 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
195
224
  <Text bold={isSelected}>
196
225
  {task.title}
197
226
  </Text>
227
+ {task.blockedBy.length > 0 && (
228
+ <Text color={theme.colors.blocked}> [B]</Text>
229
+ )}
198
230
  </Box>
199
231
  );
200
232
  })}
@@ -209,7 +241,7 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
209
241
  </Box>
210
242
  )}
211
243
 
212
- {/* Sections Panel - only show if there are sections */}
244
+ {/* Sections Panel */}
213
245
  {sections.length > 0 && (
214
246
  <Box flexDirection="column" marginBottom={1}>
215
247
  <Text bold>Sections</Text>
@@ -287,23 +319,19 @@ export function FeatureDetail({ featureId, onSelectTask, onBack }: FeatureDetail
287
319
 
288
320
  {mode === 'feature-status' ? (
289
321
  <Box flexDirection="column" borderStyle="round" borderColor={theme.colors.accent} paddingX={1} marginTop={1}>
290
- <Text bold>Set Feature Status</Text>
291
- {transitions.length === 0 ? (
292
- <Text dimColor>No transitions available</Text>
293
- ) : (
294
- transitions.map((status, idx) => {
295
- const isSelected = idx === transitionIndex;
296
- return (
297
- <Box key={status}>
298
- <Text color={isSelected ? theme.colors.highlight : undefined}>
299
- {isSelected ? '▎' : ' '}
300
- </Text>
301
- <Text bold={isSelected}> {status}</Text>
302
- </Box>
303
- );
304
- })
305
- )}
306
- <Text dimColor>Enter apply • Esc cancel</Text>
322
+ <StatusActions
323
+ currentStatus={feature.status}
324
+ nextStatus={workflowState?.nextStatus ?? null}
325
+ prevStatus={workflowState?.prevStatus ?? null}
326
+ isBlocked={feature.blockedBy.length > 0}
327
+ isTerminal={workflowState?.isTerminal ?? false}
328
+ isActive={true}
329
+ loading={isUpdatingStatus}
330
+ onAdvance={handleAdvance}
331
+ onRevert={handleRevert}
332
+ onTerminate={handleTerminate}
333
+ />
334
+ <Text dimColor>Esc: cancel</Text>
307
335
  </Box>
308
336
  ) : null}
309
337
  </Box>
@@ -1,7 +1,7 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput } from 'ink';
3
3
  import { useAdapter } from '../../ui/context/adapter-context';
4
- import type { Project, Feature, ProjectStatus, Section, EntityType } from '@allpepper/task-orchestrator';
4
+ import type { Project, Feature, Section, EntityType } from '@allpepper/task-orchestrator';
5
5
  import { StatusBadge } from '../components/status-badge';
6
6
  import { timeAgo } from '../../ui/lib/format';
7
7
  import { FormDialog } from '../components/form-dialog';
@@ -26,10 +26,8 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
26
26
  const [error, setError] = useState<string | null>(null);
27
27
  const [selectedFeatureIndex, setSelectedFeatureIndex] = useState(0);
28
28
  const [selectedSectionIndex, setSelectedSectionIndex] = useState(0);
29
- const [mode, setMode] = useState<'idle' | 'edit-project' | 'project-status'>('idle');
29
+ const [mode, setMode] = useState<'idle' | 'edit-project'>('idle');
30
30
  const [localError, setLocalError] = useState<string | null>(null);
31
- const [transitions, setTransitions] = useState<string[]>([]);
32
- const [transitionIndex, setTransitionIndex] = useState(0);
33
31
 
34
32
  const load = async () => {
35
33
  setLoading(true);
@@ -70,15 +68,6 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
70
68
  if (input === 'e' && project) {
71
69
  setMode('edit-project');
72
70
  }
73
- if (input === 's' && project) {
74
- adapter.getAllowedTransitions('PROJECT', project.status).then((result) => {
75
- if (result.success) {
76
- setTransitions(result.data);
77
- setTransitionIndex(0);
78
- setMode('project-status');
79
- }
80
- });
81
- }
82
71
  if (features.length > 0) {
83
72
  if (input === 'j' || key.downArrow) {
84
73
  setSelectedFeatureIndex((prev) => Math.min(prev + 1, features.length - 1));
@@ -95,31 +84,6 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
95
84
  }
96
85
  });
97
86
 
98
- useInput((input, key) => {
99
- if (mode !== 'project-status' || !project) return;
100
- if (input === 'j' || key.downArrow) {
101
- setTransitionIndex((prev) => (prev + 1) % Math.max(1, transitions.length));
102
- return;
103
- }
104
- if (input === 'k' || key.upArrow) {
105
- setTransitionIndex((prev) => (prev - 1 + Math.max(1, transitions.length)) % Math.max(1, transitions.length));
106
- return;
107
- }
108
- if (key.escape) {
109
- setMode('idle');
110
- return;
111
- }
112
- if (key.return) {
113
- const nextStatus = transitions[transitionIndex] as ProjectStatus | undefined;
114
- if (!nextStatus) return;
115
- adapter.setProjectStatus(project.id, nextStatus, project.version).then((result) => {
116
- if (!result.success) setLocalError(result.error);
117
- load();
118
- setMode('idle');
119
- });
120
- }
121
- }, { isActive: mode === 'project-status' });
122
-
123
87
  if (loading) {
124
88
  return (
125
89
  <Box padding={1}>
@@ -146,11 +110,9 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
146
110
 
147
111
  return (
148
112
  <Box flexDirection="column" padding={1}>
149
- {/* Project Header */}
113
+ {/* Project Header (no status - projects are stateless in v2) */}
150
114
  <Box marginBottom={1}>
151
115
  <Text bold>{project.name}</Text>
152
- <Text> </Text>
153
- <StatusBadge status={project.status} />
154
116
  </Box>
155
117
 
156
118
  {/* Divider */}
@@ -219,6 +181,9 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
219
181
  <Text bold={isSelected}>
220
182
  {feature.name}
221
183
  </Text>
184
+ {feature.blockedBy.length > 0 && (
185
+ <Text color={theme.colors.blocked}> [B]</Text>
186
+ )}
222
187
  </Box>
223
188
  );
224
189
  })}
@@ -233,7 +198,7 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
233
198
  </Box>
234
199
  )}
235
200
 
236
- {/* Sections Panel - only show if there are sections */}
201
+ {/* Sections Panel */}
237
202
  {sections.length > 0 && (
238
203
  <Box flexDirection="column" marginBottom={1}>
239
204
  <Text bold>Sections</Text>
@@ -249,7 +214,7 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
249
214
  {/* Help Footer */}
250
215
  <Box marginTop={1}>
251
216
  <Text dimColor>
252
- ESC/h: Back | r: Refresh | e: Edit | s: Status{features.length > 0 ? ' | j/k: Navigate | Enter: Select Feature' : ''}
217
+ ESC/h: Back | r: Refresh | e: Edit{features.length > 0 ? ' | j/k: Navigate | Enter: Select Feature' : ''}
253
218
  </Text>
254
219
  </Box>
255
220
 
@@ -278,28 +243,6 @@ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDet
278
243
  }}
279
244
  />
280
245
  ) : null}
281
-
282
- {mode === 'project-status' ? (
283
- <Box flexDirection="column" borderStyle="round" borderColor={theme.colors.accent} paddingX={1} marginTop={1}>
284
- <Text bold>Set Project Status</Text>
285
- {transitions.length === 0 ? (
286
- <Text dimColor>No transitions available</Text>
287
- ) : (
288
- transitions.map((status, idx) => {
289
- const isSelected = idx === transitionIndex;
290
- return (
291
- <Box key={status}>
292
- <Text color={isSelected ? theme.colors.highlight : undefined}>
293
- {isSelected ? '▎' : ' '}
294
- </Text>
295
- <Text bold={isSelected}> {status}</Text>
296
- </Box>
297
- );
298
- })
299
- )}
300
- <Text dimColor>Enter apply • Esc cancel</Text>
301
- </Box>
302
- ) : null}
303
246
  </Box>
304
247
  );
305
248
  }