@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.
- package/README.md +78 -0
- package/package.json +54 -0
- package/src/tui/app.tsx +308 -0
- package/src/tui/components/column-filter-bar.tsx +52 -0
- package/src/tui/components/confirm-dialog.tsx +45 -0
- package/src/tui/components/dependency-list.tsx +115 -0
- package/src/tui/components/empty-state.tsx +28 -0
- package/src/tui/components/entity-table.tsx +120 -0
- package/src/tui/components/error-message.tsx +41 -0
- package/src/tui/components/feature-kanban-card.tsx +216 -0
- package/src/tui/components/footer.tsx +34 -0
- package/src/tui/components/form-dialog.tsx +338 -0
- package/src/tui/components/header.tsx +54 -0
- package/src/tui/components/index.ts +16 -0
- package/src/tui/components/kanban-board.tsx +335 -0
- package/src/tui/components/kanban-card.tsx +70 -0
- package/src/tui/components/kanban-column.tsx +173 -0
- package/src/tui/components/priority-badge.tsx +16 -0
- package/src/tui/components/section-list.tsx +96 -0
- package/src/tui/components/status-actions.tsx +87 -0
- package/src/tui/components/status-badge.tsx +22 -0
- package/src/tui/components/tree-view.tsx +295 -0
- package/src/tui/components/view-mode-chips.tsx +23 -0
- package/src/tui/index.tsx +33 -0
- package/src/tui/screens/dashboard.tsx +248 -0
- package/src/tui/screens/feature-detail.tsx +312 -0
- package/src/tui/screens/index.ts +6 -0
- package/src/tui/screens/kanban-view.tsx +251 -0
- package/src/tui/screens/project-detail.tsx +305 -0
- package/src/tui/screens/project-view.tsx +498 -0
- package/src/tui/screens/search.tsx +257 -0
- package/src/tui/screens/task-detail.tsx +294 -0
- package/src/ui/adapters/direct.ts +429 -0
- package/src/ui/adapters/index.ts +14 -0
- package/src/ui/adapters/types.ts +269 -0
- package/src/ui/context/adapter-context.tsx +31 -0
- package/src/ui/context/theme-context.tsx +43 -0
- package/src/ui/hooks/index.ts +20 -0
- package/src/ui/hooks/use-data.ts +919 -0
- package/src/ui/hooks/use-debounce.ts +37 -0
- package/src/ui/hooks/use-feature-kanban.ts +151 -0
- package/src/ui/hooks/use-kanban.ts +96 -0
- package/src/ui/hooks/use-navigation.tsx +94 -0
- package/src/ui/index.ts +73 -0
- package/src/ui/lib/colors.ts +79 -0
- package/src/ui/lib/format.ts +114 -0
- package/src/ui/lib/types.ts +157 -0
- package/src/ui/themes/dark.ts +63 -0
- package/src/ui/themes/light.ts +63 -0
- 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
|
+
}
|
package/src/tui/app.tsx
ADDED
|
@@ -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
|
+
|