@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
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # Task Orchestrator TUI
2
+
3
+ Terminal User Interface for the Task Orchestrator application.
4
+
5
+ ## Overview
6
+
7
+ This package provides an interactive terminal-based interface for managing projects, features, and tasks. It's built with [Ink](https://github.com/vadimdemedes/ink) and React.
8
+
9
+ ## Architecture
10
+
11
+ The TUI is separated into two main directories:
12
+
13
+ - **`src/ui/`** - UI abstraction layer that can work with any renderer (TUI, web, etc.)
14
+ - `adapters/` - Data access layer for communicating with the domain
15
+ - `context/` - React contexts for theme and adapter
16
+ - `hooks/` - React hooks for data fetching
17
+ - `lib/` - Utility functions and types
18
+ - `themes/` - Color themes (dark/light)
19
+
20
+ - **`src/tui/`** - Terminal-specific implementation using Ink
21
+ - `components/` - Ink components for the terminal UI
22
+ - `screens/` - Screen components (Dashboard, etc.)
23
+ - `app.tsx` - Main TUI application component
24
+ - `index.tsx` - Entry point
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ bun install
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ To start the TUI:
35
+
36
+ ```bash
37
+ bun run tui
38
+ ```
39
+
40
+ Or directly:
41
+
42
+ ```bash
43
+ bun run src/tui/index.tsx
44
+ ```
45
+
46
+ ## Development
47
+
48
+ Type checking:
49
+
50
+ ```bash
51
+ bun run typecheck
52
+ ```
53
+
54
+ Running tests:
55
+
56
+ ```bash
57
+ bun test
58
+ ```
59
+
60
+ ## Dependencies
61
+
62
+ This package depends on the core `task-orchestrator-bun` package for domain logic, repositories, and database access.
63
+
64
+ The relationship is managed through a file: dependency in package.json:
65
+
66
+ ```json
67
+ "dependencies": {
68
+ "task-orchestrator-bun": "file:../task-orchestrator-bun"
69
+ }
70
+ ```
71
+
72
+ ## Key Features
73
+
74
+ - Interactive dashboard with project navigation
75
+ - Status badges with theme support
76
+ - Data hooks for efficient data fetching
77
+ - Direct adapter for in-process data access
78
+ - Support for dark and light themes
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@allpepper/task-orchestrator-tui",
3
+ "version": "1.0.0",
4
+ "description": "Terminal UI for task orchestration - Kanban boards, tree views, and task management",
5
+ "type": "module",
6
+ "bin": {
7
+ "tasks": "src/tui/index.tsx"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "!src/**/*.test.ts",
12
+ "!src/**/*.test.tsx",
13
+ "!src/**/__tests__"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/alioshr/task-orchestrator-tui.git"
21
+ },
22
+ "keywords": ["cli", "tui", "task-manager", "kanban", "ink", "react", "bun"],
23
+ "author": "alioshr",
24
+ "license": "MIT",
25
+ "bugs": {
26
+ "url": "https://github.com/alioshr/task-orchestrator-tui/issues"
27
+ },
28
+ "homepage": "https://github.com/alioshr/task-orchestrator-tui#readme",
29
+ "engines": {
30
+ "bun": ">=1.0.0"
31
+ },
32
+ "scripts": {
33
+ "start": "bun run src/tui/index.tsx",
34
+ "tui": "bun run src/tui/index.tsx",
35
+ "typecheck": "tsc --noEmit",
36
+ "test": "bun test",
37
+ "prepublishOnly": "bun test"
38
+ },
39
+ "dependencies": {
40
+ "@allpepper/task-orchestrator": "^0.1.0",
41
+ "ink": "^6.6.0",
42
+ "@inkjs/ui": "^2.0.0",
43
+ "react": "^19.2.4"
44
+ },
45
+ "devDependencies": {
46
+ "@types/bun": "latest",
47
+ "@types/react": "^19.2.13",
48
+ "ink-testing-library": "^4.0.0",
49
+ "typescript": "^5.0.0",
50
+ "semantic-release": "^24.0.0",
51
+ "@semantic-release/changelog": "^6.0.3",
52
+ "@semantic-release/git": "^10.0.1"
53
+ }
54
+ }
@@ -0,0 +1,308 @@
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import { ThemeProvider } from '../ui/context/theme-context';
4
+ import { AdapterProvider } from '../ui/context/adapter-context';
5
+ import { DirectAdapter } from '../ui/adapters/direct';
6
+ import { Header } from './components/header';
7
+ import { Footer } from './components/footer';
8
+ import { Dashboard } from './screens/dashboard';
9
+ import { ProjectView } from './screens/project-view';
10
+ import { TaskDetail } from './screens/task-detail';
11
+ import { KanbanView } from './screens/kanban-view';
12
+ import { FeatureDetail } from './screens/feature-detail';
13
+ import { ProjectDetail } from './screens/project-detail';
14
+ import { SearchScreen } from './screens/search';
15
+
16
+ export function App() {
17
+ // Setup
18
+ const { exit } = useApp();
19
+ const adapter = useMemo(() => new DirectAdapter(), []);
20
+
21
+ // Navigation state (simple for now - just track current screen)
22
+ const [screen, setScreen] = useState<'dashboard' | 'project' | 'project-detail' | 'task' | 'kanban' | 'feature' | 'search'>('dashboard');
23
+ const [searchReturnScreen, setSearchReturnScreen] = useState<'dashboard' | 'project' | 'project-detail' | 'task' | 'kanban' | 'feature'>('dashboard');
24
+ const [projectId, setProjectId] = useState<string | null>(null);
25
+ const [taskId, setTaskId] = useState<string | null>(null);
26
+ const [featureId, setFeatureId] = useState<string | null>(null);
27
+ const [taskOriginScreen, setTaskOriginScreen] = useState<'project' | 'kanban' | 'feature'>('project');
28
+
29
+ // View state persistence
30
+ // Dashboard state
31
+ const [dashboardSelectedIndex, setDashboardSelectedIndex] = useState(0);
32
+
33
+ // ProjectView state
34
+ const [projectExpandedFeatures, setProjectExpandedFeatures] = useState<Set<string>>(new Set());
35
+ const [projectExpandedGroups, setProjectExpandedGroups] = useState<Set<string>>(new Set());
36
+ const [projectSelectedIndex, setProjectSelectedIndex] = useState(0);
37
+ const [projectViewMode, setProjectViewMode] = useState<'features' | 'status' | 'feature-status'>('status');
38
+
39
+ // KanbanView state
40
+ const [kanbanActiveColumnIndex, setKanbanActiveColumnIndex] = useState(0);
41
+ const [kanbanSelectedFeatureIndex, setKanbanSelectedFeatureIndex] = useState(0);
42
+ const [kanbanExpandedFeatureId, setKanbanExpandedFeatureId] = useState<string | null>(null);
43
+ const [kanbanSelectedTaskIndex, setKanbanSelectedTaskIndex] = useState(-1);
44
+ const [kanbanActiveStatuses, setKanbanActiveStatuses] = useState<Set<string>>(new Set());
45
+ const handleKanbanActiveStatusesChange = useCallback((statuses: Set<string>) => {
46
+ setKanbanActiveStatuses(statuses);
47
+ }, []);
48
+
49
+ // Global keyboard handling
50
+ useInput((input, key) => {
51
+ if (input === 'q') {
52
+ exit();
53
+ }
54
+ if (input === '/') {
55
+ if (screen !== 'search') {
56
+ setSearchReturnScreen(screen as 'dashboard' | 'project' | 'task' | 'kanban' | 'feature');
57
+ setScreen('search');
58
+ }
59
+ return;
60
+ }
61
+ });
62
+
63
+ // Compute breadcrumbs based on current screen
64
+ const breadcrumbs = useMemo(() => {
65
+ switch (screen) {
66
+ case 'dashboard':
67
+ return ['Dashboard'];
68
+ case 'project-detail':
69
+ return ['Dashboard', 'Project'];
70
+ case 'project':
71
+ return ['Dashboard', 'Project'];
72
+ case 'feature':
73
+ return ['Dashboard', 'Project', 'Feature'];
74
+ case 'task':
75
+ return ['Dashboard', 'Project', 'Task'];
76
+ case 'kanban':
77
+ return ['Dashboard', 'Project', 'Board'];
78
+ case 'search':
79
+ return ['Search'];
80
+ default:
81
+ return ['Dashboard'];
82
+ }
83
+ }, [screen]);
84
+
85
+ // Shortcuts for footer
86
+ const shortcuts =
87
+ screen === 'search'
88
+ ? [
89
+ { key: '↑/↓', label: 'Navigate' },
90
+ { key: 'Enter/→', label: 'Open' },
91
+ { key: 'Esc/←', label: 'Back' },
92
+ { key: 'q', label: 'Quit' },
93
+ ]
94
+ : screen === 'feature'
95
+ ? [
96
+ { key: 'j/k', label: 'Navigate' },
97
+ { key: 'Enter', label: 'Open Task' },
98
+ { key: 'n', label: 'New Task' },
99
+ { key: 'q', label: 'Quit' },
100
+ { key: 'r', label: 'Refresh' },
101
+ { key: 'Esc', label: 'Back' },
102
+ ]
103
+ : [
104
+ { key: 'j/k', label: 'Navigate' },
105
+ { key: 'Enter/l', label: 'Select' },
106
+ { key: '/', label: 'Search' },
107
+ { key: 'q', label: 'Quit' },
108
+ ...(screen === 'dashboard'
109
+ ? [
110
+ { key: 'n', label: 'New Project' },
111
+ { key: 'f', label: 'Project Info' },
112
+ { key: 'e', label: 'Edit Project' },
113
+ { key: 'd', label: 'Delete Project' },
114
+ { key: 'h', label: 'Back' },
115
+ ]
116
+ : []),
117
+ ...(screen === 'project-detail'
118
+ ? [
119
+ { key: 'e', label: 'Edit' },
120
+ { key: 's', label: 'Status' },
121
+ { key: 'r', label: 'Refresh' },
122
+ { key: 'Esc/h', label: 'Back' },
123
+ ]
124
+ : []),
125
+ ...(screen === 'project'
126
+ ? [
127
+ { key: 'n', label: 'New Feature' },
128
+ { key: 't', label: 'New Task' },
129
+ { key: 'f', label: 'Feature Detail' },
130
+ { key: 'v', label: 'Toggle View' },
131
+ { key: 'b', label: 'Board View' },
132
+ { key: 'r', label: 'Refresh' },
133
+ { key: 'h/Esc', label: 'Back' },
134
+ ]
135
+ : []),
136
+ ...(screen === 'kanban'
137
+ ? kanbanExpandedFeatureId
138
+ ? [
139
+ { key: 'j/k', label: 'Tasks' },
140
+ { key: 'Enter', label: 'Open Task' },
141
+ { key: 'Esc/h', label: 'Collapse' },
142
+ { key: 'r', label: 'Refresh' },
143
+ ]
144
+ : [
145
+ { key: 'h/l', label: 'Columns' },
146
+ { key: 'j/k', label: 'Features' },
147
+ { key: 'Enter', label: 'Expand' },
148
+ { key: 'm', label: 'Move Feature' },
149
+ { key: 'f', label: 'Filter' },
150
+ { key: 'b', label: 'Tree View' },
151
+ { key: 'Esc', label: 'Back' },
152
+ ]
153
+ : []),
154
+ ...(screen === 'task'
155
+ ? [
156
+ { key: 'Tab', label: 'Switch Panel' },
157
+ { key: 'r', label: 'Refresh' },
158
+ { key: 'Esc', label: 'Back' },
159
+ ]
160
+ : []),
161
+ ];
162
+
163
+ return (
164
+ <ThemeProvider>
165
+ <AdapterProvider adapter={adapter}>
166
+ <Box flexDirection="column" width="100%">
167
+ <Header breadcrumbs={breadcrumbs} />
168
+ <Box flexGrow={1} flexDirection="column">
169
+ {screen === 'dashboard' && (
170
+ <Dashboard
171
+ selectedIndex={dashboardSelectedIndex}
172
+ onSelectedIndexChange={setDashboardSelectedIndex}
173
+ onSelectProject={(id) => {
174
+ setProjectId(id);
175
+ setScreen('project');
176
+ }}
177
+ onViewProject={(id) => {
178
+ setProjectId(id);
179
+ setScreen('project-detail');
180
+ }}
181
+ onBack={() => {
182
+ setScreen('dashboard');
183
+ setProjectId(null);
184
+ setTaskId(null);
185
+ setFeatureId(null);
186
+ }}
187
+ />
188
+ )}
189
+ {screen === 'project-detail' && projectId && (
190
+ <ProjectDetail
191
+ projectId={projectId}
192
+ onSelectFeature={(id) => {
193
+ setFeatureId(id);
194
+ setScreen('feature');
195
+ }}
196
+ onBack={() => {
197
+ setScreen('dashboard');
198
+ setProjectId(null);
199
+ }}
200
+ />
201
+ )}
202
+ {screen === 'project' && projectId && (
203
+ <ProjectView
204
+ projectId={projectId}
205
+ expandedFeatures={projectExpandedFeatures}
206
+ onExpandedFeaturesChange={setProjectExpandedFeatures}
207
+ expandedGroups={projectExpandedGroups}
208
+ onExpandedGroupsChange={setProjectExpandedGroups}
209
+ selectedIndex={projectSelectedIndex}
210
+ onSelectedIndexChange={setProjectSelectedIndex}
211
+ viewMode={projectViewMode}
212
+ onViewModeChange={setProjectViewMode}
213
+ onSelectTask={(id) => {
214
+ setTaskOriginScreen('project');
215
+ setTaskId(id);
216
+ setScreen('task');
217
+ }}
218
+ onSelectFeature={(id) => {
219
+ setFeatureId(id);
220
+ setScreen('feature');
221
+ }}
222
+ onToggleBoard={() => {
223
+ setScreen('kanban');
224
+ }}
225
+ onBack={() => {
226
+ setScreen('dashboard');
227
+ setProjectId(null);
228
+ }}
229
+ />
230
+ )}
231
+ {screen === 'kanban' && projectId && (
232
+ <KanbanView
233
+ projectId={projectId}
234
+ activeColumnIndex={kanbanActiveColumnIndex}
235
+ onActiveColumnIndexChange={setKanbanActiveColumnIndex}
236
+ selectedFeatureIndex={kanbanSelectedFeatureIndex}
237
+ onSelectedFeatureIndexChange={setKanbanSelectedFeatureIndex}
238
+ expandedFeatureId={kanbanExpandedFeatureId}
239
+ onExpandedFeatureIdChange={setKanbanExpandedFeatureId}
240
+ selectedTaskIndex={kanbanSelectedTaskIndex}
241
+ onSelectedTaskIndexChange={setKanbanSelectedTaskIndex}
242
+ activeStatuses={kanbanActiveStatuses}
243
+ onActiveStatusesChange={handleKanbanActiveStatusesChange}
244
+ onSelectTask={(id) => {
245
+ setTaskOriginScreen('kanban');
246
+ setTaskId(id);
247
+ setScreen('task');
248
+ }}
249
+ onBack={() => {
250
+ setScreen('dashboard');
251
+ setProjectId(null);
252
+ }}
253
+ />
254
+ )}
255
+ {screen === 'task' && taskId && (
256
+ <TaskDetail
257
+ taskId={taskId}
258
+ onSelectTask={(id) => {
259
+ setTaskId(id);
260
+ // Stay on task screen, just change taskId
261
+ }}
262
+ onBack={() => {
263
+ setScreen(taskOriginScreen === 'kanban' ? 'kanban' : taskOriginScreen === 'feature' ? 'feature' : 'project');
264
+ setTaskId(null);
265
+ }}
266
+ />
267
+ )}
268
+ {screen === 'feature' && featureId && (
269
+ <FeatureDetail
270
+ featureId={featureId}
271
+ onSelectTask={(id) => {
272
+ setTaskOriginScreen('feature');
273
+ setTaskId(id);
274
+ setScreen('task');
275
+ }}
276
+ onBack={() => {
277
+ setScreen('project');
278
+ setFeatureId(null);
279
+ }}
280
+ />
281
+ )}
282
+ {screen === 'search' && (
283
+ <SearchScreen
284
+ onOpenProject={(id) => {
285
+ setProjectId(id);
286
+ setScreen('project');
287
+ }}
288
+ onOpenFeature={(id) => {
289
+ setFeatureId(id);
290
+ setScreen('feature');
291
+ }}
292
+ onOpenTask={(id) => {
293
+ setTaskOriginScreen('project');
294
+ setTaskId(id);
295
+ setScreen('task');
296
+ }}
297
+ onBack={() => {
298
+ setScreen(searchReturnScreen);
299
+ }}
300
+ />
301
+ )}
302
+ </Box>
303
+ <Footer shortcuts={shortcuts} />
304
+ </Box>
305
+ </AdapterProvider>
306
+ </ThemeProvider>
307
+ );
308
+ }
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../../ui/context/theme-context';
4
+ import { getStatusColor } from '../../ui/lib/colors';
5
+
6
+ interface ColumnFilterBarProps {
7
+ allStatuses: ReadonlyArray<{ id: string; title: string; status: string }>;
8
+ activeStatuses: Set<string>;
9
+ isFilterMode: boolean;
10
+ filterCursorIndex: number;
11
+ }
12
+
13
+ export function ColumnFilterBar({
14
+ allStatuses,
15
+ activeStatuses,
16
+ isFilterMode,
17
+ filterCursorIndex,
18
+ }: ColumnFilterBarProps) {
19
+ const { theme } = useTheme();
20
+
21
+ return (
22
+ <Box marginBottom={1} flexWrap="wrap">
23
+ {isFilterMode && (
24
+ <Text color={theme.colors.accent} bold>
25
+ {'FILTER: '}
26
+ </Text>
27
+ )}
28
+ {allStatuses.map((s, i) => {
29
+ const isActive = activeStatuses.has(s.status);
30
+ const isCursor = isFilterMode && i === filterCursorIndex;
31
+ const statusColor = getStatusColor(s.status, theme);
32
+
33
+ const label = isActive ? `[${s.title}]` : s.title;
34
+ const separator = i < allStatuses.length - 1 ? ' · ' : '';
35
+
36
+ return (
37
+ <Text key={s.id}>
38
+ <Text
39
+ color={isActive ? statusColor : theme.colors.muted}
40
+ bold={isCursor}
41
+ underline={isCursor}
42
+ dimColor={!isActive && !isCursor}
43
+ >
44
+ {label}
45
+ </Text>
46
+ {separator && <Text dimColor>{separator}</Text>}
47
+ </Text>
48
+ );
49
+ })}
50
+ </Box>
51
+ );
52
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useTheme } from '../../ui/context/theme-context';
4
+
5
+ interface ConfirmDialogProps {
6
+ title: string;
7
+ message: string;
8
+ confirmLabel?: string;
9
+ cancelLabel?: string;
10
+ onConfirm: () => void;
11
+ onCancel: () => void;
12
+ isActive?: boolean;
13
+ }
14
+
15
+ export function ConfirmDialog({
16
+ title,
17
+ message,
18
+ confirmLabel = 'Yes',
19
+ cancelLabel = 'No',
20
+ onConfirm,
21
+ onCancel,
22
+ isActive = true,
23
+ }: ConfirmDialogProps) {
24
+ const { theme } = useTheme();
25
+
26
+ useInput((input, key) => {
27
+ if (!isActive) return;
28
+ if (key.return || input.toLowerCase() === 'y') {
29
+ onConfirm();
30
+ return;
31
+ }
32
+ if (key.escape || input.toLowerCase() === 'n') {
33
+ onCancel();
34
+ }
35
+ }, { isActive });
36
+
37
+ return (
38
+ <Box flexDirection="column" borderStyle="round" borderColor={theme.colors.highlight} paddingX={1} paddingY={0} marginY={1}>
39
+ <Text bold>{title}</Text>
40
+ <Text>{message}</Text>
41
+ <Text dimColor>[Enter/Y] {confirmLabel} [Esc/N] {cancelLabel}</Text>
42
+ </Box>
43
+ );
44
+ }
45
+
@@ -0,0 +1,115 @@
1
+ import React, { useState } from 'react';
2
+ import { Box, Text, useInput } from 'ink';
3
+ import type { Task } from 'task-orchestrator-bun/src/domain/types';
4
+ import type { DependencyInfo } from '../../ui/lib/types';
5
+ import { StatusBadge } from './status-badge';
6
+ import { useTheme } from '../../ui/context/theme-context';
7
+
8
+ interface DependencyListProps {
9
+ dependencies: DependencyInfo | null;
10
+ onSelectTask?: (taskId: string) => void;
11
+ isActive?: boolean;
12
+ }
13
+
14
+ export function DependencyList({
15
+ dependencies,
16
+ onSelectTask,
17
+ isActive = true,
18
+ }: DependencyListProps) {
19
+ const { theme } = useTheme();
20
+ const [selectedIndex, setSelectedIndex] = useState(0);
21
+
22
+ const allTasks: Task[] = [
23
+ ...(dependencies?.blockedBy || []),
24
+ ...(dependencies?.blocks || []),
25
+ ];
26
+
27
+ const totalTasks = allTasks.length;
28
+ const hasBlockedBy = (dependencies?.blockedBy?.length || 0) > 0;
29
+ const hasBlocks = (dependencies?.blocks?.length || 0) > 0;
30
+
31
+ useInput((input, key) => {
32
+ if (!isActive || totalTasks === 0) return;
33
+
34
+ if (input === 'j' || key.downArrow) {
35
+ const nextIndex = (selectedIndex + 1) % totalTasks;
36
+ setSelectedIndex(nextIndex);
37
+ } else if (input === 'k' || key.upArrow) {
38
+ const prevIndex = (selectedIndex - 1 + totalTasks) % totalTasks;
39
+ setSelectedIndex(prevIndex);
40
+ } else if (key.return && onSelectTask) {
41
+ const selectedTask = allTasks[selectedIndex];
42
+ if (selectedTask) {
43
+ onSelectTask(selectedTask.id);
44
+ }
45
+ }
46
+ }, { isActive });
47
+
48
+ if (!dependencies || totalTasks === 0) {
49
+ return (
50
+ <Box flexDirection="column">
51
+ <Text dimColor>No dependencies</Text>
52
+ </Box>
53
+ );
54
+ }
55
+
56
+ let currentIndex = 0;
57
+
58
+ return (
59
+ <Box flexDirection="column">
60
+ {hasBlockedBy && (
61
+ <Box flexDirection="column" marginBottom={1}>
62
+ <Text bold color={theme.colors.warning}>
63
+ Blocked By:
64
+ </Text>
65
+ {dependencies.blockedBy.map((task) => {
66
+ const isSelected = currentIndex === selectedIndex;
67
+ currentIndex++;
68
+
69
+ return (
70
+ <Box key={task.id} marginLeft={2}>
71
+ <Text color={isSelected ? theme.colors.highlight : undefined}>
72
+ {isSelected ? '▎' : ' '}
73
+ </Text>
74
+ <Text bold={isSelected}>
75
+ ○{' '}
76
+ </Text>
77
+ <StatusBadge status={task.status} />
78
+ <Text bold={isSelected}>
79
+ {' '}{task.title}
80
+ </Text>
81
+ </Box>
82
+ );
83
+ })}
84
+ </Box>
85
+ )}
86
+
87
+ {hasBlocks && (
88
+ <Box flexDirection="column">
89
+ <Text bold color={theme.colors.accent}>
90
+ Blocks:
91
+ </Text>
92
+ {dependencies.blocks.map((task) => {
93
+ const isSelected = currentIndex === selectedIndex;
94
+ currentIndex++;
95
+
96
+ return (
97
+ <Box key={task.id} marginLeft={2}>
98
+ <Text color={isSelected ? theme.colors.highlight : undefined}>
99
+ {isSelected ? '▎' : ' '}
100
+ </Text>
101
+ <Text bold={isSelected}>
102
+ ○{' '}
103
+ </Text>
104
+ <StatusBadge status={task.status} />
105
+ <Text bold={isSelected}>
106
+ {' '}{task.title}
107
+ </Text>
108
+ </Box>
109
+ );
110
+ })}
111
+ </Box>
112
+ )}
113
+ </Box>
114
+ );
115
+ }
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../../ui/context/theme-context';
4
+
5
+ interface EmptyStateProps {
6
+ message: string;
7
+ hint?: string;
8
+ }
9
+
10
+ export function EmptyState({ message, hint }: EmptyStateProps) {
11
+ const { theme } = useTheme();
12
+
13
+ return (
14
+ <Box flexDirection="column" alignItems="center" paddingY={2}>
15
+ <Box>
16
+ <Text color={theme.colors.muted}>◇ </Text>
17
+ <Text color={theme.colors.muted}>{message}</Text>
18
+ {hint && (
19
+ <>
20
+ <Text color={theme.colors.muted}> · </Text>
21
+ <Text color={theme.colors.foreground}>{hint}</Text>
22
+ </>
23
+ )}
24
+ </Box>
25
+ </Box>
26
+ );
27
+ }
28
+