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