@colmbus72/yeehaw 0.4.2 → 0.6.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/claude-plugin/.claude-plugin/plugin.json +2 -1
- package/claude-plugin/hooks/hooks.json +41 -0
- package/claude-plugin/hooks/session-status.sh +13 -0
- package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
- package/dist/app.js +228 -28
- package/dist/components/CritterHeader.d.ts +7 -0
- package/dist/components/CritterHeader.js +81 -0
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +10 -1
- package/dist/components/List.js +14 -5
- package/dist/components/Panel.js +27 -1
- package/dist/components/SplashScreen.js +1 -1
- package/dist/hooks/useSessions.js +2 -2
- package/dist/index.js +41 -1
- package/dist/lib/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.js +3 -0
- package/dist/lib/auth/linear.d.ts +20 -0
- package/dist/lib/auth/linear.js +79 -0
- package/dist/lib/auth/storage.d.ts +12 -0
- package/dist/lib/auth/storage.js +53 -0
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/context.d.ts +10 -0
- package/dist/lib/context.js +63 -0
- package/dist/lib/critters.d.ts +61 -0
- package/dist/lib/critters.js +365 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.d.ts +1 -1
- package/dist/lib/hotkeys.js +28 -20
- package/dist/lib/issues/github.d.ts +11 -0
- package/dist/lib/issues/github.js +154 -0
- package/dist/lib/issues/index.d.ts +14 -0
- package/dist/lib/issues/index.js +27 -0
- package/dist/lib/issues/linear.d.ts +24 -0
- package/dist/lib/issues/linear.js +345 -0
- package/dist/lib/issues/types.d.ts +82 -0
- package/dist/lib/issues/types.js +2 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +9 -2
- package/dist/lib/tmux.js +114 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +23 -2
- package/dist/views/BarnContext.d.ts +5 -2
- package/dist/views/BarnContext.js +202 -21
- package/dist/views/CritterDetailView.d.ts +10 -0
- package/dist/views/CritterDetailView.js +117 -0
- package/dist/views/CritterLogsView.d.ts +8 -0
- package/dist/views/CritterLogsView.js +100 -0
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/IssuesView.d.ts +2 -1
- package/dist/views/IssuesView.js +661 -98
- package/dist/views/LivestockDetailView.d.ts +2 -1
- package/dist/views/LivestockDetailView.js +19 -8
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +68 -25
- package/package.json +5 -5
package/dist/views/IssuesView.js
CHANGED
|
@@ -1,135 +1,558 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
// src/views/IssuesView.tsx
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
3
4
|
import { Box, Text, useInput } from 'ink';
|
|
5
|
+
import TextInput from 'ink-text-input';
|
|
4
6
|
import { Header } from '../components/Header.js';
|
|
5
7
|
import { Panel } from '../components/Panel.js';
|
|
6
8
|
import { List } from '../components/List.js';
|
|
7
9
|
import { ScrollableMarkdown } from '../components/ScrollableMarkdown.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
+
import { getProvider, } from '../lib/issues/index.js';
|
|
11
|
+
import { saveProject } from '../lib/config.js';
|
|
12
|
+
import { buildProjectContext } from '../lib/context.js';
|
|
13
|
+
import { openIssueInBrowser } from '../lib/github.js';
|
|
14
|
+
import { saveLinearApiKey, validateLinearApiKey, LINEAR_API_KEY_URL, clearLinearToken } from '../lib/auth/linear.js';
|
|
15
|
+
// Status indicator based on Linear state type
|
|
16
|
+
function getStatusIndicator(stateType) {
|
|
17
|
+
switch (stateType) {
|
|
18
|
+
case 'backlog':
|
|
19
|
+
case 'triage':
|
|
20
|
+
return { char: '░', color: 'gray' };
|
|
21
|
+
case 'unstarted':
|
|
22
|
+
return { char: '░', color: 'gray' };
|
|
23
|
+
case 'started':
|
|
24
|
+
return { char: '▒', color: 'yellow' };
|
|
25
|
+
case 'completed':
|
|
26
|
+
return { char: '█', color: 'blue' };
|
|
27
|
+
case 'canceled':
|
|
28
|
+
return { char: '░', color: 'red' };
|
|
29
|
+
default:
|
|
30
|
+
return { char: ' ', color: 'gray' };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Priority indicator: _ . : ! for low/med/high/urgent
|
|
34
|
+
function getPriorityIndicator(priority) {
|
|
35
|
+
switch (priority) {
|
|
36
|
+
case 1: return '!'; // urgent
|
|
37
|
+
case 2: return ':'; // high
|
|
38
|
+
case 3: return '.'; // medium
|
|
39
|
+
case 4: return '_'; // low
|
|
40
|
+
default: return ' '; // no priority
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Get initials from name (first letter of first and last name)
|
|
44
|
+
function getInitials(name) {
|
|
45
|
+
if (!name)
|
|
46
|
+
return ' ';
|
|
47
|
+
const parts = name.trim().split(/\s+/);
|
|
48
|
+
if (parts.length === 1) {
|
|
49
|
+
return parts[0].substring(0, 2).toUpperCase();
|
|
50
|
+
}
|
|
51
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
52
|
+
}
|
|
53
|
+
export function IssuesView({ project, onBack, onOpenClaude }) {
|
|
10
54
|
const [focusedPanel, setFocusedPanel] = useState('list');
|
|
11
55
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
-
const [
|
|
13
|
-
const [
|
|
14
|
-
const [
|
|
15
|
-
//
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
56
|
+
const [viewState, setViewState] = useState({ type: 'loading' });
|
|
57
|
+
const [linearProvider, setLinearProvider] = useState(null);
|
|
58
|
+
const [apiKeyInput, setApiKeyInput] = useState('');
|
|
59
|
+
// Filter state for Linear
|
|
60
|
+
const [cycles, setCycles] = useState([]);
|
|
61
|
+
const [assignees, setAssignees] = useState([]);
|
|
62
|
+
const [currentFilter, setCurrentFilter] = useState({
|
|
63
|
+
sortBy: 'priority',
|
|
64
|
+
sortDirection: 'desc',
|
|
65
|
+
});
|
|
66
|
+
const [filterInitialized, setFilterInitialized] = useState(false);
|
|
67
|
+
// Filter UI state
|
|
68
|
+
const [filterField, setFilterField] = useState('assignee');
|
|
69
|
+
const [filterSelectedIndex, setFilterSelectedIndex] = useState(0);
|
|
70
|
+
// Team name for display
|
|
71
|
+
const [teamName, setTeamName] = useState();
|
|
72
|
+
// Cached current user ID for filter
|
|
73
|
+
const [currentUserId, setCurrentUserId] = useState(null);
|
|
74
|
+
const loadIssues = useCallback(async (filter) => {
|
|
75
|
+
setViewState({ type: 'loading' });
|
|
76
|
+
const provider = getProvider(project);
|
|
77
|
+
if (!provider) {
|
|
78
|
+
setViewState({ type: 'disabled' });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Check authentication
|
|
82
|
+
const isAuthed = await provider.isAuthenticated();
|
|
83
|
+
if (!isAuthed) {
|
|
84
|
+
setViewState({ type: 'not-authenticated', providerType: provider.type });
|
|
85
|
+
if (provider.type === 'linear') {
|
|
86
|
+
setLinearProvider(provider);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// For Linear, check if team selection is needed
|
|
91
|
+
if (provider.type === 'linear') {
|
|
92
|
+
const linearProv = provider;
|
|
93
|
+
setLinearProvider(linearProv);
|
|
94
|
+
if (linearProv.needsTeamSelection()) {
|
|
95
|
+
try {
|
|
96
|
+
const teams = await linearProv.fetchTeams();
|
|
97
|
+
setViewState({ type: 'select-team', teams });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
setViewState({ type: 'error', message: `Failed to fetch teams: ${err}` });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Set team name from provider (loaded from config)
|
|
106
|
+
setTeamName(linearProv.getTeamName());
|
|
107
|
+
// Initialize default filter (current cycle, assigned to me)
|
|
108
|
+
if (!filterInitialized) {
|
|
109
|
+
try {
|
|
110
|
+
// Fetch cycles, assignees, and user ID in parallel
|
|
111
|
+
const [fetchedCycles, fetchedAssignees, userId] = await Promise.all([
|
|
112
|
+
linearProv.fetchCycles(),
|
|
113
|
+
linearProv.fetchAssignees(),
|
|
114
|
+
linearProv.getCurrentUserId(),
|
|
115
|
+
]);
|
|
116
|
+
setCycles(fetchedCycles);
|
|
117
|
+
setAssignees(fetchedAssignees);
|
|
118
|
+
setCurrentUserId(userId);
|
|
119
|
+
// Set default filter: current cycle, assigned to me
|
|
120
|
+
const activeCycleId = linearProv.getActiveCycleId();
|
|
121
|
+
const defaultFilter = {
|
|
122
|
+
cycleId: activeCycleId,
|
|
123
|
+
assigneeId: userId ?? undefined,
|
|
124
|
+
sortBy: 'priority',
|
|
125
|
+
sortDirection: 'desc',
|
|
126
|
+
};
|
|
127
|
+
setCurrentFilter(defaultFilter);
|
|
128
|
+
setFilterInitialized(true);
|
|
129
|
+
filter = defaultFilter;
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// Continue without filter if fetch fails
|
|
133
|
+
setFilterInitialized(true);
|
|
30
134
|
}
|
|
31
135
|
}
|
|
32
136
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
137
|
+
// Fetch issues
|
|
138
|
+
try {
|
|
139
|
+
const issues = await provider.fetchIssues({
|
|
140
|
+
linearFilter: filter ?? currentFilter,
|
|
141
|
+
});
|
|
142
|
+
setViewState({ type: 'ready', issues });
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
setViewState({ type: 'error', message: `Failed to fetch issues: ${err}` });
|
|
146
|
+
}
|
|
147
|
+
}, [project, currentFilter, filterInitialized]);
|
|
36
148
|
useEffect(() => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
149
|
+
loadIssues();
|
|
150
|
+
}, []); // Only run once on mount
|
|
151
|
+
const selectedIssue = viewState.type === 'ready' ? viewState.issues[selectedIndex] : null;
|
|
152
|
+
// Handle API key submission
|
|
153
|
+
const handleApiKeySubmit = async () => {
|
|
154
|
+
if (!apiKeyInput.trim())
|
|
155
|
+
return;
|
|
156
|
+
setViewState({ type: 'linear-auth-validating' });
|
|
157
|
+
const isValid = await validateLinearApiKey(apiKeyInput.trim());
|
|
158
|
+
if (isValid) {
|
|
159
|
+
saveLinearApiKey(apiKeyInput.trim());
|
|
160
|
+
setApiKeyInput('');
|
|
161
|
+
loadIssues();
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
setViewState({ type: 'error', message: 'Invalid API key. Please check and try again.' });
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Handle team selection
|
|
168
|
+
const selectTeam = async (team) => {
|
|
169
|
+
if (!linearProvider)
|
|
170
|
+
return;
|
|
171
|
+
linearProvider.setTeamId(team.id);
|
|
172
|
+
linearProvider.setTeamName(team.name);
|
|
173
|
+
setTeamName(team.name);
|
|
174
|
+
// Save team ID and name to project config
|
|
175
|
+
const updatedProject = {
|
|
176
|
+
...project,
|
|
177
|
+
issueProvider: { type: 'linear', teamId: team.id, teamName: team.name },
|
|
178
|
+
};
|
|
179
|
+
saveProject(updatedProject);
|
|
180
|
+
// Continue with existing provider (don't call loadIssues which would create new provider from stale prop)
|
|
181
|
+
setViewState({ type: 'loading' });
|
|
182
|
+
try {
|
|
183
|
+
// Fetch cycles, assignees, and user ID
|
|
184
|
+
const [fetchedCycles, fetchedAssignees, userId] = await Promise.all([
|
|
185
|
+
linearProvider.fetchCycles(),
|
|
186
|
+
linearProvider.fetchAssignees(),
|
|
187
|
+
linearProvider.getCurrentUserId(),
|
|
188
|
+
]);
|
|
189
|
+
setCycles(fetchedCycles);
|
|
190
|
+
setAssignees(fetchedAssignees);
|
|
191
|
+
setCurrentUserId(userId);
|
|
192
|
+
// Set default filter
|
|
193
|
+
const activeCycleId = linearProvider.getActiveCycleId();
|
|
194
|
+
const defaultFilter = {
|
|
195
|
+
cycleId: activeCycleId,
|
|
196
|
+
assigneeId: userId ?? undefined,
|
|
197
|
+
sortBy: 'priority',
|
|
198
|
+
sortDirection: 'desc',
|
|
199
|
+
};
|
|
200
|
+
setCurrentFilter(defaultFilter);
|
|
201
|
+
setFilterInitialized(true);
|
|
202
|
+
// Fetch issues
|
|
203
|
+
const issues = await linearProvider.fetchIssues({ linearFilter: defaultFilter });
|
|
204
|
+
setViewState({ type: 'ready', issues });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
setViewState({ type: 'error', message: `Failed to load issues: ${err}` });
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
// Build Claude context for an issue (includes project context)
|
|
211
|
+
const buildClaudeContext = (issue) => {
|
|
212
|
+
const lines = [];
|
|
213
|
+
// Include project context first (wiki sections, summary, etc.)
|
|
214
|
+
const projectContext = buildProjectContext(project.name);
|
|
215
|
+
if (projectContext) {
|
|
216
|
+
lines.push(projectContext);
|
|
217
|
+
lines.push('');
|
|
218
|
+
lines.push('---');
|
|
219
|
+
lines.push('');
|
|
220
|
+
}
|
|
221
|
+
lines.push('You are working on the following issue:');
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push(`Title: ${issue.title}`);
|
|
224
|
+
lines.push(`Identifier: ${issue.identifier}`);
|
|
225
|
+
lines.push(`State: ${issue.state}`);
|
|
226
|
+
lines.push(`Author: ${issue.author}`);
|
|
227
|
+
lines.push(`URL: ${issue.url}`);
|
|
228
|
+
if (issue.priority !== undefined && issue.priority > 0) {
|
|
229
|
+
const priorityNames = ['', 'Urgent', 'High', 'Medium', 'Low'];
|
|
230
|
+
lines.push(`Priority: ${priorityNames[issue.priority]}`);
|
|
231
|
+
}
|
|
232
|
+
if (issue.assignee) {
|
|
233
|
+
lines.push(`Assignee: ${issue.assignee.name}`);
|
|
234
|
+
}
|
|
235
|
+
if (issue.estimate !== undefined) {
|
|
236
|
+
lines.push(`Points: ${issue.estimate}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push('Description:');
|
|
240
|
+
lines.push(issue.body || '(No description provided)');
|
|
241
|
+
lines.push('');
|
|
242
|
+
if (issue.labels.length > 0) {
|
|
243
|
+
lines.push(`Labels: ${issue.labels.join(', ')}`);
|
|
244
|
+
lines.push('');
|
|
245
|
+
}
|
|
246
|
+
if (issue.comments.length > 0) {
|
|
247
|
+
lines.push('---');
|
|
248
|
+
lines.push('Comments:');
|
|
249
|
+
lines.push('');
|
|
250
|
+
for (const comment of issue.comments) {
|
|
251
|
+
const date = new Date(comment.createdAt).toLocaleDateString();
|
|
252
|
+
lines.push(`${comment.author} (${date}):`);
|
|
253
|
+
lines.push(comment.body);
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push('---');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return lines.join('\n');
|
|
259
|
+
};
|
|
260
|
+
// Apply filter and reload
|
|
261
|
+
const applyFilter = (newFilter) => {
|
|
262
|
+
setCurrentFilter(newFilter);
|
|
263
|
+
setViewState({ type: 'loading' });
|
|
264
|
+
loadIssues(newFilter);
|
|
265
|
+
};
|
|
266
|
+
// Handle input
|
|
267
|
+
useInput((input, key) => {
|
|
268
|
+
// Don't intercept input during text input mode
|
|
269
|
+
if (viewState.type === 'linear-auth-input') {
|
|
270
|
+
if (key.escape) {
|
|
271
|
+
setApiKeyInput('');
|
|
272
|
+
setViewState({ type: 'not-authenticated', providerType: 'linear' });
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Filter view navigation
|
|
277
|
+
if (viewState.type === 'filter') {
|
|
278
|
+
if (key.escape) {
|
|
279
|
+
loadIssues(currentFilter);
|
|
43
280
|
return;
|
|
44
281
|
}
|
|
45
|
-
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
|
|
282
|
+
// Tab: select current option AND move to next field
|
|
283
|
+
if (key.tab) {
|
|
284
|
+
// First, select the current option (without applying yet)
|
|
285
|
+
selectFilterOptionLocal(filterField, filterSelectedIndex);
|
|
286
|
+
// Then move to next field
|
|
287
|
+
const fields = ['assignee', 'cycle', 'status', 'sort'];
|
|
288
|
+
const currentIndex = fields.indexOf(filterField);
|
|
289
|
+
setFilterField(fields[(currentIndex + 1) % fields.length]);
|
|
290
|
+
setFilterSelectedIndex(0);
|
|
49
291
|
return;
|
|
50
292
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
...issue,
|
|
57
|
-
repoName: repo.name,
|
|
58
|
-
fullRepo: `${repo.owner}/${repo.repo}`,
|
|
59
|
-
});
|
|
60
|
-
}
|
|
293
|
+
// Navigate options with j/k
|
|
294
|
+
if (input === 'j' || key.downArrow) {
|
|
295
|
+
const options = getFilterOptions(filterField);
|
|
296
|
+
setFilterSelectedIndex((prev) => Math.min(prev + 1, options.length - 1));
|
|
297
|
+
return;
|
|
61
298
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
299
|
+
if (input === 'k' || key.upArrow) {
|
|
300
|
+
setFilterSelectedIndex((prev) => Math.max(prev - 1, 0));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Enter: select current option, apply filter, and exit
|
|
304
|
+
if (key.return) {
|
|
305
|
+
const newFilter = selectFilterOptionLocal(filterField, filterSelectedIndex);
|
|
306
|
+
applyFilter(newFilter);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
72
311
|
if (key.escape) {
|
|
73
312
|
onBack();
|
|
74
313
|
return;
|
|
75
314
|
}
|
|
76
|
-
//
|
|
315
|
+
// Handle auth prompt - press Enter to start API key input
|
|
316
|
+
if (viewState.type === 'not-authenticated' && viewState.providerType === 'linear') {
|
|
317
|
+
if (key.return) {
|
|
318
|
+
setViewState({ type: 'linear-auth-input' });
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Handle error state - 'r' to retry, 'a' to re-authenticate
|
|
323
|
+
if (viewState.type === 'error') {
|
|
324
|
+
if (input === 'r') {
|
|
325
|
+
loadIssues();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (input === 'a') {
|
|
329
|
+
clearLinearToken();
|
|
330
|
+
setViewState({ type: 'linear-auth-input' });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// In ready state
|
|
336
|
+
if (viewState.type !== 'ready')
|
|
337
|
+
return;
|
|
338
|
+
// Tab to switch focus
|
|
77
339
|
if (key.tab) {
|
|
78
340
|
setFocusedPanel((prev) => (prev === 'list' ? 'details' : 'list'));
|
|
79
341
|
return;
|
|
80
342
|
}
|
|
81
343
|
// Refresh issues
|
|
82
344
|
if (input === 'r') {
|
|
83
|
-
|
|
84
|
-
setError(null);
|
|
85
|
-
// Trigger re-fetch by updating a dependency
|
|
86
|
-
const repos = getLocalRepos();
|
|
87
|
-
const allIssues = [];
|
|
88
|
-
for (const repo of repos) {
|
|
89
|
-
const repoIssues = fetchGitHubIssues(repo.owner, repo.repo);
|
|
90
|
-
for (const issue of repoIssues) {
|
|
91
|
-
allIssues.push({
|
|
92
|
-
...issue,
|
|
93
|
-
repoName: repo.name,
|
|
94
|
-
fullRepo: `${repo.owner}/${repo.repo}`,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
allIssues.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
99
|
-
setIssues(allIssues);
|
|
100
|
-
setSelectedIndex(0);
|
|
101
|
-
setLoading(false);
|
|
345
|
+
loadIssues(currentFilter);
|
|
102
346
|
return;
|
|
103
347
|
}
|
|
104
|
-
// Open
|
|
105
|
-
if (input === '
|
|
106
|
-
|
|
348
|
+
// Open filter (Linear only)
|
|
349
|
+
if (input === 'f' && project.issueProvider?.type === 'linear') {
|
|
350
|
+
setViewState({ type: 'filter' });
|
|
351
|
+
setFilterField('assignee');
|
|
352
|
+
setFilterSelectedIndex(0);
|
|
107
353
|
return;
|
|
108
354
|
}
|
|
109
|
-
// List navigation is handled by the List component via controlled selection
|
|
110
355
|
});
|
|
111
|
-
//
|
|
356
|
+
// Get filter options for current field
|
|
357
|
+
const getFilterOptions = (field) => {
|
|
358
|
+
switch (field) {
|
|
359
|
+
case 'assignee':
|
|
360
|
+
return [
|
|
361
|
+
{ id: '__me__', label: 'Assigned to me' },
|
|
362
|
+
{ id: '__any__', label: 'Anyone' },
|
|
363
|
+
{ id: '__none__', label: 'Unassigned' },
|
|
364
|
+
...assignees.map((a) => ({ id: a.id, label: a.name })),
|
|
365
|
+
];
|
|
366
|
+
case 'cycle':
|
|
367
|
+
return [
|
|
368
|
+
{ id: '__any__', label: 'Any cycle' },
|
|
369
|
+
...cycles.map((c) => ({ id: c.id, label: c.name })),
|
|
370
|
+
];
|
|
371
|
+
case 'status':
|
|
372
|
+
return [
|
|
373
|
+
{ id: '__open__', label: 'Open (backlog, unstarted, started)' },
|
|
374
|
+
{ id: '__all__', label: 'All statuses' },
|
|
375
|
+
{ id: 'backlog', label: 'Backlog' },
|
|
376
|
+
{ id: 'unstarted', label: 'Todo' },
|
|
377
|
+
{ id: 'started', label: 'In Progress' },
|
|
378
|
+
{ id: 'completed', label: 'Done' },
|
|
379
|
+
{ id: 'canceled', label: 'Canceled' },
|
|
380
|
+
];
|
|
381
|
+
case 'sort':
|
|
382
|
+
return [
|
|
383
|
+
{ id: 'priority_desc', label: 'Priority (high first)' },
|
|
384
|
+
{ id: 'priority_asc', label: 'Priority (low first)' },
|
|
385
|
+
{ id: 'updatedAt', label: 'Recently updated' },
|
|
386
|
+
{ id: 'createdAt', label: 'Recently created' },
|
|
387
|
+
];
|
|
388
|
+
default:
|
|
389
|
+
return [];
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
// Select a filter option locally (without fetching) - used by Tab
|
|
393
|
+
// Returns the new filter for immediate use (since setState is async)
|
|
394
|
+
const selectFilterOptionLocal = (field, index) => {
|
|
395
|
+
const options = getFilterOptions(field);
|
|
396
|
+
const option = options[index];
|
|
397
|
+
if (!option)
|
|
398
|
+
return currentFilter;
|
|
399
|
+
const newFilter = { ...currentFilter };
|
|
400
|
+
switch (field) {
|
|
401
|
+
case 'assignee':
|
|
402
|
+
if (option.id === '__me__') {
|
|
403
|
+
newFilter.assigneeId = currentUserId ?? undefined;
|
|
404
|
+
}
|
|
405
|
+
else if (option.id === '__any__') {
|
|
406
|
+
delete newFilter.assigneeId;
|
|
407
|
+
}
|
|
408
|
+
else if (option.id === '__none__') {
|
|
409
|
+
newFilter.assigneeId = null;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
newFilter.assigneeId = option.id;
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
case 'cycle':
|
|
416
|
+
if (option.id === '__any__') {
|
|
417
|
+
delete newFilter.cycleId;
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
newFilter.cycleId = option.id;
|
|
421
|
+
}
|
|
422
|
+
break;
|
|
423
|
+
case 'status':
|
|
424
|
+
if (option.id === '__open__') {
|
|
425
|
+
newFilter.stateType = ['backlog', 'unstarted', 'started'];
|
|
426
|
+
}
|
|
427
|
+
else if (option.id === '__all__') {
|
|
428
|
+
delete newFilter.stateType;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
newFilter.stateType = option.id;
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
case 'sort':
|
|
435
|
+
if (option.id === 'priority_desc') {
|
|
436
|
+
newFilter.sortBy = 'priority';
|
|
437
|
+
newFilter.sortDirection = 'desc';
|
|
438
|
+
}
|
|
439
|
+
else if (option.id === 'priority_asc') {
|
|
440
|
+
newFilter.sortBy = 'priority';
|
|
441
|
+
newFilter.sortDirection = 'asc';
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
newFilter.sortBy = option.id;
|
|
445
|
+
delete newFilter.sortDirection;
|
|
446
|
+
}
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
setCurrentFilter(newFilter);
|
|
450
|
+
return newFilter;
|
|
451
|
+
};
|
|
452
|
+
// Select a filter option and apply (fetch) - used by Enter
|
|
453
|
+
const selectFilterOption = async (field, index) => {
|
|
454
|
+
const options = getFilterOptions(field);
|
|
455
|
+
const option = options[index];
|
|
456
|
+
if (!option)
|
|
457
|
+
return;
|
|
458
|
+
const newFilter = { ...currentFilter };
|
|
459
|
+
switch (field) {
|
|
460
|
+
case 'assignee':
|
|
461
|
+
if (option.id === '__me__') {
|
|
462
|
+
const userId = await linearProvider?.getCurrentUserId();
|
|
463
|
+
newFilter.assigneeId = userId ?? undefined;
|
|
464
|
+
}
|
|
465
|
+
else if (option.id === '__any__') {
|
|
466
|
+
delete newFilter.assigneeId;
|
|
467
|
+
}
|
|
468
|
+
else if (option.id === '__none__') {
|
|
469
|
+
newFilter.assigneeId = null;
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
newFilter.assigneeId = option.id;
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
case 'cycle':
|
|
476
|
+
if (option.id === '__any__') {
|
|
477
|
+
delete newFilter.cycleId;
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
newFilter.cycleId = option.id;
|
|
481
|
+
}
|
|
482
|
+
break;
|
|
483
|
+
case 'status':
|
|
484
|
+
if (option.id === '__open__') {
|
|
485
|
+
newFilter.stateType = ['backlog', 'unstarted', 'started'];
|
|
486
|
+
}
|
|
487
|
+
else if (option.id === '__all__') {
|
|
488
|
+
delete newFilter.stateType;
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
newFilter.stateType = option.id;
|
|
492
|
+
}
|
|
493
|
+
break;
|
|
494
|
+
case 'sort':
|
|
495
|
+
if (option.id === 'priority_desc') {
|
|
496
|
+
newFilter.sortBy = 'priority';
|
|
497
|
+
newFilter.sortDirection = 'desc';
|
|
498
|
+
}
|
|
499
|
+
else if (option.id === 'priority_asc') {
|
|
500
|
+
newFilter.sortBy = 'priority';
|
|
501
|
+
newFilter.sortDirection = 'asc';
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
newFilter.sortBy = option.id;
|
|
505
|
+
delete newFilter.sortDirection;
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
applyFilter(newFilter);
|
|
510
|
+
};
|
|
511
|
+
// Handle opening Claude for an issue (called from row action)
|
|
512
|
+
const handleOpenClaude = (issue) => {
|
|
513
|
+
if (!onOpenClaude)
|
|
514
|
+
return;
|
|
515
|
+
let workingDir;
|
|
516
|
+
if (issue.source.type === 'github') {
|
|
517
|
+
workingDir = issue.source.livestockPath || project.path;
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
workingDir = project.path;
|
|
521
|
+
}
|
|
522
|
+
const context = buildClaudeContext(issue);
|
|
523
|
+
onOpenClaude(workingDir, context);
|
|
524
|
+
};
|
|
112
525
|
const truncate = (str, maxLen) => {
|
|
113
526
|
if (str.length <= maxLen)
|
|
114
527
|
return str;
|
|
115
528
|
return str.slice(0, maxLen - 1) + '…';
|
|
116
529
|
};
|
|
117
|
-
// Build
|
|
118
|
-
const issueItems = issues.map((issue, i) => ({
|
|
119
|
-
id: String(i),
|
|
120
|
-
label: `[${issue.repoName}] #${issue.number} ${truncate(issue.title, 35)}`,
|
|
121
|
-
status: issue.state === 'open' ? 'active' : 'inactive',
|
|
122
|
-
}));
|
|
123
|
-
// Build issue details markdown
|
|
530
|
+
// Build issue details with better layout
|
|
124
531
|
const buildIssueDetails = (issue) => {
|
|
125
532
|
const lines = [];
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
533
|
+
// Metadata grid
|
|
534
|
+
if (issue.source.type === 'github') {
|
|
535
|
+
lines.push(`**Repo:** ${issue.source.repo} • **Livestock:** ${issue.source.livestockName}`);
|
|
536
|
+
}
|
|
537
|
+
lines.push(`**State:** ${issue.state} • **Author:** ${issue.author}`);
|
|
538
|
+
if (issue.priority !== undefined && issue.priority > 0) {
|
|
539
|
+
const priorityNames = ['', 'Urgent', 'High', 'Medium', 'Low'];
|
|
540
|
+
const priorityLine = `**Priority:** ${priorityNames[issue.priority]}`;
|
|
541
|
+
const pointsLine = issue.estimate !== undefined ? ` • **Points:** ${issue.estimate}` : '';
|
|
542
|
+
lines.push(priorityLine + pointsLine);
|
|
543
|
+
}
|
|
544
|
+
else if (issue.estimate !== undefined) {
|
|
545
|
+
lines.push(`**Points:** ${issue.estimate}`);
|
|
546
|
+
}
|
|
547
|
+
if (issue.assignee) {
|
|
548
|
+
lines.push(`**Assignee:** ${issue.assignee.name}`);
|
|
549
|
+
}
|
|
550
|
+
if (issue.cycle) {
|
|
551
|
+
lines.push(`**Cycle:** ${issue.cycle.name}`);
|
|
552
|
+
}
|
|
129
553
|
if (issue.labels.length > 0) {
|
|
130
554
|
lines.push(`**Labels:** ${issue.labels.join(', ')}`);
|
|
131
555
|
}
|
|
132
|
-
lines.push(`**Comments:** ${issue.commentsCount}`);
|
|
133
556
|
lines.push(`**Updated:** ${new Date(issue.updatedAt).toLocaleDateString()}`);
|
|
134
557
|
lines.push('');
|
|
135
558
|
lines.push('---');
|
|
@@ -140,18 +563,158 @@ export function IssuesView({ project, onBack }) {
|
|
|
140
563
|
else {
|
|
141
564
|
lines.push('*No description provided.*');
|
|
142
565
|
}
|
|
566
|
+
// Add comments section
|
|
567
|
+
if (issue.comments.length > 0) {
|
|
568
|
+
lines.push('');
|
|
569
|
+
lines.push('---');
|
|
570
|
+
lines.push('');
|
|
571
|
+
lines.push(`**Comments (${issue.comments.length}):**`);
|
|
572
|
+
lines.push('');
|
|
573
|
+
for (const comment of issue.comments) {
|
|
574
|
+
const date = new Date(comment.createdAt).toLocaleDateString();
|
|
575
|
+
lines.push(`**${comment.author}** - ${date}`);
|
|
576
|
+
lines.push('');
|
|
577
|
+
lines.push(comment.body);
|
|
578
|
+
lines.push('');
|
|
579
|
+
lines.push('---');
|
|
580
|
+
lines.push('');
|
|
581
|
+
}
|
|
582
|
+
}
|
|
143
583
|
return lines.join('\n');
|
|
144
584
|
};
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
|
|
585
|
+
// Get subtitle for header
|
|
586
|
+
const getSubtitle = () => {
|
|
587
|
+
if (project.issueProvider?.type === 'linear' && teamName) {
|
|
588
|
+
return `Issues • ${teamName}`;
|
|
589
|
+
}
|
|
590
|
+
return 'Issues';
|
|
591
|
+
};
|
|
148
592
|
// Loading state
|
|
149
|
-
if (loading) {
|
|
150
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle:
|
|
593
|
+
if (viewState.type === 'loading') {
|
|
594
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: getSubtitle(), color: project.color }), _jsx(Box, { padding: 2, children: _jsx(Text, { children: "Loading issues..." }) })] }));
|
|
595
|
+
}
|
|
596
|
+
// Disabled state
|
|
597
|
+
if (viewState.type === 'disabled') {
|
|
598
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsxs(Box, { padding: 2, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Issue tracking is disabled for this project." }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press 'e' in the project view to enable it." }) })] })] }));
|
|
599
|
+
}
|
|
600
|
+
// Not authenticated - GitHub
|
|
601
|
+
if (viewState.type === 'not-authenticated' && viewState.providerType === 'github') {
|
|
602
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsxs(Box, { padding: 2, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "GitHub CLI not authenticated" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Run this command in your terminal:" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "cyan", children: " gh auth login" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Then return here and the issues will load." }) })] })] }));
|
|
603
|
+
}
|
|
604
|
+
// Not authenticated - Linear (prompt to enter key)
|
|
605
|
+
if (viewState.type === 'not-authenticated' && viewState.providerType === 'linear') {
|
|
606
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsxs(Box, { padding: 2, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Linear authentication required" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "You need a Personal API Key from Linear." }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Create one at: ", _jsx(Text, { color: "cyan", children: LINEAR_API_KEY_URL })] }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to paste your API key" }) })] })] }));
|
|
607
|
+
}
|
|
608
|
+
// Linear API key input
|
|
609
|
+
if (viewState.type === 'linear-auth-input') {
|
|
610
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsxs(Box, { padding: 2, flexDirection: "column", children: [_jsx(Text, { color: "yellow", children: "Enter Linear API Key" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Paste your Personal API Key from Linear:" }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "API Key: " }), _jsx(TextInput, { value: apiKeyInput, onChange: setApiKeyInput, onSubmit: handleApiKeySubmit, mask: "*" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: submit, Esc: cancel" }) })] })] }));
|
|
611
|
+
}
|
|
612
|
+
// Linear validating
|
|
613
|
+
if (viewState.type === 'linear-auth-validating') {
|
|
614
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsx(Box, { padding: 2, flexDirection: "column", children: _jsx(Text, { children: "Validating API key..." }) })] }));
|
|
615
|
+
}
|
|
616
|
+
// Team selection
|
|
617
|
+
if (viewState.type === 'select-team') {
|
|
618
|
+
const teamItems = viewState.teams.map((team) => ({
|
|
619
|
+
id: team.id,
|
|
620
|
+
label: `${team.name} (${team.key})`,
|
|
621
|
+
status: 'active',
|
|
622
|
+
}));
|
|
623
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: "Issues", color: project.color }), _jsxs(Box, { padding: 2, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "yellow", children: "Select a Linear team" }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Which team's issues should be shown?" }) }), _jsx(List, { items: teamItems, focused: true, onSelect: (item) => {
|
|
624
|
+
const team = viewState.teams.find((t) => t.id === item.id);
|
|
625
|
+
if (team)
|
|
626
|
+
selectTeam(team);
|
|
627
|
+
} })] })] }));
|
|
628
|
+
}
|
|
629
|
+
// Filter view
|
|
630
|
+
if (viewState.type === 'filter') {
|
|
631
|
+
const fields = [
|
|
632
|
+
{ key: 'assignee', label: 'Assignee' },
|
|
633
|
+
{ key: 'cycle', label: 'Cycle' },
|
|
634
|
+
{ key: 'status', label: 'Status' },
|
|
635
|
+
{ key: 'sort', label: 'Sort by' },
|
|
636
|
+
];
|
|
637
|
+
const currentOptions = getFilterOptions(filterField);
|
|
638
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: getSubtitle(), color: project.color }), _jsxs(Box, { paddingX: 2, paddingY: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Filter Issues" }), _jsx(Box, { marginTop: 1, gap: 2, children: fields.map((f) => (_jsx(Text, { color: filterField === f.key ? 'cyan' : 'gray', children: filterField === f.key ? `[${f.label}]` : f.label }, f.key))) }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: currentOptions.map((opt, i) => (_jsxs(Text, { color: i === filterSelectedIndex ? 'green' : undefined, children: [i === filterSelectedIndex ? '▸ ' : ' ', opt.label] }, opt.id))) }), _jsx(Box, { marginTop: 2, children: _jsx(Text, { dimColor: true, children: "Tab: switch field \u2022 j/k: navigate \u2022 Enter: select \u2022 Esc: cancel" }) })] })] }));
|
|
151
639
|
}
|
|
152
640
|
// Error state
|
|
153
|
-
if (error) {
|
|
154
|
-
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle:
|
|
641
|
+
if (viewState.type === 'error') {
|
|
642
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: getSubtitle(), color: project.color }), _jsxs(Box, { padding: 2, flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["Error: ", viewState.message] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press 'r' to retry, 'a' to re-authenticate" }) })] })] }));
|
|
643
|
+
}
|
|
644
|
+
// Ready state - show issues
|
|
645
|
+
if (viewState.type !== 'ready') {
|
|
646
|
+
return null;
|
|
155
647
|
}
|
|
156
|
-
|
|
648
|
+
// Row-level actions for issues
|
|
649
|
+
const issueActions = onOpenClaude
|
|
650
|
+
? [{ key: 'c', label: 'claude' }, { key: 'o', label: 'open' }]
|
|
651
|
+
: [{ key: 'o', label: 'open' }];
|
|
652
|
+
const isLinear = project.issueProvider?.type === 'linear';
|
|
653
|
+
const issueItems = viewState.issues.map((issue, i) => {
|
|
654
|
+
let label;
|
|
655
|
+
if (isLinear) {
|
|
656
|
+
// Linear: status indicator + priority + identifier + title + assignee + points
|
|
657
|
+
const status = getStatusIndicator(issue.stateType);
|
|
658
|
+
const priority = getPriorityIndicator(issue.priority);
|
|
659
|
+
const initials = getInitials(issue.assignee?.name);
|
|
660
|
+
const points = issue.estimate !== undefined ? `${issue.estimate}p` : '';
|
|
661
|
+
// Format: ▒! ENG-123 Issue title here CD 2p
|
|
662
|
+
const titleMaxLen = 32;
|
|
663
|
+
const truncatedTitle = truncate(issue.title, titleMaxLen);
|
|
664
|
+
label = `${priority} ${issue.identifier} ${truncatedTitle}`;
|
|
665
|
+
// Add assignee and points at the end
|
|
666
|
+
const suffix = [initials, points].filter(Boolean).join(' ');
|
|
667
|
+
if (suffix) {
|
|
668
|
+
// Pad to align
|
|
669
|
+
const padding = Math.max(0, 50 - label.length - suffix.length);
|
|
670
|
+
label = label + ' '.repeat(padding) + suffix;
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
id: String(i),
|
|
674
|
+
label,
|
|
675
|
+
actions: issueActions,
|
|
676
|
+
prefix: _jsx(Text, { color: status.color, children: status.char }),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
// GitHub: [livestock] identifier title
|
|
681
|
+
const sourceTag = issue.source.type === 'github' ? issue.source.livestockName : '';
|
|
682
|
+
label = `[${sourceTag}] ${issue.identifier} ${truncate(issue.title, 35)}`;
|
|
683
|
+
return {
|
|
684
|
+
id: String(i),
|
|
685
|
+
label,
|
|
686
|
+
actions: issueActions,
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
// Build current filter description
|
|
691
|
+
const getFilterDescription = () => {
|
|
692
|
+
if (!isLinear)
|
|
693
|
+
return '';
|
|
694
|
+
const parts = [];
|
|
695
|
+
if (currentFilter.assigneeId) {
|
|
696
|
+
const assignee = assignees.find((a) => a.id === currentFilter.assigneeId);
|
|
697
|
+
parts.push(assignee ? assignee.name : 'Me');
|
|
698
|
+
}
|
|
699
|
+
else if (currentFilter.assigneeId === null) {
|
|
700
|
+
parts.push('Unassigned');
|
|
701
|
+
}
|
|
702
|
+
if (currentFilter.cycleId) {
|
|
703
|
+
const cycle = cycles.find((c) => c.id === currentFilter.cycleId);
|
|
704
|
+
parts.push(cycle?.name ?? 'Cycle');
|
|
705
|
+
}
|
|
706
|
+
return parts.length > 0 ? parts.join(' • ') : 'All issues';
|
|
707
|
+
};
|
|
708
|
+
const detailsHints = 'j/k scroll';
|
|
709
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: getSubtitle(), color: project.color }), isLinear && (_jsxs(Box, { paddingX: 2, children: [_jsx(Text, { dimColor: true, children: "Showing: " }), _jsx(Text, { color: "cyan", children: getFilterDescription() }), _jsxs(Text, { dimColor: true, children: [" (", viewState.issues.length, ")"] })] })), _jsxs(Box, { flexGrow: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Issues", focused: focusedPanel === 'list', width: "45%", children: issueItems.length > 0 ? (_jsx(List, { items: issueItems, focused: focusedPanel === 'list', selectedIndex: selectedIndex, onSelectionChange: setSelectedIndex, onAction: (item, actionKey) => {
|
|
710
|
+
const issue = viewState.issues[parseInt(item.id, 10)];
|
|
711
|
+
if (!issue)
|
|
712
|
+
return;
|
|
713
|
+
if (actionKey === 'c') {
|
|
714
|
+
handleOpenClaude(issue);
|
|
715
|
+
}
|
|
716
|
+
else if (actionKey === 'o') {
|
|
717
|
+
openIssueInBrowser(issue.url);
|
|
718
|
+
}
|
|
719
|
+
} })) : (_jsx(Text, { dimColor: true, children: "No issues match the current filter" })) }), _jsx(Panel, { title: selectedIssue ? `${selectedIssue.identifier} ${truncate(selectedIssue.title, 30)}` : 'Details', focused: focusedPanel === 'details', width: "55%", hints: detailsHints, children: selectedIssue ? (_jsx(ScrollableMarkdown, { focused: focusedPanel === 'details', height: 20, children: buildIssueDetails(selectedIssue) })) : (_jsx(Text, { dimColor: true, children: "Select an issue to view details" })) })] })] }));
|
|
157
720
|
}
|