@allpepper/task-orchestrator-tui 1.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.
Files changed (50) hide show
  1. package/README.md +78 -0
  2. package/package.json +54 -0
  3. package/src/tui/app.tsx +308 -0
  4. package/src/tui/components/column-filter-bar.tsx +52 -0
  5. package/src/tui/components/confirm-dialog.tsx +45 -0
  6. package/src/tui/components/dependency-list.tsx +115 -0
  7. package/src/tui/components/empty-state.tsx +28 -0
  8. package/src/tui/components/entity-table.tsx +120 -0
  9. package/src/tui/components/error-message.tsx +41 -0
  10. package/src/tui/components/feature-kanban-card.tsx +216 -0
  11. package/src/tui/components/footer.tsx +34 -0
  12. package/src/tui/components/form-dialog.tsx +338 -0
  13. package/src/tui/components/header.tsx +54 -0
  14. package/src/tui/components/index.ts +16 -0
  15. package/src/tui/components/kanban-board.tsx +335 -0
  16. package/src/tui/components/kanban-card.tsx +70 -0
  17. package/src/tui/components/kanban-column.tsx +173 -0
  18. package/src/tui/components/priority-badge.tsx +16 -0
  19. package/src/tui/components/section-list.tsx +96 -0
  20. package/src/tui/components/status-actions.tsx +87 -0
  21. package/src/tui/components/status-badge.tsx +22 -0
  22. package/src/tui/components/tree-view.tsx +295 -0
  23. package/src/tui/components/view-mode-chips.tsx +23 -0
  24. package/src/tui/index.tsx +33 -0
  25. package/src/tui/screens/dashboard.tsx +248 -0
  26. package/src/tui/screens/feature-detail.tsx +312 -0
  27. package/src/tui/screens/index.ts +6 -0
  28. package/src/tui/screens/kanban-view.tsx +251 -0
  29. package/src/tui/screens/project-detail.tsx +305 -0
  30. package/src/tui/screens/project-view.tsx +498 -0
  31. package/src/tui/screens/search.tsx +257 -0
  32. package/src/tui/screens/task-detail.tsx +294 -0
  33. package/src/ui/adapters/direct.ts +429 -0
  34. package/src/ui/adapters/index.ts +14 -0
  35. package/src/ui/adapters/types.ts +269 -0
  36. package/src/ui/context/adapter-context.tsx +31 -0
  37. package/src/ui/context/theme-context.tsx +43 -0
  38. package/src/ui/hooks/index.ts +20 -0
  39. package/src/ui/hooks/use-data.ts +919 -0
  40. package/src/ui/hooks/use-debounce.ts +37 -0
  41. package/src/ui/hooks/use-feature-kanban.ts +151 -0
  42. package/src/ui/hooks/use-kanban.ts +96 -0
  43. package/src/ui/hooks/use-navigation.tsx +94 -0
  44. package/src/ui/index.ts +73 -0
  45. package/src/ui/lib/colors.ts +79 -0
  46. package/src/ui/lib/format.ts +114 -0
  47. package/src/ui/lib/types.ts +157 -0
  48. package/src/ui/themes/dark.ts +63 -0
  49. package/src/ui/themes/light.ts +63 -0
  50. package/src/ui/themes/types.ts +71 -0
@@ -0,0 +1,251 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Text, useInput, useStdout } from 'ink';
3
+ import { useFeatureKanban } from '../../ui/hooks/use-feature-kanban';
4
+ import { useAdapter } from '../../ui/context/adapter-context';
5
+ import { KanbanBoard } from '../components/kanban-board';
6
+
7
+ interface KanbanViewProps {
8
+ projectId: string;
9
+ activeColumnIndex: number;
10
+ onActiveColumnIndexChange: (index: number) => void;
11
+ selectedFeatureIndex: number;
12
+ onSelectedFeatureIndexChange: (index: number) => void;
13
+ expandedFeatureId: string | null;
14
+ onExpandedFeatureIdChange: (id: string | null) => void;
15
+ selectedTaskIndex: number;
16
+ onSelectedTaskIndexChange: (index: number) => void;
17
+ onSelectTask: (taskId: string) => void;
18
+ onBack: () => void;
19
+ // Filter state (lifted to App for persistence)
20
+ activeStatuses: Set<string>;
21
+ onActiveStatusesChange: (statuses: Set<string>) => void;
22
+ }
23
+
24
+ export function KanbanView({
25
+ projectId,
26
+ activeColumnIndex,
27
+ onActiveColumnIndexChange,
28
+ selectedFeatureIndex,
29
+ onSelectedFeatureIndexChange,
30
+ expandedFeatureId,
31
+ onExpandedFeatureIdChange,
32
+ selectedTaskIndex,
33
+ onSelectedTaskIndexChange,
34
+ onSelectTask,
35
+ onBack,
36
+ activeStatuses,
37
+ onActiveStatusesChange,
38
+ }: KanbanViewProps) {
39
+ const { adapter } = useAdapter();
40
+ const { columns, loading, error, refresh, moveFeature } = useFeatureKanban(projectId);
41
+ const [projectName, setProjectName] = useState<string>('');
42
+ const { stdout } = useStdout();
43
+
44
+ const terminalRows = stdout?.rows ?? 24;
45
+ const terminalCols = stdout?.columns ?? 120;
46
+ const availableHeight = Math.max(10, terminalRows - 6);
47
+
48
+ // Filter mode state (local — only active while interacting with chips)
49
+ const [isFilterMode, setIsFilterMode] = useState(false);
50
+ const [filterCursorIndex, setFilterCursorIndex] = useState(0);
51
+
52
+ // Auto-populate activeStatuses from data on first load (only if empty)
53
+ useEffect(() => {
54
+ if (activeStatuses.size === 0 && columns.length > 0) {
55
+ const populated = new Set<string>();
56
+ for (const col of columns) {
57
+ if (col.features.length > 0) {
58
+ populated.add(col.status);
59
+ }
60
+ }
61
+ // If nothing has features, show all columns
62
+ if (populated.size === 0) {
63
+ for (const col of columns) {
64
+ populated.add(col.status);
65
+ }
66
+ }
67
+ onActiveStatusesChange(populated);
68
+ }
69
+ }, [columns, activeStatuses.size, onActiveStatusesChange]);
70
+
71
+ // Compute filtered columns
72
+ const filteredColumns = useMemo(() => {
73
+ if (activeStatuses.size === 0) return columns;
74
+ return columns.filter((c) => activeStatuses.has(c.status));
75
+ }, [columns, activeStatuses]);
76
+
77
+ // Fetch project name
78
+ useEffect(() => {
79
+ const fetchProject = async () => {
80
+ const result = await adapter.getProject(projectId);
81
+ if (result.success) {
82
+ setProjectName(result.data.name);
83
+ }
84
+ };
85
+ fetchProject();
86
+ }, [adapter, projectId]);
87
+
88
+ // Reset selectedFeatureIndex when activeColumnIndex changes
89
+ useEffect(() => {
90
+ if (filteredColumns.length > 0 && activeColumnIndex < filteredColumns.length) {
91
+ const activeColumn = filteredColumns[activeColumnIndex];
92
+ if (activeColumn) {
93
+ if (activeColumn.features.length === 0) {
94
+ onSelectedFeatureIndexChange(-1);
95
+ } else if (selectedFeatureIndex >= activeColumn.features.length) {
96
+ onSelectedFeatureIndexChange(0);
97
+ } else if (selectedFeatureIndex < 0 && activeColumn.features.length > 0) {
98
+ onSelectedFeatureIndexChange(0);
99
+ }
100
+ }
101
+ }
102
+ }, [activeColumnIndex, filteredColumns, selectedFeatureIndex, onSelectedFeatureIndexChange]);
103
+
104
+ // Collapse expanded feature if it no longer exists in the active column
105
+ useEffect(() => {
106
+ if (expandedFeatureId && filteredColumns.length > 0 && activeColumnIndex < filteredColumns.length) {
107
+ const activeColumn = filteredColumns[activeColumnIndex];
108
+ if (activeColumn && !activeColumn.features.some((f) => f.id === expandedFeatureId)) {
109
+ onExpandedFeatureIdChange(null);
110
+ onSelectedTaskIndexChange(-1);
111
+ }
112
+ }
113
+ }, [filteredColumns, activeColumnIndex, expandedFeatureId, onExpandedFeatureIdChange, onSelectedTaskIndexChange]);
114
+
115
+ // Clamp activeColumnIndex when filtered columns change
116
+ useEffect(() => {
117
+ if (filteredColumns.length > 0 && activeColumnIndex >= filteredColumns.length) {
118
+ onActiveColumnIndexChange(Math.max(0, filteredColumns.length - 1));
119
+ }
120
+ }, [filteredColumns.length, activeColumnIndex, onActiveColumnIndexChange]);
121
+
122
+ // Handle toggle status
123
+ const handleToggleStatus = (status: string) => {
124
+ const next = new Set(activeStatuses);
125
+ if (next.has(status)) {
126
+ // Don't allow removing the last status
127
+ if (next.size > 1) {
128
+ next.delete(status);
129
+ }
130
+ } else {
131
+ next.add(status);
132
+ }
133
+ onActiveStatusesChange(next);
134
+ };
135
+
136
+ // Handle keyboard
137
+ useInput((input, key) => {
138
+ // Don't handle keys in filter mode — board handles them
139
+ if (isFilterMode) return;
140
+
141
+ if (key.escape) {
142
+ if (expandedFeatureId) {
143
+ // Let KanbanBoard handle Esc in task mode
144
+ return;
145
+ }
146
+ onBack();
147
+ return;
148
+ }
149
+ if (input === 'b' && !expandedFeatureId) {
150
+ onBack();
151
+ return;
152
+ }
153
+ if (input === 'r') {
154
+ refresh();
155
+ return;
156
+ }
157
+ });
158
+
159
+ // Handle move feature
160
+ const handleMoveFeature = async (featureId: string, newStatus: string) => {
161
+ await moveFeature(featureId, newStatus);
162
+ // After move, adjust indices if needed
163
+ if (filteredColumns.length > 0 && activeColumnIndex < filteredColumns.length) {
164
+ const activeColumn = filteredColumns[activeColumnIndex];
165
+ if (activeColumn) {
166
+ if (activeColumn.features.length === 0) {
167
+ onSelectedFeatureIndexChange(-1);
168
+ } else if (selectedFeatureIndex >= activeColumn.features.length) {
169
+ onSelectedFeatureIndexChange(Math.max(0, activeColumn.features.length - 1));
170
+ }
171
+ }
172
+ }
173
+ };
174
+
175
+ if (loading) {
176
+ return (
177
+ <Box padding={1}>
178
+ <Text>Loading kanban board...</Text>
179
+ </Box>
180
+ );
181
+ }
182
+
183
+ if (error) {
184
+ return (
185
+ <Box padding={1}>
186
+ <Text color="red">Error: {error}</Text>
187
+ </Box>
188
+ );
189
+ }
190
+
191
+ // Clamp indices
192
+ const clampedActiveColumnIndex = Math.min(activeColumnIndex, Math.max(0, filteredColumns.length - 1));
193
+ const effectiveActiveColumnIndex = filteredColumns.length > 0 ? clampedActiveColumnIndex : 0;
194
+
195
+ const activeColumn = filteredColumns[effectiveActiveColumnIndex];
196
+ const maxFeatureIndex = activeColumn ? Math.max(0, activeColumn.features.length - 1) : 0;
197
+ const clampedFeatureIndex = activeColumn && activeColumn.features.length > 0
198
+ ? Math.min(selectedFeatureIndex, maxFeatureIndex)
199
+ : -1;
200
+
201
+ const isTaskMode = expandedFeatureId !== null;
202
+ const footerHint = isFilterMode
203
+ ? 'h/l: navigate chips Space: toggle Esc: exit filter'
204
+ : isTaskMode
205
+ ? 'j/k: tasks Enter: open task Esc/h: collapse r: refresh'
206
+ : 'h/l: columns j/k: features Enter: expand m: move f: filter r: refresh Esc: back';
207
+
208
+ return (
209
+ <Box flexDirection="column" padding={1}>
210
+ {/* Header */}
211
+ <Box marginBottom={1}>
212
+ <Text bold>{projectName}</Text>
213
+ <Text> - </Text>
214
+ <Text>Feature Board</Text>
215
+ </Box>
216
+
217
+ {/* Kanban Board */}
218
+ {filteredColumns.length === 0 ? (
219
+ <Text dimColor>No features in this project yet.</Text>
220
+ ) : (
221
+ <KanbanBoard
222
+ columns={filteredColumns}
223
+ activeColumnIndex={effectiveActiveColumnIndex}
224
+ selectedFeatureIndex={clampedFeatureIndex}
225
+ expandedFeatureId={expandedFeatureId}
226
+ selectedTaskIndex={selectedTaskIndex}
227
+ onColumnChange={onActiveColumnIndexChange}
228
+ onFeatureChange={onSelectedFeatureIndexChange}
229
+ onExpandFeature={onExpandedFeatureIdChange}
230
+ onTaskChange={onSelectedTaskIndexChange}
231
+ onSelectTask={onSelectTask}
232
+ onMoveFeature={handleMoveFeature}
233
+ isActive={true}
234
+ availableHeight={availableHeight}
235
+ availableWidth={terminalCols}
236
+ activeStatuses={activeStatuses}
237
+ isFilterMode={isFilterMode}
238
+ filterCursorIndex={filterCursorIndex}
239
+ onToggleStatus={handleToggleStatus}
240
+ onFilterCursorChange={setFilterCursorIndex}
241
+ onFilterModeChange={setIsFilterMode}
242
+ />
243
+ )}
244
+
245
+ {/* Footer hints */}
246
+ <Box marginTop={1}>
247
+ <Text dimColor>{footerHint}</Text>
248
+ </Box>
249
+ </Box>
250
+ );
251
+ }
@@ -0,0 +1,305 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useAdapter } from '../../ui/context/adapter-context';
4
+ import type { Project, Feature, ProjectStatus, Section, EntityType } from 'task-orchestrator-bun/src/domain/types';
5
+ import { StatusBadge } from '../components/status-badge';
6
+ import { timeAgo } from '../../ui/lib/format';
7
+ import { FormDialog } from '../components/form-dialog';
8
+ import { ErrorMessage } from '../components/error-message';
9
+ import { EmptyState } from '../components/empty-state';
10
+ import { useTheme } from '../../ui/context/theme-context';
11
+ import { SectionList } from '../components/section-list';
12
+
13
+ interface ProjectDetailProps {
14
+ projectId: string;
15
+ onSelectFeature: (featureId: string) => void;
16
+ onBack: () => void;
17
+ }
18
+
19
+ export function ProjectDetail({ projectId, onSelectFeature, onBack }: ProjectDetailProps) {
20
+ const { adapter } = useAdapter();
21
+ const { theme } = useTheme();
22
+ const [project, setProject] = useState<Project | null>(null);
23
+ const [features, setFeatures] = useState<Feature[]>([]);
24
+ const [sections, setSections] = useState<Section[]>([]);
25
+ const [loading, setLoading] = useState(true);
26
+ const [error, setError] = useState<string | null>(null);
27
+ const [selectedFeatureIndex, setSelectedFeatureIndex] = useState(0);
28
+ const [selectedSectionIndex, setSelectedSectionIndex] = useState(0);
29
+ const [mode, setMode] = useState<'idle' | 'edit-project' | 'project-status'>('idle');
30
+ const [localError, setLocalError] = useState<string | null>(null);
31
+ const [transitions, setTransitions] = useState<string[]>([]);
32
+ const [transitionIndex, setTransitionIndex] = useState(0);
33
+
34
+ const load = async () => {
35
+ setLoading(true);
36
+ setError(null);
37
+ const [projectResult, featuresResult, sectionsResult] = await Promise.all([
38
+ adapter.getProject(projectId),
39
+ adapter.getFeatures({ projectId }),
40
+ adapter.getSections('PROJECT' as EntityType, projectId),
41
+ ]);
42
+ if (projectResult.success) {
43
+ setProject(projectResult.data);
44
+ } else {
45
+ setError(projectResult.error);
46
+ }
47
+ if (featuresResult.success) {
48
+ setFeatures(featuresResult.data);
49
+ }
50
+ if (sectionsResult.success) {
51
+ setSections(sectionsResult.data);
52
+ }
53
+ setLoading(false);
54
+ };
55
+
56
+ useEffect(() => {
57
+ load();
58
+ // eslint-disable-next-line react-hooks/exhaustive-deps
59
+ }, [projectId]);
60
+
61
+ // Handle keyboard navigation
62
+ useInput((input, key) => {
63
+ if (mode !== 'idle') return;
64
+ if (key.escape || input === 'h' || key.leftArrow) {
65
+ onBack();
66
+ }
67
+ if (input === 'r') {
68
+ load();
69
+ }
70
+ if (input === 'e' && project) {
71
+ setMode('edit-project');
72
+ }
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
+ if (features.length > 0) {
83
+ if (input === 'j' || key.downArrow) {
84
+ setSelectedFeatureIndex((prev) => Math.min(prev + 1, features.length - 1));
85
+ }
86
+ if (input === 'k' || key.upArrow) {
87
+ setSelectedFeatureIndex((prev) => Math.max(prev - 1, 0));
88
+ }
89
+ if (key.return) {
90
+ const selectedFeature = features[selectedFeatureIndex];
91
+ if (selectedFeature) {
92
+ onSelectFeature(selectedFeature.id);
93
+ }
94
+ }
95
+ }
96
+ });
97
+
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
+ if (loading) {
124
+ return (
125
+ <Box padding={1}>
126
+ <Text>Loading project...</Text>
127
+ </Box>
128
+ );
129
+ }
130
+
131
+ if (error) {
132
+ return (
133
+ <Box padding={1}>
134
+ <Text color={theme.colors.danger}>Error: {error}</Text>
135
+ </Box>
136
+ );
137
+ }
138
+
139
+ if (!project) {
140
+ return (
141
+ <Box padding={1}>
142
+ <Text>Project not found</Text>
143
+ </Box>
144
+ );
145
+ }
146
+
147
+ return (
148
+ <Box flexDirection="column" padding={1}>
149
+ {/* Project Header */}
150
+ <Box marginBottom={1}>
151
+ <Text bold>{project.name}</Text>
152
+ <Text> </Text>
153
+ <StatusBadge status={project.status} />
154
+ </Box>
155
+
156
+ {/* Divider */}
157
+ <Box marginY={0}>
158
+ <Text dimColor>{'─'.repeat(40)}</Text>
159
+ </Box>
160
+
161
+ {/* Project Metadata */}
162
+ <Box marginBottom={1}>
163
+ <Text>Modified: </Text>
164
+ <Text dimColor>{timeAgo(new Date(project.modifiedAt))}</Text>
165
+ </Box>
166
+
167
+ {/* Divider */}
168
+ <Box marginY={0}>
169
+ <Text dimColor>{'─'.repeat(40)}</Text>
170
+ </Box>
171
+
172
+ {/* Project Details (summary) */}
173
+ <Box flexDirection="column" marginBottom={1}>
174
+ <Text bold>Details</Text>
175
+ <Box marginLeft={1}>
176
+ <Text wrap="wrap">{project.summary}</Text>
177
+ </Box>
178
+ </Box>
179
+
180
+ {/* Divider */}
181
+ {project.description && (
182
+ <Box marginY={0}>
183
+ <Text dimColor>{'─'.repeat(40)}</Text>
184
+ </Box>
185
+ )}
186
+
187
+ {/* Project Description */}
188
+ {project.description && (
189
+ <Box flexDirection="column" marginBottom={1}>
190
+ <Text bold>Description</Text>
191
+ <Box marginLeft={1}>
192
+ <Text wrap="wrap">{project.description}</Text>
193
+ </Box>
194
+ </Box>
195
+ )}
196
+
197
+ {/* Divider */}
198
+ <Box marginY={0}>
199
+ <Text dimColor>{'─'.repeat(40)}</Text>
200
+ </Box>
201
+
202
+ {/* Features List */}
203
+ <Box flexDirection="column" marginBottom={1}>
204
+ <Text bold>Features ({features.length})</Text>
205
+ {features.length === 0 ? (
206
+ <Box marginLeft={1}><EmptyState message="No features" hint="" /></Box>
207
+ ) : (
208
+ <Box flexDirection="column" marginLeft={1}>
209
+ {features.map((feature, index) => {
210
+ const isSelected = index === selectedFeatureIndex;
211
+ return (
212
+ <Box key={feature.id}>
213
+ <Text color={isSelected ? theme.colors.highlight : undefined}>
214
+ {isSelected ? '▎' : ' '}
215
+ </Text>
216
+ <Text> </Text>
217
+ <StatusBadge status={feature.status} />
218
+ <Text> </Text>
219
+ <Text bold={isSelected}>
220
+ {feature.name}
221
+ </Text>
222
+ </Box>
223
+ );
224
+ })}
225
+ </Box>
226
+ )}
227
+ </Box>
228
+
229
+ {/* Divider */}
230
+ {sections.length > 0 && (
231
+ <Box marginY={0}>
232
+ <Text dimColor>{'─'.repeat(40)}</Text>
233
+ </Box>
234
+ )}
235
+
236
+ {/* Sections Panel - only show if there are sections */}
237
+ {sections.length > 0 && (
238
+ <Box flexDirection="column" marginBottom={1}>
239
+ <Text bold>Sections</Text>
240
+ <SectionList
241
+ sections={sections}
242
+ selectedIndex={selectedSectionIndex}
243
+ onSelectedIndexChange={setSelectedSectionIndex}
244
+ isActive={true}
245
+ />
246
+ </Box>
247
+ )}
248
+
249
+ {/* Help Footer */}
250
+ <Box marginTop={1}>
251
+ <Text dimColor>
252
+ ESC/h: Back | r: Refresh | e: Edit | s: Status{features.length > 0 ? ' | j/k: Navigate | Enter: Select Feature' : ''}
253
+ </Text>
254
+ </Box>
255
+
256
+ {localError ? <ErrorMessage message={localError} onDismiss={() => setLocalError(null)} /> : null}
257
+
258
+ {mode === 'edit-project' ? (
259
+ <FormDialog
260
+ title="Edit Project"
261
+ fields={[
262
+ { key: 'name', label: 'Name', required: true, value: project.name },
263
+ { key: 'summary', label: 'Summary', required: true, value: project.summary },
264
+ { key: 'description', label: 'Description', value: project.description ?? '' },
265
+ ]}
266
+ onCancel={() => setMode('idle')}
267
+ onSubmit={(values) => {
268
+ adapter.updateProject(project.id, {
269
+ name: values.name ?? '',
270
+ summary: values.summary ?? '',
271
+ description: values.description || undefined,
272
+ version: project.version,
273
+ }).then((result) => {
274
+ if (!result.success) setLocalError(result.error);
275
+ load();
276
+ setMode('idle');
277
+ });
278
+ }}
279
+ />
280
+ ) : 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
+ </Box>
304
+ );
305
+ }